aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.config/dotnet-tools.json2
-rw-r--r--.devcontainer/devcontainer.json2
-rw-r--r--.devcontainer/install-ffmpeg.sh2
-rw-r--r--.github/ISSUE_TEMPLATE/issue report.yml2
-rw-r--r--.github/workflows/ci-codeql-analysis.yml10
-rw-r--r--.github/workflows/ci-compat.yml8
-rw-r--r--.github/workflows/ci-format.yml25
-rw-r--r--.github/workflows/ci-tests.yml6
-rw-r--r--.github/workflows/commands.yml4
-rw-r--r--.github/workflows/issue-stale.yml2
-rw-r--r--.github/workflows/issue-template-check.yml2
-rw-r--r--.github/workflows/openapi-generate.yml4
-rw-r--r--.github/workflows/openapi-pull-request.yml2
-rw-r--r--.github/workflows/pull-request-conflict.yml15
-rw-r--r--.github/workflows/pull-request-stale.yaml2
-rw-r--r--.github/workflows/release-bump-version.yaml4
-rw-r--r--.gitignore5
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Directory.Packages.props59
-rw-r--r--Emby.Naming/AudioBook/AudioBookNameParserResult.cs2
-rw-r--r--Emby.Naming/ExternalFiles/ExternalPathParser.cs2
-rw-r--r--Emby.Naming/TV/SeasonPathParser.cs23
-rw-r--r--Emby.Naming/Video/CleanDateTimeResult.cs2
-rw-r--r--Emby.Naming/Video/VideoInfo.cs8
-rw-r--r--Emby.Naming/Video/VideoListResolver.cs188
-rw-r--r--Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs2
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs20
-rw-r--r--Emby.Server.Implementations/Chapters/ChapterManager.cs10
-rw-r--r--Emby.Server.Implementations/Collections/CollectionManager.cs25
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs35
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs17
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketManager.cs9
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs67
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs46
-rw-r--r--Emby.Server.Implementations/Library/PathExtensions.cs2
-rw-r--r--Emby.Server.Implementations/Library/PathManager.cs58
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs25
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/AudioSimilarItemsProvider.cs55
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/LiveTvProgramSimilarItemsProvider.cs94
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs333
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/MusicAlbumSimilarItemsProvider.cs55
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/MusicArtistSimilarItemsProvider.cs55
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/SeriesSimilarItemsProvider.cs54
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs638
-rw-r--r--Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ab.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/af.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/as.json11
-rw-r--r--Emby.Server.Implementations/Localization/Core/be.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/bg-BG.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/bn.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/br.json63
-rw-r--r--Emby.Server.Implementations/Localization/Core/bs.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/chr.json14
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/cy.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/el.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-GB.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-US.json30
-rw-r--r--Emby.Server.Implementations/Localization/Core/enm.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/eo.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-MX.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_419.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_DO.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/et.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/eu.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/fil.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/fo.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr-CA.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/ga.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/gl.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/gsw.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/he_IL.json101
-rw-r--r--Emby.Server.Implementations/Localization/Core/hi.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json32
-rw-r--r--Emby.Server.Implementations/Localization/Core/ht.json18
-rw-r--r--Emby.Server.Implementations/Localization/Core/hu.json37
-rw-r--r--Emby.Server.Implementations/Localization/Core/hy.json13
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/is.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/jbo.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/ka.json87
-rw-r--r--Emby.Server.Implementations/Localization/Core/kab.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/kk.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/km.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/kn.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/ko.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/kw.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/lb.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/lt-LT.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/lv.json32
-rw-r--r--Emby.Server.Implementations/Localization/Core/lzh.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/mi.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/mk.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/ml.json36
-rw-r--r--Emby.Server.Implementations/Localization/Core/mn.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/mr.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/mt.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/my.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/ne.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json32
-rw-r--r--Emby.Server.Implementations/Localization/Core/nn.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/oc.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/or.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/pa.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json32
-rw-r--r--Emby.Server.Implementations/Localization/Core/pr.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-BR.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json32
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/ro.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/sk.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/sn.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/sq.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/sr.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/ta.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/te.json20
-rw-r--r--Emby.Server.Implementations/Localization/Core/th.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/ug.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/ur.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/ur_PK.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/uz.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json33
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json29
-rw-r--r--Emby.Server.Implementations/Localization/Core/zu.json15
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs237
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs3
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs2
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs19
-rw-r--r--Emby.Server.Implementations/SystemManager.cs10
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs56
-rw-r--r--Jellyfin.Api/Controllers/ArtistsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/AudioController.cs24
-rw-r--r--Jellyfin.Api/Controllers/CollectionController.cs2
-rw-r--r--Jellyfin.Api/Controllers/DashboardController.cs1
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs33
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs76
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs37
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs3
-rw-r--r--Jellyfin.Api/Controllers/ItemRefreshController.cs1
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs68
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs166
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs10
-rw-r--r--Jellyfin.Api/Controllers/MoviesController.cs281
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs2
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs28
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs2
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs15
-rw-r--r--Jellyfin.Api/Controllers/TrailersController.cs6
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs6
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs4
-rw-r--r--Jellyfin.Api/Controllers/UserController.cs4
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs19
-rw-r--r--Jellyfin.Api/Controllers/UserViewsController.cs2
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs26
-rw-r--r--Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs10
-rw-r--r--Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs14
-rw-r--r--Jellyfin.Api/Models/SystemInfoDtos/LibraryStorageDto.cs2
-rw-r--r--Jellyfin.Data/Enums/ActivityLogSortBy.cs2
-rw-r--r--Jellyfin.Server.Implementations/Activity/ActivityManager.cs6
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Library/LyricDownloadFailureLogger.cs2
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs2
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs4
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs4
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs2
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs8
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs4
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs4
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs4
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs4
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs4
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs2
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs4
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs2
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs2
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs2
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs2
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemMapper.cs2
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs155
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs49
-rw-r--r--Jellyfin.Server.Implementations/Item/ChapterRepository.cs19
-rw-r--r--Jellyfin.Server.Implementations/Item/LinkedChildrenService.cs33
-rw-r--r--Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs11
-rw-r--r--Jellyfin.Server.Implementations/Item/OrderMapper.cs4
-rw-r--r--Jellyfin.Server.Implementations/Item/PeopleRepository.cs69
-rw-r--r--Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs15
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthorizationContext.cs2
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs20
-rw-r--r--Jellyfin.Server/Filters/CachingOpenApiProvider.cs2
-rw-r--r--Jellyfin.Server/GlobalSuppressions.cs8
-rw-r--r--Jellyfin.Server/Migrations/JellyfinMigrationService.cs127
-rw-r--r--Jellyfin.Server/Migrations/MigrationOptions.cs2
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420050000_DisableTranscodingThrottling.cs (renamed from Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420060000_CreateUserLoggingConfigFile.cs (renamed from Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420070000_MigrateActivityLogDb.cs (renamed from Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420080000_RemoveDuplicateExtras.cs (renamed from Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420090000_AddDefaultPluginRepository.cs (renamed from Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420100000_MigrateUserDb.cs (renamed from Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420110000_ReaddDefaultPluginRepository.cs (renamed from Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420120000_MigrateDisplayPreferencesDb.cs (renamed from Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420130000_RemoveDownloadImagesInAdvance.cs (renamed from Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420140000_MigrateAuthenticationDb.cs (renamed from Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420150000_FixPlaylistOwner.cs (renamed from Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420160000_AddDefaultCastReceivers.cs (renamed from Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420170000_UpdateDefaultPluginRepository.cs (renamed from Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420180000_FixAudioData.cs (renamed from Jellyfin.Server/Migrations/Routines/FixAudioData.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420190000_RemoveDuplicatePlaylistChildren.cs (renamed from Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420193000_MigrateLibraryDbCompatibilityCheck.cs (renamed from Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420200000_MigrateLibraryDb.cs (renamed from Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420210000_MoveExtractedFiles.cs (renamed from Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs)10
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420230000_MoveTrickplayFiles.cs (renamed from Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250420230000_RefreshInternalDateModified.cs (renamed from Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250421000000_MigrateKeyframeData.cs (renamed from Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250618010000_MigrateLibraryUserData.cs (renamed from Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250620180000_FixDates.cs (renamed from Jellyfin.Server/Migrations/Routines/FixDates.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20250730215000_ReseedFolderFlag.cs (renamed from Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20251008120000_RefreshCleanNames.cs (renamed from Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20251009200000_CleanMusicArtist.cs (renamed from Jellyfin.Server/Migrations/Routines/CleanMusicArtist.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20260113120000_MigrateLinkedChildren.cs (renamed from Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs)42
-rw-r--r--Jellyfin.Server/Migrations/Routines/20260113230000_CleanupOrphanedExtras.cs (renamed from Jellyfin.Server/Migrations/Routines/CleanupOrphanedExtras.cs)58
-rw-r--r--Jellyfin.Server/Migrations/Routines/20260115120000_FixIncorrectOwnerIdRelationships.cs (renamed from Jellyfin.Server/Migrations/Routines/FixIncorrectOwnerIdRelationships.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20260206200000_FixLibrarySubtitleDownloadLanguages.cs (renamed from Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20260302090000_MigrateRatingLevels.cs (renamed from Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs)0
-rw-r--r--Jellyfin.Server/Migrations/Routines/20260508120000_MergeDuplicateMusicArtists.cs204
-rw-r--r--Jellyfin.Server/Migrations/Routines/20260508130000_MergeDuplicatePeople.cs300
-rw-r--r--Jellyfin.Server/Migrations/Routines/20260522092304_UpdateNormalizedUsername.cs44
-rw-r--r--Jellyfin.Server/Migrations/Routines/20260531160000_DisableLegacyAuthorization.cs32
-rw-r--r--Jellyfin.Server/ServerSetupApp/SetupServer.cs10
-rw-r--r--Jellyfin.Server/ServerSetupApp/index.mstemplate.html4
-rw-r--r--Jellyfin.Server/Startup.cs24
-rw-r--r--MediaBrowser.Common/Net/NetworkUtils.cs46
-rw-r--r--MediaBrowser.Common/Plugins/LocalPlugin.cs2
-rw-r--r--MediaBrowser.Common/Plugins/PluginManifest.cs9
-rw-r--r--MediaBrowser.Controller/Collections/ICollectionManager.cs8
-rw-r--r--MediaBrowser.Controller/Dto/DtoOptions.cs56
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs42
-rw-r--r--MediaBrowser.Controller/Entities/Extensions.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs7
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs15
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs6
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs6
-rw-r--r--MediaBrowser.Controller/Entities/TagExtensions.cs3
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs18
-rw-r--r--MediaBrowser.Controller/IO/IPathManager.cs16
-rw-r--r--MediaBrowser.Controller/Library/IBatchLocalSimilarItemsProvider.cs26
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs15
-rw-r--r--MediaBrowser.Controller/Library/ILocalSimilarItemsProvider.cs63
-rw-r--r--MediaBrowser.Controller/Library/IRemoteSimilarItemsProvider.cs62
-rw-r--r--MediaBrowser.Controller/Library/ISimilarItemsManager.cs70
-rw-r--r--MediaBrowser.Controller/Library/ISimilarItemsProvider.cs26
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs2
-rw-r--r--MediaBrowser.Controller/Library/SimilarItemReference.cs22
-rw-r--r--MediaBrowser.Controller/Library/SimilarItemsQuery.cs37
-rw-r--r--MediaBrowser.Controller/Library/SimilarItemsRecommendation.cs32
-rw-r--r--MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs6
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs208
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs56
-rw-r--r--MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs2
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs15
-rw-r--r--MediaBrowser.Controller/Net/IWebSocketConnection.cs9
-rw-r--r--MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs4
-rw-r--r--MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs7
-rw-r--r--MediaBrowser.Controller/Persistence/IPeopleRepository.cs8
-rw-r--r--MediaBrowser.Controller/Plugins/IHasEmbeddedImage.cs17
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs11
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs9
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs3
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs364
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs36
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs28
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs206
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs18
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs7
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/AssWriter.cs57
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs20
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs72
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs49
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SsaWriter.cs57
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs176
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs60
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs53
-rw-r--r--MediaBrowser.Model/Channels/ChannelFeatures.cs13
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs7
-rw-r--r--MediaBrowser.Model/Configuration/HlsAudioSeekStrategy.cs9
-rw-r--r--MediaBrowser.Model/Configuration/MetadataPluginType.cs4
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs2
-rw-r--r--MediaBrowser.Model/Configuration/TypeOptions.cs16
-rw-r--r--MediaBrowser.Model/Dlna/ConditionProcessor.cs6
-rw-r--r--MediaBrowser.Model/Dlna/ProfileConditionValue.cs3
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs76
-rw-r--r--MediaBrowser.Model/Drawing/ImageDimensions.cs1
-rw-r--r--MediaBrowser.Model/Dto/SessionInfoDto.cs8
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs30
-rw-r--r--MediaBrowser.Model/Extensions/EnumerableExtensions.cs2
-rw-r--r--MediaBrowser.Model/Globalization/ILocalizationManager.cs9
-rw-r--r--MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs2
-rw-r--r--MediaBrowser.Model/Querying/QueryFilters.cs11
-rw-r--r--MediaBrowser.Model/Session/TranscodeReason.cs1
-rw-r--r--MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs2
-rw-r--r--MediaBrowser.Model/SyncPlay/SyncPlayGroupDoesNotExistUpdate.cs2
-rw-r--r--MediaBrowser.Model/SyncPlay/SyncPlayGroupJoinedUpdate.cs2
-rw-r--r--MediaBrowser.Model/SyncPlay/SyncPlayGroupLeftUpdate.cs2
-rw-r--r--MediaBrowser.Model/SyncPlay/SyncPlayLibraryAccessDeniedUpdate.cs2
-rw-r--r--MediaBrowser.Model/SyncPlay/SyncPlayNotInGroupUpdate.cs2
-rw-r--r--MediaBrowser.Model/SyncPlay/SyncPlayPlayQueueUpdate.cs2
-rw-r--r--MediaBrowser.Model/SyncPlay/SyncPlayStateUpdate.cs2
-rw-r--r--MediaBrowser.Model/SyncPlay/SyncPlayUserJoinedUpdate.cs2
-rw-r--r--MediaBrowser.Model/SyncPlay/SyncPlayUserLeftUpdate.cs2
-rw-r--r--MediaBrowser.Model/Users/UserPolicy.cs2
-rw-r--r--MediaBrowser.Providers/Books/ComicVine/ComicVineExternalId.cs (renamed from MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalId.cs)2
-rw-r--r--MediaBrowser.Providers/Books/ComicVine/ComicVineExternalUrlProvider.cs (renamed from MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalUrlProvider.cs)2
-rw-r--r--MediaBrowser.Providers/Books/ComicVine/ComicVinePersonExternalId.cs (renamed from MediaBrowser.Providers/Plugins/ComicVine/ComicVinePersonExternalId.cs)2
-rw-r--r--MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalId.cs (renamed from MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalId.cs)2
-rw-r--r--MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalUrlProvider.cs (renamed from MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalUrlProvider.cs)2
-rw-r--r--MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs2
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs17
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj41
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/jellyfin-plugin-tadb.svg1
-rw-r--r--MediaBrowser.Providers/Plugins/ListenBrainz/Api/ListenBrainzLabsClient.cs128
-rw-r--r--MediaBrowser.Providers/Plugins/ListenBrainz/Api/Models/SimilarArtistData.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/ListenBrainz/Api/Models/SimilarArtistsResponse.cs16
-rw-r--r--MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/ListenBrainz_logo.svg60
-rw-r--r--MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/NOTICE.md23
-rw-r--r--MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/PluginConfiguration.cs65
-rw-r--r--MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/SimilarityAlgorithm.cs37
-rw-r--r--MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/SimilarityAlgorithmExtensions.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/config.html109
-rw-r--r--MediaBrowser.Providers/Plugins/ListenBrainz/ListenBrainzPlugin.cs63
-rw-r--r--MediaBrowser.Providers/Plugins/ListenBrainz/ListenBrainzSimilarArtistProvider.cs89
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/jellyfin-plugin-musicbrainz.svg36
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/Plugin.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/jellyfin-plugin-omdb.pngbin0 -> 67173 bytes
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/jellyfin-plugin-studioimages.svg1
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html15
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieSimilarProvider.cs96
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesSimilarProvider.cs96
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs48
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/jellyfin-plugin-tmdb.svg16
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs17
-rw-r--r--MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs14
-rw-r--r--README.md2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs4
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs11
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs47
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs10
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/MatchCriteria/HasMediaStreamType.cs23
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs4
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250609115616_DetachUserDataInsteadOfDelete.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250925203415_ExtendPeopleMapKey.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113102337_AddLinkedChildrenTable.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.cs38
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.cs47
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260522092303_AddNormalizedUsername.Designer.cs (renamed from src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.Designer.cs)16
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260522092303_AddNormalizedUsername.cs32
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260524120336_AddUniqueNormalizedUsernameIndex.Designer.cs1807
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260524120336_AddUniqueNormalizedUsernameIndex.cs28
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs10
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs26
-rw-r--r--src/Jellyfin.Extensions/StreamExtensions.cs174
-rw-r--r--src/Jellyfin.LiveTv/Channels/ChannelManager.cs2
-rw-r--r--src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs4
-rw-r--r--src/Jellyfin.LiveTv/Listings/ListingsManager.cs4
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs18
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/SdErrorCode.cs63
-rw-r--r--src/Jellyfin.LiveTv/LiveTvManager.cs5
-rw-r--r--src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs2
-rw-r--r--src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs4
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs2
-rw-r--r--src/Jellyfin.Networking/Manager/NetworkManager.cs6
-rw-r--r--tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperAudioBitStreamTests.cs99
-rw-r--r--tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs258
-rw-r--r--tests/Jellyfin.Extensions.Tests/StreamExtensionsTests.cs397
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Recordings/RecordingsMetadataManagerTests.cs64
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs282
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs57
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer-NoHevcRotation.json162
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-4000k-r180.json56
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs22
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs843
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs106
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkParseTests.cs97
-rw-r--r--tests/Jellyfin.Providers.Tests/ExternalId/ComicVineExternalUrlProviderTests.cs2
-rw-r--r--tests/Jellyfin.Providers.Tests/ExternalId/GoogleBooksExternalUrlProviderTests.cs2
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs3
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs18
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceTests.cs131
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs59
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs93
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerNormalizedUsernameTests.cs240
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs1
461 files changed, 11195 insertions, 5085 deletions
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 9b44eff4c6..5fc7834fcc 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "10.0.7",
+ "version": "10.0.8",
"commands": [
"dotnet-ef"
]
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index c67c292372..4ce0d7583a 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -33,7 +33,7 @@
"libfontconfig1"
]
},
- "ghcr.io/devcontainers/features/docker-in-docker:2": {
+ "ghcr.io/devcontainers/features/docker-in-docker:3": {
"dockerDashComposeVersion": "v2"
},
"ghcr.io/devcontainers/features/github-cli:1": {},
diff --git a/.devcontainer/install-ffmpeg.sh b/.devcontainer/install-ffmpeg.sh
index 1e58e6ef44..1344634630 100644
--- a/.devcontainer/install-ffmpeg.sh
+++ b/.devcontainer/install-ffmpeg.sh
@@ -15,7 +15,7 @@ sudo apt-get install software-properties-common -y
sudo add-apt-repository universe -y
sudo mkdir -p /etc/apt/keyrings
-curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/jellyfin.gpg
+curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | sudo gpg --batch --yes --dearmor -o /etc/apt/keyrings/jellyfin.gpg
export VERSION_OS="$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release )"
export VERSION_CODENAME="$( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release )"
export DPKG_ARCHITECTURE="$( dpkg --print-architecture )"
diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml
index 45235be712..0689db7a87 100644
--- a/.github/ISSUE_TEMPLATE/issue report.yml
+++ b/.github/ISSUE_TEMPLATE/issue report.yml
@@ -87,6 +87,8 @@ body:
label: Jellyfin Server version
description: What version of Jellyfin are you using?
options:
+ - 10.11.10
+ - 10.11.9
- 10.11.8
- 10.11.7
- 10.11.6
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index 65752af977..06a66bab53 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -24,21 +24,21 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup .NET
- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
+ uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
with:
dotnet-version: '10.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
+ uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
+ uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
+ uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml
index dd48209a1f..a0564027e3 100644
--- a/.github/workflows/ci-compat.yml
+++ b/.github/workflows/ci-compat.yml
@@ -11,13 +11,13 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
+ uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
with:
dotnet-version: '10.0.x'
@@ -40,14 +40,14 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
- name: Setup .NET
- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
+ uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
with:
dotnet-version: '10.0.x'
diff --git a/.github/workflows/ci-format.yml b/.github/workflows/ci-format.yml
new file mode 100644
index 0000000000..a9eebf0663
--- /dev/null
+++ b/.github/workflows/ci-format.yml
@@ -0,0 +1,25 @@
+name: Format
+on:
+ push:
+ branches:
+ - master
+ # Run formatter against the forked branch, but
+ # do not allow access to secrets
+ # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflows-in-forked-repositories
+ pull_request:
+
+env:
+ SDK_VERSION: "10.0.x"
+
+jobs:
+ format-check:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+
+ - uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
+ with:
+ dotnet-version: ${{ env.SDK_VERSION }}
+
+ - name: Run DotNet Format
+ run: dotnet format --verify-no-changes --verbosity minimal
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index 2a26bf15a4..6da1334039 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -20,9 +20,9 @@ jobs:
runs-on: "${{ matrix.os }}"
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
+ - uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
with:
dotnet-version: ${{ env.SDK_VERSION }}
@@ -35,7 +35,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
- uses: danielpalme/ReportGenerator-GitHub-Action@c31aa4ed4f12f147061186cf2a029f307b5c3636 # v5.5.9
+ uses: danielpalme/ReportGenerator-GitHub-Action@049f7ec958c672fd31d5cc1cb01622dc8d2e23ab # v5.5.10
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 9d3d99cb71..43ef0aab37 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -24,7 +24,7 @@ jobs:
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -40,7 +40,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: pull in script
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: jellyfin/jellyfin-triage-script
diff --git a/.github/workflows/issue-stale.yml b/.github/workflows/issue-stale.yml
index 339fcf569e..d6372ef6f4 100644
--- a/.github/workflows/issue-stale.yml
+++ b/.github/workflows/issue-stale.yml
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
+ - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true
diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml
index dcd1fb7cfe..ef5c7c09f2 100644
--- a/.github/workflows/issue-template-check.yml
+++ b/.github/workflows/issue-template-check.yml
@@ -10,7 +10,7 @@ jobs:
issues: write
steps:
- name: pull in script
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: jellyfin/jellyfin-triage-script
diff --git a/.github/workflows/openapi-generate.yml b/.github/workflows/openapi-generate.yml
index dbfaf9d30b..122bbd69ac 100644
--- a/.github/workflows/openapi-generate.yml
+++ b/.github/workflows/openapi-generate.yml
@@ -22,13 +22,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ inputs.ref }}
repository: ${{ inputs.repository }}
- name: Configure .NET
- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
+ uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
with:
dotnet-version: '10.0.x'
diff --git a/.github/workflows/openapi-pull-request.yml b/.github/workflows/openapi-pull-request.yml
index 4acd0f4d4f..d11b0140f7 100644
--- a/.github/workflows/openapi-pull-request.yml
+++ b/.github/workflows/openapi-pull-request.yml
@@ -10,7 +10,7 @@ jobs:
base_ref: ${{ steps.ancestor.outputs.base_ref }}
steps:
- name: Checkout Repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
diff --git a/.github/workflows/pull-request-conflict.yml b/.github/workflows/pull-request-conflict.yml
index b003636a6e..ce671eb72e 100644
--- a/.github/workflows/pull-request-conflict.yml
+++ b/.github/workflows/pull-request-conflict.yml
@@ -4,19 +4,20 @@ on:
push:
branches:
- master
- pull_request:
- issue_comment:
+ pull_request_target:
+ types: [synchronize]
permissions: {}
jobs:
- label:
- name: Labeling
+ main:
runs-on: ubuntu-latest
- if: ${{ github.repository == 'jellyfin/jellyfin' && github.event.issue.pull_request }}
+ permissions:
+ contents: read
+ pull-requests: write
+ if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Apply label
- uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
- if: ${{ github.event_name == 'push' || github.event_name == 'pull_request'}}
+ uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0
with:
dirtyLabel: 'merge conflict'
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
diff --git a/.github/workflows/pull-request-stale.yaml b/.github/workflows/pull-request-stale.yaml
index e114276c28..6f225a4714 100644
--- a/.github/workflows/pull-request-stale.yaml
+++ b/.github/workflows/pull-request-stale.yaml
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
+ - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true
diff --git a/.github/workflows/release-bump-version.yaml b/.github/workflows/release-bump-version.yaml
index 963b4a6023..5bb668c89c 100644
--- a/.github/workflows/release-bump-version.yaml
+++ b/.github/workflows/release-bump-version.yaml
@@ -33,7 +33,7 @@ jobs:
yq-version: v4.9.8
- name: Checkout Repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ env.TAG_BRANCH }}
@@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps:
- name: Checkout Repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ env.TAG_BRANCH }}
diff --git a/.gitignore b/.gitignore
index d5a0367bff..381c15909d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@ local.properties
.classpath
.settings/
.loadpath
+*.lscache
# External tool builders
.externalToolBuilders/
@@ -277,3 +278,7 @@ apiclient/generated
# Omnisharp crash logs
mono_crash.*.json
+
+# Devcontainer temp files
+.devcontainer/devcontainer-lock.json
+dotnet/
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 09a7198afe..d70ffddfd7 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -114,6 +114,7 @@
- [oddstr13](https://github.com/oddstr13)
- [olsh](https://github.com/olsh)
- [orryverducci](https://github.com/orryverducci)
+ - [PCEWLKR](https://github.com/PCEWLKR)
- [petermcneil](https://github.com/petermcneil)
- [Phlogi](https://github.com/Phlogi)
- [pjeanjean](https://github.com/pjeanjean)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index e8901b4a1d..1c26dd34e8 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -9,16 +9,16 @@
<PackageVersion Include="AutoFixture.Xunit3" Version="4.19.0" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BDInfo" Version="0.8.0" />
- <PackageVersion Include="BitFaster.Caching" Version="2.5.4" />
+ <PackageVersion Include="BitFaster.Caching" Version="2.6.0" />
<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="10.0.0" />
+ <PackageVersion Include="coverlet.collector" Version="10.0.1" />
<PackageVersion Include="Diacritics" Version="4.1.8" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="FsCheck.Xunit.v3" Version="3.3.3" />
- <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" />
+ <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.5" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
<PackageVersion Include="Ignore" Version="0.2.1" />
@@ -26,28 +26,28 @@
<PackageVersion Include="libse" Version="4.0.12" />
<PackageVersion Include="LrcParser" Version="2025.623.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" />
- <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.7" />
- <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.7" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.8" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.8" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" />
- <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.7" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.7" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.7" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.7" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.7" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.7" />
- <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.7" />
- <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.7" />
- <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.7" />
- <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.7" />
- <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.7" />
- <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
+ <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.8" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.8" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.8" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.8" />
+ <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.8" />
+ <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.670" />
<PackageVersion Include="Moq" Version="4.18.4" />
@@ -68,18 +68,17 @@
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.2.0" />
- <!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 -->
- <PackageVersion Include="SkiaSharp" Version="[3.116.1]" />
- <PackageVersion Include="SkiaSharp.HarfBuzz" Version="[3.116.1]" />
- <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.116.1]" />
+ <PackageVersion Include="SkiaSharp" Version="3.119.4" />
+ <PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.119.4" />
+ <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
- <PackageVersion Include="Svg.Skia" Version="3.4.1" />
- <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.7" />
- <PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" />
- <PackageVersion Include="System.Text.Json" Version="10.0.7" />
+ <PackageVersion Include="Svg.Skia" Version="3.7.0" />
+ <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.2.0" />
+ <PackageVersion Include="Swashbuckle.AspNetCore" Version="10.2.0" />
+ <PackageVersion Include="System.Text.Json" Version="10.0.8" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
- <PackageVersion Include="z440.atl.core" Version="7.13.0" />
+ <PackageVersion Include="z440.atl.core" Version="7.15.3" />
<PackageVersion Include="TMDbLib" Version="3.0.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
diff --git a/Emby.Naming/AudioBook/AudioBookNameParserResult.cs b/Emby.Naming/AudioBook/AudioBookNameParserResult.cs
index 3f2d7b2b0b..de78e75a91 100644
--- a/Emby.Naming/AudioBook/AudioBookNameParserResult.cs
+++ b/Emby.Naming/AudioBook/AudioBookNameParserResult.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CA1815
+
namespace Emby.Naming.AudioBook
{
/// <summary>
diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
index 3461b3c0d6..8e7da5db42 100644
--- a/Emby.Naming/ExternalFiles/ExternalPathParser.cs
+++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
@@ -70,7 +70,7 @@ namespace Emby.Naming.ExternalFiles
if (lastSeparator == -1)
{
- break;
+ break;
}
string currentSlice = languageString[lastSeparator..];
diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs
index ea4875e00a..9caebaf7ac 100644
--- a/Emby.Naming/TV/SeasonPathParser.cs
+++ b/Emby.Naming/TV/SeasonPathParser.cs
@@ -10,17 +10,25 @@ namespace Emby.Naming.TV
/// </summary>
public static partial class SeasonPathParser
{
+ private const string SeasonKeywordPattern =
+ @"시즌|シーズン|сезон" +
+ @"|season|sæson|saison|staffel|series|stagione|säsong|seizoen|seasong" +
+ @"|sezon|sezona|sezóna|sezonul|série|séria|serie|seria|temporada|kausi";
+
private static readonly Regex CleanNameRegex = new(@"[ ._\-\[\]]", RegexOptions.Compiled);
- [GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul|érie|éria|erie|eria)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
+ [GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:" + SeasonKeywordPattern + @")\s*(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
private static partial Regex ProcessPre();
- [GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul|érie|éria|erie|eria)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>\d+?)(?=\d{3,4}p|[^\d]|$)(?!\s*[Ee]\d)(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
+ [GeneratedRegex(@"^\s*(?:" + SeasonKeywordPattern + @")\s*(?<seasonnumber>\d+?)(?=\d{3,4}p|[^\d]|$)(?!\s*[Ee]\d)(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
private static partial Regex ProcessPost();
[GeneratedRegex(@"[sS](\d{1,4})(?!\d|[eE]\d)(?=\.|_|-|\[|\]|\s|$)", RegexOptions.None)]
private static partial Regex SeasonPrefix();
+ [GeneratedRegex(SeasonKeywordPattern, RegexOptions.IgnoreCase)]
+ private static partial Regex SeasonKeyword();
+
/// <summary>
/// Attempts to parse season number from path.
/// </summary>
@@ -91,14 +99,25 @@ namespace Emby.Naming.TV
return (val, true);
}
+ bool isMixedLibrary = !supportNumericSeasonFolders && !supportSpecialAliases;
var preMatch = ProcessPre().Match(filename);
if (preMatch.Success)
{
+ if (isMixedLibrary && !SeasonKeyword().IsMatch(fileName))
+ {
+ return (null, false);
+ }
+
return CheckMatch(preMatch);
}
else
{
var postMatch = ProcessPost().Match(filename);
+ if (postMatch.Success && isMixedLibrary && !SeasonKeyword().IsMatch(fileName))
+ {
+ return (null, false);
+ }
+
return CheckMatch(postMatch);
}
}
diff --git a/Emby.Naming/Video/CleanDateTimeResult.cs b/Emby.Naming/Video/CleanDateTimeResult.cs
index c675a19d0f..e367f92213 100644
--- a/Emby.Naming/Video/CleanDateTimeResult.cs
+++ b/Emby.Naming/Video/CleanDateTimeResult.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CA1815
+
namespace Emby.Naming.Video
{
/// <summary>
diff --git a/Emby.Naming/Video/VideoInfo.cs b/Emby.Naming/Video/VideoInfo.cs
index 8847ee9bc9..028b639122 100644
--- a/Emby.Naming/Video/VideoInfo.cs
+++ b/Emby.Naming/Video/VideoInfo.cs
@@ -17,8 +17,8 @@ namespace Emby.Naming.Video
{
Name = name;
- Files = Array.Empty<VideoFileInfo>();
- AlternateVersions = Array.Empty<VideoFileInfo>();
+ Files = [];
+ AlternateVersions = [];
}
/// <summary>
@@ -40,10 +40,10 @@ namespace Emby.Naming.Video
public IReadOnlyList<VideoFileInfo> Files { get; set; }
/// <summary>
- /// Gets or sets the alternate versions.
+ /// Gets or sets the alternate versions. Each alternate may itself span multiple files.
/// </summary>
/// <value>The alternate versions.</value>
- public IReadOnlyList<VideoFileInfo> AlternateVersions { get; set; }
+ public IReadOnlyList<VideoInfo> AlternateVersions { get; set; }
/// <summary>
/// Gets or sets the extra type.
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index a4bfb8d4a1..29330b132d 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -5,7 +5,8 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
-using Jellyfin.Extensions;
+using Emby.Naming.TV;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
@@ -13,8 +14,23 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
- public static partial class VideoListResolver
+ public partial class VideoListResolver
{
+ private static readonly StringComparer _numericOrdinalComparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
+
+ private readonly NamingOptions _namingOptions;
+ private readonly EpisodePathParser _episodePathParser;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="VideoListResolver"/> class.
+ /// </summary>
+ /// <param name="namingOptions">The naming options.</param>
+ public VideoListResolver(NamingOptions namingOptions)
+ {
+ _namingOptions = namingOptions;
+ _episodePathParser = new EpisodePathParser(namingOptions);
+ }
+
[GeneratedRegex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase)]
private static partial Regex ResolutionRegex();
@@ -25,12 +41,12 @@ namespace Emby.Naming.Video
/// Resolves alternative versions and extras from list of video files.
/// </summary>
/// <param name="videoInfos">List of related video files.</param>
- /// <param name="namingOptions">The naming options.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
/// <param name="parseName">Whether to parse the name or use the filename.</param>
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
+ /// <param name="collectionType">The type of the containing collection, if known.</param>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
- public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true, string? libraryRoot = "")
+ public IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, bool supportMultiVersion = true, bool parseName = true, string? libraryRoot = "", CollectionType? collectionType = null)
{
// Filter out all extras, otherwise they could cause stacks to not be resolved
// See the unit test TestStackedWithTrailer
@@ -38,7 +54,7 @@ namespace Emby.Naming.Video
.Where(i => i.ExtraType is null)
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
- var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
+ var stackResult = StackResolver.Resolve(nonExtras, _namingOptions).ToList();
var remainingFiles = new List<VideoFileInfo>();
var standaloneMedia = new List<VideoFileInfo>();
@@ -67,7 +83,7 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
- Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName, libraryRoot))
+ Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, _namingOptions, parseName, libraryRoot))
.OfType<VideoFileInfo>()
.ToList()
};
@@ -86,7 +102,9 @@ namespace Emby.Naming.Video
if (supportMultiVersion)
{
- list = GetVideosGroupedByVersion(list, namingOptions);
+ list = collectionType is CollectionType.tvshows
+ ? GetEpisodesGroupedByVersion(list)
+ : GetVideosGroupedByVersion(list);
}
// Whatever files are left, just add them
@@ -100,7 +118,7 @@ namespace Emby.Naming.Video
return list;
}
- private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions)
+ private List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos)
{
if (videos.Count == 0)
{
@@ -124,7 +142,7 @@ namespace Emby.Naming.Video
continue;
}
- if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension, namingOptions))
+ if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension))
{
return videos;
}
@@ -135,45 +153,9 @@ namespace Emby.Naming.Video
}
}
- if (videos.Count > 1)
- {
- var groups = videos
- .Select(x => (filename: x.Files[0].FileNameWithoutExtension.ToString(), value: x))
- .Select(x => (x.filename, resolutionMatch: ResolutionRegex().Match(x.filename), x.value))
- .GroupBy(x => x.resolutionMatch.Success)
- .ToList();
-
- videos.Clear();
+ var organized = OrganizeAlternateVersions(videos, primary, folderName.ToString());
- StringComparer comparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering);
- foreach (var group in groups)
- {
- if (group.Key)
- {
- videos.InsertRange(0, group
- .OrderByDescending(x => x.resolutionMatch.Value, comparer)
- .ThenBy(x => x.filename, comparer)
- .Select(x => x.value));
- }
- else
- {
- videos.AddRange(group.OrderBy(x => x.filename, comparer).Select(x => x.value));
- }
- }
- }
-
- primary ??= videos[0];
- videos.Remove(primary);
-
- var list = new List<VideoInfo>
- {
- primary
- };
-
- list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray();
- list[0].Name = folderName.ToString();
-
- return list;
+ return [organized];
}
private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos)
@@ -195,7 +177,7 @@ namespace Emby.Naming.Video
return true;
}
- private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, ReadOnlySpan<char> testFilename, NamingOptions namingOptions)
+ private bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, ReadOnlySpan<char> testFilename)
{
if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{
@@ -209,7 +191,7 @@ namespace Emby.Naming.Video
}
// There are no span overloads for regex unfortunately
- if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName))
+ if (CleanStringParser.TryClean(testFilename.ToString(), _namingOptions.CleanStringRegexes, out var cleanName))
{
testFilename = cleanName.AsSpan().Trim();
}
@@ -221,5 +203,113 @@ namespace Emby.Naming.Video
|| testFilename[0] == '.'
|| CheckMultiVersionRegex().IsMatch(testFilename);
}
+
+ private List<VideoInfo> GetEpisodesGroupedByVersion(List<VideoInfo> videos)
+ {
+ if (videos.Count < 2)
+ {
+ return videos;
+ }
+
+ var result = new List<VideoInfo>();
+ var groups = new Dictionary<string, List<VideoInfo>>(StringComparer.OrdinalIgnoreCase);
+
+ for (var i = 0; i < videos.Count; i++)
+ {
+ var video = videos[i];
+ var episodeResult = _episodePathParser.Parse(video.Files[0].Path, false);
+ string? key = null;
+ if (episodeResult.Success)
+ {
+ if (episodeResult.IsByDate
+ && episodeResult.Year.HasValue
+ && episodeResult.Month.HasValue
+ && episodeResult.Day.HasValue)
+ {
+ key = FormattableString.Invariant(
+ $"D{episodeResult.Year.Value}{episodeResult.Month.Value:D2}{episodeResult.Day.Value:D2}");
+ }
+ else if (episodeResult.EpisodeNumber.HasValue)
+ {
+ key = FormattableString.Invariant(
+ $"S{episodeResult.SeasonNumber ?? 0}E{episodeResult.EpisodeNumber.Value}");
+ }
+ }
+
+ if (key is null)
+ {
+ result.Add(video);
+ continue;
+ }
+
+ if (!groups.TryGetValue(key, out var group))
+ {
+ group = [];
+ groups[key] = group;
+ }
+
+ group.Add(video);
+ }
+
+ foreach (var group in groups.Values)
+ {
+ if (group.Count == 1)
+ {
+ result.Add(group[0]);
+ continue;
+ }
+
+ result.Add(OrganizeAlternateVersions(group));
+ }
+
+ return result;
+ }
+
+ private static VideoInfo OrganizeAlternateVersions(
+ List<VideoInfo> videos,
+ VideoInfo? primaryOverride = null,
+ string? nameOverride = null)
+ {
+ if (videos.Count > 1)
+ {
+ var groups = videos
+ .Select(x => (filename: x.Files[0].FileNameWithoutExtension.ToString(), value: x))
+ .Select(x => (x.filename, resolutionMatch: ResolutionRegex().Match(x.filename), x.value))
+ .GroupBy(x => x.resolutionMatch.Success)
+ .ToList();
+
+ videos = [];
+
+ foreach (var group in groups)
+ {
+ if (group.Key)
+ {
+ videos.InsertRange(0, group
+ .OrderByDescending(x => x.resolutionMatch.Value, _numericOrdinalComparer)
+ .ThenBy(x => x.filename, _numericOrdinalComparer)
+ .Select(x => x.value));
+ }
+ else
+ {
+ videos.AddRange(group.OrderBy(x => x.filename, _numericOrdinalComparer).Select(x => x.value));
+ }
+ }
+ }
+
+ // Prefer a stacked entry (more than one part) as primary
+ var primary = primaryOverride
+ ?? videos.FirstOrDefault(v => v.Files.Count > 1)
+ ?? videos[0];
+ videos.Remove(primary);
+
+ primary.AlternateVersions = videos;
+
+ if (nameOverride is not null)
+ {
+ primary.Name = nameOverride;
+ }
+
+ return primary;
+ }
}
}
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index ef5fa8bef9..aa19948e36 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.AppBase
}
else
{
- _configurationFactories = [.._configurationFactories, factory];
+ _configurationFactories = [.. _configurationFactories, factory];
}
_configurationStores = _configurationFactories
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index b7aa2f3d06..c81829688f 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -14,6 +14,7 @@ using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Emby.Naming.Common;
+using Emby.Naming.Video;
using Emby.Photos;
using Emby.Server.Implementations.Chapters;
using Emby.Server.Implementations.Collections;
@@ -25,6 +26,7 @@ using Emby.Server.Implementations.Dto;
using Emby.Server.Implementations.HttpServer.Security;
using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Library;
+using Emby.Server.Implementations.Library.SimilarItems;
using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.Plugins;
@@ -92,7 +94,11 @@ using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Lyric;
using MediaBrowser.Providers.Manager;
+using MediaBrowser.Providers.Plugins.ListenBrainz;
+using MediaBrowser.Providers.Plugins.ListenBrainz.Api;
using MediaBrowser.Providers.Plugins.Tmdb;
+using MediaBrowser.Providers.Plugins.Tmdb.Movies;
+using MediaBrowser.Providers.Plugins.Tmdb.TV;
using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
@@ -166,8 +172,6 @@ namespace Emby.Server.Implementations
ConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationVersion);
-
- _disposableParts.Add(_pluginManager);
}
/// <summary>
@@ -485,6 +489,11 @@ namespace Emby.Server.Implementations
serviceCollection.AddScoped<ISystemManager, SystemManager>();
serviceCollection.AddSingleton<TmdbClientManager>();
+ serviceCollection.AddSingleton<TmdbMovieSimilarProvider>();
+ serviceCollection.AddSingleton<TmdbSeriesSimilarProvider>();
+
+ serviceCollection.AddSingleton<ListenBrainzLabsClient>();
+ serviceCollection.AddSingleton<ListenBrainzSimilarArtistProvider>();
serviceCollection.AddSingleton(NetManager);
@@ -532,12 +541,15 @@ namespace Emby.Server.Implementations
serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
serviceCollection.AddSingleton<NamingOptions>();
+ serviceCollection.AddSingleton<VideoListResolver>();
serviceCollection.AddSingleton<IMusicManager, MusicManager>();
serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
serviceCollection.AddSingleton<DotIgnoreIgnoreRule>();
+ serviceCollection.AddSingleton<ISimilarItemsManager, SimilarItemsManager>();
+
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
serviceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
@@ -695,6 +707,8 @@ namespace Emby.Server.Implementations
GetExports<IExternalUrlProvider>());
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
+
+ Resolve<ISimilarItemsManager>().AddParts(GetExports<ISimilarItemsProvider>());
}
/// <summary>
@@ -1014,6 +1028,8 @@ namespace Emby.Server.Implementations
}
_disposableParts.Clear();
+
+ _pluginManager?.Dispose();
}
_disposed = true;
diff --git a/Emby.Server.Implementations/Chapters/ChapterManager.cs b/Emby.Server.Implementations/Chapters/ChapterManager.cs
index 8a4721ce62..69cbe533c6 100644
--- a/Emby.Server.Implementations/Chapters/ChapterManager.cs
+++ b/Emby.Server.Implementations/Chapters/ChapterManager.cs
@@ -240,15 +240,15 @@ public class ChapterManager : IChapterManager
public void SaveChapters(BaseItem item, IReadOnlyList<ChapterInfo> chapters)
{
if (!Supports(item))
- {
- _logger.LogWarning("Attempted to save chapters for unsupported item type {Type}: {Name} ({Id})", item.GetType().Name, item.Name, item.Id);
- return;
- }
+ {
+ _logger.LogWarning("Attempted to save chapters for unsupported item type {Type}: {Name} ({Id})", item.GetType().Name, item.Name, item.Id);
+ return;
+ }
// Remove any chapters that are outside of the runtime of the item
var validChapters = chapters.Where(c => c.StartPositionTicks < item.RunTimeTicks).ToList();
_chapterRepository.SaveChapters(item.Id, validChapters);
-}
+ }
/// <inheritdoc />
public ChapterInfo? GetChapter(Guid baseItemId, int index)
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index 0ede5665f9..295efd456c 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -4,12 +4,15 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
@@ -29,6 +32,7 @@ namespace Emby.Server.Implementations.Collections
private readonly ILibraryMonitor _iLibraryMonitor;
private readonly ILogger<CollectionManager> _logger;
private readonly IProviderManager _providerManager;
+ private readonly ILinkedChildrenService _linkedChildrenService;
private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths;
@@ -42,6 +46,7 @@ namespace Emby.Server.Implementations.Collections
/// <param name="iLibraryMonitor">The library monitor.</param>
/// <param name="loggerFactory">The logger factory.</param>
/// <param name="providerManager">The provider manager.</param>
+ /// <param name="linkedChildrenService">The linked children service.</param>
public CollectionManager(
ILibraryManager libraryManager,
IApplicationPaths appPaths,
@@ -49,13 +54,15 @@ namespace Emby.Server.Implementations.Collections
IFileSystem fileSystem,
ILibraryMonitor iLibraryMonitor,
ILoggerFactory loggerFactory,
- IProviderManager providerManager)
+ IProviderManager providerManager,
+ ILinkedChildrenService linkedChildrenService)
{
_libraryManager = libraryManager;
_fileSystem = fileSystem;
_iLibraryMonitor = iLibraryMonitor;
_logger = loggerFactory.CreateLogger<CollectionManager>();
_providerManager = providerManager;
+ _linkedChildrenService = linkedChildrenService;
_localizationManager = localizationManager;
_appPaths = appPaths;
}
@@ -120,6 +127,22 @@ namespace Emby.Server.Implementations.Collections
return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
}
+ /// <inheritdoc />
+ public IEnumerable<BoxSet> GetCollectionsContainingItem(User user, Guid itemId)
+ {
+ ArgumentNullException.ThrowIfNull(user);
+
+ if (itemId.IsEmpty())
+ {
+ return Enumerable.Empty<BoxSet>();
+ }
+
+ return _linkedChildrenService
+ .GetManualLinkedParentIds(itemId, BaseItemKind.BoxSet)
+ .Select(parentId => _libraryManager.GetItemById<BoxSet>(parentId, user))
+ .OfType<BoxSet>();
+ }
+
private IEnumerable<BoxSet> GetCollections(User user)
{
var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 321c7da1c4..f53328c7dd 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -1366,6 +1366,41 @@ namespace Emby.Server.Implementations.Dto
}
}
+ if (options.PreferEpisodeParentPoster)
+ {
+ var episodeSeason = episode.Season;
+ var seasonPrimaryTag = episodeSeason is not null
+ ? GetTagAndFillBlurhash(dto, episodeSeason, ImageType.Primary)
+ : null;
+
+ BaseItem? posterParent = null;
+ if (seasonPrimaryTag is not null)
+ {
+ dto.ParentPrimaryImageItemId = episodeSeason!.Id;
+ dto.ParentPrimaryImageTag = seasonPrimaryTag;
+ posterParent = episodeSeason;
+ }
+ else if (episodeSeries is not null && dto.SeriesPrimaryImageTag is not null)
+ {
+ dto.ParentPrimaryImageItemId = episodeSeries.Id;
+ dto.ParentPrimaryImageTag = dto.SeriesPrimaryImageTag;
+ posterParent = episodeSeries;
+ }
+
+ if (posterParent is not null)
+ {
+ if (dto.ImageTags is not null && dto.ImageTags.Remove(ImageType.Primary, out var ownPrimaryTag))
+ {
+ // Only drop the episode's own primary blurhash; keep the poster parent's.
+ dto.ImageBlurHashes?.GetValueOrDefault(ImageType.Primary)?.Remove(ownPrimaryTag);
+ }
+
+ dto.SeriesPrimaryImageTag = null;
+ dto.PrimaryImageAspectRatio = null;
+ AttachPrimaryImageAspectRatio(dto, posterParent);
+ }
+ }
+
if (options.ContainsField(ItemFields.SeriesStudio))
{
episodeSeries ??= episode.Series;
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 373b0994a6..e9bf3b93a7 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -1,5 +1,6 @@
using System;
using System.Buffers;
+using System.Globalization;
using System.IO.Pipelines;
using System.Net;
using System.Net.WebSockets;
@@ -69,6 +70,11 @@ namespace Emby.Server.Implementations.HttpServer
/// <inheritdoc />
public IPAddress? RemoteEndPoint { get; }
+ /// <summary>
+ /// Gets or initializes the UI culture captured from the upgrade request.
+ /// </summary>
+ public CultureInfo? RequestUICulture { get; init; }
+
/// <inheritdoc />
public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
@@ -82,6 +88,17 @@ namespace Emby.Server.Implementations.HttpServer
public WebSocketState State => _socket.State;
/// <inheritdoc />
+ public void ApplyRequestCulture()
+ {
+ if (RequestUICulture is null)
+ {
+ return;
+ }
+
+ CultureInfo.CurrentUICulture = RequestUICulture;
+ }
+
+ /// <inheritdoc />
public async Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
{
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
index cb5b3993b8..072034c4bf 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Net.WebSockets;
using System.Threading.Tasks;
@@ -47,14 +48,18 @@ namespace Emby.Server.Implementations.HttpServer
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
-
var connection = new WebSocketConnection(
_loggerFactory.CreateLogger<WebSocketConnection>(),
webSocket,
authorizationInfo,
context.GetNormalizedRemoteIP())
{
- OnReceive = ProcessWebSocketMessageReceived
+ RequestUICulture = CultureInfo.CurrentUICulture
+ };
+ connection.OnReceive = result =>
+ {
+ connection.ApplyRequestCulture();
+ return ProcessWebSocketMessageReceived(result);
};
await using (connection.ConfigureAwait(false))
{
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 11f1496086..ffc449d974 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -13,6 +13,7 @@ using System.Threading.Tasks;
using BitFaster.Caching.Lru;
using Emby.Naming.Common;
using Emby.Naming.TV;
+using Emby.Naming.Video;
using Emby.Server.Implementations.Library.Resolvers;
using Emby.Server.Implementations.Library.Validators;
using Emby.Server.Implementations.Playlists;
@@ -87,6 +88,7 @@ namespace Emby.Server.Implementations.Library
private readonly IPathManager _pathManager;
private readonly FastConcurrentLru<Guid, BaseItem> _cache;
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
+ private readonly IMediaStreamRepository _mediaStreamRepository;
/// <summary>
/// The _root folder sync lock.
@@ -129,6 +131,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="peopleRepository">The people repository.</param>
/// <param name="pathManager">The path manager.</param>
/// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
+ /// <param name="mediaStreamRepository">The media stream repository.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILoggerFactory loggerFactory,
@@ -151,7 +154,8 @@ namespace Emby.Server.Implementations.Library
IDirectoryService directoryService,
IPeopleRepository peopleRepository,
IPathManager pathManager,
- DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
+ DotIgnoreIgnoreRule dotIgnoreIgnoreRule,
+ IMediaStreamRepository mediaStreamRepository)
{
_appHost = appHost;
_logger = loggerFactory.CreateLogger<LibraryManager>();
@@ -181,6 +185,8 @@ namespace Emby.Server.Implementations.Library
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
+ _mediaStreamRepository = mediaStreamRepository;
+
RecordConfigurationValues(_configurationManager.Configuration);
}
@@ -787,6 +793,42 @@ namespace Emby.Server.Implementations.Library
CollectionType? collectionType = null)
=> ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent, collectionType);
+ private void SetAdditionalPartsFromStack(Video altVideo, string path)
+ {
+ if (altVideo.AdditionalParts is { Length: > 0 })
+ {
+ return;
+ }
+
+ var directory = Path.GetDirectoryName(path);
+ if (string.IsNullOrEmpty(directory))
+ {
+ return;
+ }
+
+ IEnumerable<FileSystemMetadata> siblings;
+ try
+ {
+ siblings = _fileSystem.GetFiles(directory);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to enumerate siblings to detect stack for {Path}", path);
+ return;
+ }
+
+ var stacks = StackResolver.Resolve(siblings, _namingOptions);
+ foreach (var stack in stacks)
+ {
+ if (stack.Files.Count > 1
+ && string.Equals(stack.Files[0], path, StringComparison.OrdinalIgnoreCase))
+ {
+ altVideo.AdditionalParts = stack.Files.Skip(1).ToArray();
+ return;
+ }
+ }
+ }
+
/// <inheritdoc />
public Video? ResolveAlternateVersion(string path, Type expectedVideoType, Folder? parent, CollectionType? collectionType)
{
@@ -1945,7 +1987,8 @@ namespace Emby.Server.Implementations.Library
query.TopParentIds.Length == 0 &&
string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&
- query.ItemIds.Length == 0)
+ query.ItemIds.Length == 0 &&
+ query.OwnerIds.Length == 0)
{
var userViews = UserViewManager.GetUserViews(new UserViewQuery
{
@@ -2307,6 +2350,10 @@ namespace Emby.Server.Implementations.Library
{
altVideo.OwnerId = video.Id;
altVideo.SetPrimaryVersionId(video.Id);
+ // ResolveAlternateVersion only sees the alternate's primary file.
+ // If the alternate is itself a stack (e.g. 1080p part1 + part2),
+ // detect its parts from sibling files so its AdditionalParts persist.
+ SetAdditionalPartsFromStack(altVideo, path);
allItems.Add(altVideo);
}
}
@@ -2510,6 +2557,10 @@ namespace Emby.Server.Implementations.Library
{
altVideo.OwnerId = video.Id;
altVideo.SetPrimaryVersionId(video.Id);
+ // ResolveAlternateVersion only sees the alternate's primary file.
+ // If the alternate is itself a stack (e.g. 1080p part1 + part2),
+ // detect its parts from sibling files so its AdditionalParts persist.
+ SetAdditionalPartsFromStack(altVideo, path);
allItems.Add(altVideo);
}
}
@@ -3344,6 +3395,12 @@ namespace Emby.Server.Implementations.Library
return _peopleRepository.GetPeopleNames(query);
}
+ /// <inheritdoc/>
+ public IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IReadOnlyList<string> personTypes)
+ {
+ return _peopleRepository.GetPeopleNamesByItems(itemIds, personTypes);
+ }
+
public void UpdatePeople(BaseItem item, List<PersonInfo> people)
{
UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
@@ -3800,5 +3857,11 @@ namespace Emby.Server.Implementations.Library
SetTopParentOrAncestorIds(query);
return _itemRepository.GetQueryFiltersLegacy(query);
}
+
+ /// <inheritdoc />
+ public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType)
+ {
+ return _mediaStreamRepository.GetMediaStreamLanguages(mediaStreamType);
+ }
}
}
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index fdb4c7328b..9ccfefa86e 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -24,6 +24,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
@@ -127,6 +128,11 @@ namespace Emby.Server.Implementations.Library
return true;
}
+ if (stream.IsVobSubSubtitleStream)
+ {
+ return true;
+ }
+
return false;
}
@@ -171,6 +177,7 @@ namespace Emby.Server.Implementations.Library
public async Task<IReadOnlyList<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
{
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
+ ResolveSymlinkPaths(mediaSources, enablePathSubstitution);
// If file is strm or main media stream is missing, force a metadata refresh with remote probing
if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder
@@ -187,6 +194,7 @@ namespace Emby.Server.Implementations.Library
cancellationToken).ConfigureAwait(false);
mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
+ ResolveSymlinkPaths(mediaSources, enablePathSubstitution);
}
var dynamicMediaSources = await GetDynamicMediaSources(item, cancellationToken).ConfigureAwait(false);
@@ -319,6 +327,28 @@ namespace Emby.Server.Implementations.Library
}
}
+ /// <summary>
+ /// Resolves symlinked file paths on the supplied sources to the real on-disk target.
+ /// Skipped when <paramref name="enablePathSubstitution"/> is set because the path may
+ /// already have been rewritten to a UNC/URL meant for the client to consume directly.
+ /// </summary>
+ private static void ResolveSymlinkPaths(IReadOnlyList<MediaSourceInfo> sources, bool enablePathSubstitution)
+ {
+ if (enablePathSubstitution)
+ {
+ return;
+ }
+
+ foreach (var source in sources)
+ {
+ if (source.Protocol == MediaProtocol.File
+ && FileSystemHelper.ResolveLinkTarget(source.Path, returnFinalTarget: true) is { Exists: true } target)
+ {
+ source.Path = target.FullName;
+ }
+ }
+ }
+
private static void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource)
{
var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimiter;
@@ -440,10 +470,6 @@ namespace Emby.Server.Implementations.Library
if (string.Equals(user.AudioLanguagePreference, "OriginalLanguage", StringComparison.OrdinalIgnoreCase))
{
- originalLanguage = !string.IsNullOrWhiteSpace(originalLanguage)
- ? originalLanguage.Split(',').FirstOrDefault()
- : null;
-
if (user.PlayDefaultAudioTrack)
{
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(
@@ -498,17 +524,7 @@ namespace Emby.Server.Implementations.Library
var allowRememberingSelection = item is null || item.EnableRememberingTrackSelections;
- var originalLanguage = item?.OriginalLanguage ?? item switch
- {
- Episode episode => episode.Series.OriginalLanguage,
- Video video => video.GetOwner() switch
- {
- Episode ownerEpisode => ownerEpisode.OriginalLanguage ?? ownerEpisode.Series.OriginalLanguage,
- BaseItem owner => owner.OriginalLanguage,
- null => null
- },
- _ => null
- };
+ var originalLanguage = item?.GetInheritedOriginalLanguage();
SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection, originalLanguage);
SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection);
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index cfa3e7c31d..7591359ea4 100644
--- a/Emby.Server.Implementations/Library/PathExtensions.cs
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.Library
'[' => ']',
'(' => ')',
'{' => '}',
- _ => '\0'
+ _ => '\0'
};
if (attributeCloser != '\0' && (str[attributeEnd] == '=' || str[attributeEnd] == '-'))
{
diff --git a/Emby.Server.Implementations/Library/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs
index a9b7a1274b..ef5edb9afa 100644
--- a/Emby.Server.Implementations/Library/PathManager.cs
+++ b/Emby.Server.Implementations/Library/PathManager.cs
@@ -6,6 +6,7 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
+using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library;
@@ -14,18 +15,22 @@ namespace Emby.Server.Implementations.Library;
/// </summary>
public class PathManager : IPathManager
{
+ private readonly ILogger<PathManager> _logger;
private readonly IServerConfigurationManager _config;
private readonly IApplicationPaths _appPaths;
/// <summary>
/// Initializes a new instance of the <see cref="PathManager"/> class.
/// </summary>
+ /// <param name="logger">The logger.</param>
/// <param name="config">The server configuration manager.</param>
/// <param name="appPaths">The application paths.</param>
public PathManager(
+ ILogger<PathManager> logger,
IServerConfigurationManager config,
IApplicationPaths appPaths)
{
+ _logger = logger;
_config = config;
_appPaths = appPaths;
}
@@ -35,31 +40,43 @@ public class PathManager : IPathManager
private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
/// <inheritdoc />
- public string GetAttachmentPath(string mediaSourceId, string fileName)
+ public string? GetAttachmentPath(string mediaSourceId, string fileName)
{
- return Path.Combine(GetAttachmentFolderPath(mediaSourceId), fileName);
+ var folder = GetAttachmentFolderPath(mediaSourceId);
+ return folder is null ? null : Path.Combine(folder, fileName);
}
/// <inheritdoc />
- public string GetAttachmentFolderPath(string mediaSourceId)
+ public string? GetAttachmentFolderPath(string mediaSourceId)
{
- var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
+ if (!Guid.TryParse(mediaSourceId, out var parsed))
+ {
+ _logger.LogDebug("MediaSource Id '{MediaSourceId}' is not a GUID; no on-disk attachment folder.", mediaSourceId);
+ return null;
+ }
+ var id = parsed.ToString("D", CultureInfo.InvariantCulture).AsSpan();
return Path.Join(AttachmentCachePath, id[..2], id);
}
/// <inheritdoc />
- public string GetSubtitleFolderPath(string mediaSourceId)
+ public string? GetSubtitleFolderPath(string mediaSourceId)
{
- var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
+ if (!Guid.TryParse(mediaSourceId, out var parsed))
+ {
+ _logger.LogDebug("MediaSource Id '{MediaSourceId}' is not a GUID; no on-disk subtitle folder.", mediaSourceId);
+ return null;
+ }
+ var id = parsed.ToString("D", CultureInfo.InvariantCulture).AsSpan();
return Path.Join(SubtitleCachePath, id[..2], id);
}
/// <inheritdoc />
- public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension)
+ public string? GetSubtitlePath(string mediaSourceId, int streamIndex, string extension)
{
- return Path.Combine(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
+ var folder = GetSubtitleFolderPath(mediaSourceId);
+ return folder is null ? null : Path.Combine(folder, streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
}
/// <inheritdoc />
@@ -90,12 +107,23 @@ public class PathManager : IPathManager
public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item)
{
var mediaSourceId = item.Id.ToString("N", CultureInfo.InvariantCulture);
- return [
- GetAttachmentFolderPath(mediaSourceId),
- GetSubtitleFolderPath(mediaSourceId),
- GetTrickplayDirectory(item, false),
- GetTrickplayDirectory(item, true),
- GetChapterImageFolderPath(item)
- ];
+ List<string> paths = [];
+ var attachmentFolder = GetAttachmentFolderPath(mediaSourceId);
+ if (attachmentFolder is not null)
+ {
+ paths.Add(attachmentFolder);
+ }
+
+ var subtitleFolder = GetSubtitleFolderPath(mediaSourceId);
+ if (subtitleFolder is not null)
+ {
+ paths.Add(subtitleFolder);
+ }
+
+ paths.Add(GetTrickplayDirectory(item, false));
+ paths.Add(GetTrickplayDirectory(item, true));
+ paths.Add(GetChapterImageFolderPath(item));
+
+ return paths;
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 98e8f5350b..68b66ab7f5 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -28,15 +28,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
public partial class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
{
private readonly IImageProcessor _imageProcessor;
+ private readonly VideoListResolver _videoListResolver;
- private static readonly CollectionType[] _validCollectionTypes = new[]
- {
+ private static readonly CollectionType[] _validCollectionTypes =
+ [
CollectionType.movies,
CollectionType.homevideos,
CollectionType.musicvideos,
CollectionType.tvshows,
CollectionType.photos
- };
+ ];
/// <summary>
/// Initializes a new instance of the <see cref="MovieResolver"/> class.
@@ -45,10 +46,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param>
- public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
+ /// <param name="videoListResolver">The video list resolver.</param>
+ public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService, VideoListResolver videoListResolver)
: base(logger, namingOptions, directoryService)
{
_imageProcessor = imageProcessor;
+ _videoListResolver = videoListResolver;
}
/// <summary>
@@ -228,7 +231,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (collectionType == CollectionType.tvshows)
{
- return ResolveVideos<Episode>(parent, files, false, collectionType, true);
+ return ResolveVideos<Episode>(parent, files, true, collectionType, true);
}
return null;
@@ -274,7 +277,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
.Where(f => f is not null)
.ToList();
- var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath);
+ var resolverResult = _videoListResolver.Resolve(videoInfos, supportMultiEditions, parseName, parent.ContainingFolderPath, collectionType);
var result = new MultiItemResolverResult
{
@@ -302,7 +305,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
ProductionYear = video.Year,
Name = parseName ? video.Name : firstVideo.Name,
AdditionalParts = additionalParts,
- LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToArray()
+ LocalAlternateVersions = video.AlternateVersions.Select(av => av.Files[0].Path).ToArray()
};
SetVideoType(videoItem, firstVideo);
@@ -331,9 +334,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
for (var j = 0; j < current.AlternateVersions.Count; j++)
{
- if (ContainsFile(current.AlternateVersions[j], file))
+ var alternate = current.AlternateVersions[j];
+ for (var k = 0; k < alternate.Files.Count; k++)
{
- return true;
+ if (ContainsFile(alternate.Files[k], file))
+ {
+ return true;
+ }
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index c81a0adb89..769d721665 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -31,7 +31,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
- public SeriesResolver(ILogger<SeriesResolver> logger, NamingOptions namingOptions)
+ public SeriesResolver(ILogger<SeriesResolver> logger, NamingOptions namingOptions)
{
_logger = logger;
_namingOptions = namingOptions;
diff --git a/Emby.Server.Implementations/Library/SimilarItems/AudioSimilarItemsProvider.cs b/Emby.Server.Implementations/Library/SimilarItems/AudioSimilarItemsProvider.cs
new file mode 100644
index 0000000000..1cc670b8ee
--- /dev/null
+++ b/Emby.Server.Implementations/Library/SimilarItems/AudioSimilarItemsProvider.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.Library.SimilarItems;
+
+/// <summary>
+/// Provides similar items for audio tracks.
+/// </summary>
+public class AudioSimilarItemsProvider : ILocalSimilarItemsProvider<Audio>
+{
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AudioSimilarItemsProvider"/> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ public AudioSimilarItemsProvider(ILibraryManager libraryManager)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ /// <inheritdoc/>
+ public string Name => "Local Genre/Tag";
+
+ /// <inheritdoc/>
+ public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
+
+ /// <inheritdoc/>
+ public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(Audio item, SimilarItemsQuery query, CancellationToken cancellationToken)
+ {
+ var internalQuery = new InternalItemsQuery(query.User)
+ {
+ Genres = item.Genres,
+ Tags = item.Tags,
+ Limit = query.Limit,
+ DtoOptions = query.DtoOptions ?? new DtoOptions(),
+ ExcludeItemIds = [.. query.ExcludeItemIds],
+ ExcludeArtistIds = [.. query.ExcludeArtistIds],
+ IncludeItemTypes = [BaseItemKind.Audio],
+ EnableGroupByMetadataKey = false,
+ EnableTotalRecordCount = true,
+ OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
+ };
+
+ return Task.FromResult(_libraryManager.GetItemList(internalQuery));
+ }
+}
diff --git a/Emby.Server.Implementations/Library/SimilarItems/LiveTvProgramSimilarItemsProvider.cs b/Emby.Server.Implementations/Library/SimilarItems/LiveTvProgramSimilarItemsProvider.cs
new file mode 100644
index 0000000000..7665ee2f79
--- /dev/null
+++ b/Emby.Server.Implementations/Library/SimilarItems/LiveTvProgramSimilarItemsProvider.cs
@@ -0,0 +1,94 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.Library.SimilarItems;
+
+/// <summary>
+/// Provides similar items for Live TV programs.
+/// </summary>
+public class LiveTvProgramSimilarItemsProvider : ILocalSimilarItemsProvider<LiveTvProgram>
+{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LiveTvProgramSimilarItemsProvider"/> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="serverConfigurationManager">The server configuration manager.</param>
+ public LiveTvProgramSimilarItemsProvider(
+ ILibraryManager libraryManager,
+ IServerConfigurationManager serverConfigurationManager)
+ {
+ _libraryManager = libraryManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ /// <inheritdoc/>
+ public string Name => "Local Genre/Tag";
+
+ /// <inheritdoc/>
+ public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
+
+ /// <inheritdoc/>
+ public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(LiveTvProgram item, SimilarItemsQuery query, CancellationToken cancellationToken)
+ {
+ BaseItemKind[] includeItemTypes;
+ bool enableGroupByMetadataKey;
+ bool enableTotalRecordCount;
+
+ if (item.IsMovie)
+ {
+ // Movie-like program
+ var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
+
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ {
+ itemTypes.Add(BaseItemKind.Trailer);
+ itemTypes.Add(BaseItemKind.LiveTvProgram);
+ }
+
+ includeItemTypes = [.. itemTypes];
+ enableGroupByMetadataKey = true;
+ enableTotalRecordCount = false;
+ }
+ else if (item.IsSeries)
+ {
+ // Series-like program
+ includeItemTypes = [BaseItemKind.Series];
+ enableGroupByMetadataKey = false;
+ enableTotalRecordCount = true;
+ }
+ else
+ {
+ // Default - match same type
+ includeItemTypes = [item.GetBaseItemKind()];
+ enableGroupByMetadataKey = false;
+ enableTotalRecordCount = true;
+ }
+
+ var internalQuery = new InternalItemsQuery(query.User)
+ {
+ Genres = item.Genres,
+ Tags = item.Tags,
+ Limit = query.Limit,
+ DtoOptions = query.DtoOptions ?? new DtoOptions(),
+ ExcludeItemIds = [.. query.ExcludeItemIds],
+ IncludeItemTypes = includeItemTypes,
+ EnableGroupByMetadataKey = enableGroupByMetadataKey,
+ EnableTotalRecordCount = enableTotalRecordCount,
+ OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
+ };
+
+ return Task.FromResult(_libraryManager.GetItemList(internalQuery));
+ }
+}
diff --git a/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs b/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs
new file mode 100644
index 0000000000..b4ed12a20c
--- /dev/null
+++ b/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs
@@ -0,0 +1,333 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Extensions;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Configuration;
+using Microsoft.EntityFrameworkCore;
+using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
+
+namespace Emby.Server.Implementations.Library.SimilarItems;
+
+/// <summary>
+/// Provides similar items for movies and trailers using weighted scoring.
+/// </summary>
+public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie>, ILocalSimilarItemsProvider<Trailer>, IBatchLocalSimilarItemsProvider
+{
+ private const int GenreWeight = 10;
+ private const int TagWeight = 5;
+ private const int StudioWeight = 5;
+ private const int DirectorWeight = 50;
+ private const int ActorWeight = 15;
+
+ // Caps the batch fan-out so downstream IN-list sizes (per-source scores, accessible-id
+ // load, navigation includes) stay bounded regardless of caller input.
+ private const int MaxBatchSourceItems = 64;
+
+ private static readonly (ItemValueType Type, int Weight)[] _itemValueDimensions =
+ [
+ (ItemValueType.Genre, GenreWeight),
+ (ItemValueType.Tags, TagWeight),
+ (ItemValueType.Studios, StudioWeight)
+ ];
+
+ private static readonly Dictionary<string, int> _personTypeWeights = new(StringComparer.Ordinal)
+ {
+ [nameof(PersonKind.Director)] = DirectorWeight,
+ [nameof(PersonKind.Actor)] = ActorWeight,
+ [nameof(PersonKind.GuestStar)] = ActorWeight,
+ };
+
+ private static readonly string[] _scoredPersonTypes = [.. _personTypeWeights.Keys];
+
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+ private readonly IItemQueryHelpers _queryHelpers;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MovieSimilarItemsProvider"/> class.
+ /// </summary>
+ /// <param name="dbProvider">The database context factory.</param>
+ /// <param name="queryHelpers">The shared query helpers.</param>
+ /// <param name="serverConfigurationManager">The server configuration manager.</param>
+ public MovieSimilarItemsProvider(
+ IDbContextFactory<JellyfinDbContext> dbProvider,
+ IItemQueryHelpers queryHelpers,
+ IServerConfigurationManager serverConfigurationManager)
+ {
+ _dbProvider = dbProvider;
+ _queryHelpers = queryHelpers;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ /// <inheritdoc/>
+ public string Name => "Local Genre/Tag";
+
+ /// <inheritdoc/>
+ public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
+
+ /// <inheritdoc/>
+ public async Task<IReadOnlyList<BaseItemDto>> GetSimilarItemsAsync(Movie item, SimilarItemsQuery query, CancellationToken cancellationToken)
+ {
+ var results = await GetBatchSimilarItemsAsync([item], query, cancellationToken).ConfigureAwait(false);
+ return results.TryGetValue(item.Id, out var items) ? items : [];
+ }
+
+ /// <inheritdoc/>
+ public async Task<IReadOnlyList<BaseItemDto>> GetSimilarItemsAsync(Trailer item, SimilarItemsQuery query, CancellationToken cancellationToken)
+ {
+ var results = await GetBatchSimilarItemsAsync([item], query, cancellationToken).ConfigureAwait(false);
+ return results.TryGetValue(item.Id, out var items) ? items : [];
+ }
+
+ bool ILocalSimilarItemsProvider.Supports(Type itemType)
+ => typeof(Movie).IsAssignableFrom(itemType) || typeof(Trailer).IsAssignableFrom(itemType);
+
+ Task<IReadOnlyList<BaseItem>> ILocalSimilarItemsProvider.GetSimilarItemsAsync(BaseItem item, SimilarItemsQuery query, CancellationToken cancellationToken)
+ => item switch
+ {
+ Movie movie => GetSimilarItemsAsync(movie, query, cancellationToken),
+ Trailer trailer => GetSimilarItemsAsync(trailer, query, cancellationToken),
+ _ => throw new ArgumentException($"Unsupported item type {item.GetType()}", nameof(item))
+ };
+
+ /// <inheritdoc/>
+ public async Task<Dictionary<Guid, IReadOnlyList<BaseItemDto>>> GetBatchSimilarItemsAsync(
+ IReadOnlyList<BaseItemDto> sourceItems,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken)
+ {
+ var includeItemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ {
+ includeItemTypes.Add(BaseItemKind.Trailer);
+ includeItemTypes.Add(BaseItemKind.LiveTvProgram);
+ }
+
+ var limit = query.Limit ?? 50;
+ var dtoOptions = query.DtoOptions ?? new DtoOptions();
+
+ if (sourceItems.Count > MaxBatchSourceItems)
+ {
+ sourceItems = sourceItems.Take(MaxBatchSourceItems).ToList();
+ }
+
+ var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (context.ConfigureAwait(false))
+ {
+ // Phase 1: Score all candidates per source item
+ var sourceIds = sourceItems.Select(i => i.Id).ToList();
+ var perSourceScores = await ComputeBatchScoresAsync(sourceIds, context, cancellationToken).ConfigureAwait(false);
+
+ var allCandidateIds = new HashSet<Guid>();
+ foreach (var (_, scores) in perSourceScores)
+ {
+ allCandidateIds.UnionWith(
+ scores.OrderByDescending(kvp => kvp.Value)
+ .Take(limit * 3)
+ .Select(kvp => kvp.Key));
+ }
+
+ var result = new Dictionary<Guid, IReadOnlyList<BaseItemDto>>();
+ if (allCandidateIds.Count == 0)
+ {
+ return result;
+ }
+
+ // Phase 2: One access filter for all candidates
+ var filter = new InternalItemsQuery(query.User)
+ {
+ IncludeItemTypes = [.. includeItemTypes],
+ ExcludeItemIds = [.. query.ExcludeItemIds],
+ DtoOptions = dtoOptions,
+ EnableGroupByMetadataKey = true,
+ EnableTotalRecordCount = false,
+ IsMovie = true,
+ IsPlayed = false
+ };
+
+ _queryHelpers.PrepareFilterQuery(filter);
+ var baseQuery = _queryHelpers.PrepareItemQuery(context, filter);
+ baseQuery = _queryHelpers.TranslateQuery(baseQuery, context, filter);
+
+ var allCandidateIdsList = allCandidateIds.ToList();
+ var accessibleItems = await baseQuery
+ .WhereOneOrMany(allCandidateIdsList, e => e.Id)
+ .Select(e => new { e.Id, e.PresentationUniqueKey })
+ .ToListAsync(cancellationToken).ConfigureAwait(false);
+
+ // Phase 3: Pick top IDs per source, dedup by PresentationUniqueKey
+ var allOrderedIds = new HashSet<Guid>();
+ var perSourceOrderedIds = new Dictionary<Guid, List<Guid>>();
+
+ foreach (var item in sourceItems)
+ {
+ if (!perSourceScores.TryGetValue(item.Id, out var scores))
+ {
+ continue;
+ }
+
+ var orderedIds = accessibleItems
+ .Where(x => scores.ContainsKey(x.Id))
+ .OrderByDescending(x => scores.GetValueOrDefault(x.Id))
+ .DistinctBy(x => x.PresentationUniqueKey)
+ .Take(limit)
+ .Select(x => x.Id)
+ .ToList();
+
+ if (orderedIds.Count > 0)
+ {
+ perSourceOrderedIds[item.Id] = orderedIds;
+ allOrderedIds.UnionWith(orderedIds);
+ }
+ }
+
+ if (allOrderedIds.Count == 0)
+ {
+ return result;
+ }
+
+ // Phase 4: One entity load for all results
+ var allOrderedIdsList = allOrderedIds.ToList();
+ var entities = await _queryHelpers.ApplyNavigations(
+ context.BaseItems.AsNoTracking().WhereOneOrMany(allOrderedIdsList, e => e.Id),
+ filter)
+ .AsSplitQuery()
+ .ToListAsync(cancellationToken).ConfigureAwait(false);
+
+ var entitiesById = entities
+ .Select(e => _queryHelpers.DeserializeBaseItem(e, filter.SkipDeserialization))
+ .Where(dto => dto is not null)
+ .ToDictionary(i => i!.Id);
+
+ // Phase 5: Split by source, preserving score order
+ foreach (var (sourceId, orderedIds) in perSourceOrderedIds)
+ {
+ var items = orderedIds
+ .Where(entitiesById.ContainsKey)
+ .Select(id => entitiesById[id]!)
+ .ToList();
+
+ if (items.Count > 0)
+ {
+ result[sourceId] = items;
+ }
+ }
+
+ return result;
+ }
+ }
+
+ private static async Task<Dictionary<Guid, Dictionary<Guid, int>>> ComputeBatchScoresAsync(List<Guid> sourceIds, JellyfinDbContext context, CancellationToken cancellationToken)
+ {
+ var result = new Dictionary<Guid, Dictionary<Guid, int>>();
+ foreach (var id in sourceIds)
+ {
+ result[id] = [];
+ }
+
+ foreach (var (valueType, weight) in _itemValueDimensions)
+ {
+ var sourceRows = await context.ItemValuesMap.AsNoTracking()
+ .Where(m => sourceIds.Contains(m.ItemId) && m.ItemValue.Type == valueType)
+ .Select(m => new { m.ItemId, Key = m.ItemValue.CleanValue })
+ .ToListAsync(cancellationToken).ConfigureAwait(false);
+
+ var sourceMap = sourceRows.GroupBy(r => r.ItemId).ToDictionary(g => g.Key, g => g.Select(x => x.Key).ToHashSet());
+ var allKeys = sourceMap.Values.SelectMany(v => v).Distinct().ToList();
+ if (allKeys.Count == 0)
+ {
+ continue;
+ }
+
+ var candidateRows = await context.ItemValuesMap.AsNoTracking()
+ .Where(m => m.ItemValue.Type == valueType && allKeys.Contains(m.ItemValue.CleanValue))
+ .Select(m => new { m.ItemId, Key = m.ItemValue.CleanValue })
+ .ToListAsync(cancellationToken).ConfigureAwait(false);
+
+ var keyToCandidates = candidateRows.GroupBy(r => r.Key).ToDictionary(g => g.Key, g => g.Select(x => x.ItemId).ToList());
+ ApplyDimensionScores(sourceIds, sourceMap, keyToCandidates, weight, result);
+ }
+
+ var personSourceRows = await context.PeopleBaseItemMap.AsNoTracking()
+ .Where(m => sourceIds.Contains(m.ItemId) && _scoredPersonTypes.Contains(m.People.PersonType))
+ .Select(m => new { m.ItemId, m.PeopleId, m.People.PersonType })
+ .ToListAsync(cancellationToken).ConfigureAwait(false);
+
+ if (personSourceRows.Count > 0)
+ {
+ var personCandidateRows = await context.PeopleBaseItemMap.AsNoTracking()
+ .Where(m => context.PeopleBaseItemMap
+ .Where(s => sourceIds.Contains(s.ItemId) && _scoredPersonTypes.Contains(s.People.PersonType))
+ .Select(s => s.PeopleId)
+ .Contains(m.PeopleId))
+ .Select(m => new { m.ItemId, m.PeopleId })
+ .ToListAsync(cancellationToken).ConfigureAwait(false);
+
+ var personToCandidates = personCandidateRows
+ .GroupBy(r => r.PeopleId)
+ .ToDictionary(g => g.Key, g => g.Select(x => x.ItemId).ToList());
+
+ foreach (var weightGroup in personSourceRows.GroupBy(r => _personTypeWeights[r.PersonType!]))
+ {
+ var sourceMap = weightGroup
+ .GroupBy(r => r.ItemId)
+ .ToDictionary(g => g.Key, g => g.Select(x => x.PeopleId).ToHashSet());
+ ApplyDimensionScores(sourceIds, sourceMap, personToCandidates, weightGroup.Key, result);
+ }
+ }
+
+ foreach (var sourceId in sourceIds)
+ {
+ var scoreMap = result[sourceId];
+ scoreMap.Remove(sourceId);
+ if (scoreMap.Count == 0)
+ {
+ result.Remove(sourceId);
+ }
+ }
+
+ return result;
+ }
+
+ private static void ApplyDimensionScores<TKey>(
+ List<Guid> sourceIds,
+ Dictionary<Guid, HashSet<TKey>> sourceMap,
+ Dictionary<TKey, List<Guid>> keyToCandidates,
+ int weight,
+ Dictionary<Guid, Dictionary<Guid, int>> result)
+ where TKey : notnull
+ {
+ foreach (var sourceId in sourceIds)
+ {
+ if (!sourceMap.TryGetValue(sourceId, out var sourceKeys))
+ {
+ continue;
+ }
+
+ var scoreMap = result[sourceId];
+ foreach (var key in sourceKeys)
+ {
+ if (!keyToCandidates.TryGetValue(key, out var candidates))
+ {
+ continue;
+ }
+
+ foreach (var candidateId in candidates)
+ {
+ scoreMap[candidateId] = scoreMap.GetValueOrDefault(candidateId) + weight;
+ }
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/SimilarItems/MusicAlbumSimilarItemsProvider.cs b/Emby.Server.Implementations/Library/SimilarItems/MusicAlbumSimilarItemsProvider.cs
new file mode 100644
index 0000000000..c13045deda
--- /dev/null
+++ b/Emby.Server.Implementations/Library/SimilarItems/MusicAlbumSimilarItemsProvider.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.Library.SimilarItems;
+
+/// <summary>
+/// Provides similar items for music albums.
+/// </summary>
+public class MusicAlbumSimilarItemsProvider : ILocalSimilarItemsProvider<MusicAlbum>
+{
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MusicAlbumSimilarItemsProvider"/> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ public MusicAlbumSimilarItemsProvider(ILibraryManager libraryManager)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ /// <inheritdoc/>
+ public string Name => "Local Genre/Tag";
+
+ /// <inheritdoc/>
+ public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
+
+ /// <inheritdoc/>
+ public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(MusicAlbum item, SimilarItemsQuery query, CancellationToken cancellationToken)
+ {
+ var internalQuery = new InternalItemsQuery(query.User)
+ {
+ Genres = item.Genres,
+ Tags = item.Tags,
+ Limit = query.Limit,
+ DtoOptions = query.DtoOptions ?? new DtoOptions(),
+ ExcludeItemIds = [.. query.ExcludeItemIds],
+ ExcludeArtistIds = [.. query.ExcludeArtistIds],
+ IncludeItemTypes = [BaseItemKind.MusicAlbum],
+ EnableGroupByMetadataKey = false,
+ EnableTotalRecordCount = true,
+ OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
+ };
+
+ return Task.FromResult(_libraryManager.GetItemList(internalQuery));
+ }
+}
diff --git a/Emby.Server.Implementations/Library/SimilarItems/MusicArtistSimilarItemsProvider.cs b/Emby.Server.Implementations/Library/SimilarItems/MusicArtistSimilarItemsProvider.cs
new file mode 100644
index 0000000000..3331419442
--- /dev/null
+++ b/Emby.Server.Implementations/Library/SimilarItems/MusicArtistSimilarItemsProvider.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.Library.SimilarItems;
+
+/// <summary>
+/// Provides similar items for music artists.
+/// </summary>
+public class MusicArtistSimilarItemsProvider : ILocalSimilarItemsProvider<MusicArtist>
+{
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MusicArtistSimilarItemsProvider"/> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ public MusicArtistSimilarItemsProvider(ILibraryManager libraryManager)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ /// <inheritdoc/>
+ public string Name => "Local Genre/Tag";
+
+ /// <inheritdoc/>
+ public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
+
+ /// <inheritdoc/>
+ public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(MusicArtist item, SimilarItemsQuery query, CancellationToken cancellationToken)
+ {
+ var internalQuery = new InternalItemsQuery(query.User)
+ {
+ Genres = item.Genres,
+ Tags = item.Tags,
+ Limit = query.Limit,
+ DtoOptions = query.DtoOptions ?? new DtoOptions(),
+ ExcludeItemIds = [.. query.ExcludeItemIds],
+ ExcludeArtistIds = [.. query.ExcludeArtistIds],
+ IncludeItemTypes = [BaseItemKind.MusicArtist],
+ EnableGroupByMetadataKey = false,
+ EnableTotalRecordCount = true,
+ OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
+ };
+
+ return Task.FromResult(_libraryManager.GetItemList(internalQuery));
+ }
+}
diff --git a/Emby.Server.Implementations/Library/SimilarItems/SeriesSimilarItemsProvider.cs b/Emby.Server.Implementations/Library/SimilarItems/SeriesSimilarItemsProvider.cs
new file mode 100644
index 0000000000..0366fb752e
--- /dev/null
+++ b/Emby.Server.Implementations/Library/SimilarItems/SeriesSimilarItemsProvider.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+
+namespace Emby.Server.Implementations.Library.SimilarItems;
+
+/// <summary>
+/// Provides similar items for TV series.
+/// </summary>
+public class SeriesSimilarItemsProvider : ILocalSimilarItemsProvider<Series>
+{
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SeriesSimilarItemsProvider"/> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ public SeriesSimilarItemsProvider(ILibraryManager libraryManager)
+ {
+ _libraryManager = libraryManager;
+ }
+
+ /// <inheritdoc/>
+ public string Name => "Local Genre/Tag";
+
+ /// <inheritdoc/>
+ public MetadataPluginType Type => MetadataPluginType.LocalSimilarityProvider;
+
+ /// <inheritdoc/>
+ public Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(Series item, SimilarItemsQuery query, CancellationToken cancellationToken)
+ {
+ var internalQuery = new InternalItemsQuery(query.User)
+ {
+ Genres = item.Genres,
+ Tags = item.Tags,
+ Limit = query.Limit,
+ DtoOptions = query.DtoOptions ?? new DtoOptions(),
+ ExcludeItemIds = [.. query.ExcludeItemIds],
+ IncludeItemTypes = [BaseItemKind.Series],
+ EnableGroupByMetadataKey = false,
+ EnableTotalRecordCount = true,
+ OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
+ };
+
+ return Task.FromResult(_libraryManager.GetItemList(internalQuery));
+ }
+}
diff --git a/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs b/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs
new file mode 100644
index 0000000000..d923cff07e
--- /dev/null
+++ b/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs
@@ -0,0 +1,638 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Querying;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.Library.SimilarItems;
+
+/// <summary>
+/// Manages similar items providers and orchestrates similar items operations.
+/// </summary>
+public class SimilarItemsManager : ISimilarItemsManager
+{
+ private readonly ILogger<SimilarItemsManager> _logger;
+ private readonly IServerApplicationPaths _appPaths;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+ private ISimilarItemsProvider[] _similarItemsProviders = [];
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SimilarItemsManager"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="appPaths">The server application paths.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="fileSystem">The file system.</param>
+ /// <param name="serverConfigurationManager">The server configuration manager.</param>
+ public SimilarItemsManager(
+ ILogger<SimilarItemsManager> logger,
+ IServerApplicationPaths appPaths,
+ ILibraryManager libraryManager,
+ IFileSystem fileSystem,
+ IServerConfigurationManager serverConfigurationManager)
+ {
+ _logger = logger;
+ _appPaths = appPaths;
+ _libraryManager = libraryManager;
+ _fileSystem = fileSystem;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ /// <inheritdoc/>
+ public void AddParts(IEnumerable<ISimilarItemsProvider> providers)
+ {
+ _similarItemsProviders = providers.ToArray();
+ }
+
+ /// <inheritdoc/>
+ public IReadOnlyList<ISimilarItemsProvider> GetSimilarItemsProviders<T>()
+ where T : BaseItem
+ {
+ var itemType = typeof(T);
+ return _similarItemsProviders
+ .Where(p => (p is ILocalSimilarItemsProvider local && local.Supports(itemType))
+ || (p is IRemoteSimilarItemsProvider remote && remote.Supports(itemType)))
+ .ToList();
+ }
+
+ /// <inheritdoc/>
+ public async Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(
+ BaseItem item,
+ IReadOnlyList<Guid> excludeArtistIds,
+ User? user,
+ DtoOptions dtoOptions,
+ int? limit,
+ LibraryOptions? libraryOptions,
+ CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(item);
+ ArgumentNullException.ThrowIfNull(excludeArtistIds);
+
+ var itemType = item.GetType();
+ var requestedLimit = limit ?? 50;
+ var itemKind = item.GetBaseItemKind();
+
+ // Ensure ProviderIds is included in DtoOptions for matching remote provider responses
+ if (!dtoOptions.Fields.Contains(ItemFields.ProviderIds))
+ {
+ dtoOptions.Fields = dtoOptions.Fields.Concat([ItemFields.ProviderIds]).ToArray();
+ }
+
+ // Local providers are always enabled. Remote providers must be explicitly enabled.
+ var localProviders = _similarItemsProviders
+ .OfType<ILocalSimilarItemsProvider>()
+ .Where(p => p.Supports(itemType))
+ .ToList();
+ var remoteProviders = _similarItemsProviders
+ .OfType<IRemoteSimilarItemsProvider>()
+ .Where(p => p.Supports(itemType));
+ var matchingProviders = new List<ISimilarItemsProvider>(localProviders);
+
+ var typeOptions = libraryOptions?.GetTypeOptions(itemType.Name);
+ if (typeOptions?.SimilarItemProviders?.Length > 0)
+ {
+ matchingProviders.AddRange(remoteProviders
+ .Where(p => typeOptions.SimilarItemProviders.Contains(p.Name, StringComparer.OrdinalIgnoreCase)));
+ }
+
+ var orderConfig = typeOptions?.SimilarItemProviderOrder is { Length: > 0 } order
+ ? order
+ : typeOptions?.SimilarItemProviders;
+ var orderedProviders = matchingProviders
+ .OrderBy(p => GetConfiguredSimilarProviderOrder(orderConfig, p.Name))
+ .ToList();
+
+ var allResults = new List<(BaseItem Item, float Score)>();
+ var excludeIds = new HashSet<Guid> { item.Id };
+ var excludeKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { item.GetPresentationUniqueKey() };
+ foreach (var (providerOrder, provider) in orderedProviders.Index())
+ {
+ if (allResults.Count >= requestedLimit || cancellationToken.IsCancellationRequested)
+ {
+ break;
+ }
+
+ try
+ {
+ if (provider is ILocalSimilarItemsProvider localProvider)
+ {
+ var query = new SimilarItemsQuery
+ {
+ User = user,
+ Limit = requestedLimit - allResults.Count,
+ DtoOptions = dtoOptions,
+ ExcludeItemIds = [.. excludeIds],
+ ExcludeArtistIds = excludeArtistIds
+ };
+
+ var items = await localProvider.GetSimilarItemsAsync(item, query, cancellationToken).ConfigureAwait(false);
+
+ foreach (var (position, resultItem) in items.Index())
+ {
+ var isNewId = excludeIds.Add(resultItem.Id);
+ var isNewKey = excludeKeys.Add(resultItem.GetPresentationUniqueKey());
+ if (isNewId && isNewKey)
+ {
+ var score = CalculateScore(null, providerOrder, position);
+ allResults.Add((resultItem, score));
+ }
+ }
+ }
+ else if (provider is IRemoteSimilarItemsProvider remoteProvider)
+ {
+ var cachePath = GetSimilarItemsCachePath(provider.Name, itemType.Name, item.Id);
+
+ var cachedReferences = await TryReadSimilarItemsCacheAsync(cachePath, cancellationToken).ConfigureAwait(false);
+ if (cachedReferences is not null)
+ {
+ var resolvedItems = ResolveRemoteReferences(cachedReferences, providerOrder, user, dtoOptions, itemKind, excludeIds, excludeKeys);
+ allResults.AddRange(resolvedItems);
+ continue;
+ }
+
+ var query = new SimilarItemsQuery
+ {
+ User = user,
+ Limit = requestedLimit - allResults.Count,
+ DtoOptions = dtoOptions,
+ ExcludeItemIds = [.. excludeIds],
+ ExcludeArtistIds = excludeArtistIds
+ };
+
+ // Collect references in batches and resolve against local library.
+ // Stop fetching once we have enough resolved local items.
+ const int BatchSize = 20;
+ var remaining = requestedLimit - allResults.Count;
+ var collectedReferences = new List<SimilarItemReference>();
+ var pendingBatch = new List<SimilarItemReference>();
+
+ await foreach (var reference in remoteProvider.GetSimilarItemsAsync(item, query, cancellationToken).ConfigureAwait(false))
+ {
+ collectedReferences.Add(reference);
+ pendingBatch.Add(reference);
+
+ if (pendingBatch.Count >= BatchSize)
+ {
+ var resolvedItems = ResolveRemoteReferences(pendingBatch, providerOrder, user, dtoOptions, itemKind, excludeIds, excludeKeys);
+ allResults.AddRange(resolvedItems);
+ remaining -= resolvedItems.Count;
+ pendingBatch.Clear();
+
+ if (remaining <= 0)
+ {
+ break;
+ }
+ }
+ }
+
+ // Resolve any remaining references in the last partial batch
+ if (pendingBatch.Count > 0)
+ {
+ var resolvedItems = ResolveRemoteReferences(pendingBatch, providerOrder, user, dtoOptions, itemKind, excludeIds, excludeKeys);
+ allResults.AddRange(resolvedItems);
+ }
+
+ if (collectedReferences.Count > 0 && provider.CacheDuration is not null)
+ {
+ await SaveSimilarItemsCacheAsync(cachePath, collectedReferences, provider.CacheDuration.Value, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Similar items provider {ProviderName} failed for item {ItemId}", provider.Name, item.Id);
+ }
+ }
+
+ return allResults
+ .OrderByDescending(x => x.Score)
+ .Select(x => x.Item)
+ .Take(requestedLimit)
+ .ToList();
+ }
+
+ /// <inheritdoc/>
+ public async Task<IReadOnlyList<SimilarItemsRecommendation>> GetMovieRecommendationsAsync(
+ User? user,
+ Guid parentId,
+ int categoryLimit,
+ int itemLimit,
+ DtoOptions dtoOptions,
+ CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(dtoOptions);
+
+ var recentlyPlayedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = [BaseItemKind.Movie],
+ OrderBy = [(ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending)],
+ Limit = 7,
+ ParentId = parentId,
+ Recursive = true,
+ IsPlayed = true,
+ EnableGroupByMetadataKey = true,
+ DtoOptions = dtoOptions
+ });
+
+ var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
+ if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
+ {
+ itemTypes.Add(BaseItemKind.Trailer);
+ itemTypes.Add(BaseItemKind.LiveTvProgram);
+ }
+
+ var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = itemTypes.ToArray(),
+ IsMovie = true,
+ OrderBy = [(ItemSortBy.Random, SortOrder.Descending)],
+ Limit = 10,
+ IsFavoriteOrLiked = true,
+ ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(),
+ EnableGroupByMetadataKey = true,
+ ParentId = parentId,
+ Recursive = true,
+ DtoOptions = dtoOptions
+ });
+
+ var mostRecentMovies = recentlyPlayedMovies.Take(Math.Min(recentlyPlayedMovies.Count, 6)).ToList();
+ var recentDirectors = GetPeopleNames(mostRecentMovies, [PersonType.Director]);
+ var recentActors = GetPeopleNames(mostRecentMovies, [PersonType.Actor, PersonType.GuestStar]);
+
+ // Cap baseline items to categoryLimit - the round-robin can't use more categories than that.
+ var recentlyPlayedBaseline = recentlyPlayedMovies.Count > categoryLimit
+ ? recentlyPlayedMovies.Take(categoryLimit).ToList()
+ : recentlyPlayedMovies;
+ var likedBaseline = likedMovies.Count > categoryLimit
+ ? likedMovies.Take(categoryLimit).ToList()
+ : likedMovies;
+
+ var batchQuery = new SimilarItemsQuery
+ {
+ User = user,
+ Limit = itemLimit,
+ DtoOptions = dtoOptions
+ };
+
+ var similarToRecentlyPlayed = await GetSimilarItemsRecommendationsAsync(
+ recentlyPlayedBaseline,
+ RecommendationType.SimilarToRecentlyPlayed,
+ batchQuery,
+ cancellationToken).ConfigureAwait(false);
+
+ var similarToLiked = await GetSimilarItemsRecommendationsAsync(
+ likedBaseline,
+ RecommendationType.SimilarToLikedItem,
+ batchQuery,
+ cancellationToken).ConfigureAwait(false);
+
+ var hasDirectorFromRecentlyPlayed = GetPersonRecommendations(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed, itemTypes);
+ var hasActorFromRecentlyPlayed = GetPersonRecommendations(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed, itemTypes);
+
+ // Use a single enumerator per list, listed twice so MoveNext advances it
+ // twice per round-robin pass (giving these categories double weight).
+ // IMPORTANT: Declare as IEnumerator<T> to box the List<T>.Enumerator struct once;
+ // using var would box separately per list insertion, creating independent copies.
+ IEnumerator<SimilarItemsRecommendation> similarToRecentlyPlayedEnum = similarToRecentlyPlayed.GetEnumerator();
+ IEnumerator<SimilarItemsRecommendation> similarToLikedEnum = similarToLiked.GetEnumerator();
+
+ var categoryTypes = new List<IEnumerator<SimilarItemsRecommendation>>
+ {
+ similarToRecentlyPlayedEnum,
+ similarToRecentlyPlayedEnum,
+ similarToLikedEnum,
+ similarToLikedEnum,
+ hasDirectorFromRecentlyPlayed.GetEnumerator(),
+ hasActorFromRecentlyPlayed.GetEnumerator()
+ };
+
+ var categories = new List<SimilarItemsRecommendation>();
+ while (categories.Count < categoryLimit)
+ {
+ var allEmpty = true;
+ foreach (var category in categoryTypes)
+ {
+ if (category.MoveNext())
+ {
+ categories.Add(category.Current);
+ allEmpty = false;
+
+ if (categories.Count >= categoryLimit)
+ {
+ break;
+ }
+ }
+ }
+
+ if (allEmpty)
+ {
+ break;
+ }
+ }
+
+ return [.. categories.OrderBy(i => i.RecommendationType)];
+ }
+
+ private async Task<IReadOnlyList<SimilarItemsRecommendation>> GetSimilarItemsRecommendationsAsync(
+ IReadOnlyList<BaseItem> baselineItems,
+ RecommendationType recommendationType,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken)
+ {
+ var batchProvider = _similarItemsProviders
+ .OfType<IBatchLocalSimilarItemsProvider>()
+ .FirstOrDefault();
+
+ if (batchProvider is null || baselineItems.Count == 0)
+ {
+ return [];
+ }
+
+ var batchResults = await batchProvider.GetBatchSimilarItemsAsync(baselineItems, query, cancellationToken).ConfigureAwait(false);
+
+ var recommendations = new List<SimilarItemsRecommendation>(baselineItems.Count);
+ foreach (var baseline in baselineItems)
+ {
+ if (batchResults.TryGetValue(baseline.Id, out var similar) && similar.Count > 0)
+ {
+ recommendations.Add(new SimilarItemsRecommendation
+ {
+ BaselineItemName = baseline.Name,
+ CategoryId = baseline.Id,
+ RecommendationType = recommendationType,
+ Items = similar
+ });
+ }
+ }
+
+ return recommendations;
+ }
+
+ private IEnumerable<SimilarItemsRecommendation> GetPersonRecommendations(
+ User? user,
+ IReadOnlyList<string> names,
+ int itemLimit,
+ DtoOptions dtoOptions,
+ RecommendationType type,
+ IReadOnlyList<BaseItemKind> itemTypes)
+ {
+ var personTypes = type == RecommendationType.HasDirectorFromRecentlyPlayed
+ ? [PersonType.Director]
+ : Array.Empty<string>();
+
+ foreach (var name in names)
+ {
+ var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ Person = name,
+ Limit = itemLimit + 2,
+ PersonTypes = personTypes,
+ IncludeItemTypes = itemTypes.ToArray(),
+ IsMovie = true,
+ IsPlayed = false,
+ EnableGroupByMetadataKey = true,
+ DtoOptions = dtoOptions
+ })
+ .DistinctBy(i => i.GetProviderId(MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
+ .Take(itemLimit)
+ .ToList();
+
+ if (items.Count > 0)
+ {
+ yield return new SimilarItemsRecommendation
+ {
+ BaselineItemName = name,
+ CategoryId = name.GetMD5(),
+ RecommendationType = type,
+ Items = items
+ };
+ }
+ }
+ }
+
+ private IReadOnlyList<string> GetPeopleNames(IReadOnlyList<BaseItem> items, IReadOnlyList<string> personTypes)
+ {
+ var itemIds = items.Select(i => i.Id).ToArray();
+ return _libraryManager.GetPeopleNamesByItems(itemIds, personTypes)
+ .Values
+ .SelectMany(names => names)
+ .Distinct()
+ .ToArray();
+ }
+
+ private List<(BaseItem Item, float Score)> ResolveRemoteReferences(
+ IReadOnlyList<SimilarItemReference> references,
+ int providerOrder,
+ User? user,
+ DtoOptions dtoOptions,
+ BaseItemKind itemKind,
+ HashSet<Guid> excludeIds,
+ HashSet<string> excludeKeys)
+ {
+ if (references.Count == 0)
+ {
+ return [];
+ }
+
+ var resolvedByKey = new Dictionary<string, (BaseItem Item, float Score)>(StringComparer.OrdinalIgnoreCase);
+ var providerLookup = new Dictionary<(string ProviderName, string ProviderId), (float? Score, int Position)>(StringTupleComparer.Instance);
+
+ foreach (var (position, match) in references.Index())
+ {
+ var lookupKey = (match.ProviderName, match.ProviderId);
+ if (!providerLookup.TryGetValue(lookupKey, out var existing))
+ {
+ providerLookup[lookupKey] = (match.Score, position);
+ }
+ else if (match.Score > existing.Score || (match.Score == existing.Score && position < existing.Position))
+ {
+ providerLookup[lookupKey] = (match.Score, position);
+ }
+ }
+
+ var allProviderIds = providerLookup
+ .GroupBy(kvp => kvp.Key.ProviderName)
+ .ToDictionary(g => g.Key, g => g.Select(x => x.Key.ProviderId).ToArray());
+
+ var query = new InternalItemsQuery(user)
+ {
+ HasAnyProviderIds = allProviderIds,
+ IncludeItemTypes = [itemKind],
+ DtoOptions = dtoOptions
+ };
+
+ var items = _libraryManager.GetItemList(query);
+
+ foreach (var item in items)
+ {
+ if (excludeIds.Contains(item.Id))
+ {
+ continue;
+ }
+
+ var presentationKey = item.GetPresentationUniqueKey();
+ if (excludeKeys.Contains(presentationKey))
+ {
+ continue;
+ }
+
+ foreach (var providerName in allProviderIds.Keys)
+ {
+ if (item.TryGetProviderId(providerName, out var itemProviderId) && providerLookup.TryGetValue((providerName, itemProviderId), out var matchInfo))
+ {
+ var score = CalculateScore(matchInfo.Score, providerOrder, matchInfo.Position);
+ if (!resolvedByKey.TryGetValue(presentationKey, out var existing) || existing.Score < score)
+ {
+ resolvedByKey[presentationKey] = (item, score);
+ }
+
+ break;
+ }
+ }
+ }
+
+ foreach (var (key, entry) in resolvedByKey)
+ {
+ excludeIds.Add(entry.Item.Id);
+ excludeKeys.Add(key);
+ }
+
+ return [.. resolvedByKey.Values];
+ }
+
+ private static float CalculateScore(float? matchScore, int providerOrder, int position)
+ {
+ // Use provider-supplied score if available, otherwise derive from position
+ var baseScore = matchScore ?? (1.0f - (position * 0.02f));
+
+ // Apply small boost based on provider order (higher priority providers get small bonus)
+ var priorityBoost = Math.Max(0, 10 - providerOrder) * 0.005f;
+
+ return Math.Clamp(baseScore + priorityBoost, 0f, 1f);
+ }
+
+ private static int GetConfiguredSimilarProviderOrder(string[]? orderConfig, string providerName)
+ {
+ if (orderConfig is null || orderConfig.Length == 0)
+ {
+ return int.MaxValue;
+ }
+
+ var index = Array.FindIndex(orderConfig, name => string.Equals(name, providerName, StringComparison.OrdinalIgnoreCase));
+ return index >= 0 ? index : int.MaxValue;
+ }
+
+ private string GetSimilarItemsCachePath(string providerName, string baseItemType, Guid itemId)
+ {
+ var dataPath = Path.Combine(
+ _appPaths.CachePath,
+ $"{providerName.ToLowerInvariant()}-similar-{baseItemType.ToLowerInvariant()}");
+ return Path.Combine(dataPath, $"{itemId.ToString("N", CultureInfo.InvariantCulture)}.json");
+ }
+
+ private async Task<List<SimilarItemReference>?> TryReadSimilarItemsCacheAsync(string cachePath, CancellationToken cancellationToken)
+ {
+ var fileInfo = _fileSystem.GetFileSystemInfo(cachePath);
+ if (!fileInfo.Exists || fileInfo.Length == 0)
+ {
+ return null;
+ }
+
+ try
+ {
+ var stream = File.OpenRead(cachePath);
+ await using (stream.ConfigureAwait(false))
+ {
+ var cache = await JsonSerializer.DeserializeAsync<SimilarItemsCache>(stream, JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
+ if (cache?.References is not null && DateTime.UtcNow < cache.ExpiresAt)
+ {
+ return cache.References;
+ }
+ }
+ }
+ catch (IOException ex)
+ {
+ _logger.LogWarning(ex, "Failed to read similar items cache from {CachePath}", cachePath);
+ }
+ catch (JsonException ex)
+ {
+ _logger.LogWarning(ex, "Failed to parse similar items cache from {CachePath}", cachePath);
+ }
+
+ return null;
+ }
+
+ private async Task SaveSimilarItemsCacheAsync(string cachePath, List<SimilarItemReference> references, TimeSpan cacheDuration, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var directory = Path.GetDirectoryName(cachePath);
+ if (!string.IsNullOrEmpty(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ var cache = new SimilarItemsCache
+ {
+ References = references,
+ ExpiresAt = DateTime.UtcNow.Add(cacheDuration)
+ };
+
+ var stream = File.Create(cachePath);
+ await using (stream.ConfigureAwait(false))
+ {
+ await JsonSerializer.SerializeAsync(stream, cache, JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ catch (IOException ex)
+ {
+ _logger.LogWarning(ex, "Failed to save similar items cache to {CachePath}", cachePath);
+ }
+ }
+
+ private sealed class SimilarItemsCache
+ {
+ public List<SimilarItemReference>? References { get; set; }
+
+ public DateTime ExpiresAt { get; set; }
+ }
+
+ private sealed class StringTupleComparer : IEqualityComparer<(string Key, string Value)>
+ {
+ public static readonly StringTupleComparer Instance = new();
+
+ public bool Equals((string Key, string Value) x, (string Key, string Value) y)
+ => string.Equals(x.Key, y.Key, StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(x.Value, y.Value, StringComparison.OrdinalIgnoreCase);
+
+ public int GetHashCode((string Key, string Value) obj)
+ => HashCode.Combine(
+ StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Key),
+ StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Value));
+ }
+}
diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
index 71ce3b6012..7c605036cf 100644
--- a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
@@ -80,7 +80,7 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
ImageTypes = [imageType],
Limit = 30,
// TODO max parental rating configurable
- MaxParentalRating = new(10, null),
+ MaxParentalRating = new(13, null),
OrderBy =
[
(ItemSortBy.Random, SortOrder.Ascending)
diff --git a/Emby.Server.Implementations/Localization/Core/ab.json b/Emby.Server.Implementations/Localization/Core/ab.json
index d6d257c5ba..d67f2d67e9 100644
--- a/Emby.Server.Implementations/Localization/Core/ab.json
+++ b/Emby.Server.Implementations/Localization/Core/ab.json
@@ -1,5 +1,3 @@
{
- "Albums": "аальбомқәа",
- "AppDeviceValues": "Апп: {0}, Априбор: {1}",
- "Application": "Апрограмма"
+ "AppDeviceValues": "Апп: {0}, Априбор: {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index 80c1bd0940..308341ad49 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -1,11 +1,8 @@
{
"Artists": "Kunstenare",
- "Channels": "Kanale",
"Folders": "Lêergidse",
"Favorites": "Gunstelinge",
"HeaderFavoriteShows": "Gunsteling Vertonings",
- "ValueSpecialEpisodeName": "Spesiale - {0}",
- "HeaderAlbumArtists": "Album kunstenaars",
"Books": "Boeke",
"HeaderNextUp": "Volgende",
"Movies": "Flieks",
@@ -13,24 +10,13 @@
"HeaderContinueWatching": "Hou aan kyk",
"HeaderFavoriteEpisodes": "Gunsteling Episodes",
"Photos": "Foto's",
- "Playlists": "Snitlyste",
- "HeaderFavoriteArtists": "Gunsteling Kunstenaars",
- "HeaderFavoriteAlbums": "Gunsteling Albums",
- "Sync": "Sinkroniseer",
- "HeaderFavoriteSongs": "Gunsteling Liedjies",
- "Songs": "Liedjies",
- "DeviceOnlineWithName": "{0} is aanlyn",
- "DeviceOfflineWithName": "{0} is ontkoppel",
"Collections": "Versamelings",
"Inherit": "Ontvang",
"HeaderLiveTV": "Lewendige TV",
- "Application": "Program",
"AppDeviceValues": "App: {0}, Toestel: {1}",
"VersionNumber": "Weergawe {0}",
- "ValueHasBeenAddedToLibrary": "{0} is by jou media biblioteek bygevoeg",
"UserStoppedPlayingItemWithValues": "{0} het klaar {1} op {2} gespeel",
"UserStartedPlayingItemWithValues": "{0} is besig om {1} op {2} te speel",
- "UserPolicyUpdatedWithName": "Gebruiker beleid is verander vir {0}",
"UserPasswordChangedWithName": "Gebruiker {0} se wagwoord is verander",
"UserOnlineFromDevice": "{0} is aanlyn van {1}",
"UserOfflineFromDevice": "{0} is ontkoppel van {1}",
@@ -38,19 +24,13 @@
"UserDownloadingItemWithValues": "{0} is besig om {1} af te laai",
"UserDeletedWithName": "Gebruiker {0} is verwyder",
"UserCreatedWithName": "Gebruiker {0} is geskep",
- "User": "Gebruiker",
"TvShows": "TV Programme",
- "System": "Stelsel",
"SubtitleDownloadFailureFromForItem": "Ondertitels het misluk om af te laai van {0} vir {1}",
"StartupEmbyServerIsLoading": "Jellyfin Bediener is besig om te laai. Probeer weer in 'n kort tyd.",
- "ServerNameNeedsToBeRestarted": "{0} moet herbegin word",
- "ScheduledTaskStartedWithName": "{0} het begin",
"ScheduledTaskFailedWithName": "{0} het misluk",
- "ProviderValue": "Voorsiener: {0}",
"PluginUpdatedWithName": "{0} was opgedateer",
"PluginUninstalledWithName": "{0} was verwyder",
"PluginInstalledWithName": "{0} is geïnstalleer",
- "Plugin": "Inprop module",
"NotificationOptionVideoPlaybackStopped": "Video terugspeel het gestop",
"NotificationOptionVideoPlayback": "Video terugspeel het begin",
"NotificationOptionUserLockedOut": "Gebruiker uitgeslyt",
@@ -74,23 +54,14 @@
"MusicVideos": "Musiek Videos",
"Music": "Musiek",
"MixedContent": "Gemengde inhoud",
- "MessageServerConfigurationUpdated": "Bediener konfigurasie is opgedateer",
- "MessageNamedServerConfigurationUpdatedWithValue": "Bediener konfigurasie seksie {0} is opgedateer",
- "MessageApplicationUpdatedTo": "Jellyfin Bediener is opgedateer na {0}",
- "MessageApplicationUpdated": "Jellyfin Bediener is opgedateer",
"Latest": "Nuutste",
"LabelRunningTimeValue": "Werktyd: {0}",
"LabelIpAddressValue": "IP adres: {0}",
- "ItemRemovedWithName": "{0} is uit versameling verwyder",
- "ItemAddedWithName": "{0} is by die versameling gevoeg",
"HomeVideos": "Tuis Videos",
- "HeaderRecordingGroups": "Groep Opnames",
"Genres": "Genres",
"FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}",
"ChapterNameValue": "Hoofstuk {0}",
- "CameraImageUploadedFrom": "'n Nuwe kamera foto is opgelaai vanaf {0}",
"AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
- "Albums": "Albums",
"TasksChannelsCategory": "Internet kanale",
"TasksApplicationCategory": "aansoek",
"TasksLibraryCategory": "biblioteek",
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index b80737d3b9..17af935562 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -1,41 +1,24 @@
{
- "Albums": "الألبومات",
"AppDeviceValues": "التطبيق: {0}، الجهاز: {1}",
- "Application": "التطبيق",
"Artists": "الفنانون",
"AuthenticationSucceededWithUserName": "تمت مصادقة {0} بنجاح",
"Books": "الكتب",
- "CameraImageUploadedFrom": "تم رفع صورة كاميرا جديدة من {0}",
- "Channels": "القنوات",
"ChapterNameValue": "الفصل {0}",
"Collections": "المجموعات",
- "DeviceOfflineWithName": "انقطع اتصال {0}",
- "DeviceOnlineWithName": "{0} متصل",
"FailedLoginAttemptWithUserName": "محاولة تسجيل دخول فاشلة من {0}",
"Favorites": "المفضلة",
"Folders": "المجلدات",
"Genres": "الأنواع",
- "HeaderAlbumArtists": "فنانو الألبوم",
"HeaderContinueWatching": "متابعة المشاهدة",
- "HeaderFavoriteAlbums": "الألبومات المفضلة",
- "HeaderFavoriteArtists": "الفنانون المفضلون",
"HeaderFavoriteEpisodes": "الحلقات المفضلة",
"HeaderFavoriteShows": "المسلسلات المفضلة",
- "HeaderFavoriteSongs": "الأغاني المفضلة",
"HeaderLiveTV": "البث التلفزيوني المباشر",
"HeaderNextUp": "التالي",
- "HeaderRecordingGroups": "مجموعات التسجيل",
"HomeVideos": "فيديوهات منزلية",
"Inherit": "وراثة",
- "ItemAddedWithName": "تمت إضافة {0} إلى المكتبة",
- "ItemRemovedWithName": "تمت إزالة {0} من المكتبة",
"LabelIpAddressValue": "عنوان IP: {0}",
"LabelRunningTimeValue": "مدة التشغيل: {0}",
"Latest": "الأحدث",
- "MessageApplicationUpdated": "تم تحديث خادم Jellyfin",
- "MessageApplicationUpdatedTo": "تم تحديث خادم Jellyfin إلى {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "تم تحديث قسم إعدادات الخادم {0}",
- "MessageServerConfigurationUpdated": "تم تحديث إعدادات الخادم",
"MixedContent": "محتوى مختلط",
"Movies": "الأفلام",
"Music": "الموسيقى",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "بدأ تشغيل الفيديو",
"NotificationOptionVideoPlaybackStopped": "توقف تشغيل الفيديو",
"Photos": "الصور",
- "Playlists": "قوائم التشغيل",
- "Plugin": "الملحق",
"PluginInstalledWithName": "تم تثبيت {0}",
"PluginUninstalledWithName": "تمت إزالة {0}",
"PluginUpdatedWithName": "تم تحديث {0}",
- "ProviderValue": "المزوّد: {0}",
"ScheduledTaskFailedWithName": "فشلت {0}",
- "ScheduledTaskStartedWithName": "بدأت {0}",
- "ServerNameNeedsToBeRestarted": "يحتاج {0} إلى إعادة التشغيل",
"Shows": "المسلسلات",
- "Songs": "الأغاني",
"StartupEmbyServerIsLoading": "يتم الآن تحميل خادم Jellyfin. يرجى المحاولة مرة أخرى بعد قليل.",
"SubtitleDownloadFailureFromForItem": "فشل تنزيل الترجمات من {0} لـ {1}",
- "Sync": "مزامنة",
- "System": "النظام",
"TvShows": "البرامج التلفزيونية",
- "User": "المستخدم",
"UserCreatedWithName": "تم إنشاء المستخدم {0}",
"UserDeletedWithName": "تم حذف المستخدم {0}",
"UserDownloadingItemWithValues": "{0} يقوم بتنزيل {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "انقطع اتصال {0} من {1}",
"UserOnlineFromDevice": "{0} متصل من {1}",
"UserPasswordChangedWithName": "تم تغيير كلمة المرور للمستخدم {0}",
- "UserPolicyUpdatedWithName": "تم تحديث سياسة المستخدم لـ {0}",
"UserStartedPlayingItemWithValues": "{0} يقوم بتشغيل {1} على {2}",
"UserStoppedPlayingItemWithValues": "أنهى {0} تشغيل {1} على {2}",
- "ValueHasBeenAddedToLibrary": "تمت إضافة {0} إلى مكتبة المحتوى الخاصة بك",
- "ValueSpecialEpisodeName": "خاص - {0}",
"VersionNumber": "الإصدار {0}",
"TaskCleanCacheDescription": "يحذف ملفات ذاكرة التخزين المؤقت التي لم يعد النظام بحاجة إليها.",
"TaskCleanCache": "تنظيف مجلد ذاكرة التخزين المؤقت",
@@ -135,5 +106,7 @@
"TaskMoveTrickplayImages": "نقل موقع صور معاينات التنقل",
"TaskMoveTrickplayImagesDescription": "ينقل ملفات معاينات التنقل الحالية وفقاً لإعدادات المكتبة.",
"CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
- "CleanupUserDataTaskDescription": "ينظف جميع بيانات المستخدم (مثل حالة المشاهدة وحالة المفضلة وغيرها) للمحتوى الذي لم يعد موجوداً لمدة 90 يوماً على الأقل."
+ "CleanupUserDataTaskDescription": "ينظف جميع بيانات المستخدم (مثل حالة المشاهدة وحالة المفضلة وغيرها) للمحتوى الذي لم يعد موجوداً لمدة 90 يوماً على الأقل.",
+ "Original": "فريد",
+ "LyricDownloadFailureFromForItem": "فشل تحميل الكلمات من {0} إلى {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/as.json b/Emby.Server.Implementations/Localization/Core/as.json
index 7c7dd26e92..bc0c2c5ff5 100644
--- a/Emby.Server.Implementations/Localization/Core/as.json
+++ b/Emby.Server.Implementations/Localization/Core/as.json
@@ -1,18 +1,13 @@
{
- "Albums": "এলবাম",
- "Application": "আবেদন",
"AppDeviceValues": "এপ্‌: {0}, ডিভাইচ: {1}",
"Artists": "শিল্পী",
- "Channels": "চেনেলস",
"Default": "ডিফল্ট",
"AuthenticationSucceededWithUserName": "{0} সফলভাবে প্রমাণিত",
"Books": "পুস্তক",
"Movies": "চলচ্চিত্ৰ",
- "CameraImageUploadedFrom": "একটি নতুন ক্যামেরা চিত্র আপলোড করা হয়েছে {0}",
"Collections": "সংগ্রহ",
"HeaderFavoriteShows": "প্রিয় শোসমূহ",
"Latest": "শেহতীয়া",
- "MessageApplicationUpdated": "জেলিফিন চাইভাৰ আপডেট কৰা হৈছে",
"MixedContent": "মিশ্ৰিত সমগ্ৰতা",
"NewVersionIsAvailable": "ডাউনলোড কৰিবলৈ জেলিফিন চাইভাৰৰ এটা নতুন সংস্কৰণ উপলব্ধ আছে.",
"NotificationOptionCameraImageUploaded": "কেমেৰাৰ চিত্ৰ আপল'ড কৰা হ'ল",
@@ -21,20 +16,14 @@
"Folders": "ফোল্ডাৰ",
"Forced": "বলপূর্বক",
"Genres": "শ্রেণী",
- "HeaderAlbumArtists": "অ্যালবাম শিল্পী",
"HeaderContinueWatching": "দেখা চালিয়ে যান",
"FailedLoginAttemptWithUserName": "লগইন ব্যর্থ চেষ্টা কৰা হৈছে থেকে {0}",
- "HeaderFavoriteAlbums": "প্রিয় অ্যালবামসমূহ",
- "HeaderFavoriteArtists": "প্রিয় শিল্পীসমূহ",
"HeaderFavoriteEpisodes": "প্রিয় পর্বসমূহ",
- "HeaderFavoriteSongs": "প্ৰিয় গীত",
"HeaderLiveTV": "প্ৰতিবেদন টিভি",
"HeaderNextUp": "পৰৱৰ্তী অংশ",
- "HeaderRecordingGroups": "অলংকৰণ গোষ্ঠীসমূহ",
"HearingImpaired": "শ্ৰবণ অক্ষম",
"HomeVideos": "ঘৰৰ ভিডিঅ'সমূহ",
"Inherit": "উত্তপ্ত কৰা",
- "MessageServerConfigurationUpdated": "চাইভাৰ কনফিগাৰেশ্যন আপডেট কৰা হৈছে",
"NotificationOptionApplicationUpdateAvailable": "অ্যাপ্লিকেশ্যন আপডেট উপলব্ধ",
"NotificationOptionApplicationUpdateInstalled": "অ্যাপ্লিকেশ্যন আপডেট ইনষ্টল কৰা হ'ল",
"NotificationOptionAudioPlayback": "অডিঅ' প্লেবেক আৰম্ভ হ'ল",
diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json
index 543d227e73..5d0ef65842 100644
--- a/Emby.Server.Implementations/Localization/Core/be.json
+++ b/Emby.Server.Implementations/Localization/Core/be.json
@@ -1,17 +1,10 @@
{
- "Sync": "Сінхранізаваць",
- "Playlists": "Плэй-лісты",
"Latest": "Апошняе",
"LabelIpAddressValue": "IP-адрас: {0}",
- "ItemAddedWithName": "{0} дададзены ў бібліятэку",
- "MessageApplicationUpdated": "Сервер Jellyfin абноўлены",
"NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана",
"PluginInstalledWithName": "{0} быў усталяваны",
"UserCreatedWithName": "Карыстальнік {0} быў створаны",
- "Albums": "Альбомы",
- "Application": "Праграма",
"AuthenticationSucceededWithUserName": "{0} паспяхова аўтарызаваны",
- "Channels": "Каналы",
"ChapterNameValue": "Раздзел {0}",
"Collections": "Калекцыі",
"Default": "Прадвызначана",
@@ -21,16 +14,11 @@
"External": "Знешні",
"Genres": "Жанры",
"HeaderContinueWatching": "Працягнуць прагляд",
- "HeaderFavoriteAlbums": "Абраныя альбомы",
"HeaderFavoriteEpisodes": "Абраныя серыі",
"HeaderFavoriteShows": "Абраныя шоу",
- "HeaderFavoriteSongs": "Абраныя песні",
"HeaderLiveTV": "Прамы эфір",
- "HeaderAlbumArtists": "Выканаўцы альбома",
"LabelRunningTimeValue": "Працягласць: {0}",
"HomeVideos": "Хатнія відэа",
- "ItemRemovedWithName": "{0} выдалены з бібліятэкі",
- "MessageApplicationUpdatedTo": "Сервер Jellyfin абноўлены да версіі {0}",
"Movies": "Фільмы",
"Music": "Музыка",
"MusicVideos": "Музычныя кліпы",
@@ -41,19 +29,13 @@
"NotificationOptionPluginUpdateInstalled": "Абнаўленне плагіна ўсталявана",
"NotificationOptionServerRestartRequired": "Патрабуецца перазапуск сервера",
"Photos": "Фотаздымкі",
- "Plugin": "Плагін",
"PluginUninstalledWithName": "{0} быў выдалены",
"PluginUpdatedWithName": "{0} быў абноўлены",
- "ProviderValue": "Пастаўшчык: {0}",
- "Songs": "Песні",
- "System": "Сістэма",
- "User": "Карыстальнік",
"UserDeletedWithName": "Карыстальнік {0} быў выдалены",
"UserDownloadingItemWithValues": "{0} спампоўваецца {1}",
"TaskOptimizeDatabase": "Аптымізацыя базы даных",
"Artists": "Выканаўцы",
"UserOfflineFromDevice": "{0} адлучыўся ад {1}",
- "UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}",
"TaskCleanActivityLogDescription": "Выдаляе запісы старэйшыя за зададзены ўзрост ў журнале актыўнасці.",
"TaskRefreshChapterImagesDescription": "Стварае мініяцюры для відэа, якія маюць раздзелы.",
"TaskCleanLogsDescription": "Выдаляе файлы журналу, якім больш за {0} дзён.",
@@ -65,17 +47,10 @@
"TasksApplicationCategory": "Праграма",
"AppDeviceValues": "Праграма: {0}, Прылада: {1}",
"Books": "Кнігі",
- "CameraImageUploadedFrom": "Новая выява камеры была загружана з {0}",
- "DeviceOfflineWithName": "{0} адлучыўся",
- "DeviceOnlineWithName": "{0} падлучаны",
"Forced": "Прымусова",
- "HeaderRecordingGroups": "Групы запісаў",
"HeaderNextUp": "Наступнае",
- "HeaderFavoriteArtists": "Абраныя выканаўцы",
"HearingImpaired": "Са слабым слыхам",
"Inherit": "Атрымаць у спадчыну",
- "MessageNamedServerConfigurationUpdatedWithValue": "Канфігурацыя сервера (секцыя {0}) абноўлена",
- "MessageServerConfigurationUpdated": "Канфігурацыя сервера абноўлена",
"MixedContent": "Змешаны змест",
"NameSeasonUnknown": "Невядомы сезон",
"NotificationOptionInstallationFailed": "Збой усталёўкі",
@@ -91,8 +66,6 @@
"NotificationOptionVideoPlayback": "Пачалося прайграванне відэа",
"NotificationOptionVideoPlaybackStopped": "Прайграванне відэа спынена",
"ScheduledTaskFailedWithName": "{0} не атрымалася",
- "ScheduledTaskStartedWithName": "{0} пачалося",
- "ServerNameNeedsToBeRestarted": "{0} патрабуе перазапуску",
"Shows": "Шоу",
"StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.",
"SubtitleDownloadFailureFromForItem": "Субцітры для {1} не ўдалося спампаваць з {0}",
@@ -103,8 +76,6 @@
"UserPasswordChangedWithName": "Пароль быў зменены для карыстальніка {0}",
"UserStartedPlayingItemWithValues": "{0} прайграваецца {1} на {2}",
"UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}",
- "ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку",
- "ValueSpecialEpisodeName": "Спецвыпуск - {0}",
"VersionNumber": "Версія {0}",
"TasksMaintenanceCategory": "Абслугоўванне",
"TasksLibraryCategory": "Бібліятэка",
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index 7340180241..0710a39708 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -1,41 +1,24 @@
{
- "Albums": "Албуми",
"AppDeviceValues": "Програма: {0}, Устройство: {1}",
- "Application": "Програма",
"Artists": "Артисти",
"AuthenticationSucceededWithUserName": "{0} се удостовери успешно",
"Books": "Книги",
- "CameraImageUploadedFrom": "Нова снимка от камера беше качена от {0}",
- "Channels": "Канали",
"ChapterNameValue": "Глава {0}",
"Collections": "Колекции",
- "DeviceOfflineWithName": "{0} се разкачи",
- "DeviceOnlineWithName": "{0} е свързан",
"FailedLoginAttemptWithUserName": "Неуспешен опит за влизане от {0}",
"Favorites": "Любими",
"Folders": "Папки",
"Genres": "Жанрове",
- "HeaderAlbumArtists": "Изпълнители на албума",
"HeaderContinueWatching": "Продължаване на гледането",
- "HeaderFavoriteAlbums": "Любими албуми",
- "HeaderFavoriteArtists": "Любими изпълнители",
"HeaderFavoriteEpisodes": "Любими епизоди",
"HeaderFavoriteShows": "Любими сериали",
- "HeaderFavoriteSongs": "Любими песни",
"HeaderLiveTV": "Телевизия на живо",
"HeaderNextUp": "Следва",
- "HeaderRecordingGroups": "Запис групи",
"HomeVideos": "Домашни Клипове",
"Inherit": "Наследяване",
- "ItemAddedWithName": "{0} е добавено към библиотеката",
- "ItemRemovedWithName": "{0} е премахнато от библиотеката",
"LabelIpAddressValue": "IP адрес: {0}",
"LabelRunningTimeValue": "Продължителност: {0}",
"Latest": "Последни",
- "MessageApplicationUpdated": "Сървърът беше обновен",
- "MessageApplicationUpdatedTo": "Сървърът беше обновен до {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Секцията {0} от сървърната конфигурация беше актуализирана",
- "MessageServerConfigurationUpdated": "Конфигурацията на сървъра беше актуализирана",
"MixedContent": "Смесено съдържание",
"Movies": "Филми",
"Music": "Музика",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Възпроизвеждането на видео започна",
"NotificationOptionVideoPlaybackStopped": "Възпроизвеждането на видео е спряно",
"Photos": "Снимки",
- "Playlists": "Списъци",
- "Plugin": "Добавка",
"PluginInstalledWithName": "{0} е инсталиранa",
"PluginUninstalledWithName": "{0} е деинсталиранa",
"PluginUpdatedWithName": "{0} е обновенa",
- "ProviderValue": "Доставчик: {0}",
"ScheduledTaskFailedWithName": "{0} се провали",
- "ScheduledTaskStartedWithName": "{0} започна",
- "ServerNameNeedsToBeRestarted": "{0} трябва да се рестартира",
"Shows": "Сериали",
- "Songs": "Песни",
"StartupEmbyServerIsLoading": "Сървърът зарежда. Моля, опитайте отново след малко.",
"SubtitleDownloadFailureFromForItem": "Субтитрите за {1} от {0} не можаха да бъдат изтеглени",
- "Sync": "Синхронизиране",
- "System": "Система",
"TvShows": "Телевизионни сериали",
- "User": "Потребител",
"UserCreatedWithName": "Потребителят {0} е създаден",
"UserDeletedWithName": "Потребителят {0} е изтрит",
"UserDownloadingItemWithValues": "{0} изтегля {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} се разкачи от {1}",
"UserOnlineFromDevice": "{0} е на линия от {1}",
"UserPasswordChangedWithName": "Паролата на потребителя {0} е променена",
- "UserPolicyUpdatedWithName": "Потребителската политика за {0} се актуализира",
"UserStartedPlayingItemWithValues": "{0} пусна {1}",
"UserStoppedPlayingItemWithValues": "{0} спря {1}",
- "ValueHasBeenAddedToLibrary": "{0} беше добавен във Вашата библиотека",
- "ValueSpecialEpisodeName": "Специални - {0}",
"VersionNumber": "Версия {0}",
"TaskDownloadMissingSubtitlesDescription": "Търси Интернет за липсващи субтитри, на база конфигурацията за мета-данни.",
"TaskDownloadMissingSubtitles": "Изтегляне на липсващи субтитри",
diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index c6cfbe3c67..eb7fdf2b68 100644
--- a/Emby.Server.Implementations/Localization/Core/bn.json
+++ b/Emby.Server.Implementations/Localization/Core/bn.json
@@ -1,31 +1,19 @@
{
- "DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে",
- "DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
"Collections": "সংগ্রহশালা",
"ChapterNameValue": "অধ্যায় {0}",
- "Channels": "চ্যানেলসমূহ",
- "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
"Books": "পুস্তকসমূহ",
"AuthenticationSucceededWithUserName": "{0} সফলভাবে অথেন্টিকেট করেছেন",
"Artists": "শিল্পীগণ",
- "Application": "অ্যাপ্লিকেশন",
- "Albums": "অ্যালবামসমূহ",
"HeaderFavoriteEpisodes": "প্রিয় পর্বগুলো",
- "HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
- "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন",
- "HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ",
"Genres": "ধরণ",
"Folders": "ফোল্ডারসমূহ",
"Favorites": "পছন্দসমূহ",
"FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
"AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {1}",
"VersionNumber": "সংস্করণ {0}",
- "ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}",
- "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
"UserStoppedPlayingItemWithValues": "{2}তে {1} প্লে শেষ করেছেন {0}",
"UserStartedPlayingItemWithValues": "{2}তে {1} প্লে করেছেন {0}",
- "UserPolicyUpdatedWithName": "{0} এর জন্য ব্যবহার নীতি আপডেট করা হয়েছে",
"UserPasswordChangedWithName": "ব্যবহারকারী {0} এর পাসওয়ার্ড পরিবর্তিত হয়েছে",
"UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন আছে",
"UserOfflineFromDevice": "{0} {1} থেকে বিচ্ছিন্ন হয়ে গেছে",
@@ -33,23 +21,14 @@
"UserDownloadingItemWithValues": "{0}, {1} ডাউনলোড করছে",
"UserDeletedWithName": "ব্যবহারকারী {0}কে বাদ দেয়া হয়েছে",
"UserCreatedWithName": "ব্যবহারকারী {0} সৃষ্টি করা হয়েছে",
- "User": "ব্যবহারকারী",
"TvShows": "টিভি শোগুলো",
- "System": "সিস্টেম",
- "Sync": "সমন্বয় করুন",
"SubtitleDownloadFailureFromForItem": "{0} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ হয়েছে",
"StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।",
- "Songs": "সঙ্গীত সমূহ",
"Shows": "শো সমূহ",
- "ServerNameNeedsToBeRestarted": "{0} রিস্টার্ট করা প্রয়োজন",
- "ScheduledTaskStartedWithName": "{0} শুরু হয়েছে",
"ScheduledTaskFailedWithName": "{0} ব্যর্থ",
- "ProviderValue": "প্রদানকারী: {0}",
"PluginUpdatedWithName": "{0} আপডেট করা হয়েছে",
"PluginUninstalledWithName": "{0} আনইন্সটল হয়েছে",
"PluginInstalledWithName": "{0} ইন্সটল হয়েছে",
- "Plugin": "প্লাগিন",
- "Playlists": "প্লে লিস্ট সমূহ",
"Photos": "ছবিসমূহ",
"NotificationOptionVideoPlaybackStopped": "ভিডিও প্লেব্যাক বন্ধ হয়েছে",
"NotificationOptionVideoPlayback": "ভিডিও প্লেব্যাক শুরু হয়েছে",
@@ -75,21 +54,13 @@
"Music": "গান",
"Movies": "চলচ্চিত্রসমূহ",
"MixedContent": "মিশ্র কন্টেন্ট",
- "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
- "HeaderRecordingGroups": "রেকর্ডিং গ্রুপগুলো",
- "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভার কনফিগারেশন সেকশন {0} আপডেট করা হয়েছে",
- "MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে",
- "MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে",
"Latest": "সর্বশেষ",
"LabelRunningTimeValue": "চলার সময়: {0}",
"LabelIpAddressValue": "আইপি এড্রেস: {0}",
- "ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
- "ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
"Inherit": "উত্তরাধিকারসূত্র থেকে গ্রহণ করুন",
"HomeVideos": "হোম ভিডিও",
"HeaderNextUp": "এরপরে আসছে",
"HeaderLiveTV": "লাইভ টিভি",
- "HeaderFavoriteSongs": "প্রিয় গানগুলো",
"HeaderFavoriteShows": "প্রিয় শোগুলো",
"TasksLibraryCategory": "লাইব্রেরি",
"TasksMaintenanceCategory": "রক্ষণাবেক্ষণ",
diff --git a/Emby.Server.Implementations/Localization/Core/br.json b/Emby.Server.Implementations/Localization/Core/br.json
new file mode 100644
index 0000000000..cedc87e5a6
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/br.json
@@ -0,0 +1,63 @@
+{
+ "Artists": "Arzourien",
+ "AuthenticationSucceededWithUserName": "{0} kennasket gant berzh",
+ "Books": "Levrioù",
+ "ChapterNameValue": "Pennad {0}",
+ "Collections": "Dastumadegoù",
+ "Default": "Dre ziouer",
+ "External": "Diavaez",
+ "FailedLoginAttemptWithUserName": "Kennaskañ c'hwitet gant {0}",
+ "Favorites": "Sinedoù",
+ "Folders": "Teuliadoù",
+ "Forced": "Rediet",
+ "Genres": "Doareoù",
+ "HeaderContinueWatching": "Kenderc'hel da sellet",
+ "HeaderFavoriteEpisodes": "Rannoù Karetañ",
+ "HeaderFavoriteShows": "Heuliadennoù Karetañ",
+ "HeaderLiveTV": "TV war-eeun",
+ "HeaderNextUp": "Da c'houde",
+ "HearingImpaired": "Tud fall o C'hleved",
+ "HomeVideos": "Videoioù Personel",
+ "Inherit": "Hêrezhiñ",
+ "LabelIpAddressValue": "Chomlec'h IP : {0}",
+ "LabelRunningTimeValue": "Padelezh : {0}",
+ "Latest": "Diwezhañ",
+ "AppDeviceValues": "Arload : {0}, Trobarzhell : {1}",
+ "LyricDownloadFailureFromForItem": "C'hwitet eo pellgargañ ar c'homzoù eus {0} evit {1}",
+ "MixedContent": "Danvez mesket",
+ "Movies": "Filmoù",
+ "Music": "Sonerezh",
+ "MusicVideos": "Videoioù Sonerezh",
+ "NameInstallFailed": "{0} c'hwitet war ar staliadur",
+ "NameSeasonNumber": "Koulzad {0}",
+ "NameSeasonUnknown": "Koulzad Dianav",
+ "NewVersionIsAvailable": "Ur stumm Servijer Jellyfin nevez a c'haller pellgargañ.",
+ "NotificationOptionApplicationUpdateAvailable": "Hizivadur an arload zo da gaout",
+ "NotificationOptionApplicationUpdateInstalled": "Hizivadur an arload staliet",
+ "NotificationOptionAudioPlayback": "Lenn aodio lañset",
+ "NotificationOptionAudioPlaybackStopped": "Lenn aodio ehanet",
+ "Original": "Orin",
+ "Photos": "Fotoioù",
+ "Shows": "Heuliadennoù",
+ "Undefined": "Dianav",
+ "TasksMaintenanceCategory": "Trezalc’h",
+ "TasksLibraryCategory": "Levraoueg",
+ "TasksApplicationCategory": "Arload",
+ "NotificationOptionInstallationFailed": "C'hwitet war staliañ",
+ "NotificationOptionPluginError": "Fazi Askouezh",
+ "NotificationOptionPluginInstalled": "Askouezh staliet",
+ "NotificationOptionPluginUninstalled": "Askouezh distaliet",
+ "ScheduledTaskFailedWithName": "c'hwitadenn war {0}",
+ "TvShows": "Heuliadennoù TV",
+ "VersionNumber": "Stumm {0}",
+ "TasksChannelsCategory": "Chadennoù enlinenn",
+ "TaskAudioNormalization": "Normalizadur an aodio",
+ "TaskRefreshPeople": "Freskaat ar gomedianed",
+ "TaskUpdatePlugins": "Hizivaat an askouezhioù",
+ "TaskRefreshChannels": "Freskaat ar chadennoù",
+ "TaskOptimizeDatabase": "Gwellekaat an diaz roadennoù",
+ "TaskKeyframeExtractor": "Eztenner skeudennoù-alc'hwez",
+ "NotificationOptionCameraImageUploaded": "Karget eo skeudenn ar benveg",
+ "NotificationOptionNewLibraryContent": "Danvez nevez ouzhpennet",
+ "NotificationOptionPluginUpdateInstalled": "Staliet eo hizivadur an askouezh"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/bs.json b/Emby.Server.Implementations/Localization/Core/bs.json
index 3bf5ebd35a..aa7fe4eb24 100644
--- a/Emby.Server.Implementations/Localization/Core/bs.json
+++ b/Emby.Server.Implementations/Localization/Core/bs.json
@@ -1,52 +1,32 @@
{
- "Albums": "Albumi",
"Artists": "Umjetnici",
"Books": "Knjige",
- "Channels": "Kanalima",
"Collections": "Zbirke",
"Default": "Zadano",
"Favorites": "Omiljeni",
"Folders": "Mape",
"Genres": "Žanrovi",
- "HeaderAlbumArtists": "Umjetnici albuma",
"HeaderContinueWatching": "Nastavi gledati",
"Movies": "Filmovi",
"MusicVideos": "Muzički spotovi",
"Photos": "Slike",
- "Playlists": "Plejliste",
"Shows": "Pokazuje",
- "Songs": "Pjesme",
- "ValueSpecialEpisodeName": "Posebno - {0}",
"AppDeviceValues": "Aplikacija: {0}, Uređaj: {1}",
- "Application": "Prijava",
"AuthenticationSucceededWithUserName": "{0} uspješno autentificirano",
- "CameraImageUploadedFrom": "Nova slika s kamere je postavljena sa {0}",
"ChapterNameValue": "Poglavlje {0}",
- "DeviceOfflineWithName": "{0} se odspojio",
- "DeviceOnlineWithName": "{0} je povezan",
"External": "Vanjsko",
"FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave sa {0}",
"Forced": "Prisilno",
- "HeaderFavoriteAlbums": "Omiljeni albumi",
- "HeaderFavoriteArtists": "Omiljeni umjetnici",
"HeaderFavoriteEpisodes": "Omiljene epizode",
"HeaderFavoriteShows": "Omiljene emisije",
- "HeaderFavoriteSongs": "Omiljene pjesme",
"HeaderLiveTV": "TV uživo",
"HeaderNextUp": "Slijedi",
- "HeaderRecordingGroups": "Grupe za snimanje",
"HearingImpaired": "Oštećen sluh",
"HomeVideos": "Kućni videozapisi",
"Inherit": "Nasljedi",
- "ItemAddedWithName": "{0} je dodan u biblioteku",
- "ItemRemovedWithName": "{0} je uklonjen iz biblioteke",
"LabelIpAddressValue": "IP adresa: {0}",
"LabelRunningTimeValue": "Trajanje: {0}",
"Latest": "Posljednje dodano",
- "MessageApplicationUpdated": "Jellyfin Server je ažuriran",
- "MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Sekcija za konfiguraciju servera {0} je ažurirana",
- "MessageServerConfigurationUpdated": "Konfiguracija servera je ažurirana",
"MixedContent": "Miješani sadržaj",
"Music": "Muzika",
"NameInstallFailed": "{0} instalacija je propala",
@@ -69,21 +49,14 @@
"NotificationOptionUserLockedOut": "Korisnik je zaključan",
"NotificationOptionVideoPlayback": "Pokrenuto je reproduciranje videa",
"NotificationOptionVideoPlaybackStopped": "Reprodukcija videa je zaustavljena",
- "Plugin": "Plugin",
"PluginInstalledWithName": "{0} je instaliran",
"PluginUninstalledWithName": "{0} je deinstaliran",
"PluginUpdatedWithName": "{0} je ažurirano",
- "ProviderValue": "Pružatelj: {0}",
"ScheduledTaskFailedWithName": "{0} nije uspjelo",
- "ScheduledTaskStartedWithName": "{0} počelo",
- "ServerNameNeedsToBeRestarted": "{0} treba ponovo pokrenuti",
"StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Molimo pokušajte ponovo za kratko vrijeme.",
"SubtitleDownloadFailureFromForItem": "Podtitlovi nisu uspjeli preuzeti sa {0} za {1}",
- "Sync": "Sinkronizacija",
- "System": "Sistem",
"TvShows": "TV serije",
"Undefined": "Nedefinirano",
- "User": "Korisnik",
"UserCreatedWithName": "Korisnik {0} je kreiran",
"UserDeletedWithName": "Korisnik {0} je izbrisan",
"UserDownloadingItemWithValues": "{0} preuzima {1}",
@@ -91,10 +64,8 @@
"UserOfflineFromDevice": "{0} se odspojio od {1}",
"UserOnlineFromDevice": "{0} je online od {1}",
"UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}",
- "UserPolicyUpdatedWithName": "Pravila za korisnike su ažurirana za {0}",
"UserStartedPlayingItemWithValues": "{0} igra protiv {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} je završio igru protiv {1} na {2}",
- "ValueHasBeenAddedToLibrary": "{0} je dodan u vašu medijsku biblioteku",
"VersionNumber": "Verzija {0}",
"TasksMaintenanceCategory": "Održavanje",
"TasksLibraryCategory": "Biblioteka",
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index f9543e6f4c..6c81726ee6 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -1,41 +1,24 @@
{
- "Albums": "Àlbums",
"AppDeviceValues": "Aplicació: {0}, Dispositiu: {1}",
- "Application": "Aplicació",
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament",
"Books": "Llibres",
- "CameraImageUploadedFrom": "S'ha pujat una nova imatge de càmera des de {0}",
- "Channels": "Canals",
"ChapterNameValue": "Capítol {0}",
"Collections": "Col·leccions",
- "DeviceOfflineWithName": "{0} s'ha desconnectat",
- "DeviceOnlineWithName": "{0} està connectat",
"FailedLoginAttemptWithUserName": "Intent de connexió fallit des de {0}",
"Favorites": "Preferits",
"Folders": "Directoris",
"Genres": "Gèneres",
- "HeaderAlbumArtists": "Artistes de l'àlbum",
"HeaderContinueWatching": "Continueu mirant",
- "HeaderFavoriteAlbums": "Àlbums preferits",
- "HeaderFavoriteArtists": "Artistes preferits",
"HeaderFavoriteEpisodes": "Episodis preferits",
"HeaderFavoriteShows": "Sèries preferides",
- "HeaderFavoriteSongs": "Cançons preferides",
"HeaderLiveTV": "TV en directe",
"HeaderNextUp": "A continuació",
- "HeaderRecordingGroups": "Grups musicals",
"HomeVideos": "Vídeos domèstics",
"Inherit": "Heretat",
- "ItemAddedWithName": "{0} s'ha afegit a la mediateca",
- "ItemRemovedWithName": "{0} s'ha eliminat de la mediateca",
"LabelIpAddressValue": "Adreça IP: {0}",
"LabelRunningTimeValue": "Temps en marxa: {0}",
"Latest": "Darrers",
- "MessageApplicationUpdated": "El servidor de Jellyfin ha estat actualitzat",
- "MessageApplicationUpdatedTo": "El servidor de Jellyfin ha estat actualitzat a {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "La secció {0} de la configuració del servidor ha estat actualitzada",
- "MessageServerConfigurationUpdated": "S'ha actualitzat la configuració del servidor",
"MixedContent": "Contingut barrejat",
"Movies": "Pel·lícules",
"Music": "Música",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Reproducció de vídeo iniciada",
"NotificationOptionVideoPlaybackStopped": "Reproducció de vídeo aturada",
"Photos": "Fotos",
- "Playlists": "Llistes de reproducció",
- "Plugin": "Complement",
"PluginInstalledWithName": "S'ha instal·lat {0}",
"PluginUninstalledWithName": "S'ha desinstal·lat {0}",
"PluginUpdatedWithName": "S'ha actualitzat {0}",
- "ProviderValue": "Proveïdor: {0}",
"ScheduledTaskFailedWithName": "{0} ha fallat",
- "ScheduledTaskStartedWithName": "S'ha iniciat {0}",
- "ServerNameNeedsToBeRestarted": "S'ha de reiniciar {0}",
"Shows": "Sèries",
- "Songs": "Cançons",
"StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho de nou en una estona.",
"SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
- "Sync": "Sincronitza",
- "System": "Sistema",
"TvShows": "Sèries de TV",
- "User": "Usuari",
"UserCreatedWithName": "S'ha creat l'usuari {0}",
"UserDeletedWithName": "S'ha eliminat l'usuari {0}",
"UserDownloadingItemWithValues": "{0} està descarregant {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} s'ha desconnectat de {1}",
"UserOnlineFromDevice": "{0} està connectat des de {1}",
"UserPasswordChangedWithName": "S'ha canviat la contrasenya per a l'usuari {0}",
- "UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}",
"UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1} a {2}",
"UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1} a {2}",
- "ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la mediateca",
- "ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versió {0}",
"TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
"TaskDownloadMissingSubtitles": "Descàrrega dels subtítols que faltin",
@@ -135,5 +106,7 @@
"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"
+ "CleanupUserDataTask": "Tasca de neteja de dades d'usuari",
+ "Original": "Original",
+ "LyricDownloadFailureFromForItem": "No s'han pogut descarregar les lletres des de {0} per a {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/chr.json b/Emby.Server.Implementations/Localization/Core/chr.json
index 85d1f4c881..7e039bafc2 100644
--- a/Emby.Server.Implementations/Localization/Core/chr.json
+++ b/Emby.Server.Implementations/Localization/Core/chr.json
@@ -1,44 +1,30 @@
{
"ChapterNameValue": "Didanedi {0}",
- "HeaderAlbumArtists": "Didanidanolisgisgi",
- "HeaderFavoriteAlbums": "Dvganidi didanidisgisgi",
"HeaderLiveTV": "Anigadi didanidisgosgi",
- "HeaderRecordingGroups": "Didanisquodiisgisgi",
"HomeVideos": "Diganadi dinagadisgisgi",
"Inherit": "Anigwe",
- "MessageApplicationUpdatedTo": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe anigadi {0}",
"MixedContent": "Ganinidi dininoladisgisgi",
"Movies": "Anidvnisgisgi",
"MusicVideos": "Danodisgisgi didanidisgosgi",
"NotificationOptionAudioPlayback": "Didanidigwe diganuyisgisgi anigadi",
"NotificationOptionInstallationFailed": "Diudvdi anadvnatisgisgi",
"NotificationOptionPluginUninstalled": "Ditsigvhnidv anawvdisgisgi",
- "Albums": "Anigawidaniyv",
- "Application": "Didanvyi",
"Artists": "Dinidaniyi",
"AuthenticationSucceededWithUserName": "{0} Sesoquonisdi nagadani",
"Books": "Didanedi",
- "CameraImageUploadedFrom": "Anigawidaniyv nasgi didagwalanvyi {0}",
- "Channels": "Diganadasgi",
"Collections": "Diganadisgi",
"Default": "Dinadi",
- "DeviceOfflineWithName": "{0} Aniyvolehvi nasgi",
"External": "Amohdi",
"Favorites": "Nvdayelvdisgi",
"Folders": "Didanididisgi",
"Forced": "Ganedi",
"Genres": "Diganadisgi",
"HeaderContinueWatching": "Uwoditsu asdanidisgisgi",
- "HeaderFavoriteArtists": "Dvganidi dinidanolisgisgi",
"HeaderFavoriteEpisodes": "Dvganidi didanidilisgadisgisgi",
"HeaderFavoriteShows": "Dvganidi didanididanolisgisgi)",
- "HeaderFavoriteSongs": "Dvganidi danodisgisgi",
"HeaderNextUp": "Anidvli uwodoli",
"HearingImpaired": "Anitsunidi talunidisgisgi",
- "ItemAddedWithName": "{0} Dinigwe anididanidisgi",
"Latest": "Uwodoli",
- "MessageApplicationUpdated": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe",
- "MessageServerConfigurationUpdated": "Sedanidvdi anigadi diganidinonvhi",
"Music": "Danodisgisgi",
"NameSeasonUnknown": "Tsunita anidvdisgi",
"NewVersionIsAvailable": "Danodigwe anigadi Jellyfin Server tsadanidigwe adisdi uwodvdi diganidinonvhi.",
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 8d43839110..28f0e2df97 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -1,41 +1,24 @@
{
- "Albums": "Alba",
"AppDeviceValues": "Aplikace: {0}, Zařízení: {1}",
- "Application": "Aplikace",
"Artists": "Umělci",
"AuthenticationSucceededWithUserName": "{0} úspěšně ověřen",
"Books": "Knihy",
- "CameraImageUploadedFrom": "Z {0} byla nahrána nová fotografie z fotoaparátu",
- "Channels": "Kanály",
"ChapterNameValue": "Kapitola {0}",
"Collections": "Kolekce",
- "DeviceOfflineWithName": "{0} se odpojil",
- "DeviceOnlineWithName": "{0} je připojen",
"FailedLoginAttemptWithUserName": "Neúspěšný pokus o přihlášení z {0}",
"Favorites": "Oblíbené",
"Folders": "Složky",
"Genres": "Žánry",
- "HeaderAlbumArtists": "Umělci alba",
"HeaderContinueWatching": "Pokračovat ve sledování",
- "HeaderFavoriteAlbums": "Oblíbená alba",
- "HeaderFavoriteArtists": "Oblíbení interpreti",
"HeaderFavoriteEpisodes": "Oblíbené epizody",
"HeaderFavoriteShows": "Oblíbené seriály",
- "HeaderFavoriteSongs": "Oblíbená hudba",
"HeaderLiveTV": "TV vysílání",
"HeaderNextUp": "Další díly",
- "HeaderRecordingGroups": "Skupiny nahrávek",
"HomeVideos": "Domácí videa",
"Inherit": "Zdědit",
- "ItemAddedWithName": "{0} byl přidán do knihovny",
- "ItemRemovedWithName": "{0} byl odstraněn z knihovny",
"LabelIpAddressValue": "IP adresa: {0}",
"LabelRunningTimeValue": "Délka média: {0}",
"Latest": "Nejnovější",
- "MessageApplicationUpdated": "Jellyfin Server byl aktualizován",
- "MessageApplicationUpdatedTo": "Jellyfin server byl aktualizován na verzi {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurace sekce {0} na serveru byla aktualizována",
- "MessageServerConfigurationUpdated": "Konfigurace serveru aktualizována",
"MixedContent": "Smíšený obsah",
"Movies": "Filmy",
"Music": "Hudba",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Přehrávání videa zahájeno",
"NotificationOptionVideoPlaybackStopped": "Přehrávání videa ukončeno",
"Photos": "Fotky",
- "Playlists": "Seznamy skladeb",
- "Plugin": "Zásuvný modul",
"PluginInstalledWithName": "{0} byl nainstalován",
"PluginUninstalledWithName": "{0} byl odinstalován",
"PluginUpdatedWithName": "{0} byl aktualizován",
- "ProviderValue": "Poskytl: {0}",
"ScheduledTaskFailedWithName": "{0} selhalo",
- "ScheduledTaskStartedWithName": "{0} zahájeno",
- "ServerNameNeedsToBeRestarted": "{0} vyžaduje restart",
"Shows": "Seriály",
- "Songs": "Skladby",
"StartupEmbyServerIsLoading": "Jellyfin Server je spouštěn. Zkuste to prosím v brzké době znovu.",
"SubtitleDownloadFailureFromForItem": "Stažení titulků pro {1} z {0} selhalo",
- "Sync": "Synchronizace",
- "System": "Systém",
"TvShows": "Seriály",
- "User": "Uživatel",
"UserCreatedWithName": "Uživatel {0} byl vytvořen",
"UserDeletedWithName": "Uživatel {0} byl smazán",
"UserDownloadingItemWithValues": "{0} stahuje {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} se odpojil ze zařízení {1}",
"UserOnlineFromDevice": "{0} se připojil ze zařízení {1}",
"UserPasswordChangedWithName": "Provedena změna hesla pro uživatele {0}",
- "UserPolicyUpdatedWithName": "Zásady uživatele pro {0} byly aktualizovány",
"UserStartedPlayingItemWithValues": "{0} spustil přehrávání {1}",
"UserStoppedPlayingItemWithValues": "{0} zastavil přehrávání {1}",
- "ValueHasBeenAddedToLibrary": "{0} byl přidán do vaší knihovny médií",
- "ValueSpecialEpisodeName": "Speciál - {0}",
"VersionNumber": "Verze {0}",
"TaskDownloadMissingSubtitlesDescription": "Vyhledá na internetu chybějící titulky na základě nastavení metadat.",
"TaskDownloadMissingSubtitles": "Stáhnout chybějící titulky",
@@ -135,5 +106,7 @@
"TaskMoveTrickplayImages": "Přesunout úložiště obrázků Trickplay",
"TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny.",
"CleanupUserDataTaskDescription": "Odstraní všechna uživatelská data (stav zhlédnutí, oblíbené atd.) z médií, které již neexistují více než 90 dní.",
- "CleanupUserDataTask": "Pročistit uživatelská data"
+ "CleanupUserDataTask": "Pročistit uživatelská data",
+ "Original": "Originál",
+ "LyricDownloadFailureFromForItem": "Nepodařilo se stáhnout texty pro {1} ze služby {0}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/cy.json b/Emby.Server.Implementations/Localization/Core/cy.json
index af0e89bf80..e4d8ee7731 100644
--- a/Emby.Server.Implementations/Localization/Core/cy.json
+++ b/Emby.Server.Implementations/Localization/Core/cy.json
@@ -1,16 +1,11 @@
{
- "DeviceOnlineWithName": "Mae {0} wedi'i gysylltu",
- "DeviceOfflineWithName": "Mae {0} wedi datgysylltu",
"Default": "Diofyn",
"Collections": "Casgliadau",
"ChapterNameValue": "Pennod {0}",
- "Channels": "Sianeli",
- "CameraImageUploadedFrom": "Mae delwedd camera newydd wedi'i lanlwytho o {0}",
"Books": "Llyfrau",
"AuthenticationSucceededWithUserName": "{0} wedi’i ddilysu’n llwyddiannus",
"Artists": "Crewyr",
"AppDeviceValues": "Ap: {0}, Dyfais: {1}",
- "Albums": "Albwmau",
"Genres": "Genres",
"Folders": "Ffolderi",
"Favorites": "Ffefrynnau",
@@ -20,9 +15,7 @@
"TaskRefreshPeople": "Adnewyddu Pobl",
"TasksChannelsCategory": "Sianeli Internet",
"VersionNumber": "Fersiwn {0}",
- "ScheduledTaskStartedWithName": "{0} wedi dechrau",
"ScheduledTaskFailedWithName": "{0} wedi methu",
- "ProviderValue": "Darparwr: {0}",
"NotificationOptionInstallationFailed": "Fethu Gosod",
"NameSeasonUnknown": "Tymor Anhysbys",
"NameSeasonNumber": "Tymor {0}",
@@ -30,31 +23,20 @@
"MixedContent": "Cynnwys amrywiol",
"HomeVideos": "Genres",
"HeaderNextUp": "Nesaf i Fyny",
- "HeaderFavoriteArtists": "Ffefryn Artistiaid",
- "HeaderFavoriteAlbums": "Ffefryn Albwmau",
"HeaderContinueWatching": "Parhewch i Wylio",
"TasksApplicationCategory": "Rhaglen",
"TasksLibraryCategory": "Llyfrgell",
"TasksMaintenanceCategory": "Cynnal a Chadw",
- "System": "System",
- "Plugin": "Ategyn",
"Music": "Cerddoriaeth",
"Latest": "Diweddaraf",
"Inherit": "Etifeddu",
"Forced": "Orfodi",
- "Application": "Rhaglen",
- "HeaderAlbumArtists": "Artistiaid albwm",
- "Sync": "Cysoni",
- "Songs": "Caneuon",
"Shows": "Rhaglenni",
- "Playlists": "Rhestri Chwarae",
"Photos": "Lluniau",
- "ValueSpecialEpisodeName": "Arbennig - {0}",
"Movies": "Ffilmiau",
"Undefined": "Heb ddiffiniad",
"TvShows": "Rhaglenni teledu",
"HeaderLiveTV": "Teledu Byw",
- "User": "Defnyddiwr",
"TaskCleanLogsDescription": "Dileu ffeiliau log sy'n fwy na {0} diwrnod oed.",
"TaskCleanLogs": "Glanhau ffolder log",
"TaskRefreshLibraryDescription": "Sganio'ch llyfrgell gyfryngau am ffeiliau newydd ac yn adnewyddu metaddata.",
@@ -65,13 +47,9 @@
"NotificationOptionPluginError": "Methodd ategyn",
"NotificationOptionAudioPlaybackStopped": "Stopiwyd chwarae sain",
"NotificationOptionAudioPlayback": "Dechreuwyd chwarae sain",
- "MessageServerConfigurationUpdated": "Mae gosodiadau gweinydd wedi'i ddiweddaru",
- "MessageNamedServerConfigurationUpdatedWithValue": "Mae adran gosodiadau gweinydd {0} wedi'i diweddaru",
"FailedLoginAttemptWithUserName": "Cais mewngofnodi wedi methu o {0}",
- "ValueHasBeenAddedToLibrary": "{0} wedi'i hychwanegu at eich llyfrgell gyfryngau",
"UserStoppedPlayingItemWithValues": "{0} wedi gorffen chwarae {1} ar {2}",
"UserStartedPlayingItemWithValues": "{0} yn chwarae {1} ar {2}",
- "UserPolicyUpdatedWithName": "Polisi defnyddiwr wedi'i newid ar gyfer {0}",
"UserPasswordChangedWithName": "Cyfrinair wedi'i newid ar gyfer defnyddiwr {0}",
"UserOnlineFromDevice": "Mae {0} ar-lein o {1}",
"UserOfflineFromDevice": "Mae {0} wedi datgysylltu o {1}",
@@ -80,7 +58,6 @@
"UserDeletedWithName": "Defnyddiwr {0} wedi'i ddileu",
"UserCreatedWithName": "Defnyddiwr {0} wedi'i greu",
"StartupEmbyServerIsLoading": "Gweinydd Jellyfin yn llwytho. Triwch eto mewn ychydig.",
- "ServerNameNeedsToBeRestarted": "Mae angen ailddechrau {0}",
"PluginUpdatedWithName": "{0} wedi'i ddiweddaru",
"PluginUninstalledWithName": "{0} wedi'i ddadosod",
"PluginInstalledWithName": "{0} wedi'i osod",
@@ -98,13 +75,7 @@
"NotificationOptionApplicationUpdateAvailable": "Diweddariad ap ar gael",
"NewVersionIsAvailable": "Mae fersiwn diweddarach o'r gweinydd Jellyfin ar gael.",
"NameInstallFailed": "Gosodiad {0} wedi methu",
- "MessageApplicationUpdatedTo": "Gweinydd Jellyfin wedi'i ddiweddaru i {0}",
- "MessageApplicationUpdated": "Gweinydd Jellyfin wedi'i ddiweddaru",
"LabelIpAddressValue": "Cyfeiriad IP: {0}",
- "ItemRemovedWithName": "{0} wedi'i dynnu o'r llyfrgell",
- "ItemAddedWithName": "{0} wedi'i adio i'r llyfrgell",
- "HeaderRecordingGroups": "Grwpiau Recordio",
- "HeaderFavoriteSongs": "Ffefryn Ganeuon",
"HeaderFavoriteShows": "Ffefryn Shoeau",
"HeaderFavoriteEpisodes": "Ffefryn Rhaglenni",
"TaskDownloadMissingSubtitlesDescription": "Chwilio'r rhyngrwyd am is-deitlau coll yn seiliedig ar gosodiadau metaddata.",
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index 7d905f3300..697d9c090f 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -1,41 +1,24 @@
{
- "Albums": "Albummer",
"AppDeviceValues": "App: {0}, Enhed: {1}",
- "Application": "Applikation",
"Artists": "Kunstnere",
"AuthenticationSucceededWithUserName": "{0} er logget ind",
"Books": "Bøger",
- "CameraImageUploadedFrom": "Et nyt kamerabillede er blevet uploadet fra {0}",
- "Channels": "Kanaler",
"ChapterNameValue": "Kapitel {0}",
"Collections": "Samlinger",
- "DeviceOfflineWithName": "{0} har afbrudt forbindelsen",
- "DeviceOnlineWithName": "{0} er forbundet",
"FailedLoginAttemptWithUserName": "Mislykket loginforsøg fra {0}",
"Favorites": "Favoritter",
"Folders": "Mapper",
"Genres": "Genrer",
- "HeaderAlbumArtists": "Albumkunstnere",
"HeaderContinueWatching": "Fortsæt afspilning",
- "HeaderFavoriteAlbums": "Favoritalbum",
- "HeaderFavoriteArtists": "Favoritkunstnere",
"HeaderFavoriteEpisodes": "Yndlingsafsnit",
"HeaderFavoriteShows": "Yndlingsserier",
- "HeaderFavoriteSongs": "Yndlingssange",
"HeaderLiveTV": "Live-TV",
"HeaderNextUp": "Næste",
- "HeaderRecordingGroups": "Optagelsesgrupper",
"HomeVideos": "Hjemmevideoer",
"Inherit": "Nedarv",
- "ItemAddedWithName": "{0} blev tilføjet til biblioteket",
- "ItemRemovedWithName": "{0} blev fjernet fra biblioteket",
"LabelIpAddressValue": "IP-adresse: {0}",
"LabelRunningTimeValue": "Spilletid: {0}",
"Latest": "Seneste",
- "MessageApplicationUpdated": "Jellyfin Server er blevet opdateret",
- "MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfiguration sektion {0} er blevet opdateret",
- "MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret",
"MixedContent": "Blandet indhold",
"Movies": "Film",
"Music": "Musik",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Videoafspilning påbegyndt",
"NotificationOptionVideoPlaybackStopped": "Videoafspilning blev stoppet",
"Photos": "Fotos",
- "Playlists": "Afspilningslister",
- "Plugin": "Plugin",
"PluginInstalledWithName": "{0} blev installeret",
"PluginUninstalledWithName": "{0} blev afinstalleret",
"PluginUpdatedWithName": "{0} blev opdateret",
- "ProviderValue": "Udbyder: {0}",
"ScheduledTaskFailedWithName": "{0} mislykkedes",
- "ScheduledTaskStartedWithName": "{0} påbegyndte",
- "ServerNameNeedsToBeRestarted": "{0} skal genstartes",
"Shows": "Serier",
- "Songs": "Sange",
"StartupEmbyServerIsLoading": "Jellyfin er i gang med at starte. Prøv igen om et øjeblik.",
"SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}",
- "Sync": "Synkroniser",
- "System": "System",
"TvShows": "TV-serier",
- "User": "Bruger",
"UserCreatedWithName": "Bruger {0} er blevet oprettet",
"UserDeletedWithName": "Brugeren {0} er nu slettet",
"UserDownloadingItemWithValues": "{0} henter {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} har afbrudt fra {1}",
"UserOnlineFromDevice": "{0} er online fra {1}",
"UserPasswordChangedWithName": "Adgangskode er ændret for brugeren {0}",
- "UserPolicyUpdatedWithName": "Brugerpolitikken er blevet opdateret for {0}",
"UserStartedPlayingItemWithValues": "{0} afspiller {1} på {2}",
"UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
- "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
- "ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
@@ -135,5 +106,7 @@
"TaskMoveTrickplayImagesDescription": "Flyt eksisterende trickplay-billeder jævnfør biblioteksindstillinger.",
"TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment.",
"CleanupUserDataTask": "Brugerdata oprydningsopgave",
- "CleanupUserDataTaskDescription": "Rydder alle brugerdata (eks. visning- og favoritstatus) fra medier, der har været utilgængelige i mindst 90 dage."
+ "CleanupUserDataTaskDescription": "Rydder alle brugerdata (eks. visning- og favoritstatus) fra medier, der har været utilgængelige i mindst 90 dage.",
+ "LyricDownloadFailureFromForItem": "Sangtekster kunne ikke downloades fra {0} til {1}",
+ "Original": "Original"
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index ab1a7d2cbd..8ac5fdf6fc 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -1,41 +1,24 @@
{
- "Albums": "Alben",
"AppDeviceValues": "App: {0}, Gerät: {1}",
- "Application": "Anwendung",
"Artists": "Interpreten",
"AuthenticationSucceededWithUserName": "{0} erfolgreich authentifiziert",
"Books": "Bücher",
- "CameraImageUploadedFrom": "Ein neues Kamerabild wurde von {0} hochgeladen",
- "Channels": "Kanäle",
"ChapterNameValue": "Kapitel {0}",
"Collections": "Sammlungen",
- "DeviceOfflineWithName": "{0} ist offline",
- "DeviceOnlineWithName": "{0} ist online",
"FailedLoginAttemptWithUserName": "Anmeldung von {0} fehlgeschlagen",
"Favorites": "Favoriten",
"Folders": "Verzeichnisse",
"Genres": "Genres",
- "HeaderAlbumArtists": "Album-Interpreten",
"HeaderContinueWatching": "Weiterschauen",
- "HeaderFavoriteAlbums": "Lieblingsalben",
- "HeaderFavoriteArtists": "Lieblingsinterpreten",
"HeaderFavoriteEpisodes": "Lieblingsfolgen",
"HeaderFavoriteShows": "Lieblingsserien",
- "HeaderFavoriteSongs": "Lieblingssongs",
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Als Nächstes",
- "HeaderRecordingGroups": "Aufnahme-Gruppen",
"HomeVideos": "Heimvideos",
"Inherit": "Vererben",
- "ItemAddedWithName": "{0} wurde der Bibliothek hinzugefügt",
- "ItemRemovedWithName": "{0} wurde aus der Bibliothek entfernt",
"LabelIpAddressValue": "IP-Adresse: {0}",
"LabelRunningTimeValue": "Laufzeit: {0}",
"Latest": "Neueste",
- "MessageApplicationUpdated": "Jellyfin-Server wurde aktualisiert",
- "MessageApplicationUpdatedTo": "Jellyfin-Server wurde auf Version {0} aktualisiert",
- "MessageNamedServerConfigurationUpdatedWithValue": "Der Server-Einstellungsbereich {0} wurde aktualisiert",
- "MessageServerConfigurationUpdated": "Servereinstellungen wurden aktualisiert",
"MixedContent": "Gemischte Inhalte",
"Movies": "Filme",
"Music": "Musik",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Video wird abgespielt",
"NotificationOptionVideoPlaybackStopped": "Videowiedergabe gestoppt",
"Photos": "Fotos",
- "Playlists": "Wiedergabelisten",
- "Plugin": "Plugin",
"PluginInstalledWithName": "{0} wurde installiert",
"PluginUninstalledWithName": "{0} wurde deinstalliert",
"PluginUpdatedWithName": "{0} wurde aktualisiert",
- "ProviderValue": "Anbieter: {0}",
"ScheduledTaskFailedWithName": "{0} ist fehlgeschlagen",
- "ScheduledTaskStartedWithName": "{0} wurde gestartet",
- "ServerNameNeedsToBeRestarted": "{0} muss neu gestartet werden",
"Shows": "Serien",
- "Songs": "Lieder",
"StartupEmbyServerIsLoading": "Jellyfin-Server lädt. Bitte versuche es gleich noch einmal.",
"SubtitleDownloadFailureFromForItem": "Untertitel von {0} für {1} konnten nicht heruntergeladen werden",
- "Sync": "Synchronisation",
- "System": "System",
"TvShows": "Serien",
- "User": "Benutzer",
"UserCreatedWithName": "Benutzer {0} wurde erstellt",
"UserDeletedWithName": "Benutzer {0} wurde gelöscht",
"UserDownloadingItemWithValues": "{0} lädt {1} herunter",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} wurde getrennt von {1}",
"UserOnlineFromDevice": "{0} ist online von {1}",
"UserPasswordChangedWithName": "Das Passwort für Benutzer {0} wurde geändert",
- "UserPolicyUpdatedWithName": "Benutzerrichtlinie von {0} wurde aktualisiert",
"UserStartedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} gestartet",
"UserStoppedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} beendet",
- "ValueHasBeenAddedToLibrary": "{0} wurde deiner Bibliothek hinzugefügt",
- "ValueSpecialEpisodeName": "Extra – {0}",
"VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Sucht im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.",
"TaskDownloadMissingSubtitles": "Fehlende Untertitel herunterladen",
@@ -135,5 +106,7 @@
"TaskMoveTrickplayImages": "Verzeichnis für Trickplay-Bilder migrieren",
"TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben.",
"CleanupUserDataTask": "Aufgabe zur Bereinigung von Benutzerdaten",
- "CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Abspielstatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind."
+ "CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Abspielstatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind.",
+ "Original": "Original",
+ "LyricDownloadFailureFromForItem": "Fehler beim Download der Songtexte von {0} für {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index 0443207ea6..d84afdc1b6 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -1,41 +1,24 @@
{
- "Albums": "Άλμπουμ",
"AppDeviceValues": "Εφαρμογή: {0}, Συσκευή: {1}",
- "Application": "Εφαρμογή",
"Artists": "Καλλιτέχνες",
"AuthenticationSucceededWithUserName": "Ο χρήστης {0} επαληθεύτηκε επιτυχώς",
"Books": "Βιβλία",
- "CameraImageUploadedFrom": "Μια νέα φωτογραφία φορτώθηκε από {0}",
- "Channels": "Κανάλια",
"ChapterNameValue": "Κεφάλαιο {0}",
"Collections": "Συλλογές",
- "DeviceOfflineWithName": "Ο/Η {0} αποσυνδέθηκε",
- "DeviceOnlineWithName": "Ο/Η {0} συνδέθηκε",
"FailedLoginAttemptWithUserName": "Αποτυχία προσπάθειας σύνδεσης από {0}",
"Favorites": "Αγαπημένα",
"Folders": "Φάκελοι",
"Genres": "Είδη",
- "HeaderAlbumArtists": "Καλλιτέχνες άλμπουμ",
"HeaderContinueWatching": "Συνεχίστε την παρακολούθηση",
- "HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
- "HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",
"HeaderFavoriteEpisodes": "Αγαπημένα Επεισόδια",
"HeaderFavoriteShows": "Αγαπημένες Σειρές",
- "HeaderFavoriteSongs": "Αγαπημένα Τραγούδια",
"HeaderLiveTV": "Ζωντανή Τηλεόραση",
"HeaderNextUp": "Επόμενο",
- "HeaderRecordingGroups": "Ομάδες Ηχογράφησης",
"HomeVideos": "Προσωπικά Βίντεο",
"Inherit": "Κληρονόμηση",
- "ItemAddedWithName": "Το {0} προστέθηκε στη βιβλιοθήκη",
- "ItemRemovedWithName": "Το {0} διαγράφτηκε από τη βιβλιοθήκη",
"LabelIpAddressValue": "Διεύθυνση IP: {0}",
"LabelRunningTimeValue": "Διάρκεια: {0}",
"Latest": "Πρόσφατα",
- "MessageApplicationUpdated": "Ο διακομιστής Jellyfin έχει ενημερωθεί",
- "MessageApplicationUpdatedTo": "Ο διακομιστής Jellyfin αναβαθμίστηκε στην έκδοση {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Η ενότητα {0} ρύθμισης παραμέτρων του διακομιστή έχει ενημερωθεί",
- "MessageServerConfigurationUpdated": "Η ρύθμιση παραμέτρων του διακομιστή έχει ενημερωθεί",
"MixedContent": "Ανάμεικτο Περιεχόμενο",
"Movies": "Ταινίες",
"Music": "Μουσική",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Η αναπαραγωγή βίντεο ξεκίνησε",
"NotificationOptionVideoPlaybackStopped": "Η αναπαραγωγή βίντεο σταμάτησε",
"Photos": "Φωτογραφίες",
- "Playlists": "Λίστες αναπαραγωγής",
- "Plugin": "Πρόσθετο",
"PluginInstalledWithName": "Το {0} εγκαταστάθηκε",
"PluginUninstalledWithName": "Το {0} έχει απεγκατασταθεί",
"PluginUpdatedWithName": "Το {0} ενημερώθηκε",
- "ProviderValue": "Πάροχος: {0}",
"ScheduledTaskFailedWithName": "{0} αποτυχία",
- "ScheduledTaskStartedWithName": "{0} ξεκίνησε",
- "ServerNameNeedsToBeRestarted": "{0} χρειάζεται επανεκκίνηση",
"Shows": "Σειρές",
- "Songs": "Τραγούδια",
"StartupEmbyServerIsLoading": "Ο διακομιστής Jellyfin φορτώνει. Περιμένετε λίγο και δοκιμάστε ξανά.",
"SubtitleDownloadFailureFromForItem": "Αποτυχίες μεταφόρτωσης υποτίτλων από {0} για {1}",
- "Sync": "Συγχρονισμός",
- "System": "Σύστημα",
"TvShows": "Τηλεοπτικές Σειρές",
- "User": "Χρήστης",
"UserCreatedWithName": "Ο χρήστης {0} δημιουργήθηκε",
"UserDeletedWithName": "Ο χρήστης {0} έχει διαγραφεί",
"UserDownloadingItemWithValues": "{0} κατεβάζει {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} αποσυνδέθηκε από {1}",
"UserOnlineFromDevice": "{0} είναι online απο {1}",
"UserPasswordChangedWithName": "Ο κωδικός του χρήστη {0} έχει αλλάξει",
- "UserPolicyUpdatedWithName": "Η πολιτική χρήστη έχει ενημερωθεί για {0}",
"UserStartedPlayingItemWithValues": "{0} παίζει {1} σε {2}",
"UserStoppedPlayingItemWithValues": "{0} τελείωσε να παίζει {1} σε {2}",
- "ValueHasBeenAddedToLibrary": "{0} προστέθηκαν στη βιβλιοθήκη πολυμέσων σας",
- "ValueSpecialEpisodeName": "Σπέσιαλ - {0}",
"VersionNumber": "Έκδοση {0}",
"TaskRefreshPeople": "Ανανέωση Ατόμων",
"TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.",
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index b0094e33c3..be152b515f 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -1,41 +1,24 @@
{
- "Albums": "Albums",
"AppDeviceValues": "App: {0}, Device: {1}",
- "Application": "Application",
"Artists": "Artists",
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
"Books": "Books",
- "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
- "Channels": "Channels",
"ChapterNameValue": "Chapter {0}",
"Collections": "Collections",
- "DeviceOfflineWithName": "{0} has disconnected",
- "DeviceOnlineWithName": "{0} is connected",
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
"Favorites": "Favourites",
"Folders": "Folders",
"Genres": "Genres",
- "HeaderAlbumArtists": "Album artists",
"HeaderContinueWatching": "Continue Watching",
- "HeaderFavoriteAlbums": "Favourite Albums",
- "HeaderFavoriteArtists": "Favourite Artists",
"HeaderFavoriteEpisodes": "Favourite Episodes",
"HeaderFavoriteShows": "Favourite Shows",
- "HeaderFavoriteSongs": "Favourite Songs",
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Next Up",
- "HeaderRecordingGroups": "Recording Groups",
"HomeVideos": "Home Videos",
"Inherit": "Inherit",
- "ItemAddedWithName": "{0} was added to the library",
- "ItemRemovedWithName": "{0} was removed from the library",
"LabelIpAddressValue": "IP address: {0}",
"LabelRunningTimeValue": "Running time: {0}",
"Latest": "Latest",
- "MessageApplicationUpdated": "Jellyfin Server has been updated",
- "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
- "MessageServerConfigurationUpdated": "Server configuration has been updated",
"MixedContent": "Mixed content",
"Movies": "Movies",
"Music": "Music",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Video playback started",
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
"Photos": "Photos",
- "Playlists": "Playlists",
- "Plugin": "Plugin",
"PluginInstalledWithName": "{0} was installed",
"PluginUninstalledWithName": "{0} was uninstalled",
"PluginUpdatedWithName": "{0} was updated",
- "ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} failed",
- "ScheduledTaskStartedWithName": "{0} started",
- "ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
"Shows": "Shows",
- "Songs": "Songs",
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
- "Sync": "Sync",
- "System": "System",
"TvShows": "TV Shows",
- "User": "User",
"UserCreatedWithName": "User {0} has been created",
"UserDeletedWithName": "User {0} has been deleted",
"UserDownloadingItemWithValues": "{0} is downloading {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} has disconnected from {1}",
"UserOnlineFromDevice": "{0} is online from {1}",
"UserPasswordChangedWithName": "Password has been changed for user {0}",
- "UserPolicyUpdatedWithName": "User policy has been updated for {0}",
"UserStartedPlayingItemWithValues": "{0} has started playing {1}",
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
- "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
- "ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration.",
"TaskDownloadMissingSubtitles": "Download missing subtitles",
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index 9b5049c8c7..856941c61a 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -1,45 +1,29 @@
{
- "Albums": "Albums",
"AppDeviceValues": "App: {0}, Device: {1}",
- "Application": "Application",
"Artists": "Artists",
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
"Books": "Books",
- "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
- "Channels": "Channels",
"ChapterNameValue": "Chapter {0}",
"Collections": "Collections",
"Default": "Default",
- "DeviceOfflineWithName": "{0} has disconnected",
- "DeviceOnlineWithName": "{0} is connected",
"External": "External",
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
"Favorites": "Favorites",
"Folders": "Folders",
"Forced": "Forced",
"Genres": "Genres",
- "HeaderAlbumArtists": "Album artists",
"HeaderContinueWatching": "Continue Watching",
- "HeaderFavoriteAlbums": "Favorite Albums",
- "HeaderFavoriteArtists": "Favorite Artists",
"HeaderFavoriteEpisodes": "Favorite Episodes",
"HeaderFavoriteShows": "Favorite Shows",
- "HeaderFavoriteSongs": "Favorite Songs",
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Next Up",
- "HeaderRecordingGroups": "Recording Groups",
"HearingImpaired": "Hearing Impaired",
"HomeVideos": "Home Videos",
"Inherit": "Inherit",
- "ItemAddedWithName": "{0} was added to the library",
- "ItemRemovedWithName": "{0} was removed from the library",
"LabelIpAddressValue": "IP address: {0}",
"LabelRunningTimeValue": "Running time: {0}",
"Latest": "Latest",
- "MessageApplicationUpdated": "Jellyfin Server has been updated",
- "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
- "MessageServerConfigurationUpdated": "Server configuration has been updated",
+ "LyricDownloadFailureFromForItem": "Lyrics failed to download from {0} for {1}",
"MixedContent": "Mixed content",
"Movies": "Movies",
"Music": "Music",
@@ -66,24 +50,15 @@
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
"Original": "Original",
"Photos": "Photos",
- "Playlists": "Playlists",
- "Plugin": "Plugin",
"PluginInstalledWithName": "{0} was installed",
"PluginUninstalledWithName": "{0} was uninstalled",
"PluginUpdatedWithName": "{0} was updated",
- "ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} failed",
- "ScheduledTaskStartedWithName": "{0} started",
- "ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
"Shows": "Shows",
- "Songs": "Songs",
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
- "Sync": "Sync",
- "System": "System",
"TvShows": "TV Shows",
"Undefined": "Undefined",
- "User": "User",
"UserCreatedWithName": "User {0} has been created",
"UserDeletedWithName": "User {0} has been deleted",
"UserDownloadingItemWithValues": "{0} is downloading {1}",
@@ -91,11 +66,8 @@
"UserOfflineFromDevice": "{0} has disconnected from {1}",
"UserOnlineFromDevice": "{0} is online from {1}",
"UserPasswordChangedWithName": "Password has been changed for user {0}",
- "UserPolicyUpdatedWithName": "User policy has been updated for {0}",
"UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}",
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
- "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
- "ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}",
"TasksMaintenanceCategory": "Maintenance",
"TasksLibraryCategory": "Library",
diff --git a/Emby.Server.Implementations/Localization/Core/enm.json b/Emby.Server.Implementations/Localization/Core/enm.json
deleted file mode 100644
index 0967ef424b..0000000000
--- a/Emby.Server.Implementations/Localization/Core/enm.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json
index 42cce1096f..133a2755a8 100644
--- a/Emby.Server.Implementations/Localization/Core/eo.json
+++ b/Emby.Server.Implementations/Localization/Core/eo.json
@@ -7,35 +7,22 @@
"NameInstallFailed": "{0} instalado fiaskis",
"Music": "Muziko",
"Movies": "Filmoj",
- "ItemRemovedWithName": "{0} forigis el la plurmediteko",
- "ItemAddedWithName": "{0} aldonis al la plurmediteko",
"HeaderLiveTV": "TV-etero",
"HeaderContinueWatching": "Daŭrigi Spektadon",
- "HeaderAlbumArtists": "Artistoj de albumo",
"Folders": "Dosierujoj",
- "DeviceOnlineWithName": "{0} estas konektita",
"Default": "Defaŭlte",
"Collections": "Kolektoj",
"ChapterNameValue": "Ĉapitro {0}",
- "Channels": "Kanaloj",
"Books": "Libroj",
"Artists": "Artistoj",
- "Application": "Aplikaĵo",
"AppDeviceValues": "Aplikaĵo: {0}, Aparato: {1}",
- "Albums": "Albumoj",
"TasksLibraryCategory": "Plurmediteko",
"VersionNumber": "Versio {0}",
"UserDownloadingItemWithValues": "{0} elŝutas {1}",
"UserCreatedWithName": "Uzanto {0} kreiĝis",
- "User": "Uzanto",
- "System": "Sistemo",
- "Songs": "Kantoj",
- "ScheduledTaskStartedWithName": "{0} lanĉis",
"ScheduledTaskFailedWithName": "{0} malsukcesis",
"PluginUninstalledWithName": "{0} malinstaliĝis",
"PluginInstalledWithName": "{0} instaliĝis",
- "Plugin": "Kromprogramo",
- "Playlists": "Ludlistoj",
"Photos": "Fotoj",
"NotificationOptionPluginUninstalled": "Kromprogramo malinstaliĝis",
"NotificationOptionNewLibraryContent": "Nova enhavo aldoniĝis",
@@ -43,36 +30,28 @@
"MusicVideos": "Muzikvideoj",
"LabelIpAddressValue": "IP-adreso: {0}",
"Genres": "Ĝenroj",
- "DeviceOfflineWithName": "{0} malkonektis",
- "HeaderFavoriteArtists": "Favorataj Artistoj",
"Shows": "Serioj",
"HeaderFavoriteShows": "Favorataj Serioj",
"TvShows": "TV-serioj",
"Favorites": "Favorataj",
"TaskCleanLogs": "Purigi Ĵurnalan Katalogon",
"TaskRefreshLibrary": "Skani Plurmeditekon",
- "ValueSpecialEpisodeName": "Speciala - {0}",
"TaskOptimizeDatabase": "Optimumigi datenbazon",
"TaskRefreshChannels": "Refreŝigi Kanalojn",
"TaskUpdatePlugins": "Ĝisdatigi Kromprogramojn",
"TaskRefreshPeople": "Refreŝigi Homojn",
"TasksChannelsCategory": "Interretaj Kanaloj",
- "ProviderValue": "Provizanto: {0}",
"NotificationOptionPluginError": "Kromprogramo malsukcesis",
"MixedContent": "Miksita enhavo",
"TasksApplicationCategory": "Aplikaĵo",
"TasksMaintenanceCategory": "Prizorgado",
"Undefined": "Nedifinita",
- "Sync": "Sinkronigo",
"Latest": "Plej novaj",
"Inherit": "Hereda",
"HomeVideos": "Hejmaj Videoj",
"HeaderNextUp": "Sekva Plue",
- "HeaderFavoriteSongs": "Favorataj Kantoj",
"HeaderFavoriteEpisodes": "Favorataj Epizodoj",
- "HeaderFavoriteAlbums": "Favorataj Albumoj",
"Forced": "Forcita",
- "ServerNameNeedsToBeRestarted": "{0} devas esti relanĉita",
"NotificationOptionVideoPlayback": "La videoludado lanĉis",
"NotificationOptionServerRestartRequired": "Servila relanĉigo bezonata",
"TaskOptimizeDatabaseDescription": "Kompaktigas datenbazon kaj trunkas liberan lokon. Lanĉi ĉi tiun taskon post la plurmediteka skanado aŭ fari aliajn ŝanĝojn, kiuj implicas datenbazajn modifojn, povus plibonigi rendimenton.",
@@ -85,22 +64,16 @@
"TaskCleanCacheDescription": "Forigas stapla dosierojn ne plu necesajn de la sistemo.",
"TaskCleanActivityLogDescription": "Forigas aktivecan ĵurnalaĵojn pli malnovajn ol la agordita aĝo.",
"TaskCleanTranscodeDescription": "Forigas transkodajn dosierojn aĝajn pli ol unu tagon.",
- "ValueHasBeenAddedToLibrary": "{0} estis aldonita al via plurmediteko",
"SubtitleDownloadFailureFromForItem": "Subtekstoj malsukcesis elŝuti de {0} por {1}",
"StartupEmbyServerIsLoading": "Jellyfin Server ŝarĝas. Provi denove baldaŭ.",
"TaskRefreshChapterImagesDescription": "Kreas bildetojn por videoj kiuj havas ĉapitrojn.",
"UserStoppedPlayingItemWithValues": "{0} finis ludi {1} ĉe {2}",
- "UserPolicyUpdatedWithName": "Uzanta politiko estis ĝisdatigita por {0}",
"UserPasswordChangedWithName": "Pasvorto estis ŝanĝita por uzanto {0}",
"UserStartedPlayingItemWithValues": "{0} ludas {1} ĉe {2}",
"UserLockedOutWithName": "Uzanto {0} estas elŝlosita",
"UserOnlineFromDevice": "{0} estas enreta de {1}",
"UserOfflineFromDevice": "{0} malkonektis de {1}",
"UserDeletedWithName": "Uzanto {0} estis forigita",
- "MessageServerConfigurationUpdated": "Servila agordaro estis ĝisdatigita",
- "MessageNamedServerConfigurationUpdatedWithValue": "Servila agorda sekcio {0} estis ĝisdatigita",
- "MessageApplicationUpdatedTo": "Jellyfin Server estis ĝisdatigita al {0}",
- "MessageApplicationUpdated": "Jellyfin Server estis ĝisdatigita",
"TaskRefreshChannelsDescription": "Refreŝigas informon pri interretaj kanaloj.",
"TaskDownloadMissingSubtitles": "Elŝuti mankantajn subtekstojn",
"TaskCleanTranscode": "Malplenigi Transkodadan Katalogon",
@@ -116,9 +89,7 @@
"NotificationOptionApplicationUpdateInstalled": "Aplikaĵa ĝisdatigo instalita",
"NotificationOptionApplicationUpdateAvailable": "Ĝisdatigo de aplikaĵo havebla",
"LabelRunningTimeValue": "Ludada tempo: {0}",
- "HeaderRecordingGroups": "Rikordadaj Grupoj",
"FailedLoginAttemptWithUserName": "Malsukcesa ensaluta provo de {0}",
- "CameraImageUploadedFrom": "Nova kamera bildo estis alŝutita de {0}",
"AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis",
"TaskKeyframeExtractorDescription": "Eltiras ĉefkadrojn el videodosieroj por krei pli precizajn HLS-ludlistojn. Ĉi tiu tasko povas funkcii dum longa tempo.",
"TaskKeyframeExtractor": "Eltiri Ĉefkadrojn",
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index 7fda507797..28366a41b7 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -1,41 +1,24 @@
{
- "Albums": "Álbumes",
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
- "Application": "Aplicación",
"Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
"Books": "Libros",
- "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
- "Channels": "Canales",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Colecciones",
- "DeviceOfflineWithName": "{0} se ha desconectado",
- "DeviceOnlineWithName": "{0} está conectado",
"FailedLoginAttemptWithUserName": "Error al intentar iniciar sesión de {0}",
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
- "HeaderAlbumArtists": "Artistas del álbum",
"HeaderContinueWatching": "Seguir viendo",
- "HeaderFavoriteAlbums": "Álbumes favoritos",
- "HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Capítulos favoritos",
"HeaderFavoriteShows": "Series favoritas",
- "HeaderFavoriteSongs": "Canciones favoritas",
"HeaderLiveTV": "TV en vivo",
"HeaderNextUp": "Siguiente",
- "HeaderRecordingGroups": "Grupos de grabación",
"HomeVideos": "Videos caseros",
"Inherit": "Heredar",
- "ItemAddedWithName": "{0} se ha añadido a la biblioteca",
- "ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}",
"LabelRunningTimeValue": "Tiempo de funcionamiento: {0}",
"Latest": "Últimos",
- "MessageApplicationUpdated": "El servidor Jellyfin fue actualizado",
- "MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Se ha actualizado la sección {0} de la configuración del servidor",
- "MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor",
"MixedContent": "Contenido mezclado",
"Movies": "Películas",
"Music": "Música",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Se inició la reproducción de video",
"NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida",
"Photos": "Fotos",
- "Playlists": "Listas de reproducción",
- "Plugin": "Complemento",
"PluginInstalledWithName": "{0} fue instalado",
"PluginUninstalledWithName": "{0} fue desinstalado",
"PluginUpdatedWithName": "{0} fue actualizado",
- "ProviderValue": "Proveedor: {0}",
"ScheduledTaskFailedWithName": "{0} falló",
- "ScheduledTaskStartedWithName": "{0} iniciado",
- "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
"Shows": "Series",
- "Songs": "Canciones",
"StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.",
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtitulos desde {0} para {1}",
- "Sync": "Sincronizar",
- "System": "Sistema",
"TvShows": "Series de TV",
- "User": "Usuario",
"UserCreatedWithName": "El usuario {0} ha sido creado",
"UserDeletedWithName": "El usuario {0} ha sido borrado",
"UserDownloadingItemWithValues": "{0} está descargando {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} se ha desconectado de {1}",
"UserOnlineFromDevice": "{0} está en línea desde {1}",
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
- "UserPolicyUpdatedWithName": "Las política de usuario ha sido actualizada para {0}",
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
- "ValueHasBeenAddedToLibrary": "{0} ha sido añadido a tu biblioteca multimedia",
- "ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versión {0}",
"TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten basándose en la configuración de los metadatos.",
"TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json
index d03d3ed2ff..ac489b9e77 100644
--- a/Emby.Server.Implementations/Localization/Core/es-MX.json
+++ b/Emby.Server.Implementations/Localization/Core/es-MX.json
@@ -1,41 +1,24 @@
{
- "Albums": "Álbumes",
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
- "Application": "Aplicación",
"Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado con éxito",
"Books": "Libros",
- "CameraImageUploadedFrom": "Una nueva imagen de cámara ha sido subida desde {0}",
- "Channels": "Canales",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Colecciones",
- "DeviceOfflineWithName": "{0} se ha desconectado",
- "DeviceOnlineWithName": "{0} está conectado",
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
- "HeaderAlbumArtists": "Artistas del Álbum",
"HeaderContinueWatching": "Continuar viendo",
- "HeaderFavoriteAlbums": "Álbumes favoritos",
- "HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Episodios favoritos",
"HeaderFavoriteShows": "Programas favoritos",
- "HeaderFavoriteSongs": "Canciones favoritas",
"HeaderLiveTV": "TV en vivo",
"HeaderNextUp": "A continuación",
- "HeaderRecordingGroups": "Grupos de grabación",
"HomeVideos": "Videos Caseros",
"Inherit": "Heredar",
- "ItemAddedWithName": "{0} fue agregado a la biblioteca",
- "ItemRemovedWithName": "{0} fue removido de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}",
"LabelRunningTimeValue": "Tiempo corriendo: {0}",
"Latest": "Recientes",
- "MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
- "MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Se ha actualizado la sección {0} de la configuración del servidor",
- "MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor",
"MixedContent": "Contenido mezclado",
"Movies": "Películas",
"Music": "Música",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Reproducción de video iniciada",
"NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida",
"Photos": "Fotos",
- "Playlists": "Listas de reproducción",
- "Plugin": "Complemento",
"PluginInstalledWithName": "{0} fue instalado",
"PluginUninstalledWithName": "{0} fue desinstalado",
"PluginUpdatedWithName": "{0} fue actualizado",
- "ProviderValue": "Proveedor: {0}",
"ScheduledTaskFailedWithName": "{0} falló",
- "ScheduledTaskStartedWithName": "{0} iniciado",
- "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
"Shows": "Programas",
- "Songs": "Canciones",
"StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.",
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
- "Sync": "Sincronizar",
- "System": "Sistema",
"TvShows": "Programas de TV",
- "User": "Usuario",
"UserCreatedWithName": "El usuario {0} ha sido creado",
"UserDeletedWithName": "El usuario {0} ha sido eliminado",
"UserDownloadingItemWithValues": "{0} está descargando {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
"UserOnlineFromDevice": "{0} está en línea desde {1}",
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
- "UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}",
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
- "ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca de medios",
- "ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versión {0}",
"TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
"TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index cf118077c6..563dce8fe6 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -1,41 +1,24 @@
{
- "Albums": "Álbumes",
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
- "Application": "Aplicación",
"Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
"Books": "Libros",
- "CameraImageUploadedFrom": "Se ha subido una nueva imagen por cámara desde {0}",
- "Channels": "Canales",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Colecciones",
- "DeviceOfflineWithName": "{0} se ha desconectado",
- "DeviceOnlineWithName": "{0} está conectado",
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
- "HeaderAlbumArtists": "Artistas del álbum",
"HeaderContinueWatching": "Seguir viendo",
- "HeaderFavoriteAlbums": "Álbumes favoritos",
- "HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Episodios favoritos",
"HeaderFavoriteShows": "Series favoritas",
- "HeaderFavoriteSongs": "Canciones favoritas",
"HeaderLiveTV": "Televisión en directo",
"HeaderNextUp": "Siguiente",
- "HeaderRecordingGroups": "Grupos de grabación",
"HomeVideos": "Vídeos caseros",
"Inherit": "Heredar",
- "ItemAddedWithName": "{0} se ha añadido a la biblioteca",
- "ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}",
"LabelRunningTimeValue": "Duración: {0}",
"Latest": "Últimas",
- "MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin",
- "MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de configuración del servidor ha sido actualizada",
- "MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor",
"MixedContent": "Contenido mixto",
"Movies": "Películas",
"Music": "Música",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Se inició la reproducción de vídeo",
"NotificationOptionVideoPlaybackStopped": "Reproducción de vídeo detenida",
"Photos": "Fotos",
- "Playlists": "Listas de reproducción",
- "Plugin": "Plugin",
"PluginInstalledWithName": "{0} se ha instalado",
"PluginUninstalledWithName": "{0} se ha desinstalado",
"PluginUpdatedWithName": "{0} se actualizó",
- "ProviderValue": "Proveedor: {0}",
"ScheduledTaskFailedWithName": "{0} falló",
- "ScheduledTaskStartedWithName": "{0} iniciada",
- "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
"Shows": "Series",
- "Songs": "Canciones",
"StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.",
"SubtitleDownloadFailureFromForItem": "Fallo en la descarga de subtítulos desde {0} para {1}",
- "Sync": "Sincronizar",
- "System": "Sistema",
"TvShows": "Series",
- "User": "Usuario",
"UserCreatedWithName": "El usuario {0} ha sido creado",
"UserDeletedWithName": "El usuario {0} ha sido borrado",
"UserDownloadingItemWithValues": "{0} está descargando {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
"UserOnlineFromDevice": "{0} está en línea desde {1}",
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
- "UserPolicyUpdatedWithName": "Actualizada política de usuario para {0}",
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
- "ValueHasBeenAddedToLibrary": "{0} ha sido añadido a tu biblioteca multimedia",
- "ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versión {0}",
"TasksMaintenanceCategory": "Mantenimiento",
"TasksLibraryCategory": "Biblioteca",
@@ -135,5 +106,7 @@
"TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de plugins habilitados para MediaSegment.",
"TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay",
"CleanupUserDataTask": "Tarea de limpieza de datos del usuario",
- "CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, favoritos, etc.) de los medios que ya no están disponibles desde hace al menos 90 días."
+ "CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, favoritos, etc.) de los medios que ya no están disponibles desde hace al menos 90 días.",
+ "Original": "Original",
+ "LyricDownloadFailureFromForItem": "No se pudieron descargar las letras desde {0} para {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
index dec82b73e3..1f13451060 100644
--- a/Emby.Server.Implementations/Localization/Core/es_419.json
+++ b/Emby.Server.Implementations/Localization/Core/es_419.json
@@ -1,29 +1,19 @@
{
"LabelRunningTimeValue": "Tiempo en ejecución: {0}",
- "ValueSpecialEpisodeName": "Especial - {0}",
- "Sync": "Sincronizar",
- "Songs": "Canciones",
"Shows": "Programas",
- "Playlists": "Listas de reproducción",
"Photos": "Fotos",
"Movies": "Películas",
"HeaderNextUp": "A continuación",
"HeaderLiveTV": "TV en vivo",
- "HeaderFavoriteSongs": "Canciones favoritas",
- "HeaderFavoriteArtists": "Artistas favoritos",
- "HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteEpisodes": "Episodios favoritos",
"HeaderFavoriteShows": "Programas favoritos",
"HeaderContinueWatching": "Continuar viendo",
- "HeaderAlbumArtists": "Artistas de álbum",
"Genres": "Géneros",
"Folders": "Carpetas",
"Favorites": "Favoritos",
"Collections": "Colecciones",
- "Channels": "Canales",
"Books": "Libros",
"Artists": "Artistas",
- "Albums": "Álbumes",
"TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
"TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
"TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
@@ -47,10 +37,8 @@
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Mantenimiento",
"VersionNumber": "Versión {0}",
- "ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca de medios",
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
- "UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}",
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
"UserOnlineFromDevice": "{0} está en línea desde {1}",
"UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
@@ -58,19 +46,13 @@
"UserDownloadingItemWithValues": "{0} está descargando {1}",
"UserDeletedWithName": "El usuario {0} ha sido eliminado",
"UserCreatedWithName": "El usuario {0} ha sido creado",
- "User": "Usuario",
"TvShows": "Programas de TV",
- "System": "Sistema",
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
"StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.",
- "ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado",
- "ScheduledTaskStartedWithName": "{0} iniciado",
"ScheduledTaskFailedWithName": "{0} falló",
- "ProviderValue": "Proveedor: {0}",
"PluginUpdatedWithName": "{0} fue actualizado",
"PluginUninstalledWithName": "{0} fue desinstalado",
"PluginInstalledWithName": "{0} fue instalado",
- "Plugin": "Complemento",
"NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida",
"NotificationOptionVideoPlayback": "Reproducción de video iniciada",
"NotificationOptionUserLockedOut": "Usuario bloqueado",
@@ -94,24 +76,13 @@
"MusicVideos": "Videos musicales",
"Music": "Música",
"MixedContent": "Contenido mezclado",
- "MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor",
- "MessageNamedServerConfigurationUpdatedWithValue": "Se ha actualizado la sección {0} de la configuración del servidor",
- "MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
- "MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
"Latest": "Recientes",
"LabelIpAddressValue": "Dirección IP: {0}",
- "ItemRemovedWithName": "{0} fue removido de la biblioteca",
- "ItemAddedWithName": "{0} fue agregado a la biblioteca",
"Inherit": "Heredar",
"HomeVideos": "Videos caseros",
- "HeaderRecordingGroups": "Grupos de grabación",
"FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido desde {0}",
- "DeviceOnlineWithName": "{0} está conectado",
- "DeviceOfflineWithName": "{0} se ha desconectado",
"ChapterNameValue": "Capítulo {0}",
- "CameraImageUploadedFrom": "Una nueva imagen de cámara ha sido subida desde {0}",
"AuthenticationSucceededWithUserName": "{0} autenticado con éxito",
- "Application": "Aplicación",
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
"TaskCleanActivityLog": "Limpiar registro de actividades",
diff --git a/Emby.Server.Implementations/Localization/Core/es_DO.json b/Emby.Server.Implementations/Localization/Core/es_DO.json
index 8d991fa74a..a1b9944125 100644
--- a/Emby.Server.Implementations/Localization/Core/es_DO.json
+++ b/Emby.Server.Implementations/Localization/Core/es_DO.json
@@ -1,35 +1,24 @@
{
- "Channels": "Canales",
"Books": "Libros",
- "Albums": "Álbumes",
"Collections": "Colecciones",
"Artists": "Artistas",
- "DeviceOnlineWithName": "{0} está conectado",
- "DeviceOfflineWithName": "{0} se ha desconectado",
"ChapterNameValue": "Capítulo {0}",
- "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
"AuthenticationSucceededWithUserName": "{0} autenticado con éxito",
- "Application": "Aplicación",
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
"HeaderContinueWatching": "Continuar Viendo",
- "HeaderAlbumArtists": "Artistas del álbum",
"Genres": "Géneros",
"Folders": "Carpetas",
"Favorites": "Favoritos",
"FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido desde {0}",
- "HeaderFavoriteSongs": "Canciones Favoritas",
"HeaderFavoriteEpisodes": "Episodios Favoritos",
- "HeaderFavoriteArtists": "Artistas Favoritos",
"External": "Externo",
"Default": "Predeterminado",
"Movies": "Películas",
- "MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de la configuración ha sido actualizada",
"MixedContent": "Contenido mixto",
"Music": "Música",
"NotificationOptionCameraImageUploaded": "Imagen de la cámara subida",
"NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor",
"NotificationOptionVideoPlayback": "Reproducción de video iniciada",
- "Sync": "Sincronizar",
"Shows": "Series",
"UserDownloadingItemWithValues": "{0} está descargando {1}",
"UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
@@ -48,17 +37,12 @@
"HeaderFavoriteShows": "Programas favoritos",
"TaskCleanActivityLog": "Limpiar registro de actividades",
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
- "System": "Sistema",
- "User": "Usuario",
"Forced": "Forzado",
"PluginInstalledWithName": "{0} ha sido instalado",
- "HeaderFavoriteAlbums": "Álbumes favoritos",
"TaskUpdatePlugins": "Actualizar Plugins",
"Latest": "Recientes",
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
- "Songs": "Canciones",
"NotificationOptionPluginError": "Falla de plugin",
- "ScheduledTaskStartedWithName": "{0} iniciado",
"TasksApplicationCategory": "Aplicación",
"UserDeletedWithName": "El usuario {0} ha sido eliminado",
"TaskRefreshChapterImages": "Extraer imágenes de los capítulos",
@@ -71,34 +55,26 @@
"NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida",
"TasksLibraryCategory": "Biblioteca",
"NotificationOptionPluginInstalled": "Plugin instalado",
- "UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}",
"VersionNumber": "Versión {0}",
"HeaderNextUp": "A continuación",
- "ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}",
"NameSeasonNumber": "Temporada {0}",
"NotificationOptionNewLibraryContent": "Nuevo contenido agregado",
- "Plugin": "Plugin",
"NotificationOptionAudioPlayback": "Reproducción de audio iniciada",
"NotificationOptionTaskFailed": "Falló la tarea programada",
"LabelRunningTimeValue": "Tiempo en ejecución: {0}",
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
"TaskRefreshLibrary": "Escanear biblioteca de medios",
- "ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado",
"TasksMaintenanceCategory": "Mantenimiento",
- "ProviderValue": "Proveedor: {0}",
"UserCreatedWithName": "El usuario {0} ha sido creado",
"PluginUninstalledWithName": "{0} ha sido desinstalado",
- "ValueSpecialEpisodeName": "Especial - {0}",
"ScheduledTaskFailedWithName": "{0} falló",
"TaskCleanLogs": "Limpiar directorio de registros",
"NameInstallFailed": "Falló la instalación de {0}",
"UserLockedOutWithName": "El usuario {0} ha sido bloqueado",
"TaskRefreshLibraryDescription": "Escanea tu biblioteca de medios para encontrar archivos nuevos y actualizar los metadatos.",
"StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo en un momento.",
- "Playlists": "Listas de reproducción",
"TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
- "MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor",
"TaskRefreshPeople": "Actualizar personas",
"NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida",
"HeaderLiveTV": "TV en vivo",
@@ -108,15 +84,10 @@
"TaskCleanCache": "Limpiar directorio caché",
"TaskRefreshChapterImagesDescription": "Crea miniaturas para videos que tienen capítulos.",
"Inherit": "Heredar",
- "HeaderRecordingGroups": "Grupos de grabación",
- "ItemAddedWithName": "{0} fue agregado a la biblioteca",
"TaskOptimizeDatabase": "Optimizar base de datos",
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
"HearingImpaired": "Discapacidad auditiva",
"HomeVideos": "Videos caseros",
- "ItemRemovedWithName": "{0} fue removido de la biblioteca",
- "MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
- "MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
"MusicVideos": "Videos musicales",
"NewVersionIsAvailable": "Una nueva versión de Jellyfin está disponible para descargar.",
"PluginUpdatedWithName": "{0} ha sido actualizado",
diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json
index d751e35af2..e6bf1f25b5 100644
--- a/Emby.Server.Implementations/Localization/Core/et.json
+++ b/Emby.Server.Implementations/Localization/Core/et.json
@@ -1,7 +1,6 @@
{
"TaskCleanActivityLogDescription": "Kustutab määratud ajast vanemad tegevuslogi kirjed.",
"UserDownloadingItemWithValues": "{0} laadib alla {1}",
- "HeaderRecordingGroups": "Salvestusrühmad",
"TaskOptimizeDatabaseDescription": "Tihendab ja puhastab andmebaasi. Selle toimingu tegemine pärast meediakogu andmebaasiga seotud muudatuste skannimist võib jõudlust parandada.",
"TaskOptimizeDatabase": "Optimeeri andmebaasi",
"TaskDownloadMissingSubtitlesDescription": "Otsib veebist puuduvaid subtiitreid vastavalt määratud metaandmete seadetele.",
@@ -29,30 +28,19 @@
"TasksLibraryCategory": "Meediakogu",
"TasksMaintenanceCategory": "Hooldus",
"VersionNumber": "Versioon {0}",
- "ValueSpecialEpisodeName": "Eriepisood - {0}",
- "ValueHasBeenAddedToLibrary": "{0} lisati meediakogusse",
"UserStartedPlayingItemWithValues": "{0} taasesitab {1} seadmes {2}",
"UserPasswordChangedWithName": "Kasutaja {0} parool muudeti",
"UserLockedOutWithName": "Kasutaja {0} lukustati",
"UserDeletedWithName": "Kasutaja {0} kustutati",
"UserCreatedWithName": "Kasutaja {0} on loodud",
- "ScheduledTaskStartedWithName": "{0} käivitati",
- "ProviderValue": "Allikas: {0}",
"StartupEmbyServerIsLoading": "Jellyfin server laadib. Proovi varsti uuesti.",
- "User": "Kasutaja",
"Undefined": "Määratlemata",
"TvShows": "Sarjad",
- "System": "Süsteem",
- "Sync": "Sünkrooni",
- "Songs": "Lood",
"Shows": "Sarjad",
- "ServerNameNeedsToBeRestarted": "{0} tuleb taaskäivitada",
"ScheduledTaskFailedWithName": "{0} nurjus",
"PluginUpdatedWithName": "{0} uuendati",
"PluginUninstalledWithName": "{0} eemaldati",
"PluginInstalledWithName": "{0} paigaldati",
- "Plugin": "Plugin",
- "Playlists": "Esitusloendid",
"Photos": "Fotod",
"NotificationOptionVideoPlaybackStopped": "Video taasesitus lõppes",
"NotificationOptionVideoPlayback": "Video taasesitus algas",
@@ -78,46 +66,29 @@
"Music": "Muusika",
"Movies": "Filmid",
"MixedContent": "Segatud sisu",
- "MessageServerConfigurationUpdated": "Serveri seadistust uuendati",
- "MessageNamedServerConfigurationUpdatedWithValue": "Serveri seadistusosa {0} uuendati",
- "MessageApplicationUpdatedTo": "Jellyfin server uuendati versioonile {0}",
- "MessageApplicationUpdated": "Jellyfin server uuendati",
"Latest": "Uusimad",
"LabelRunningTimeValue": "Kestus: {0}",
"LabelIpAddressValue": "IP aadress: {0}",
- "ItemRemovedWithName": "{0} eemaldati meediakogust",
- "ItemAddedWithName": "{0} lisati meediakogusse",
"Inherit": "Päri",
"HomeVideos": "Koduvideod",
"HeaderNextUp": "Järgmisena",
"HeaderLiveTV": "Otse TV",
- "HeaderFavoriteSongs": "Lemmiklood",
"HeaderFavoriteShows": "Lemmiksarjad",
"HeaderFavoriteEpisodes": "Lemmikepisoodid",
- "HeaderFavoriteArtists": "Lemmikesitajad",
- "HeaderFavoriteAlbums": "Lemmikalbumid",
"HeaderContinueWatching": "Jätka vaatamist",
- "HeaderAlbumArtists": "Albumi esitajad",
"Genres": "Žanrid",
"Forced": "Sunnitud",
"Folders": "Kaustad",
"Favorites": "Lemmikud",
"FailedLoginAttemptWithUserName": "Sisselogimine nurjus aadressilt {0}",
- "DeviceOnlineWithName": "{0} on ühendatud",
- "DeviceOfflineWithName": "{0} katkestas ühenduse",
"Default": "Vaikimisi",
"ChapterNameValue": "Peatükk {0}",
- "Channels": "Kanalid",
- "CameraImageUploadedFrom": "Uus kaamera pilt laaditi üles allikalt {0}",
"Books": "Raamatud",
"AuthenticationSucceededWithUserName": "{0} autentimine õnnestus",
"Artists": "Esitajad",
- "Application": "Rakendus",
"AppDeviceValues": "Rakendus: {0}, seade: {1}",
- "Albums": "Albumid",
"UserOfflineFromDevice": "{0} katkestas ühenduse seadmega {1}",
"SubtitleDownloadFailureFromForItem": "Subtiitrite allalaadimine {0} > {1} nurjus",
- "UserPolicyUpdatedWithName": "Kasutaja {0} õigusi värskendati",
"UserStoppedPlayingItemWithValues": "{0} lõpetas {1} taasesituse seadmes {2}",
"UserOnlineFromDevice": "{0} on ühendatud seadmest {1}",
"External": "Väline",
@@ -135,5 +106,7 @@
"TaskExtractMediaSegmentsDescription": "Eraldab või võtab meedialõigud MediaSegment'i toega pluginatest.",
"TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht",
"CleanupUserDataTask": "Puhasta kasutajaandmed",
- "CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud."
+ "CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud.",
+ "LyricDownloadFailureFromForItem": "Laulusõnade hankimine teenusest {0} loole {1} nurjus",
+ "Original": "Algne"
}
diff --git a/Emby.Server.Implementations/Localization/Core/eu.json b/Emby.Server.Implementations/Localization/Core/eu.json
index 9e1390484f..71c351adcd 100644
--- a/Emby.Server.Implementations/Localization/Core/eu.json
+++ b/Emby.Server.Implementations/Localization/Core/eu.json
@@ -1,23 +1,16 @@
{
- "ValueSpecialEpisodeName": "Berezia - {0}",
- "Sync": "Sinkronizatu",
- "Songs": "Abestiak",
"Shows": "Serieak",
- "Playlists": "Erreprodukzio-zerrendak",
"Photos": "Argazkiak",
"MusicVideos": "Bideo musikalak",
"Movies": "Filmak",
"HeaderContinueWatching": "Ikusten jarraitu",
- "HeaderAlbumArtists": "Albumeko artistak",
"Genres": "Generoak",
"Folders": "Karpetak",
"Favorites": "Gogokoak",
"Default": "Lehenetsia",
"Collections": "Bildumak",
- "Channels": "Kanalak",
"Books": "Liburuak",
"Artists": "Artistak",
- "Albums": "Albumak",
"TaskOptimizeDatabase": "Datu basea optimizatu",
"TaskDownloadMissingSubtitlesDescription": "Falta diren azpitituluak bilatzen ditu interneten metadatuen konfigurazioaren arabera.",
"TaskDownloadMissingSubtitles": "Falta diren azpitituluak deskargatu",
@@ -44,10 +37,8 @@
"TasksLibraryCategory": "Liburutegia",
"TasksMaintenanceCategory": "Mantenua",
"VersionNumber": "Bertsioa {0}",
- "ValueHasBeenAddedToLibrary": "{0} zure multimedia liburutegian gehitu da",
"UserStoppedPlayingItemWithValues": "{0} {1} ikusten bukatu du {2}-(e)n",
"UserStartedPlayingItemWithValues": "{0} {1} ikusten ari da {2}-(e)n",
- "UserPolicyUpdatedWithName": "{0} erabiltzailearen politikak aldatu dira",
"UserPasswordChangedWithName": "{0} erabiltzailearen pasahitza aldatu da",
"UserOnlineFromDevice": "{0} online dago {1}-(e)tik",
"UserOfflineFromDevice": "{0} {1}-(e)tik deskonektatu da",
@@ -55,19 +46,14 @@
"UserDownloadingItemWithValues": "{0} {1} deskargatzen ari da",
"UserDeletedWithName": "{0} Erabiltzailea ezabatu da",
"UserCreatedWithName": "{0} Erabiltzailea sortu da",
- "User": "Erabiltzailea",
"Undefined": "Ezezaguna",
"TvShows": "TB serieak",
- "System": "Sistema",
"SubtitleDownloadFailureFromForItem": "{1}-en azpitutuluak {0}-tik deskargatzeak huts egin du",
"StartupEmbyServerIsLoading": "Jellyfin zerbitzaria kargatzen. Saiatu berriro beranduago.",
- "ServerNameNeedsToBeRestarted": "{0} berrabiarazi behar da",
- "ScheduledTaskStartedWithName": "{0} hasi da",
"ScheduledTaskFailedWithName": "{0} huts egin du",
"PluginUpdatedWithName": "{0} eguneratu da",
"PluginUninstalledWithName": "{0} desinstalatu da",
"PluginInstalledWithName": "{0} instalatu da",
- "Plugin": "Plugin",
"NotificationOptionVideoPlaybackStopped": "Bideoa geldituta",
"NotificationOptionVideoPlayback": "Bideoa martxan",
"NotificationOptionUserLockedOut": "Erabiltzailea blokeatua",
@@ -90,37 +76,22 @@
"NameInstallFailed": "{0} instalazioak huts egin du",
"Music": "Musika",
"MixedContent": "Eduki mistoa",
- "MessageServerConfigurationUpdated": "Zerbitzariaren konfigurazioa eguneratu da",
- "MessageNamedServerConfigurationUpdatedWithValue": "Zerbitzariaren {0} konfigurazio atala eguneratu da",
- "MessageApplicationUpdatedTo": "Jellyfin zerbitzaria {0}-ra eguneratu da",
- "MessageApplicationUpdated": "Jellyfin zerbitzaria eguneratu da",
"Latest": "Azkena",
"LabelRunningTimeValue": "Iraupena: {0}",
"LabelIpAddressValue": "IP helbidea: {0}",
- "ItemRemovedWithName": "{0} liburutegitik kendu da",
- "ItemAddedWithName": "{0} liburutegira gehitu da",
"HomeVideos": "Etxeko bideoak",
"HeaderNextUp": "Hurrengoa",
"HeaderLiveTV": "Zuzeneko TB",
- "HeaderFavoriteSongs": "Gogoko abestiak",
"HeaderFavoriteShows": "Gogoko serieak",
"HeaderFavoriteEpisodes": "Gogoko atalak",
- "HeaderFavoriteArtists": "Gogoko artistak",
- "HeaderFavoriteAlbums": "Gogoko albumak",
"Forced": "Behartuta",
"FailedLoginAttemptWithUserName": "{0}-tik saioa hasteak huts egin du",
"External": "Kanpokoa",
- "DeviceOnlineWithName": "{0} konektatu da",
- "DeviceOfflineWithName": "{0} deskonektatu da",
"ChapterNameValue": "{0} Kapitulua",
- "CameraImageUploadedFrom": "{0}-tik kamera irudi berri bat igo da",
"AuthenticationSucceededWithUserName": "{0} ongi autentifikatu da",
- "Application": "Aplikazioa",
"AppDeviceValues": "App: {0}, Gailua: {1}",
"HearingImpaired": "Entzumen urritasuna",
- "ProviderValue": "Hornitzailea: {0}",
"TaskKeyframeExtractorDescription": "Bideo fitxategietako fotograma gakoak ateratzen ditu HLS erreprodukzio-zerrenda zehatzagoak sortzeko. Zeregin honek denbora asko iraun dezake.",
- "HeaderRecordingGroups": "Grabaketa taldeak",
"Inherit": "Oinordetu",
"TaskOptimizeDatabaseDescription": "Datu-basea trinkotu eta bertatik espazioa askatzen du. Liburutegia eskaneatu ondoren edo datu-basean aldaketak egin ondoren ataza hau exekutatzeak errendimendua hobetu lezake.",
"TaskKeyframeExtractor": "Fotograma gakoen erauzgailua",
@@ -135,5 +106,7 @@
"TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.",
"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"
+ "CleanupUserDataTask": "Erabiltzaileen datuak garbitzeko zeregina",
+ "LyricDownloadFailureFromForItem": "Ezin izan dira {1}-ren letrak deskargatu {0}-tik",
+ "Original": "Jatorrizkoa"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index 435485db7c..17ed54b117 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -1,41 +1,24 @@
{
- "Albums": "آلبوم‌ها",
"AppDeviceValues": "برنامه: {0} ، دستگاه: {1}",
- "Application": "برنامه",
"Artists": "هنرمندان",
"AuthenticationSucceededWithUserName": "{0} با موفقیت تایید اعتبار شد",
"Books": "کتاب‌ها",
- "CameraImageUploadedFrom": "یک عکس جدید از دوربین ارسال شده است {0}",
- "Channels": "کانالها",
"ChapterNameValue": "قسمت {0}",
"Collections": "مجموعه‌ها",
- "DeviceOfflineWithName": "ارتباط {0} قطع شد",
- "DeviceOnlineWithName": "{0} متصل شد",
"FailedLoginAttemptWithUserName": "تلاش برای ورود از {0} ناموفق بود",
"Favorites": "مورد علاقه‌ها",
"Folders": "پوشه‌ها",
"Genres": "ژانرها",
- "HeaderAlbumArtists": "هنرمندان آلبوم",
"HeaderContinueWatching": "ادامه تماشا",
- "HeaderFavoriteAlbums": "آلبوم‌های مورد علاقه",
- "HeaderFavoriteArtists": "هنرمندان مورد علاقه",
"HeaderFavoriteEpisodes": "قسمت‌های مورد علاقه",
"HeaderFavoriteShows": "سریال‌های مورد علاقه",
- "HeaderFavoriteSongs": "آهنگ‌های مورد علاقه",
"HeaderLiveTV": "پخش زنده",
"HeaderNextUp": "قسمت بعدی",
- "HeaderRecordingGroups": "گروه‌های ضبط",
"HomeVideos": "ویدیوهای خانگی",
"Inherit": "به ارث برده",
- "ItemAddedWithName": "{0} به کتابخانه افزوده شد",
- "ItemRemovedWithName": "{0} از کتابخانه حذف شد",
"LabelIpAddressValue": "آدرس آی پی: {0}",
"LabelRunningTimeValue": "زمان اجرا: {0}",
"Latest": "جدیدترین‌ها",
- "MessageApplicationUpdated": "سرور Jellyfin بروزرسانی شد",
- "MessageApplicationUpdatedTo": "سرور Jellyfin به نسخه {0} بروزرسانی شد",
- "MessageNamedServerConfigurationUpdatedWithValue": "پکربندی بخش {0} سرور بروزرسانی شد",
- "MessageServerConfigurationUpdated": "پیکربندی سرور بروزرسانی شد",
"MixedContent": "محتوای مخلوط",
"Movies": "فیلم ها",
"Music": "موسیقی",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "پخش ویدیو آغاز شد",
"NotificationOptionVideoPlaybackStopped": "پخش ویدیو متوقف شد",
"Photos": "عکس‌ها",
- "Playlists": "لیست‌های پخش",
- "Plugin": "افزونه",
"PluginInstalledWithName": "{0} نصب شد",
"PluginUninstalledWithName": "{0} حذف شد",
"PluginUpdatedWithName": "{0} آپدیت شد",
- "ProviderValue": "ارائه دهنده: {0}",
"ScheduledTaskFailedWithName": "{0} شکست خورد",
- "ScheduledTaskStartedWithName": "{0} شروع شد",
- "ServerNameNeedsToBeRestarted": "{0} نیاز به راه اندازی مجدد دارد",
"Shows": "سریال‌ها",
- "Songs": "موسیقی‌ها",
"StartupEmbyServerIsLoading": "سرور Jellyfin در حال بارگیری است. لطفا کمی بعد دوباره تلاش کنید.",
"SubtitleDownloadFailureFromForItem": "بارگیری زیرنویس برای {1} از {0} شکست خورد",
- "Sync": "همگام‌سازی",
- "System": "سیستم",
"TvShows": "سریال‌های تلویزیونی",
- "User": "کاربر",
"UserCreatedWithName": "کاربر {0} ایجاد شد",
"UserDeletedWithName": "کاربر {0} حذف شد",
"UserDownloadingItemWithValues": "{0} در حال بارگیری {1} می‌باشد",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "ارتباط {0} از {1} قطع شد",
"UserOnlineFromDevice": "{0} از {1} آنلاین می‌باشد",
"UserPasswordChangedWithName": "گذرواژه برای کاربر {0} تغییر کرد",
- "UserPolicyUpdatedWithName": "سیاست کاربری برای {0} بروزرسانی شد",
"UserStartedPlayingItemWithValues": "{0} در حال پخش {1} بر روی {2} است",
"UserStoppedPlayingItemWithValues": "{0} پخش {1} را بر روی {2} به پایان رساند",
- "ValueHasBeenAddedToLibrary": "{0} به کتابخانه‌ی رسانه‌ی شما افزوده شد",
- "ValueSpecialEpisodeName": "ویژه - {0}",
"VersionNumber": "نسخه {0}",
"TaskCleanTranscodeDescription": "فایل‌های کدگذاری که قدیمی‌تر از یک روز هستند را حذف می‌کند.",
"TaskCleanTranscode": "پاکسازی مسیر کد گذاری",
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index d3237db8b0..d080eb0236 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -8,57 +8,33 @@
"Music": "Musiikki",
"Movies": "Elokuvat",
"MixedContent": "Sekalainen sisältö",
- "MessageServerConfigurationUpdated": "Palvelimen asetukset on päivitetty",
- "MessageNamedServerConfigurationUpdatedWithValue": "Palvelimen asetusten osio {0} on päivitetty",
- "MessageApplicationUpdatedTo": "Jellyfin-palvelin on päivitetty versioon {0}",
- "MessageApplicationUpdated": "Jellyfin-palvelin on päivitetty",
"Latest": "Viimeisimmät",
"LabelRunningTimeValue": "Kesto: {0}",
"LabelIpAddressValue": "IP-osoite: {0}",
- "ItemRemovedWithName": "{0} poistettiin kirjastosta",
- "ItemAddedWithName": "{0} lisättiin kirjastoon",
"Inherit": "Peri",
"HomeVideos": "Kotivideot",
- "HeaderRecordingGroups": "Tallennusryhmät",
"HeaderNextUp": "Seuraavaksi",
- "HeaderFavoriteSongs": "Suosikkikappaleet",
"HeaderFavoriteShows": "Suosikkisarjat",
"HeaderFavoriteEpisodes": "Suosikkijaksot",
- "HeaderFavoriteArtists": "Suosikkiesittäjät",
- "HeaderFavoriteAlbums": "Suosikkialbumit",
"HeaderContinueWatching": "Jatka katselua",
- "HeaderAlbumArtists": "Albumin esittäjät",
"Genres": "Tyylilajit",
"Folders": "Kansiot",
"Favorites": "Suosikit",
"FailedLoginAttemptWithUserName": "Epäonnistunut kirjautumisyritys lähteestä \"{0}\"",
- "DeviceOnlineWithName": "{0} on yhdistetty",
- "DeviceOfflineWithName": "{0} on katkaissut yhteyden",
"Collections": "Kokoelmat",
"ChapterNameValue": "Kappale {0}",
- "Channels": "Kanavat",
- "CameraImageUploadedFrom": "Uusi kameran kuva on sirretty lähteestä {0}",
"Books": "Kirjat",
"AuthenticationSucceededWithUserName": "{0} todennus onnistunut",
"Artists": "Artistit",
- "Application": "Sovellus",
"AppDeviceValues": "Sovellus: {0}, Laite: {1}",
- "Albums": "Albumit",
- "User": "Käyttäjä",
- "System": "Järjestelmä",
"ScheduledTaskFailedWithName": "{0} epäonnistui",
"PluginUpdatedWithName": "{0} päivitettiin",
"PluginInstalledWithName": "{0} asennettiin",
"Photos": "Valokuvat",
- "ScheduledTaskStartedWithName": "\"{0}\" käynnistetty",
"PluginUninstalledWithName": "{0} poistettiin",
- "Playlists": "Soittolistat",
"VersionNumber": "Versio {0}",
- "ValueSpecialEpisodeName": "Erikoisjakso - {0}",
- "ValueHasBeenAddedToLibrary": "\"{0}\" on lisätty mediakirjastoon",
"UserStoppedPlayingItemWithValues": "{0} lopetti kohteen \"{1}\" toiston sijainnissa \"{2}\"",
"UserStartedPlayingItemWithValues": "{0} toistaa kohdetta \"{1}\" sijainnissa \"{2}\"",
- "UserPolicyUpdatedWithName": "Käyttäjän {0} käyttöoikeudet on päivitetty",
"UserPasswordChangedWithName": "Käyttäjän {0} salasana on vaihdettu",
"UserOnlineFromDevice": "{0} on yhdistänyt sijainnista \"{1}\"",
"UserOfflineFromDevice": "{0} on katkaissut yhteyden sijainnista \"{1}\"",
@@ -67,14 +43,9 @@
"UserDeletedWithName": "Käyttäjä {0} on poistettu",
"UserCreatedWithName": "Käyttäjä {0} on luotu",
"TvShows": "Sarjat",
- "Sync": "Synkronointi",
"SubtitleDownloadFailureFromForItem": "Tekstityksen lataus lähteestä \"{0}\" kohteelle \"{1}\" epäonnistui",
"StartupEmbyServerIsLoading": "Jellyfin-palvelin on latautumassa. Yritä hetken kuluttua uudelleen.",
- "Songs": "Kappaleet",
"Shows": "Sarjat",
- "ServerNameNeedsToBeRestarted": "\"{0}\" on käynnistettävä uudelleen",
- "ProviderValue": "Lähde: {0}",
- "Plugin": "Lisäosa",
"NotificationOptionVideoPlaybackStopped": "Videon toisto lopetettu",
"NotificationOptionVideoPlayback": "Videon toisto aloitettu",
"NotificationOptionUserLockedOut": "Käyttäjä on lukittu",
@@ -135,5 +106,7 @@
"TaskMoveTrickplayImages": "Siirrä Trickplay-kuvien sijainti",
"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."
+ "CleanupUserDataTaskDescription": "Puhdistaa kaikki käyttäjätiedot (katselutila, suosikit ym.) medioista, joita ei ole ollut saatavilla yli 90 päivään.",
+ "LyricDownloadFailureFromForItem": "Sanoitusten lataus kohteesta {0} kappaleelle {1} epäonnistui",
+ "Original": "Alkuperäinen"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json
index 28c1d2be5e..30d90e9f98 100644
--- a/Emby.Server.Implementations/Localization/Core/fil.json
+++ b/Emby.Server.Implementations/Localization/Core/fil.json
@@ -1,10 +1,7 @@
{
"VersionNumber": "Bersyon {0}",
- "ValueSpecialEpisodeName": "Espesyal - {0}",
- "ValueHasBeenAddedToLibrary": "Naidagdag na ang {0} sa iyong librerya ng medya",
"UserStoppedPlayingItemWithValues": "Natapos ni {0} ang {1} sa {2}",
"UserStartedPlayingItemWithValues": "Si {0} ay nagpla-play ng {1} sa {2}",
- "UserPolicyUpdatedWithName": "Ang user policy ay nai-update para kay {0}",
"UserPasswordChangedWithName": "Napalitan na ang password ni {0}",
"UserOnlineFromDevice": "Si {0} ay naka-konekta galing sa {1}",
"UserOfflineFromDevice": "Si {0} ay na-diskonekta galing sa {1}",
@@ -12,23 +9,14 @@
"UserDownloadingItemWithValues": "Nagdadownload si {0} ng {1}",
"UserDeletedWithName": "Natanggal na is user {0}",
"UserCreatedWithName": "Nagawa na si user {0}",
- "User": "User",
"TvShows": "Mga Palabas sa Telebisyon",
- "System": "Sistema",
- "Sync": "Pag-sync",
"SubtitleDownloadFailureFromForItem": "Hindi nai-download ang subtitles {0} para sa {1}",
"StartupEmbyServerIsLoading": "Naglo-load ang Jellyfin Server. Mangyaring subukan ulit sandali.",
- "Songs": "Mga Kanta",
"Shows": "Mga Pelikula",
- "ServerNameNeedsToBeRestarted": "Kailangan irestart ang {0}",
- "ScheduledTaskStartedWithName": "Nagsimula na ang {0}",
"ScheduledTaskFailedWithName": "Hindi gumana ang {0}",
- "ProviderValue": "Tagapagtustos: {0}",
"PluginUpdatedWithName": "Naiupdate na ang {0}",
"PluginUninstalledWithName": "Naiuninstall na ang {0}",
"PluginInstalledWithName": "Nainstall na ang {0}",
- "Plugin": "Plugin",
- "Playlists": "Mga Playlist",
"Photos": "Mga Larawan",
"NotificationOptionVideoPlaybackStopped": "Huminto na ang pelikula",
"NotificationOptionVideoPlayback": "Nagsimula na ang pelikula",
@@ -54,42 +42,25 @@
"Music": "Mga Kanta",
"Movies": "Mga Pelikula",
"MixedContent": "Halo-halong content",
- "MessageServerConfigurationUpdated": "Naiupdate na ang server configuration",
- "MessageNamedServerConfigurationUpdatedWithValue": "Naiupdate na ang server configuration section {0}",
- "MessageApplicationUpdatedTo": "Ang bersyon ng Jellyfin Server ay naiupdate sa {0}",
- "MessageApplicationUpdated": "Naiupdate na ang Jellyfin Server",
"Latest": "Pinakabago",
"LabelRunningTimeValue": "Oras: {0}",
"LabelIpAddressValue": "IP address: {0}",
- "ItemRemovedWithName": "Naitanggal ang {0} sa librerya",
- "ItemAddedWithName": "Naidagdag ang {0} sa librerya",
"Inherit": "Manahin",
- "HeaderRecordingGroups": "Pagtatalang Grupo",
"HeaderNextUp": "Susunod",
"HeaderLiveTV": "Live TV",
- "HeaderFavoriteSongs": "Mga Paboritong Kanta",
"HeaderFavoriteShows": "Mga Paboritong Pelikula",
"HeaderFavoriteEpisodes": "Mga Paboritong Yugto",
- "HeaderFavoriteArtists": "Mga Paboritong Artista",
- "HeaderFavoriteAlbums": "Mga Paboritong Album",
"HeaderContinueWatching": "Magpatuloy sa Panonood",
- "HeaderAlbumArtists": "Mga Artista ng Album",
"Genres": "Mga Kategorya",
"Folders": "Mga Folder",
"Favorites": "Mga Paborito",
"FailedLoginAttemptWithUserName": "Maling login galing kay/sa {0}",
- "DeviceOnlineWithName": "Nakakonekta si/ang {0}",
- "DeviceOfflineWithName": "Nadiskonekta si/ang {0}",
"Collections": "Mga Koleksyon",
"ChapterNameValue": "Kabanata {0}",
- "Channels": "Mga Channel",
- "CameraImageUploadedFrom": "May bagong larawan na naupload galing sa/kay {0}",
"Books": "Mga Libro",
"AuthenticationSucceededWithUserName": "Napatunayan si/ang {0}",
"Artists": "Mga Artista",
- "Application": "Aplikasyon",
"AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}",
- "Albums": "Mga Album",
"TaskRefreshLibrary": "Suriin and Librerya ng Medya",
"TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata.",
"TaskRefreshChapterImages": "Kunin ang mga larawan ng kabanata",
diff --git a/Emby.Server.Implementations/Localization/Core/fo.json b/Emby.Server.Implementations/Localization/Core/fo.json
index 044abc7fa3..4fb9f4329c 100644
--- a/Emby.Server.Implementations/Localization/Core/fo.json
+++ b/Emby.Server.Implementations/Localization/Core/fo.json
@@ -2,21 +2,15 @@
"Artists": "Listafólk",
"Collections": "Søvn",
"Default": "Sjálvgildi",
- "DeviceOfflineWithName": "{0} hevur slitið sambandið",
"External": "Ytri",
"Genres": "Greinar",
- "Albums": "Album",
"AppDeviceValues": "App: {0}, Eind: {1}",
- "Application": "Nýtsluskipan",
"Books": "Bøkur",
- "Channels": "Rásir",
"ChapterNameValue": "Kapittul {0}",
- "DeviceOnlineWithName": "{0} er sambundið",
"Favorites": "Yndis",
"Folders": "Mappur",
"Forced": "Kravt",
"FailedLoginAttemptWithUserName": "Miseydnað innritanarroynd frá {0}",
"HeaderFavoriteEpisodes": "Yndispartar",
- "HeaderFavoriteSongs": "Yndissangir",
"LabelIpAddressValue": "IP atsetur: {0}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index b05e0d10b4..e05cce47b0 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -1,41 +1,24 @@
{
- "Albums": "Albums",
"AppDeviceValues": "App : {0}, Appareil : {1}",
- "Application": "Application",
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
"Books": "Livres",
- "CameraImageUploadedFrom": "Une nouvelle photo a été téléversée depuis {0}",
- "Channels": "Chaînes",
"ChapterNameValue": "Chapitre {0}",
"Collections": "Collections",
- "DeviceOfflineWithName": "{0} s'est déconnecté",
- "DeviceOnlineWithName": "{0} est connecté",
"FailedLoginAttemptWithUserName": "Tentative de connexion échouée par {0}",
"Favorites": "Favoris",
"Folders": "Dossiers",
"Genres": "Genres",
- "HeaderAlbumArtists": "Artistes de l'album",
"HeaderContinueWatching": "Reprendre le visionnement",
- "HeaderFavoriteAlbums": "Albums favoris",
- "HeaderFavoriteArtists": "Artistes favoris",
"HeaderFavoriteEpisodes": "Épisodes favoris",
"HeaderFavoriteShows": "Séries favorites",
- "HeaderFavoriteSongs": "Chansons favorites",
"HeaderLiveTV": "TV en direct",
"HeaderNextUp": "À Suivre",
- "HeaderRecordingGroups": "Groupes d'enregistrements",
"HomeVideos": "Vidéos personnelles",
"Inherit": "Hérite",
- "ItemAddedWithName": "{0} a été ajouté à la médiathèque",
- "ItemRemovedWithName": "{0} a été supprimé de la médiathèque",
"LabelIpAddressValue": "Adresse IP : {0}",
"LabelRunningTimeValue": "Durée : {0}",
"Latest": "Plus récent",
- "MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
- "MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour vers la version {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour",
- "MessageServerConfigurationUpdated": "La configuration du serveur a été mise à jour",
"MixedContent": "Contenu mixte",
"Movies": "Films",
"Music": "Musique",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Lecture vidéo démarrée",
"NotificationOptionVideoPlaybackStopped": "Lecture vidéo arrêtée",
"Photos": "Photos",
- "Playlists": "Listes de lecture",
- "Plugin": "Extension",
"PluginInstalledWithName": "{0} a été installé",
"PluginUninstalledWithName": "{0} a été désinstallé",
"PluginUpdatedWithName": "{0} a été mis à jour",
- "ProviderValue": "Fournisseur : {0}",
"ScheduledTaskFailedWithName": "{0} a échoué",
- "ScheduledTaskStartedWithName": "{0} a commencé",
- "ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
"Shows": "Séries",
- "Songs": "Chansons",
"StartupEmbyServerIsLoading": "Serveur Jellyfin en cours de chargement. Réessayez dans quelques instants.",
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
- "Sync": "Synchroniser",
- "System": "Système",
"TvShows": "Séries Télé",
- "User": "Utilisateur",
"UserCreatedWithName": "L'utilisateur {0} a été créé",
"UserDeletedWithName": "L'utilisateur {0} supprimé",
"UserDownloadingItemWithValues": "{0} télécharge {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} s'est déconnecté de {1}",
"UserOnlineFromDevice": "{0} s'est connecté de {1}",
"UserPasswordChangedWithName": "Le mot de passe de utilisateur {0} a été modifié",
- "UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}",
"UserStartedPlayingItemWithValues": "{0} joue {1} sur {2}",
"UserStoppedPlayingItemWithValues": "{0} a terminé la lecture de {1} sur {2}",
- "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque",
- "ValueSpecialEpisodeName": "Spécial - {0}",
"VersionNumber": "Version {0}",
"TasksLibraryCategory": "Médiathèque",
"TasksMaintenanceCategory": "Entretien",
@@ -135,5 +106,7 @@
"TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay",
"TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.",
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
- "CleanupUserDataTask": "Tâche de nettoyage des données utilisateur"
+ "CleanupUserDataTask": "Tâche de nettoyage des données utilisateur",
+ "LyricDownloadFailureFromForItem": "Le téléchargement des paroles a échoué de {0} pour {1}",
+ "Original": "Original"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 8937b1d0c9..ceba1dcb41 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -1,41 +1,24 @@
{
- "Albums": "Albums",
"AppDeviceValues": "Application : {0}, Appareil : {1}",
- "Application": "Application",
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
"Books": "Livres",
- "CameraImageUploadedFrom": "Une photo a été téléchargée depuis {0}",
- "Channels": "Chaînes",
"ChapterNameValue": "Chapitre {0}",
"Collections": "Collections",
- "DeviceOfflineWithName": "{0} s'est déconnecté",
- "DeviceOnlineWithName": "{0} est connecté",
"FailedLoginAttemptWithUserName": "Échec de connexion depuis {0}",
"Favorites": "Favoris",
"Folders": "Dossiers",
"Genres": "Genres",
- "HeaderAlbumArtists": "Artistes d'albums",
"HeaderContinueWatching": "Continuer de regarder",
- "HeaderFavoriteAlbums": "Albums favoris",
- "HeaderFavoriteArtists": "Artistes préférés",
"HeaderFavoriteEpisodes": "Épisodes favoris",
"HeaderFavoriteShows": "Séries favorites",
- "HeaderFavoriteSongs": "Chansons préférées",
"HeaderLiveTV": "TV en direct",
"HeaderNextUp": "Prochain à venir",
- "HeaderRecordingGroups": "Groupes d'enregistrements",
"HomeVideos": "Vidéos personnelles",
"Inherit": "Hériter",
- "ItemAddedWithName": "{0} a été ajouté à la médiathèque",
- "ItemRemovedWithName": "{0} a été supprimé de la médiathèque",
"LabelIpAddressValue": "Adresse IP : {0}",
"LabelRunningTimeValue": "Durée : {0}",
"Latest": "Derniers",
- "MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
- "MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour en version {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour",
- "MessageServerConfigurationUpdated": "La configuration du serveur a été mise à jour",
"MixedContent": "Contenu mixte",
"Movies": "Films",
"Music": "Musique",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Lecture vidéo démarrée",
"NotificationOptionVideoPlaybackStopped": "Lecture vidéo arrêtée",
"Photos": "Photos",
- "Playlists": "Listes de lecture",
- "Plugin": "Extension",
"PluginInstalledWithName": "{0} a été installé",
"PluginUninstalledWithName": "{0} a été désinstallé",
"PluginUpdatedWithName": "{0} a été mis à jour",
- "ProviderValue": "Fournisseur : {0}",
"ScheduledTaskFailedWithName": "{0} a échoué",
- "ScheduledTaskStartedWithName": "{0} a démarré",
- "ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
"Shows": "Séries",
- "Songs": "Chansons",
"StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.",
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
- "Sync": "Synchroniser",
- "System": "Système",
"TvShows": "Séries TV",
- "User": "Utilisateur",
"UserCreatedWithName": "L'utilisateur {0} a été créé",
"UserDeletedWithName": "L'utilisateur {0} a été supprimé",
"UserDownloadingItemWithValues": "{0} est en train de télécharger {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} s'est déconnecté depuis {1}",
"UserOnlineFromDevice": "{0} s'est connecté depuis {1}",
"UserPasswordChangedWithName": "Le mot de passe pour l'utilisateur {0} a été modifié",
- "UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}",
"UserStartedPlayingItemWithValues": "{0} est en train de lire {1} sur {2}",
"UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}",
- "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque",
- "ValueSpecialEpisodeName": "Spécial - {0}",
"VersionNumber": "Version {0}",
"TasksChannelsCategory": "Chaînes en ligne",
"TaskDownloadMissingSubtitlesDescription": "Recherche les sous-titres manquants sur Internet en se basant sur la configuration des métadonnées.",
@@ -135,5 +106,7 @@
"TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.",
"TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.",
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
- "CleanupUserDataTask": "Tâche de nettoyage des données utilisateur"
+ "CleanupUserDataTask": "Tâche de nettoyage des données utilisateur",
+ "LyricDownloadFailureFromForItem": "Le téléchargement des paroles à échoué de {0} pour {1}",
+ "Original": "Original"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ga.json b/Emby.Server.Implementations/Localization/Core/ga.json
index 5742e6224d..1ee606cc64 100644
--- a/Emby.Server.Implementations/Localization/Core/ga.json
+++ b/Emby.Server.Implementations/Localization/Core/ga.json
@@ -1,15 +1,10 @@
{
- "Albums": "Albaim",
"Artists": "Ealaíontóirí",
"AuthenticationSucceededWithUserName": "D'éirigh le fíordheimhniú {0}",
"Books": "Leabhair",
- "CameraImageUploadedFrom": "Uaslódáladh íomhá ceamara nua ó {0}",
- "Channels": "Cainéil",
"ChapterNameValue": "Caibidil {0}",
"Collections": "Bailiúcháin",
"Default": "Réamhshocrú",
- "DeviceOfflineWithName": "Tá {0} dícheangailte",
- "DeviceOnlineWithName": "Tá {0} nasctha",
"External": "Seachtrach",
"FailedLoginAttemptWithUserName": "Theip ar iarracht logáil isteach ó {0}",
"Favorites": "Ceanáin",
@@ -36,32 +31,20 @@
"TaskOptimizeDatabaseDescription": "Comhdhlúthaíonn bunachar sonraí agus gearrtar spás saor in aisce. Má ritheann tú an tasc seo tar éis scanadh a dhéanamh ar an leabharlann nó athruithe eile a dhéanamh a thugann le tuiscint gur cheart go bhfeabhsófaí an fheidhmíocht.",
"TaskMoveTrickplayImagesDescription": "Bogtar comhaid trickplay atá ann cheana de réir socruithe na leabharlainne.",
"AppDeviceValues": "Aip: {0}, Gléas: {1}",
- "Application": "Feidhmchlár",
"Folders": "Fillteáin",
"Forced": "Éigean",
"Genres": "Seánraí",
- "HeaderAlbumArtists": "Ealaíontóirí albam",
"HeaderContinueWatching": "Leanúint ar aghaidh ag Breathnú",
- "HeaderFavoriteAlbums": "Albam is fearr leat",
- "HeaderFavoriteArtists": "Ealaíontóirí is Fearr",
"HeaderFavoriteEpisodes": "Eipeasóid is fearr leat",
"HeaderFavoriteShows": "Seónna is Fearr",
- "HeaderFavoriteSongs": "Amhráin is fearr leat",
"HeaderLiveTV": "Teilifís beo",
"HeaderNextUp": "Ar Aghaidh Suas",
- "HeaderRecordingGroups": "Grúpaí Taifeadta",
"HearingImpaired": "Lag éisteachta",
"HomeVideos": "Físeáin Baile",
"Inherit": "Oidhreacht",
- "ItemAddedWithName": "Cuireadh {0} leis an leabharlann",
- "ItemRemovedWithName": "Baineadh {0} den leabharlann",
"LabelIpAddressValue": "Seoladh IP: {0}",
"LabelRunningTimeValue": "Am rite: {0}",
"Latest": "Is déanaí",
- "MessageApplicationUpdated": "Tá Freastalaí Jellyfin nuashonraithe",
- "MessageApplicationUpdatedTo": "Nuashonraíodh Freastalaí Jellyfin go {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Nuashonraíodh an chuid cumraíochta freastalaí {0}",
- "MessageServerConfigurationUpdated": "Nuashonraíodh cumraíocht an fhreastalaí",
"MixedContent": "Ábhar measctha",
"Movies": "Scannáin",
"Music": "Ceol",
@@ -87,24 +70,15 @@
"NotificationOptionVideoPlayback": "Cuireadh tús le hathsheinm físe",
"NotificationOptionVideoPlaybackStopped": "Cuireadh deireadh le hathsheinm físe",
"Photos": "Grianghraif",
- "Playlists": "Seinmliostaí",
- "Plugin": "Breiseán",
"PluginInstalledWithName": "Suiteáladh {0}",
"PluginUninstalledWithName": "Díshuiteáladh {0}",
"PluginUpdatedWithName": "Nuashonraíodh {0}",
- "ProviderValue": "Soláthraí: {0}",
"ScheduledTaskFailedWithName": "Theip ar {0}",
- "ScheduledTaskStartedWithName": "Thosaigh {0}",
- "ServerNameNeedsToBeRestarted": "Ní mór {0} a atosú",
"Shows": "Seónna",
- "Songs": "Amhráin",
"StartupEmbyServerIsLoading": "Tá freastalaí Jellyfin á luchtú. Bain triail eile as gan mhoill.",
"SubtitleDownloadFailureFromForItem": "Theip ar fhotheidil a íoslódáil ó {0} le haghaidh {1}",
- "Sync": "Sioncrónaigh",
- "System": "Córas",
"TvShows": "Seónna Teilifíse",
"Undefined": "Neamhshainithe",
- "User": "Úsáideoir",
"UserCreatedWithName": "Cruthaíodh úsáideoir {0}",
"UserDeletedWithName": "Scriosadh úsáideoir {0}",
"UserDownloadingItemWithValues": "Tá {0} á íoslódáil {1}",
@@ -112,11 +86,8 @@
"UserOfflineFromDevice": "Tá {0} dícheangailte ó {1}",
"UserOnlineFromDevice": "Tá {0} ar líne ó {1}",
"UserPasswordChangedWithName": "Athraíodh pasfhocal don úsáideoir {0}",
- "UserPolicyUpdatedWithName": "Nuashonraíodh polasaí úsáideora le haghaidh {0}",
"UserStartedPlayingItemWithValues": "Tá {0} ag seinnt {1} ar {2}",
"UserStoppedPlayingItemWithValues": "Chríochnaigh {0} ag imirt {1} ar {2}",
- "ValueHasBeenAddedToLibrary": "Cuireadh {0} le do leabharlann meán",
- "ValueSpecialEpisodeName": "Speisialta - {0}",
"VersionNumber": "Leagan {0}",
"TasksMaintenanceCategory": "Cothabháil",
"TasksLibraryCategory": "Leabharlann",
@@ -135,5 +106,7 @@
"TaskCleanTranscode": "Eolaire Transcode Glan",
"TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh",
"CleanupUserDataTask": "Tasc glantacháin sonraí úsáideora",
- "CleanupUserDataTaskDescription": "Glanann sé gach sonraí úsáideora (stádas faire, stádas is fearr leat srl.) ó mheáin nach bhfuil i láthair a thuilleadh ar feadh 90 lá ar a laghad."
+ "CleanupUserDataTaskDescription": "Glanann sé gach sonraí úsáideora (stádas faire, stádas is fearr leat srl.) ó mheáin nach bhfuil i láthair a thuilleadh ar feadh 90 lá ar a laghad.",
+ "Original": "Bunaidh",
+ "LyricDownloadFailureFromForItem": "Theip ar liricí a íoslódáil ó {0} do {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json
index fc5c3fd53d..d3740130ee 100644
--- a/Emby.Server.Implementations/Localization/Core/gl.json
+++ b/Emby.Server.Implementations/Localization/Core/gl.json
@@ -1,13 +1,9 @@
{
- "Albums": "Álbums",
"Collections": "Coleccións",
"ChapterNameValue": "Capítulo {0}",
- "Channels": "Canles",
- "CameraImageUploadedFrom": "Cargouse unha nova imaxe de cámara dende {0}",
"Books": "Libros",
"AuthenticationSucceededWithUserName": "{0} autenticouse correctamente",
"Artists": "Artistas",
- "Application": "Aplicación",
"NotificationOptionServerRestartRequired": "Necesario o reinicio do servidor",
"NotificationOptionPluginUpdateInstalled": "Actualización do plugin instalada",
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
@@ -28,63 +24,41 @@
"Music": "Música",
"Movies": "Películas",
"MixedContent": "Contido mixto",
- "MessageServerConfigurationUpdated": "Actualizouse a configuración do servidor",
- "MessageNamedServerConfigurationUpdatedWithValue": "Actualizouse a sección de configuración {0} do servidor",
- "MessageApplicationUpdatedTo": "O servidor Jellyfin actualizouse a {0}",
- "MessageApplicationUpdated": "O servidor Jellyfin actualizouse",
"Latest": "Último",
"LabelRunningTimeValue": "Tempo en execución: {0}",
"LabelIpAddressValue": "Enderezo IP: {0}",
- "ItemRemovedWithName": "{0} eliminouse da biblioteca",
- "ItemAddedWithName": "{0} engadiuse á biblioteca",
"Inherit": "Herdar",
"HomeVideos": "Videos caseiros",
- "HeaderRecordingGroups": "Grupos de grabación",
"HeaderNextUp": "De seguido",
"HeaderLiveTV": "TV en directo",
- "HeaderFavoriteSongs": "Cancións favoritas",
"HeaderFavoriteShows": "Series de TV favoritas",
"HeaderFavoriteEpisodes": "Episodios favoritos",
- "HeaderFavoriteArtists": "Artistas favoritos",
- "HeaderFavoriteAlbums": "Álbums favoritos",
"HeaderContinueWatching": "Seguir vendo",
- "HeaderAlbumArtists": "Artistas do álbum",
"Genres": "Xéneros",
"Forced": "Forzado",
"Folders": "Cartafoles",
"Favorites": "Favoritos",
"FailedLoginAttemptWithUserName": "Fallo de intento de inicio de sesión dende {0}",
- "DeviceOnlineWithName": "{0} conectouse",
- "DeviceOfflineWithName": "{0} desconectouse",
"Default": "Por defecto",
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"TaskCleanLogs": "Limpar directorio de rexistros",
"TaskCleanActivityLog": "Limpar rexistro de actividade",
"TasksChannelsCategory": "Canles da Internet",
"TaskUpdatePlugins": "Actualizar plugins",
- "User": "Usuario",
"Undefined": "Sen definir",
"TvShows": "Programas de TV",
- "System": "Sistema",
- "Sync": "Sincronizar",
"SubtitleDownloadFailureFromForItem": "Fallou a descarga de subtítulos para {1} dende {0}",
"StartupEmbyServerIsLoading": "O servidor Jellyfin está cargando. Por favor, ténteo axiña outra vez.",
- "Songs": "Cancións",
"Shows": "Programas",
- "ServerNameNeedsToBeRestarted": "{0} precisa ser reiniciado",
- "ScheduledTaskStartedWithName": "{0} comezou",
"ScheduledTaskFailedWithName": "{0} fallou",
- "ProviderValue": "Provedor: {0}",
"PluginUpdatedWithName": "{0} foi actualizado",
"PluginUninstalledWithName": "{0} foi desinstalado",
"PluginInstalledWithName": "{0} foi instalado",
- "Playlists": "Listas de reproducción",
"Photos": "Fotos",
"UserLockedOutWithName": "O usuario {0} foi bloqueado",
"UserDownloadingItemWithValues": "{0} está a ser transferido {1}",
"UserDeletedWithName": "O usuario {0} foi borrado",
"UserCreatedWithName": "O usuario {0} foi creado",
- "Plugin": "Plugin",
"NotificationOptionVideoPlaybackStopped": "Reproducción de vídeo detida",
"NotificationOptionVideoPlayback": "Reproducción de vídeo iniciada",
"NotificationOptionUserLockedOut": "Usuario bloqueado",
@@ -109,12 +83,9 @@
"TaskCleanCache": "Limpar directorio de caché",
"TaskCleanActivityLogDescription": "Borra do rexistro de actividade as entradas anteriores á data configurada.",
"TasksApplicationCategory": "Aplicación",
- "ValueSpecialEpisodeName": "Especial - {0}",
- "ValueHasBeenAddedToLibrary": "{0} engadiuse á túa biblioteca de medios",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Mantemento",
"VersionNumber": "Versión {0}",
- "UserPolicyUpdatedWithName": "A política de usuario foi actualizada para {0}",
"UserPasswordChangedWithName": "Cambiouse o contrasinal para o usuario {0}",
"UserOnlineFromDevice": "{0} está en liña desde {1}",
"UserOfflineFromDevice": "{0} desconectouse dende {1}",
diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json
index 9be6f05ee1..b95e5edecc 100644
--- a/Emby.Server.Implementations/Localization/Core/gsw.json
+++ b/Emby.Server.Implementations/Localization/Core/gsw.json
@@ -1,41 +1,24 @@
{
- "Albums": "Alben",
"AppDeviceValues": "App: {0}, Gerät: {1}",
- "Application": "Applikation",
"Artists": "Künstler",
"AuthenticationSucceededWithUserName": "{0} hat sich angemeldet",
"Books": "Bücher",
- "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
- "Channels": "Kanäle",
"ChapterNameValue": "Kapitel {0}",
"Collections": "Sammlungen",
- "DeviceOfflineWithName": "{0} wurde getrennt",
- "DeviceOnlineWithName": "{0} ist verbunden",
"FailedLoginAttemptWithUserName": "Fählgschlagene Ameldeversuech vo {0}",
"Favorites": "Favorite",
"Folders": "Ordner",
"Genres": "Genre",
- "HeaderAlbumArtists": "Album-Künschtler",
"HeaderContinueWatching": "weiter schauen",
- "HeaderFavoriteAlbums": "Lieblingsalben",
- "HeaderFavoriteArtists": "Lieblings-Künstler",
"HeaderFavoriteEpisodes": "Lieblingsepisoden",
"HeaderFavoriteShows": "Lieblingsserien",
- "HeaderFavoriteSongs": "Lieblingslieder",
"HeaderLiveTV": "Live-Fernseh",
"HeaderNextUp": "Als Nächstes",
- "HeaderRecordingGroups": "Aufnahme-Gruppen",
"HomeVideos": "Heimvideos",
"Inherit": "Vererben",
- "ItemAddedWithName": "{0} wurde der Bibliothek hinzugefügt",
- "ItemRemovedWithName": "{0} wurde aus der Bibliothek entfernt",
"LabelIpAddressValue": "IP-Adresse: {0}",
"LabelRunningTimeValue": "Laufzeit: {0}",
"Latest": "Neueste",
- "MessageApplicationUpdated": "Jellyfin-Server wurde aktualisiert",
- "MessageApplicationUpdatedTo": "Jellyfin-Server wurde auf Version {0} aktualisiert",
- "MessageNamedServerConfigurationUpdatedWithValue": "Der Server-Einstellungsbereich {0} wurde aktualisiert",
- "MessageServerConfigurationUpdated": "Serveriistöuige send aktualisiert worde",
"MixedContent": "Gmeschti Inhäut",
"Movies": "Film",
"Music": "Musig",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Videowedergab gstartet",
"NotificationOptionVideoPlaybackStopped": "Videowedergab gstoppt",
"Photos": "Fotis",
- "Playlists": "Wedergabeliste",
- "Plugin": "Plugin",
"PluginInstalledWithName": "{0} esch installiert worde",
"PluginUninstalledWithName": "{0} esch deinstalliert worde",
"PluginUpdatedWithName": "{0} esch updated worde",
- "ProviderValue": "Aabieter: {0}",
"ScheduledTaskFailedWithName": "{0} esch fäugschlage",
- "ScheduledTaskStartedWithName": "{0} het gstartet",
- "ServerNameNeedsToBeRestarted": "{0} mues nöi gstartet wärde",
"Shows": "Serie",
- "Songs": "Lieder",
"StartupEmbyServerIsLoading": "Jellyfin Server ladt. Bitte grad noeinisch probiere.",
"SubtitleDownloadFailureFromForItem": "Ondertetle vo {0} för {1} hend ned chönne abeglade wärde",
- "Sync": "Synchronisation",
- "System": "System",
"TvShows": "Färnsehserie",
- "User": "Benotzer",
"UserCreatedWithName": "Benotzer {0} esch erstöut worde",
"UserDeletedWithName": "Benotzer {0} esch glösche worde",
"UserDownloadingItemWithValues": "{0} ladt {1} abe",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} esch vo {1} trennt worde",
"UserOnlineFromDevice": "{0} esch online vo {1}",
"UserPasswordChangedWithName": "S'Passwort för Benotzer {0} esch gänderet worde",
- "UserPolicyUpdatedWithName": "Benotzerrechtlinie för {0} esch aktualisiert worde",
"UserStartedPlayingItemWithValues": "{0} hed d'Wedergab vo {1} of {2} gstartet",
"UserStoppedPlayingItemWithValues": "{0} het d'Wedergab vo {1} of {2} gstoppt",
- "ValueHasBeenAddedToLibrary": "{0} esch dinnere Biblithek hinzuegfüegt worde",
- "ValueSpecialEpisodeName": "Extra - {0}",
"VersionNumber": "Version {0}",
"TaskCleanLogs": "Lösche Log Pfad",
"TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 606f464503..af34bf092e 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -1,41 +1,24 @@
{
- "Albums": "אלבומים",
"AppDeviceValues": "יישום: {0}, מכשיר: {1}",
- "Application": "יישום",
"Artists": "אומנים",
"AuthenticationSucceededWithUserName": "{0} אומת בהצלחה",
"Books": "ספרים",
- "CameraImageUploadedFrom": "תמונת מצלמה חדשה הועלתה מתוך {0}",
- "Channels": "ערוצים",
"ChapterNameValue": "פרק {0}",
"Collections": "אוספים",
- "DeviceOfflineWithName": "{0} התנתק",
- "DeviceOnlineWithName": "{0} מחובר",
"FailedLoginAttemptWithUserName": "ניסיון כניסה שגוי דרך {0}",
"Favorites": "מועדפים",
"Folders": "תיקיות",
"Genres": "ז׳אנרים",
- "HeaderAlbumArtists": "אמני האלבום",
"HeaderContinueWatching": "המשך צפייה",
- "HeaderFavoriteAlbums": "אלבומים מועדפים",
- "HeaderFavoriteArtists": "אמנים מועדפים",
"HeaderFavoriteEpisodes": "פרקים מועדפים",
"HeaderFavoriteShows": "תוכניות מועדפות",
- "HeaderFavoriteSongs": "שירים מועדפים",
"HeaderLiveTV": "שידורים חיים",
"HeaderNextUp": "הבא בתור",
- "HeaderRecordingGroups": "קבוצות הקלטה",
"HomeVideos": "סרטונים בייתים",
"Inherit": "הורש",
- "ItemAddedWithName": "{0} נוסף לספרייה",
- "ItemRemovedWithName": "{0} נמחק מהספרייה",
"LabelIpAddressValue": "Ip כתובת: {0}",
"LabelRunningTimeValue": "משך צפייה: {0}",
"Latest": "אחרון",
- "MessageApplicationUpdated": "שרת Jellyfin עודכן",
- "MessageApplicationUpdatedTo": "שרת Jellyfin עודכן לגרסה {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "סעיף הגדרת השרת {0} עודכן",
- "MessageServerConfigurationUpdated": "תצורת השרת עודכנה",
"MixedContent": "תוכן מעורב",
"Movies": "סרטים",
"Music": "מוזיקה",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "ניגון וידאו החל",
"NotificationOptionVideoPlaybackStopped": "ניגון וידאו הופסק",
"Photos": "צילומים",
- "Playlists": "רשימות נגינה",
- "Plugin": "תוסף",
"PluginInstalledWithName": "{0} הותקן",
"PluginUninstalledWithName": "{0} הוסר",
"PluginUpdatedWithName": "{0} עודכן",
- "ProviderValue": "ספק: {0}",
"ScheduledTaskFailedWithName": "{0} נכשל",
- "ScheduledTaskStartedWithName": "{0} החל",
- "ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
"Shows": "סדרות",
- "Songs": "שירים",
"StartupEmbyServerIsLoading": "שרת Jellyfin בתהליך טעינה. נא לנסות שוב בקרוב.",
"SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה",
- "Sync": "סנכרון",
- "System": "מערכת",
"TvShows": "סדרות טלוויזיה",
- "User": "משתמש",
"UserCreatedWithName": "המשתמש {0} נוצר",
"UserDeletedWithName": "המשתמש {0} הוסר",
"UserDownloadingItemWithValues": "{0} מוריד את {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} התנתק מ־{1}",
"UserOnlineFromDevice": "{0} מחובר מ־{1}",
"UserPasswordChangedWithName": "הסיסמה שונתה עבור המשתמש {0}",
- "UserPolicyUpdatedWithName": "מדיניות המשתמש {0} עודכנה",
"UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}",
"UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}",
- "ValueHasBeenAddedToLibrary": "{0} התווסף לספריית המדיה שלך",
- "ValueSpecialEpisodeName": "מיוחד- {0}",
"VersionNumber": "גרסה {0}",
"TaskRefreshLibrary": "סרוק ספריית מדיה",
"TaskRefreshChapterImages": "חלץ תמונות פרקים",
diff --git a/Emby.Server.Implementations/Localization/Core/he_IL.json b/Emby.Server.Implementations/Localization/Core/he_IL.json
index e8812c8a1d..b551608fd0 100644
--- a/Emby.Server.Implementations/Localization/Core/he_IL.json
+++ b/Emby.Server.Implementations/Localization/Core/he_IL.json
@@ -1,27 +1,112 @@
{
"Books": "ספרים",
"NameSeasonNumber": "עונה {0}",
- "Channels": "ערוצים",
"Movies": "סרטים",
"Music": "מוזיקה",
"Collections": "אוספים",
- "Albums": "אלבומים",
- "Application": "אפליקציה",
"Artists": "אמנים",
"ChapterNameValue": "פרק {0}",
"External": "חיצונית",
"Favorites": "מועדפים",
"Folders": "תיקיות",
"Genres": "ז'אנרים",
- "HeaderAlbumArtists": "אמני אלבומים",
"HeaderContinueWatching": "להמשיך לצפות",
- "HeaderFavoriteAlbums": "אלבומים אהובים",
- "HeaderFavoriteArtists": "אמנים אהובים",
"HeaderFavoriteEpisodes": "פרקים אהובים",
"HeaderFavoriteShows": "תוכניות אהובות",
- "HeaderFavoriteSongs": "שירים אהובים",
"HeaderLiveTV": "טלוויזיה בשידור חי",
"HeaderNextUp": "הבא",
"HearingImpaired": "ללקויי שמיעה",
- "HomeVideos": "סרטונים ביתיים"
+ "HomeVideos": "סרטונים ביתיים",
+ "AppDeviceValues": "אפליקציה: {0}, מכשיר: {1}",
+ "AuthenticationSucceededWithUserName": "{0} אומת בהצלחה",
+ "Default": "בררת מחדל",
+ "FailedLoginAttemptWithUserName": "התחברות נכשלה מ {0}",
+ "Forced": "בכוח",
+ "Inherit": "ירש",
+ "LabelIpAddressValue": "כתובת IP: {0}",
+ "LabelRunningTimeValue": "זמן ריצה: {0}",
+ "Latest": "הכי חדש",
+ "LyricDownloadFailureFromForItem": "מילות שיר נכשלו לרדת מ{0} בשביל {1}",
+ "MixedContent": "תוכן מעורב",
+ "MusicVideos": "סרטוני מוזיקה",
+ "NameInstallFailed": "{0} התכנות כושלות",
+ "NameSeasonUnknown": "עונה לא ידוע",
+ "NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
+ "NotificationOptionApplicationUpdateAvailable": "גרסת אפליקציה חדשה זמינה להורדה",
+ "NotificationOptionApplicationUpdateInstalled": "עדכון אפליקציה הותקן",
+ "NotificationOptionAudioPlayback": "החלה השמעת אודיו",
+ "NotificationOptionAudioPlaybackStopped": "ניגון השמע הופסק",
+ "NotificationOptionCameraImageUploaded": "תמונת מצלמה עודכן",
+ "NotificationOptionInstallationFailed": "התקנה נכשלה",
+ "NotificationOptionNewLibraryContent": "תוכן חדש נוסף",
+ "NotificationOptionPluginError": "תוסף נכשל",
+ "NotificationOptionPluginInstalled": "תוסף הותקן",
+ "NotificationOptionPluginUninstalled": "תוסף נמחק",
+ "NotificationOptionPluginUpdateInstalled": "עידכון לתוסף הותקן",
+ "NotificationOptionServerRestartRequired": "נדרש התחול מחדש לשרת",
+ "NotificationOptionTaskFailed": "כשל במשימה מתוכננת",
+ "NotificationOptionUserLockedOut": "המשתמש ננעל",
+ "NotificationOptionVideoPlayback": "החלה הפעלת וידאו",
+ "NotificationOptionVideoPlaybackStopped": "הפעלת הסרטון הופסקה",
+ "Original": "מקורי",
+ "Photos": "תמונות",
+ "PluginInstalledWithName": "{0} הותקן",
+ "PluginUninstalledWithName": "{0} נמחק",
+ "PluginUpdatedWithName": "{0} עודכן",
+ "ScheduledTaskFailedWithName": "{0} נכשל",
+ "Shows": "סדרות",
+ "StartupEmbyServerIsLoading": "שרת Jellyfin נטען. אנא נסה שוב בקרוב.",
+ "SubtitleDownloadFailureFromForItem": "הורדת הכתוביות מ-{0} עבור {1} נכשלה",
+ "TvShows": "תוכניות טלויזיה",
+ "Undefined": "לא מוגדר",
+ "UserCreatedWithName": "המשתמש {0} נוצר",
+ "UserDeletedWithName": "המשתמש {0} נמחק",
+ "UserDownloadingItemWithValues": "{0} מוריד את {1}",
+ "UserLockedOutWithName": "המשתמש {0} ננעל בחוץ",
+ "UserOfflineFromDevice": "{0} התנתק מ-{1}",
+ "UserOnlineFromDevice": "{0} מחובר מ-{1}",
+ "UserPasswordChangedWithName": "הסיסמה שונתה עבור המשתמש {0}",
+ "UserStartedPlayingItemWithValues": "{0} מנגן ב-{1} ב-{2}",
+ "UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} ב-{2}",
+ "VersionNumber": "גרסה {0}",
+ "TasksMaintenanceCategory": "תחזוקה",
+ "TasksLibraryCategory": "ספריה",
+ "TasksApplicationCategory": "אפליקציה",
+ "TasksChannelsCategory": "ערוצי אינטרנט",
+ "TaskCleanActivityLog": "נקה יומן פעילות",
+ "TaskCleanActivityLogDescription": "מוחק רשומות יומן פעילות ישנות יותר מהגיל שהוגדר.",
+ "TaskCleanCache": "נקה ספריית מטמון",
+ "TaskCleanCacheDescription": "מוחק קבצי מטמון שאינם נחוצים עוד על ידי המערכת.",
+ "TaskRefreshChapterImages": "חלץ תמונות פרק",
+ "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות עבור סרטונים שיש להם פרקים.",
+ "TaskAudioNormalization": "נורמליזציה של שמע",
+ "TaskAudioNormalizationDescription": "סורק קבצים לאיתור נתוני נרמול שמע.",
+ "TaskRefreshLibrary": "סרוק ספריית מדיה",
+ "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך לאיתור קבצים חדשים ומרענן מטא-דאטה.",
+ "TaskCleanLogs": "נקה ספריית יומן",
+ "TaskCleanLogsDescription": "מוחק קבצי יומן שגילם עולה על {0} ימים.",
+ "TaskRefreshPeople": "רענן אנשים",
+ "TaskRefreshPeopleDescription": "מעדכן מטא-דאטה עבור שחקנים ובמאים בספריית המדיה שלך.",
+ "TaskRefreshTrickplayImages": "צור תמונות Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "יוצר תצוגות מקדימות של trickplay עבור סרטונים בספריות מופעלות.",
+ "TaskUpdatePlugins": "עדכן פלאגינים",
+ "TaskUpdatePluginsDescription": "מוריד ומתקין עדכונים עבור תוספים שתצורתם נקבעה לעדכון אוטומטי.",
+ "TaskCleanTranscode": "נקה ספריית קידוד",
+ "TaskCleanTranscodeDescription": "תמחוק את קבצי הקידוד בני יותר מיום.",
+ "TaskRefreshChannels": "רענן ערוצים",
+ "TaskRefreshChannelsDescription": "מרענן את פרטי ערוץ האינטרנט.",
+ "TaskDownloadMissingLyrics": "הורד מילות שיר חסרות",
+ "TaskDownloadMissingLyricsDescription": "הורדות מילים לשירים",
+ "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות",
+ "TaskDownloadMissingSubtitlesDescription": "מחפש באינטרנט אחר כתוביות חסרות בהתבסס על תצורת מטא-דאטה.",
+ "TaskOptimizeDatabase": "בצע אופטימיזציה של מסד הנתונים",
+ "TaskOptimizeDatabaseDescription": "דוחס את מסד הנתונים וקיצוץ שטח פנוי. הפעלת משימה זו לאחר סריקת הספרייה או ביצוע שינויים אחרים שמשמעותם שינויים בבסיס הנתונים עשויה לשפר את הביצועים.",
+ "TaskKeyframeExtractor": "מחלץ פריים מרכזי",
+ "TaskKeyframeExtractorDescription": "מחלץ פריימים מרכזיים מקבצי וידאו כדי ליצור רשימות השמעה HLS מדויקות יותר. משימה זו עשויה להימשך זמן רב.",
+ "TaskExtractMediaSegments": "סריקת מקטעי מדיה",
+ "TaskExtractMediaSegmentsDescription": "מחלץ או משיג קטעי מדיה מתוספים התומכים ב-MediaSegment.",
+ "TaskMoveTrickplayImages": "העברת מיקום תמונת Trickplay",
+ "TaskMoveTrickplayImagesDescription": "מעביר קבצי trickplay קיימים בהתאם להגדרות הספרייה.",
+ "CleanupUserDataTask": "משימת ניקוי נתוני משתמש",
+ "CleanupUserDataTaskDescription": "מנקה את כל נתוני המשתמש (מצב צפייה, סטטוס מועדף וכו') ממדיה שכבר לא הייתה קיימת במשך 90 יום לפחות."
}
diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json
index 9968c56b21..e98a5fbac1 100644
--- a/Emby.Server.Implementations/Localization/Core/hi.json
+++ b/Emby.Server.Implementations/Localization/Core/hi.json
@@ -1,31 +1,20 @@
{
- "Albums": "एल्बम",
- "HeaderRecordingGroups": "रिकॉर्डिंग समूह",
"HeaderNextUp": "इसके बाद",
"HeaderLiveTV": "लाइव टीवी",
- "HeaderFavoriteSongs": "पसंदीदा गीत",
"HeaderFavoriteShows": "पसंदीदा शो",
"HeaderFavoriteEpisodes": "पसंदीदा प्रकरण",
- "HeaderFavoriteArtists": "पसंदीदा कलाकार",
- "HeaderFavoriteAlbums": "पसंदीदा एलबम्स",
"HeaderContinueWatching": "देखना जारी रखें",
- "HeaderAlbumArtists": "एल्बम कलाकार",
"Genres": "शैलियां",
"Forced": "बलपूर्वक",
"Folders": "फ़ोल्डर",
"Favorites": "पसंदीदा",
"FailedLoginAttemptWithUserName": "{0} से संप्रवेश असफल हुआ",
- "DeviceOnlineWithName": "{0} कनेक्ट हो गया है",
- "DeviceOfflineWithName": "{0} डिस्कनेक्ट हो गया है",
"Default": "प्राथमिक",
"Collections": "संग्रह",
"ChapterNameValue": "अध्याय {0}",
- "Channels": "चैनल",
- "CameraImageUploadedFrom": "{0} से एक नया कैमरा छवि अपलोड की गई है",
"Books": "पुस्तकें",
"AuthenticationSucceededWithUserName": "{0} सफलतापूर्वक प्रमाणित किया गया",
"Artists": "कलाकार",
- "Application": "एप्लिकेशन",
"AppDeviceValues": "एप: {0}, उपकरण: {1}",
"NotificationOptionPluginUninstalled": "प्लगइन अनइंस्टाल हो गया",
"NotificationOptionPluginInstalled": "प्लगइन इनस्टॉल हो गया",
@@ -44,13 +33,8 @@
"Music": "संगीत",
"Movies": "फ़िल्म",
"MixedContent": "मिला-जुला कंटेंट",
- "MessageServerConfigurationUpdated": "सर्वर कॉन्फ़िगरेशन अपडेट हो गया है",
- "MessageNamedServerConfigurationUpdatedWithValue": "सर्वर कॉन्फ़िगरेशन भाग {0} अपडेट हो गया है",
- "MessageApplicationUpdatedTo": "जैलीफिन सर्वर {0} में अपडेट हो गया है",
- "MessageApplicationUpdated": "जैलीफिन सर्वर अपडेट हो गया है",
"Latest": "सबसे नया",
"LabelIpAddressValue": "आई पी एड्रेस: {0}",
- "ItemRemovedWithName": "{0} लाइब्रेरी में से निकाल दिया है",
"HomeVideos": "होम चलचित्र",
"NotificationOptionVideoPlayback": "वीडियो प्लेबैक शुरू हुआ",
"NotificationOptionUserLockedOut": "उपयोगकर्ता लॉक हो गया",
@@ -59,22 +43,16 @@
"NotificationOptionPluginUpdateInstalled": "प्लगइन अद्यतन स्थापित",
"NotificationOptionNewLibraryContent": "नई सामग्री जोड़ी गई",
"LabelRunningTimeValue": "चलने का समय: {0}",
- "ItemAddedWithName": "{0} को लाइब्रेरी में जोड़ा गया",
"Inherit": "इनहेरिट",
"NotificationOptionVideoPlaybackStopped": "चलचित्र रुका हुआ",
"PluginUninstalledWithName": "{0} अनइंस्टॉल हुए",
"PluginInstalledWithName": "{0} इंस्टॉल हुए",
- "Plugin": "प्लग-इन",
- "Playlists": "प्लेलिस्ट",
"Photos": "तस्वीरें",
"External": "बाहरी",
"PluginUpdatedWithName": "{0} अपडेट हुए",
- "ScheduledTaskStartedWithName": "{0} शुरू हुए",
- "Songs": "गाने",
"UserStartedPlayingItemWithValues": "{0} {2} पर {1} खेल रहे हैं",
"UserStoppedPlayingItemWithValues": "{0} ने {2} पर {1} खेलना खत्म किया",
"StartupEmbyServerIsLoading": "जेलीफ़िन सर्वर लोड हो रहा है। कृपया शीघ्र ही पुन: प्रयास करें।",
- "ServerNameNeedsToBeRestarted": "{0} रीस्टार्ट करने की आवश्यकता है",
"UserCreatedWithName": "उपयोगकर्ता {0} बनाया गया",
"UserDownloadingItemWithValues": "{0} डाउनलोड हो रहा है",
"UserOfflineFromDevice": "{0} {1} से डिस्कनेक्ट हो गया है",
@@ -83,20 +61,13 @@
"Shows": "शो",
"UserPasswordChangedWithName": "उपयोगकर्ता {0} के लिए पासवर्ड बदल दिया गया है",
"UserDeletedWithName": "उपयोगकर्ता {0} हटा दिया गया",
- "UserPolicyUpdatedWithName": "{0} के लिए उपयोगकर्ता नीति अपडेट कर दी गई है",
- "User": "उपयोगकर्ता",
"SubtitleDownloadFailureFromForItem": "{1} के लिए {0} से उपशीर्षक डाउनलोड करने में विफल",
- "ProviderValue": "प्रदाता: {0}",
"ScheduledTaskFailedWithName": "{0}असफल",
"UserLockedOutWithName": "उपयोगकर्ता {0} को लॉक आउट कर दिया गया है",
- "System": "प्रणाली",
"TvShows": "टीवी शो",
"HearingImpaired": "मूक बधिर",
- "ValueSpecialEpisodeName": "विशेष - {0}",
"TasksMaintenanceCategory": "रखरखाव",
- "Sync": "समाकलयति",
"VersionNumber": "{0} पाठान्तर",
- "ValueHasBeenAddedToLibrary": "{0} आपके माध्यम ग्रन्थालय में उपजात हो गया हैं",
"TasksLibraryCategory": "संग्रहालय",
"TaskOptimizeDatabase": "जानकारी प्रवृद्धि",
"TaskDownloadMissingSubtitles": "लापता अनुलेख डाउनलोड करें",
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index e3bea78a3f..8794339fb1 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -1,41 +1,24 @@
{
- "Albums": "Albumi",
"AppDeviceValues": "Aplikacija: {0}, Uređaj: {1}",
- "Application": "Aplikacija",
"Artists": "Izvođači",
"AuthenticationSucceededWithUserName": "{0} uspješno ovjerena",
"Books": "Knjige",
- "CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}",
- "Channels": "Kanali",
"ChapterNameValue": "Poglavlje {0}",
"Collections": "Zbirke",
- "DeviceOfflineWithName": "{0} je prekinuo vezu",
- "DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Neuspješan pokušaj prijave od {0}",
"Favorites": "Favoriti",
"Folders": "Mape",
"Genres": "Žanrovi",
- "HeaderAlbumArtists": "Izvođači albuma",
"HeaderContinueWatching": "Nastavi gledati",
- "HeaderFavoriteAlbums": "Omiljeni albumi",
- "HeaderFavoriteArtists": "Omiljeni izvođači",
"HeaderFavoriteEpisodes": "Omiljene epizode",
"HeaderFavoriteShows": "Omiljene serije",
- "HeaderFavoriteSongs": "Omiljene pjesme",
"HeaderLiveTV": "TV uživo",
"HeaderNextUp": "Sljedeće na redu",
- "HeaderRecordingGroups": "Grupa snimka",
"HomeVideos": "Kućni video",
"Inherit": "Naslijedi",
- "ItemAddedWithName": "{0} je dodano u biblioteku",
- "ItemRemovedWithName": "{0} je uklonjeno iz biblioteke",
"LabelIpAddressValue": "IP adresa: {0}",
"LabelRunningTimeValue": "Vrijeme rada: {0}",
"Latest": "Najnovije",
- "MessageApplicationUpdated": "Jellyfin server je ažuriran",
- "MessageApplicationUpdatedTo": "Jellyfin server je ažuriran na {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Dio konfiguracije servera {0} je ažuriran",
- "MessageServerConfigurationUpdated": "Konfiguracija servera je ažurirana",
"MixedContent": "Miješani sadržaj",
"Movies": "Filmovi",
"Music": "Glazba",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Reprodukcija videa započela",
"NotificationOptionVideoPlaybackStopped": "Reprodukcija videa zaustavljena",
"Photos": "Fotografije",
- "Playlists": "Popisi za reprodukciju",
- "Plugin": "Dodatak",
"PluginInstalledWithName": "{0} je instalirano",
"PluginUninstalledWithName": "{0} je deinstalirano",
"PluginUpdatedWithName": "{0} je ažurirano",
- "ProviderValue": "Pružatelj: {0}",
"ScheduledTaskFailedWithName": "{0} neuspjelo",
- "ScheduledTaskStartedWithName": "{0} pokrenuto",
- "ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti",
"Shows": "Emisije",
- "Songs": "Pjesme",
"StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.",
"SubtitleDownloadFailureFromForItem": "Titlovi nisu uspješno preuzeti od {0} za {1}",
- "Sync": "Sinkronizacija",
- "System": "Sustav",
"TvShows": "TV emisije",
- "User": "Korisnik",
"UserCreatedWithName": "Korisnik {0} je kreiran",
"UserDeletedWithName": "Korisnik {0} je obrisan",
"UserDownloadingItemWithValues": "{0} preuzima {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} prekinuo vezu od {1}",
"UserOnlineFromDevice": "{0} povezan od {1}",
"UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}",
- "UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}",
"UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} je završio reprodukciju {1} na {2}",
- "ValueHasBeenAddedToLibrary": "{0} je dodano u biblioteku medija",
- "ValueSpecialEpisodeName": "Posebno – {0}",
"VersionNumber": "Verzija {0}",
"TaskRefreshLibraryDescription": "Skenira biblioteku medija radi novih datoteka i osvježava metapodatke.",
"TaskRefreshLibrary": "Skeniraj biblioteku medija",
@@ -135,5 +106,6 @@
"TaskMoveTrickplayImages": "Premjesti mjesto slika brzog pregledavanja",
"TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja u postavke biblioteke.",
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
- "CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana."
+ "CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana.",
+ "Original": "Original"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ht.json b/Emby.Server.Implementations/Localization/Core/ht.json
index 183c422a85..da90a92339 100644
--- a/Emby.Server.Implementations/Localization/Core/ht.json
+++ b/Emby.Server.Implementations/Localization/Core/ht.json
@@ -1,33 +1,22 @@
{
"Books": "Liv",
"TasksLibraryCategory": "Libreri",
- "Albums": "Albòm yo",
"Artists": "Atis yo",
- "Application": "Aplikasyon",
- "Channels": "Kanal yo",
"ChapterNameValue": "Chapit {0}",
"Default": "Defo",
- "DeviceOnlineWithName": "{0} konekte",
- "DeviceOfflineWithName": "{0} dekonekte",
"External": "Extèn",
"Collections": "Koleksyon yo",
"Favorites": "Pi Renmen",
"Folders": "Dosye",
"Genres": "Jan yo",
"Forced": "Fòse",
- "HeaderAlbumArtists": "Albòm Atis",
"HeaderContinueWatching": "Kontinye Kade",
- "HeaderFavoriteAlbums": "Albòm Pi Renmen",
- "HeaderFavoriteArtists": "Atis Pi Renmen",
"HeaderFavoriteEpisodes": "Epizòd Pi Renmen",
"HeaderFavoriteShows": "Emisyon Pi Renmen",
- "HeaderFavoriteSongs": "Mizik Pi Renmen",
"HeaderLiveTV": "Televizyon an Direk",
"HeaderNextUp": "Pwochen an",
"HomeVideos": "Videyo Lakay",
"Latest": "Pi Resan",
- "MessageApplicationUpdated": "Sèvè Jellyfin met a jou",
- "MessageApplicationUpdatedTo": "Sèvè Jellyfin met a jou sou {0}",
"Movies": "Fim",
"MixedContent": "Kontni Melanje",
"Music": "Mizik",
@@ -42,12 +31,8 @@
"PluginUninstalledWithName": "{0} te dezenstale",
"PluginUpdatedWithName": "{0} te mi a jou",
"ScheduledTaskFailedWithName": "{0} echwe",
- "ScheduledTaskStartedWithName": "{0} komanse",
- "Songs": "Mizik yo",
"Shows": "Emisyon yo",
- "System": "Sistèm",
"TvShows": "Emisyon Tele",
- "User": "Itilizatè",
"UserCreatedWithName": "Itilizatè {0} kreye",
"UserDeletedWithName": "Itilizatè {0} a efase",
"UserDownloadingItemWithValues": "{0} ap telechaje {1}",
@@ -55,11 +40,10 @@
"UserStartedPlayingItemWithValues": "{0} ap jwe {1} sou {2}",
"UserStoppedPlayingItemWithValues": "{0} fin jwe {1} sou {2}",
"UserPasswordChangedWithName": "Modpas la chanje pou Itilizatè {0}",
- "ValueSpecialEpisodeName": "Spesyal - {0}",
"VersionNumber": "Vesyon {0}",
"TasksApplicationCategory": "Aplikasyon",
"TasksMaintenanceCategory": "Antretyen",
"AppDeviceValues": "Aplikasyon: {0}, Aparèy: {1}",
"AuthenticationSucceededWithUserName": "{0} otantifye avèk siksè",
- "CameraImageUploadedFrom": "Une nouvelle image de la caméra a été téléchargée depuis {0}"
+ "Original": "Original"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index 8d9e5b08ba..1995d7a4cf 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -1,45 +1,28 @@
{
- "Albums": "Albumok",
- "AppDeviceValues": "Program: {0}, Eszköz: {1}",
- "Application": "Alkalmazás",
+ "AppDeviceValues": "alkalmazás: {0}, eszköz: {1}",
"Artists": "Előadók",
"AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve",
"Books": "Könyvek",
- "CameraImageUploadedFrom": "Új kamerakép lett feltöltve innen: {0}",
- "Channels": "Csatornák",
"ChapterNameValue": "{0}. jelenet",
"Collections": "Gyűjtemények",
- "DeviceOfflineWithName": "{0} kijelentkezett",
- "DeviceOnlineWithName": "{0} belépett",
"FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet innen: {0}",
"Favorites": "Kedvencek",
"Folders": "Mappák",
"Genres": "Műfajok",
- "HeaderAlbumArtists": "Albumelőadók",
"HeaderContinueWatching": "Megtekintés folytatása",
- "HeaderFavoriteAlbums": "Kedvenc albumok",
- "HeaderFavoriteArtists": "Kedvenc előadók",
"HeaderFavoriteEpisodes": "Kedvenc epizódok",
"HeaderFavoriteShows": "Kedvenc sorozatok",
- "HeaderFavoriteSongs": "Kedvenc számok",
"HeaderLiveTV": "Élő TV",
"HeaderNextUp": "Következik",
- "HeaderRecordingGroups": "Felvételi csoportok",
"HomeVideos": "Otthoni videók",
"Inherit": "Öröklés",
- "ItemAddedWithName": "{0} hozzáadva a médiatárhoz",
- "ItemRemovedWithName": "{0} eltávolítva a médiatárból",
"LabelIpAddressValue": "IP-cím: {0}",
"LabelRunningTimeValue": "Lejátszási idő: {0}",
"Latest": "Legújabb",
- "MessageApplicationUpdated": "A Jellyfin kiszolgáló frissítve lett",
- "MessageApplicationUpdatedTo": "A Jellyfin kiszolgáló frissítve lett a következőre: {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "A kiszolgálókonfigurációs rész frissítve lett: {0}",
- "MessageServerConfigurationUpdated": "A kiszolgálókonfiguráció frissítve lett",
"MixedContent": "Vegyes tartalom",
"Movies": "Filmek",
"Music": "Zenék",
- "MusicVideos": "Zenei videóklipek",
+ "MusicVideos": "Zenei videók",
"NameInstallFailed": "{0} sikertelen telepítés",
"NameSeasonNumber": "{0}. évad",
"NameSeasonUnknown": "Ismeretlen évad",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Videólejátszás elkezdve",
"NotificationOptionVideoPlaybackStopped": "Videólejátszás leállítva",
"Photos": "Fényképek",
- "Playlists": "Lejátszási listák",
- "Plugin": "Bővítmény",
"PluginInstalledWithName": "{0} telepítve",
"PluginUninstalledWithName": "{0} eltávolítva",
"PluginUpdatedWithName": "{0} frissítve",
- "ProviderValue": "Szolgáltató: {0}",
"ScheduledTaskFailedWithName": "{0} sikertelen",
- "ScheduledTaskStartedWithName": "{0} elkezdve",
- "ServerNameNeedsToBeRestarted": "A(z) {0} újraindítása szükséges",
"Shows": "Sorozatok",
- "Songs": "Számok",
"StartupEmbyServerIsLoading": "A Jellyfin kiszolgáló betöltődik. Próbálja újra hamarosan.",
"SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0}, ehhez: {1}",
- "Sync": "Szinkronizálás",
- "System": "Rendszer",
"TvShows": "TV műsorok",
- "User": "Felhasználó",
"UserCreatedWithName": "{0} felhasználó létrehozva",
"UserDeletedWithName": "{0} felhasználó törölve",
"UserDownloadingItemWithValues": "{0} letölti: {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} kijelentkezett innen: {1}",
"UserOnlineFromDevice": "{0} online innen: {1}",
"UserPasswordChangedWithName": "{0} jelszava megváltozott",
- "UserPolicyUpdatedWithName": "{0} felhasználói házirendje frissült",
"UserStartedPlayingItemWithValues": "{0} elkezdte lejátszani a következőt: {1}, itt: {2}",
"UserStoppedPlayingItemWithValues": "{0} befejezte a következő lejátszását: {1}, itt: {2}",
- "ValueHasBeenAddedToLibrary": "{0} hozzáadva a médiatárhoz",
- "ValueSpecialEpisodeName": "Különkiadás – {0}",
"VersionNumber": "Verzió: {0}",
"TaskCleanTranscode": "Átkódolási könyvtár ürítése",
"TaskUpdatePluginsDescription": "Letölti és telepíti a frissítéseket azokhoz a bővítményekhez, amelyeknél az automatikus frissítés engedélyezve van.",
@@ -135,5 +106,7 @@
"TaskMoveTrickplayImagesDescription": "A médiatár-beállításoknak megfelelően áthelyezi a meglévő trickplay fájlokat.",
"TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből.",
"CleanupUserDataTaskDescription": "Legalább 90 napja nem elérhető médiákhoz kapcsolódó összes felhasználói adat (pl. megtekintési állapot, kedvencek) törlése.",
- "CleanupUserDataTask": "Felhasználói adatok tisztítása feladat"
+ "CleanupUserDataTask": "Felhasználói adatok tisztítása feladat",
+ "Original": "Eredeti",
+ "LyricDownloadFailureFromForItem": "Dalszöveg letöltése {0}-tól {1}-hez sikertelen"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hy.json b/Emby.Server.Implementations/Localization/Core/hy.json
index 563f842923..b79b540bf4 100644
--- a/Emby.Server.Implementations/Localization/Core/hy.json
+++ b/Emby.Server.Implementations/Localization/Core/hy.json
@@ -2,38 +2,27 @@
"TasksLibraryCategory": "Գրադարան",
"TasksApplicationCategory": "Հավելված",
"TaskCleanActivityLog": "Մաքրել ակտիվության մատյանը",
- "Application": "Հավելված",
"AuthenticationSucceededWithUserName": "{0} հաջողությամբ վավերականացվել են",
"Books": "Գրքեր",
- "CameraImageUploadedFrom": "Նոր լուսանկար է վերբեռնվել {0}-ի կողմից",
- "Channels": "Ալիքներ",
- "DeviceOfflineWithName": "{0}ը անջատվեց",
"External": "Արտաքին",
"FailedLoginAttemptWithUserName": "Ձախողված մուտքի փործ {0}-ի կողմից",
"Folders": "Պանակներ",
"HeaderContinueWatching": "Շարունակել դիտումը",
"Inherit": "Ժառանգել",
- "ItemAddedWithName": "{0}ը ավացված է գրադարանի մեջ",
- "ItemRemovedWithName": "{0}ը հեռացված է գրադարանից",
"LabelIpAddressValue": "IP հասցե` {0}",
"Movies": "Ֆիլմեր",
"Music": "Երաժշտություն",
"NameSeasonNumber": "Սեզոն {0}",
"Photos": "Լուսանկարներ",
"PluginInstalledWithName": "{0}ն տեղադրված է",
- "Songs": "Երգեր",
- "System": "Համակարգ",
"TvShows": "Հեռուստասերիալներ",
- "User": "Օգտատեր",
"VersionNumber": "Տարբերակ {0}",
"TasksMaintenanceCategory": "Սպասարկում",
"TasksChannelsCategory": "Ինտերնետային ալիքներ",
"TaskRefreshPeople": "Թարմացնել մարդկանց",
"TaskRefreshChannels": "Թարմացնել ալիքները",
"TaskDownloadMissingSubtitles": "Ներբեռնել պակասող ենթագրերը",
- "Albums": "Ալբոմներ",
"AppDeviceValues": "Հավելված` {0}, Սարք `{1}",
"ChapterNameValue": "Գլուխ {0}",
- "Collections": "Հավաքածուներ",
- "DeviceOnlineWithName": "{0}-ն միացված է"
+ "Collections": "Հավաքածուներ"
}
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index fb228baf40..65c03e70f2 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -1,48 +1,31 @@
{
- "Albums": "Album",
"AuthenticationSucceededWithUserName": "{0} berhasil diautentikasi",
"AppDeviceValues": "Aplikasi : {0}, Perangkat : {1}",
"LabelRunningTimeValue": "Waktu berjalan: {0}",
- "MessageApplicationUpdatedTo": "Jellyfin Server sudah diperbarui ke {0}",
- "MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",
"Latest": "Terbaru",
"LabelIpAddressValue": "Alamat IP: {0}",
- "ItemRemovedWithName": "{0} sudah dihapus dari pustaka",
- "ItemAddedWithName": "{0} telah dimasukkan ke dalam pustaka",
"Inherit": "Warisi",
"HomeVideos": "Video Rumahan",
- "HeaderRecordingGroups": "Grup Rekaman",
"HeaderNextUp": "Selanjutnya",
"HeaderLiveTV": "Siaran langsung",
- "HeaderFavoriteSongs": "Lagu Favorit",
"HeaderFavoriteShows": "Tayangan Favorit",
"HeaderFavoriteEpisodes": "Episode Favorit",
- "HeaderFavoriteArtists": "Artis Favorit",
- "HeaderFavoriteAlbums": "Album Favorit",
"HeaderContinueWatching": "Lanjut Menonton",
- "HeaderAlbumArtists": "Album Artis",
"Genres": "Aliran",
"Folders": "Folder",
"Favorites": "Favorit",
"Collections": "Koleksi",
"Books": "Buku",
"Artists": "Artis",
- "Application": "Aplikasi",
"ChapterNameValue": "Bagian {0}",
- "Channels": "Saluran",
"TvShows": "Seri TV",
"SubtitleDownloadFailureFromForItem": "Subtitel gagal diunduh dari {0} untuk {1}",
"StartupEmbyServerIsLoading": "Server Jellyfin sedang dimuat. Silakan coba lagi nanti.",
- "Songs": "Lagu",
- "Playlists": "Daftar putar",
"NotificationOptionPluginUninstalled": "Plugin dihapus",
"MusicVideos": "Video Musik",
"VersionNumber": "Versi {0}",
- "ValueSpecialEpisodeName": "Spesial - {0}",
- "ValueHasBeenAddedToLibrary": "{0} telah ditambahkan ke pustaka media Anda",
"UserStoppedPlayingItemWithValues": "{0} telah selesai memutar {1} pada {2}",
"UserStartedPlayingItemWithValues": "{0} sedang memutar {1} pada {2}",
- "UserPolicyUpdatedWithName": "Kebijakan pengguna telah diperbarui untuk {0}",
"UserPasswordChangedWithName": "Kata sandi telah diubah untuk pengguna {0}",
"UserOnlineFromDevice": "{0} sedang daring dari {1}",
"UserOfflineFromDevice": "{0} telah terputus dari {1}",
@@ -50,17 +33,10 @@
"UserDownloadingItemWithValues": "{0} sedang mengunduh {1}",
"UserDeletedWithName": "Pengguna {0} telah dihapus",
"UserCreatedWithName": "Pengguna {0} telah dibuat",
- "User": "Pengguna",
- "System": "Sistem",
- "Sync": "Sinkron",
"Shows": "Tayangan",
- "ServerNameNeedsToBeRestarted": "{0} perlu dimuat ulang",
- "ScheduledTaskStartedWithName": "{0} dimulai",
"ScheduledTaskFailedWithName": "{0} gagal",
- "ProviderValue": "Penyedia: {0}",
"PluginUpdatedWithName": "{0} telah diperbarui",
"PluginInstalledWithName": "{0} telah dipasang",
- "Plugin": "Plugin",
"Photos": "Foto",
"NotificationOptionUserLockedOut": "Pengguna terkunci",
"NotificationOptionTaskFailed": "Kegagalan tugas terjadwal",
@@ -79,12 +55,7 @@
"NameInstallFailed": "{0} penginstalan gagal",
"Music": "Musik",
"Movies": "Film",
- "MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
- "MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
"FailedLoginAttemptWithUserName": "Gagal upaya login dari {0}",
- "CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}",
- "DeviceOfflineWithName": "{0} telah terputus",
- "DeviceOnlineWithName": "{0} telah terhubung",
"NotificationOptionVideoPlaybackStopped": "Pemutaran video berhenti",
"NotificationOptionVideoPlayback": "Pemutaran video dimulai",
"NotificationOptionAudioPlaybackStopped": "Pemutaran audio berhenti",
diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json
index 900502ccdd..c9ca00afdf 100644
--- a/Emby.Server.Implementations/Localization/Core/is.json
+++ b/Emby.Server.Implementations/Localization/Core/is.json
@@ -1,36 +1,22 @@
{
"LabelIpAddressValue": "IP tala: {0}",
- "ItemRemovedWithName": "{0} var fjarlægt úr safninu",
- "ItemAddedWithName": "{0} var bætt í safnið",
"Inherit": "Erfa",
"HomeVideos": "Heimamyndbönd",
- "HeaderRecordingGroups": "Upptökuhópar",
"HeaderNextUp": "Næst á dagskrá",
"HeaderLiveTV": "Sjónvarp í beinni útsendingu",
- "HeaderFavoriteSongs": "Uppáhalds Lög",
"HeaderFavoriteShows": "Uppáhalds Sjónvarpsþættir",
"HeaderFavoriteEpisodes": "Uppáhalds Þættir",
- "HeaderFavoriteArtists": "Uppáhalds Listamenn",
- "HeaderFavoriteAlbums": "Uppáhalds Plötur",
"HeaderContinueWatching": "Halda áfram að horfa",
- "HeaderAlbumArtists": "Listamaður á umslagi",
"Genres": "Stefnur",
"Folders": "Möppur",
"Favorites": "Uppáhalds",
"FailedLoginAttemptWithUserName": "{0} mistókst að auðkenna sig",
- "DeviceOnlineWithName": "{0} hefur tengst",
- "DeviceOfflineWithName": "{0} hefur aftengst",
"Collections": "Söfn",
"ChapterNameValue": "Kafli {0}",
- "Channels": "Rásir",
- "CameraImageUploadedFrom": "{0} hefur hlaðið upp nýrri ljósmynd úr myndavél sinni",
"Books": "Bækur",
"AuthenticationSucceededWithUserName": "Auðkenning fyrir {0} tókst",
"Artists": "Listamenn",
- "Application": "Forrit",
"AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}",
- "Albums": "Plötur",
- "Plugin": "Viðbótarvirkni",
"Photos": "Ljósmyndir",
"NotificationOptionVideoPlaybackStopped": "Myndbandsafspilun stöðvuð",
"NotificationOptionVideoPlayback": "Myndbandsafspilun hafin",
@@ -49,13 +35,8 @@
"NameSeasonUnknown": "Þáttaröð óþekkt",
"NameSeasonNumber": "Þáttaröð {0}",
"MixedContent": "Blandað efni",
- "MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar",
- "MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}",
- "MessageApplicationUpdated": "Jellyfin þjónn hefur verið uppfærður",
"Latest": "Nýjasta",
"LabelRunningTimeValue": "spilunartími: {0}",
- "User": "Notandi",
- "System": "Kerfi",
"NotificationOptionNewLibraryContent": "Nýju efni bætt við",
"NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er tilbúin til niðurhals.",
"NameInstallFailed": "{0} uppsetning mistókst",
@@ -65,10 +46,6 @@
"UserDeletedWithName": "Notanda {0} hefur verið eytt",
"UserCreatedWithName": "Notandi {0} hefur verið stofnaður",
"TvShows": "Sjónvarpsþættir",
- "Sync": "Samstilla",
- "Songs": "Lög",
- "ServerNameNeedsToBeRestarted": "{0} þarf að vera endurræstur",
- "ScheduledTaskStartedWithName": "{0} hafin",
"ScheduledTaskFailedWithName": "{0} mistókst",
"PluginUpdatedWithName": "{0} var uppfært",
"PluginUninstalledWithName": "{0} var fjarlægt",
@@ -76,21 +53,15 @@
"NotificationOptionTaskFailed": "Tímasett verkefni mistókst",
"StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að ræsa sig upp. Vinsamlegast reyndu aftur fljótlega.",
"VersionNumber": "Útgáfa {0}",
- "ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt",
"UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}",
"UserStartedPlayingItemWithValues": "{0} er að spila {1} á {2}",
- "UserPolicyUpdatedWithName": "Notandaregla hefur verið uppfærð fyrir {0}",
"UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt",
"UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}",
"UserOfflineFromDevice": "{0} hefur aftengst frá {1}",
"UserLockedOutWithName": "Notandi {0} hefur verið læstur úti",
"UserDownloadingItemWithValues": "{0} hleður niður {1}",
"SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}",
- "ProviderValue": "Efnisveita: {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón",
- "ValueSpecialEpisodeName": "Sérstaktur - {0}",
"Shows": "Þættir",
- "Playlists": "Efnisskrár",
"TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.",
"TaskRefreshChannels": "Endurhlaða Rásir",
"TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.",
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 782f5ce53d..f13944e6be 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -1,41 +1,24 @@
{
- "Albums": "Album",
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
- "Application": "Applicazione",
"Artists": "Artisti",
"AuthenticationSucceededWithUserName": "{0} autenticato correttamente",
"Books": "Libri",
- "CameraImageUploadedFrom": "È stata caricata una nuova fotografia da {0}",
- "Channels": "Canali",
"ChapterNameValue": "Capitolo {0}",
"Collections": "Collezioni",
- "DeviceOfflineWithName": "{0} si è disconnesso",
- "DeviceOnlineWithName": "{0} è connesso",
"FailedLoginAttemptWithUserName": "Tentativo di accesso non riuscito da {0}",
"Favorites": "Preferiti",
"Folders": "Cartelle",
"Genres": "Generi",
- "HeaderAlbumArtists": "Artisti dell'album",
"HeaderContinueWatching": "Continua a guardare",
- "HeaderFavoriteAlbums": "Album preferiti",
- "HeaderFavoriteArtists": "Artisti preferiti",
"HeaderFavoriteEpisodes": "Episodi preferiti",
"HeaderFavoriteShows": "Serie TV preferite",
- "HeaderFavoriteSongs": "Brani preferiti",
"HeaderLiveTV": "Diretta TV",
"HeaderNextUp": "Prossimo",
- "HeaderRecordingGroups": "Gruppi di registrazione",
"HomeVideos": "Video personali",
"Inherit": "Eredita",
- "ItemAddedWithName": "{0} è stato aggiunto alla libreria",
- "ItemRemovedWithName": "{0} è stato rimosso dalla libreria",
"LabelIpAddressValue": "Indirizzo IP: {0}",
"LabelRunningTimeValue": "Durata: {0}",
"Latest": "Novità",
- "MessageApplicationUpdated": "Jellyfin Server è stato aggiornato",
- "MessageApplicationUpdatedTo": "Jellyfin Server è stato aggiornato a {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "La sezione {0} della configurazione server è stata aggiornata",
- "MessageServerConfigurationUpdated": "La configurazione del server è stata aggiornata",
"MixedContent": "Contenuto misto",
"Movies": "Film",
"Music": "Musica",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Riproduzione video iniziata",
"NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta",
"Photos": "Foto",
- "Playlists": "Scalette",
- "Plugin": "Plugin",
"PluginInstalledWithName": "{0} è stato installato",
"PluginUninstalledWithName": "{0} è stato disinstallato",
"PluginUpdatedWithName": "{0} è stato aggiornato",
- "ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} non riuscito",
- "ScheduledTaskStartedWithName": "{0} avviato",
- "ServerNameNeedsToBeRestarted": "{0} deve essere riavviato",
"Shows": "Serie TV",
- "Songs": "Brani",
"StartupEmbyServerIsLoading": "Jellyfin Server si sta avviando. Riprova più tardi.",
"SubtitleDownloadFailureFromForItem": "Impossibile scaricare i sottotitoli da {0} per {1}",
- "Sync": "Sincronizza",
- "System": "Sistema",
"TvShows": "Serie TV",
- "User": "Utente",
"UserCreatedWithName": "L'utente {0} è stato creato",
"UserDeletedWithName": "L'utente {0} è stato eliminato",
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} si è disconnesso da {1}",
"UserOnlineFromDevice": "{0} è online su {1}",
"UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
- "UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
"UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1} su {2}",
"UserStoppedPlayingItemWithValues": "{0} ha interrotto la riproduzione di {1} su {2}",
- "ValueHasBeenAddedToLibrary": "{0} è stato aggiunto alla tua libreria multimediale",
- "ValueSpecialEpisodeName": "Speciale - {0}",
"VersionNumber": "Versione {0}",
"TaskRefreshChannelsDescription": "Aggiorna le informazioni dei canali internet.",
"TaskDownloadMissingSubtitlesDescription": "Cerca su internet i sottotitoli mancanti basandosi sulle configurazioni dei metadati.",
@@ -135,5 +106,7 @@
"TaskExtractMediaSegmentsDescription": "Estrae o ottiene segmenti multimediali dai plugin abilitati MediaSegment.",
"TaskExtractMediaSegments": "Scansiona Segmento Media",
"CleanupUserDataTask": "Task di pulizia dei dati utente",
- "CleanupUserDataTaskDescription": "Pulisce tutti i dati utente (stato di visione, status preferiti, ecc.) dai contenuti non più presenti da almeno 90 giorni."
+ "CleanupUserDataTaskDescription": "Pulisce tutti i dati utente (stato di visione, status preferiti, ecc.) dai contenuti non più presenti da almeno 90 giorni.",
+ "Original": "Originale",
+ "LyricDownloadFailureFromForItem": "Scaricamento dei testi non riuscito da {0} per {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index 7b0bdb296f..39e5af717c 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -1,41 +1,24 @@
{
- "Albums": "アルバム",
"AppDeviceValues": "アプリ: {0}, デバイス: {1}",
- "Application": "アプリケーション",
"Artists": "アーティスト",
"AuthenticationSucceededWithUserName": "{0} 認証に成功しました",
"Books": "ブック",
- "CameraImageUploadedFrom": "新しいカメライメージが {0}からアップロードされました",
- "Channels": "チャンネル",
"ChapterNameValue": "チャプター {0}",
"Collections": "コレクション",
- "DeviceOfflineWithName": "{0} が切断しました",
- "DeviceOnlineWithName": "{0} が接続しました",
"FailedLoginAttemptWithUserName": "{0} からのログインに失敗しました",
"Favorites": "お気に入り",
"Folders": "フォルダー",
"Genres": "ジャンル",
- "HeaderAlbumArtists": "アルバムアーティスト",
"HeaderContinueWatching": "再生を続ける",
- "HeaderFavoriteAlbums": "お気に入りのアルバム",
- "HeaderFavoriteArtists": "お気に入りのアーティスト",
"HeaderFavoriteEpisodes": "お気に入りのエピソード",
"HeaderFavoriteShows": "お気に入りの番組",
- "HeaderFavoriteSongs": "お気に入りの曲",
"HeaderLiveTV": "ライブTV",
"HeaderNextUp": "次",
- "HeaderRecordingGroups": "レコーディンググループ",
"HomeVideos": "ホームビデオ",
"Inherit": "継承",
- "ItemAddedWithName": "{0} をライブラリーに追加しました",
- "ItemRemovedWithName": "{0} をライブラリーから削除しました",
"LabelIpAddressValue": "IPアドレス: {0}",
"LabelRunningTimeValue": "時間: {0}",
"Latest": "最新",
- "MessageApplicationUpdated": "Jellyfin Server を更新しました",
- "MessageApplicationUpdatedTo": "Jellyfin Server を {0}に更新しました",
- "MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} を更新しました",
- "MessageServerConfigurationUpdated": "サーバー設定を更新しました",
"MixedContent": "ミックスコンテンツ",
"Movies": "映画",
"Music": "音楽",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "ビデオの再生を開始",
"NotificationOptionVideoPlaybackStopped": "ビデオの再生を停止",
"Photos": "フォト",
- "Playlists": "プレイリスト",
- "Plugin": "プラグイン",
"PluginInstalledWithName": "{0} をインストールしました",
"PluginUninstalledWithName": "{0} をアンインストールしました",
"PluginUpdatedWithName": "{0} を更新しました",
- "ProviderValue": "プロバイダ: {0}",
"ScheduledTaskFailedWithName": "{0} が失敗しました",
- "ScheduledTaskStartedWithName": "{0} を開始",
- "ServerNameNeedsToBeRestarted": "{0} を再起動してください",
"Shows": "番組",
- "Songs": "曲",
"StartupEmbyServerIsLoading": "Jellyfin Server は現在読み込み中です。しばらくしてからもう一度お試しください。",
"SubtitleDownloadFailureFromForItem": "{0} から {1}の字幕のダウンロードに失敗しました",
- "Sync": "同期",
- "System": "システム",
"TvShows": "テレビ番組",
- "User": "ユーザー",
"UserCreatedWithName": "ユーザー {0} が作成されました",
"UserDeletedWithName": "User {0} を削除しました",
"UserDownloadingItemWithValues": "{0} が {1} をダウンロードしています",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} は {1} から切断しました",
"UserOnlineFromDevice": "{0} は {1} からオンラインになりました",
"UserPasswordChangedWithName": "ユーザー {0} のパスワードは変更されました",
- "UserPolicyUpdatedWithName": "ユーザーポリシーが{0}に更新されました",
"UserStartedPlayingItemWithValues": "{0} は {2}で{1} を再生しています",
"UserStoppedPlayingItemWithValues": "{0} は{2}で{1} の再生が終わりました",
- "ValueHasBeenAddedToLibrary": "{0} をメディアライブラリーに追加しました",
- "ValueSpecialEpisodeName": "スペシャル - {0}",
"VersionNumber": "バージョン {0}",
"TaskCleanLogsDescription": "{0} 日以上前のログを消去します。",
"TaskCleanLogs": "ログの掃除",
diff --git a/Emby.Server.Implementations/Localization/Core/jbo.json b/Emby.Server.Implementations/Localization/Core/jbo.json
index 1b47bb2f23..50d6d49601 100644
--- a/Emby.Server.Implementations/Localization/Core/jbo.json
+++ b/Emby.Server.Implementations/Localization/Core/jbo.json
@@ -1,7 +1,4 @@
{
- "Albums": "lo albuma",
"Artists": "lo larpra",
- "Books": "lo cukta",
- "HeaderAlbumArtists": "lo albuma larpra",
- "Playlists": "lo zgipor"
+ "Books": "lo cukta"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ka.json b/Emby.Server.Implementations/Localization/Core/ka.json
index 4f291e466b..f7ca19d7f0 100644
--- a/Emby.Server.Implementations/Localization/Core/ka.json
+++ b/Emby.Server.Implementations/Localization/Core/ka.json
@@ -1,12 +1,8 @@
{
"Genres": "ჟანრები",
- "HeaderAlbumArtists": "ალბომის შემსრულებლები",
- "HeaderFavoriteAlbums": "რჩეული ალბომები",
"TasksApplicationCategory": "აპლიკაცია",
- "Albums": "ალბომები",
"AppDeviceValues": "აპლიკაცია: {0}, მოწყობილობა: {1}",
- "Application": "აპლიკაცია",
- "Artists": "არტისტი",
+ "Artists": "შემსრულებლები",
"AuthenticationSucceededWithUserName": "{0} -ის ავთენტიკაცია წარმატებულია",
"Books": "წიგნები",
"Forced": "იძულებითი",
@@ -15,106 +11,81 @@
"Movies": "ფილმები",
"Music": "მუსიკა",
"Photos": "ფოტოები",
- "Playlists": "დასაკრავი სიები",
- "Plugin": "მოდული",
"Shows": "სერიალები",
- "Songs": "სიმღერები",
- "Sync": "სინქრონიზაცია",
- "System": "სისტემა",
"Undefined": "განუსაზღვრელი",
- "User": "მომხმარებელი",
"TasksMaintenanceCategory": "რემონტი",
"TasksLibraryCategory": "ბიბლიოთეკა",
"ChapterNameValue": "თავი {0}",
"HeaderContinueWatching": "ყურების გაგრძელება",
- "HeaderFavoriteArtists": "რჩეული შემსრულებლები",
- "DeviceOfflineWithName": "{0} გამოეთიშა",
"External": "გარე",
"HeaderFavoriteEpisodes": "რჩეული ეპიზოდები",
- "HeaderFavoriteSongs": "რჩეული სიმღერები",
- "HeaderRecordingGroups": "ჩამწერი ჯგუფები",
"HearingImpaired": "სმენადაქვეითებული",
- "LabelRunningTimeValue": "ხანგრძლივობა: {0}",
- "MessageApplicationUpdatedTo": "Jellyfin-ის სერვერი განახლდა {0}-ზე",
- "MessageNamedServerConfigurationUpdatedWithValue": "სერვერის კონფიგურაციის სექცია {0} განახლდა",
+ "LabelRunningTimeValue": "გაშვების დრო: {0}",
"MixedContent": "შერეული შემცველობა",
- "MusicVideos": "მუსიკალური ვიდეოები",
+ "MusicVideos": "მუსიკის ვიდეოები",
"NotificationOptionInstallationFailed": "დაყენების შეცდომა",
"NotificationOptionApplicationUpdateInstalled": "აპლიკაციის განახლება დაყენებულია",
"NotificationOptionAudioPlayback": "აუდიოს დაკვრა დაწყებულია",
"NotificationOptionCameraImageUploaded": "კამერის გამოსახულება ატვირთულია",
"NotificationOptionVideoPlaybackStopped": "ვიდეოს დაკვრა გაჩერებულია",
"PluginUninstalledWithName": "{0} წაიშალა",
- "ScheduledTaskStartedWithName": "{0} დაიწყო",
"VersionNumber": "ვერსია {0}",
"TasksChannelsCategory": "ინტერნეტ-არხები",
- "ValueSpecialEpisodeName": "დამატებითი - {0}",
- "TaskRefreshChannelsDescription": "ინტერნეტ-არხის ინფორმაციის განახლება.",
- "Channels": "არხები",
+ "TaskRefreshChannelsDescription": "განაახლებს ინტერნეტ-არხის ინფორმაციას.",
"Collections": "კოლექციები",
- "Default": "ნაგულისხმები",
+ "Default": "ნაგულისხმევი",
"Favorites": "რჩეულები",
"Folders": "საქაღალდეები",
"HeaderFavoriteShows": "რჩეული სერიალები",
- "HeaderLiveTV": "ლაივ ტელევიზია",
+ "HeaderLiveTV": "ცოცხალი ტელევიზია",
"HeaderNextUp": "შემდეგი",
"HomeVideos": "სახლის ვიდეოები",
"NameSeasonNumber": "სეზონი {0}",
"NameSeasonUnknown": "სეზონი უცნობია",
- "NotificationOptionPluginError": "მოდულის შეცდომა",
- "NotificationOptionPluginInstalled": "მოდული დაყენებულია",
+ "NotificationOptionPluginError": "დამატების შეცდომა",
+ "NotificationOptionPluginInstalled": "დამატება დაყენებულია",
"NotificationOptionPluginUninstalled": "მოდული წაიშალა",
- "ProviderValue": "მომწოდებელი: {0}",
- "ScheduledTaskFailedWithName": "{0} ვერ შესრულდა",
+ "ScheduledTaskFailedWithName": "{0} ჩავარდა",
"TvShows": "სატელევიზიო სერიალები",
"TaskRefreshPeople": "ხალხის განახლება",
- "TaskUpdatePlugins": "მოდულების განახლება",
+ "TaskUpdatePlugins": "დამატებების განახლება",
"TaskRefreshChannels": "არხების განახლება",
"TaskOptimizeDatabase": "მონაცემთა ბაზის ოპტიმიზაცია",
"TaskKeyframeExtractor": "საკვანძო კადრის გამომღები",
- "DeviceOnlineWithName": "{0} დაკავშირდა",
"LabelIpAddressValue": "IP მისამართი: {0}",
- "NameInstallFailed": "{0}-ის დაყენების შეცდომა",
+ "NameInstallFailed": "{0}-ის დაყენების ჩავარდა",
"NotificationOptionApplicationUpdateAvailable": "ხელმისაწვდომია აპლიკაციის განახლება",
"NotificationOptionAudioPlaybackStopped": "აუდიოს დაკვრა გაჩერებულია",
"NotificationOptionNewLibraryContent": "ახალი შემცველობა დამატებულია",
- "NotificationOptionPluginUpdateInstalled": "მოდულიs განახლება დაყენებულია",
+ "NotificationOptionPluginUpdateInstalled": "დამატების განახლება დაყენებულია",
"NotificationOptionServerRestartRequired": "საჭიროა სერვერის გადატვირთვა",
- "NotificationOptionTaskFailed": "გეგმიური დავალების შეცდომა",
+ "NotificationOptionTaskFailed": "დაგეგმილი ამოცანა ჩავარდა",
"NotificationOptionUserLockedOut": "მომხმარებელი დაიბლოკა",
"NotificationOptionVideoPlayback": "ვიდეოს დაკვრა დაწყებულია",
"PluginInstalledWithName": "{0} დაყენებულია",
"PluginUpdatedWithName": "{0} განახლდა",
"TaskCleanActivityLog": "აქტივობების ჟურნალის გასუფთავება",
- "TaskCleanCache": "ქეშის საქაღალდის გასუფთავება",
- "TaskRefreshChapterImages": "თავის სურათების გაშლა",
+ "TaskCleanCache": "კეშის საქაღალდის გასუფთავება",
+ "TaskRefreshChapterImages": "თავის სურათების ამოღება",
"TaskRefreshLibrary": "მედიის ბიბლიოთეკის სკანირება",
"TaskCleanLogs": "ჟურნალის საქაღალდის გასუფთავება",
"TaskCleanTranscode": "ტრანსკოდირების საქაღალდის გასუფთავება",
- "TaskDownloadMissingSubtitles": "მიუწვდომელი სუბტიტრების გადმოწერა",
- "UserDownloadingItemWithValues": "{0} -ი {1}-ს იწერს",
+ "TaskDownloadMissingSubtitles": "ნაკლული სუბტიტრების გადმოწერა",
+ "UserDownloadingItemWithValues": "{0} იწერს {1}-ს",
"FailedLoginAttemptWithUserName": "შესვლის წარუმატებელი მცდელობა {0}-დან",
- "MessageApplicationUpdated": "Jellyfin-ის სერვერი განახლდა",
- "MessageServerConfigurationUpdated": "სერვერის კონფიგურაცია განახლდა",
- "ServerNameNeedsToBeRestarted": "საჭიროა {0}-ის გადატვირთვა",
"UserCreatedWithName": "მომხმარებელი {0} შეიქმნა",
- "UserDeletedWithName": "მომხმარებელი {0} წაშლილია",
- "UserOnlineFromDevice": "{0}-ი დაკავშირდა {1}-დან",
- "UserOfflineFromDevice": "{0}-ი {1}-დან გაეთიშა",
- "ItemAddedWithName": "{0} ჩამატებულია ბიბლიოთეკაში",
- "ItemRemovedWithName": "{0} წაშლილია ბიბლიოთეკიდან",
+ "UserDeletedWithName": "მომხმარებელი {0} წაიშალა",
+ "UserOnlineFromDevice": "{0} ხაზზეა {1}-დან",
+ "UserOfflineFromDevice": "{0} გაითიშა {1}-დან",
"UserLockedOutWithName": "მომხმარებელი {0} დაბლოკილია",
- "UserStartedPlayingItemWithValues": "{0} უყურებს {1}-ს {2}-ზე",
+ "UserStartedPlayingItemWithValues": "{0} უკრავს {1}-ს {2}-ზე",
"UserPasswordChangedWithName": "მომხმარებელი {0}-სთვის პაროლი შეიცვალა",
- "UserPolicyUpdatedWithName": "{0}-ის მომხმარებლის პოლიტიკა განახლდა",
"UserStoppedPlayingItemWithValues": "{0}-მა დაასრულა {1}-ის ყურება {2}-ზე",
"TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა.",
"TaskKeyframeExtractorDescription": "უფრო ზუსტი HLS დასაკრავი სიებისითვის ვიდეოდან საკვანძო გადრების ამოღება. შეიძლება საკმაო დრო დასჭირდეს.",
"NewVersionIsAvailable": "გადმოსაწერად ხელმისაწვდომია Jellyfin -ის ახალი ვერსია.",
- "CameraImageUploadedFrom": "ახალი კამერის გამოსახულება ატვირთულია {0}-დან",
"StartupEmbyServerIsLoading": "Jellyfin სერვერი იტვირთება. მოგვიანებით სცადეთ.",
"SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერა ვერ შესრულდა",
- "ValueHasBeenAddedToLibrary": "{0} დაემატა თქვენს მედიის ბიბლიოთეკას",
"TaskCleanActivityLogDescription": "შლის მითითებულ ასაკზე ძველ ჟურნალის ჩანაწერებს.",
"TaskCleanCacheDescription": "შლის სისტემისთვის არასაჭირო ქეშის ფაილებს.",
"TaskRefreshLibraryDescription": "ეძებს ახალ ფაილებს თქვენს მედიის ბიბლიოთეკაში და ანახლებს მეტამონაცემებს.",
@@ -125,15 +96,17 @@
"TaskDownloadMissingSubtitlesDescription": "ეძებს ბიბლიოთეკაში მიუწვდომელ სუბტიტრებს ინტერნეტში მეტამონაცემებზე დაყრდნობით.",
"TaskOptimizeDatabaseDescription": "კუმშავს მონაცემთა ბაზას ადგილის გათავისუფლებლად. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.",
"TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის დაშვებულ ბიბლიოთეკებში.",
- "TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება",
- "TaskAudioNormalization": "აუდიოს ნორმალიზება",
+ "TaskRefreshTrickplayImages": "Trickplay სურათების გენერაცია",
+ "TaskAudioNormalization": "აუდიოს ნორმალიზაცია",
"TaskAudioNormalizationDescription": "აანალიზებს ფაილებს აუდიოს ნორმალიზაციისთვის.",
"TaskDownloadMissingLyrics": "მიუწვდომელი ლირიკების ჩამოტვირთვა",
- "TaskDownloadMissingLyricsDescription": "ჩამოტვირთავს ამჟამად ბიბლიოთეკაში არარსებულ ლირიკებს სიმღერებისთვის",
- "TaskExtractMediaSegments": "მედია სეგმენტების სკანირება",
+ "TaskDownloadMissingLyricsDescription": "გადმოწერს ლირიკას სიმღერებისთვის",
+ "TaskExtractMediaSegments": "მედიის სეგმენტების სკანირება",
"TaskExtractMediaSegmentsDescription": "მედია სეგმენტების სკანირება მხარდაჭერილი მოდულებისთვის.",
- "TaskMoveTrickplayImages": "Trickplay სურათების მიგრაცია",
+ "TaskMoveTrickplayImages": "Trickplay-ის გამოსახულებების მდებარეობის მიგრაცია",
"TaskMoveTrickplayImagesDescription": "გადააქვს trickplay ფაილები ბიბლიოთეკის პარამეტრებზე დაყრდნობით.",
- "CleanupUserDataTask": "მომხმარებლების მონაცემების გასუფთავება",
- "CleanupUserDataTaskDescription": "ასუფთავებს მომხმარებლების მონაცემებს (ყურების სტატუსი, ფავორიტები ანდ ა.შ) მედია ელემენტებისთვის რომლების 90 დღეზე მეტია აღარ არსებობენ."
+ "CleanupUserDataTask": "მომხმარებლების მონაცემების გასუფთავების ამოცანა",
+ "CleanupUserDataTaskDescription": "ასუფთავებს მომხმარებლების მონაცემებს (ყურების სტატუსი, ფავორიტები ანდ ა.შ) მედია ელემენტებისთვის რომლების 90 დღეზე მეტია აღარ არსებობენ.",
+ "LyricDownloadFailureFromForItem": "{1}-ისთვის {0}-დან ლირიკის გადმოწერა ჩავარდა",
+ "Original": "ორიგინალი"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kab.json b/Emby.Server.Implementations/Localization/Core/kab.json
index 9551f0e5c1..0d0932b585 100644
--- a/Emby.Server.Implementations/Localization/Core/kab.json
+++ b/Emby.Server.Implementations/Localization/Core/kab.json
@@ -1,14 +1,10 @@
{
"Music": "Aẓawan",
- "Sync": "Amtawi",
"Photos": "Tiwlafin",
"Movies": "Isura",
"External": "Azɣaray",
- "User": "Aseqdac",
"Folders": "Ikaramen",
"Favorites": "Ismenyifen",
"Default": "Lexṣas",
- "Collections": "Tigrummiwin",
- "Channels": "Ibuda",
- "Albums": "Iseɣraz"
+ "Collections": "Tigrummiwin"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json
index fc5fcf3c4d..ddcf60944d 100644
--- a/Emby.Server.Implementations/Localization/Core/kk.json
+++ b/Emby.Server.Implementations/Localization/Core/kk.json
@@ -1,41 +1,24 @@
{
- "Albums": "Älbomdar",
"AppDeviceValues": "Qoldanba: {0}, Qūrylğy: {1}",
- "Application": "Qoldanba",
"Artists": "Oryndauşylar",
"AuthenticationSucceededWithUserName": "{0} tüpnūsqalyq rastaluy sättı aiaqtaldy",
"Books": "Kıtaptar",
- "CameraImageUploadedFrom": "{0} kamerasynan jaña suret jüktep salyndy",
- "Channels": "Arnalar",
"ChapterNameValue": "{0}-sahna",
"Collections": "Jiyntyqtar",
- "DeviceOfflineWithName": "{0} ajyratylğan",
- "DeviceOnlineWithName": "{0} qosylğan",
"FailedLoginAttemptWithUserName": "{0} tarapynan kıru äreketı sätsız aiaqtaldy",
"Favorites": "Tañdaulylar",
"Folders": "Qaltalar",
"Genres": "Janrlar",
- "HeaderAlbumArtists": "Älbom oryndauşylary",
"HeaderContinueWatching": "Qaraudy jalğastyru",
- "HeaderFavoriteAlbums": "Tañdauly älbomdar",
- "HeaderFavoriteArtists": "Tañdauly oryndauşylar",
"HeaderFavoriteEpisodes": "Tañdauly telebölımder",
"HeaderFavoriteShows": "Tañdauly körsetımder",
- "HeaderFavoriteSongs": "Tañdauly äuender",
"HeaderLiveTV": "Efir",
"HeaderNextUp": "Kezektı",
- "HeaderRecordingGroups": "Jazba toptary",
"HomeVideos": "Üilık beineler",
"Inherit": "İelenu",
- "ItemAddedWithName": "{0} tasyğyşhanağa üstelindı",
- "ItemRemovedWithName": "{0} tasyğyşhanadan alastaldy",
"LabelIpAddressValue": "IP-mekenjaiy: {0}",
"LabelRunningTimeValue": "Oinatu uaqyty: {0}",
"Latest": "Eñ keiıngı",
- "MessageApplicationUpdated": "Jellyfin Serverı jañartyldy",
- "MessageApplicationUpdatedTo": "Jellyfin Serverı {0} nūsqasyna jañartyldy",
- "MessageNamedServerConfigurationUpdatedWithValue": "Server teñşelımderınıñ {0} bölımı jañartyldy",
- "MessageServerConfigurationUpdated": "Server teñşelımderı jañartyldy",
"MixedContent": "Aralas mazmūn",
"Movies": "Filmder",
"Music": "Muzyka",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Beine oinatuy bastaldy",
"NotificationOptionVideoPlaybackStopped": "Beine oinatuy toqtatyldy",
"Photos": "Fotosuretter",
- "Playlists": "Oinatu tızımderı",
- "Plugin": "Plagin",
"PluginInstalledWithName": "{0} ornatyldy",
"PluginUninstalledWithName": "{0} joiyldy",
"PluginUpdatedWithName": "{0} jañartyldy",
- "ProviderValue": "Jetkızuşı: {0}",
"ScheduledTaskFailedWithName": "{0} sätsız",
- "ScheduledTaskStartedWithName": "{0} ıske qosyldy",
- "ServerNameNeedsToBeRestarted": "{0} qaita ıske qosu qajet",
"Shows": "Körsetımder",
- "Songs": "Äuender",
"StartupEmbyServerIsLoading": "Jellyfin Server jüktelude. Ärekettı köp ūzamai qaitalañyz.",
"SubtitleDownloadFailureFromForItem": "{1} üşın subtitrlerdı {0} közınen jüktep alu sätsız",
- "Sync": "Ündestıru",
- "System": "Jüie",
"TvShows": "TD-körsetımder",
- "User": "Paidalanuşy",
"UserCreatedWithName": "Paidalanuşy {0} jasalğan",
"UserDeletedWithName": "Paidalanuşy {0} joiylğan",
"UserDownloadingItemWithValues": "{0} — {1} jüktep aluda",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} — {1} tarapynan ajyratyldy",
"UserOnlineFromDevice": "{0} — {1} tarapynan qosyldy",
"UserPasswordChangedWithName": "Paidalanuşy {0} üşın paröl özgertıldı",
- "UserPolicyUpdatedWithName": "Paidalanuşy {0} üşın saiasattary jañartyldy",
"UserStartedPlayingItemWithValues": "{0} — {2} tarapynan {1} oinatuda",
"UserStoppedPlayingItemWithValues": "{0} — {2} tarapynan {1} oinatuyn toqtatty",
- "ValueHasBeenAddedToLibrary": "{0} tasyğyşhanağa üstelındı",
- "ValueSpecialEpisodeName": "Arnaiy - {0}",
"VersionNumber": "Nūsqasy {0}",
"Default": "Ädepkı",
"TaskDownloadMissingSubtitles": "Joq subtitrlerdı jüktep alu",
diff --git a/Emby.Server.Implementations/Localization/Core/km.json b/Emby.Server.Implementations/Localization/Core/km.json
index c40b96cf24..b4057eb8eb 100644
--- a/Emby.Server.Implementations/Localization/Core/km.json
+++ b/Emby.Server.Implementations/Localization/Core/km.json
@@ -1,88 +1,62 @@
{
- "Albums": "អាលប៊ុម",
- "MessageApplicationUpdatedTo": "ម៉ាស៊ីនមេនៃJellyfinត្រូវបានអាប់ដេតទៅកាន់ {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "ការកំណត់ម៉ាស៊ីនមេ ផ្នែក {0} ត្រូវបានអាប់ដេត",
- "MessageServerConfigurationUpdated": "ការកំណត់ម៉ាស៊ីនមេត្រូវបានអាប់ដេត",
"AppDeviceValues": "កម្មវិធី: {0}, ឧបករណ៍: {1}",
"MixedContent": "មាតិកាចម្រុះ",
"UserLockedOutWithName": "អ្នកប្រើប្រាស់ {0} ត្រូវ​បាន​ផ្អាក",
- "Application": "កម្មវិធី",
"Artists": "សិល្បករ",
"AuthenticationSucceededWithUserName": "{0} បានផ្ទៀងផ្ទាត់ដោយជោគជ័យ",
"Books": "សៀវភៅ",
"NameSeasonNumber": "រដូវកាលទី {0}",
"NotificationOptionPluginInstalled": "Plugin បានដំឡើងរួច",
- "CameraImageUploadedFrom": "រូបភាពកាមេរ៉ាថ្មីត្រូវបានបង្ហោះពី {0}",
- "Channels": "ប៉ុស្ត៍",
"ChapterNameValue": "ជំពូក {0}",
"Collections": "បណ្តុំ",
"External": "ខាងក្រៅ",
"Default": "លំនាំដើម",
"NotificationOptionInstallationFailed": "ការដំឡើងមិនបានសម្រេច",
- "DeviceOfflineWithName": "{0} បានផ្តាច់",
"Folders": "ថតឯកសារ",
- "DeviceOnlineWithName": "{0} បានភ្ចាប់",
"HearingImpaired": "ខ្សោយការស្តាប់",
"HomeVideos": "វីឌីអូថតខ្លួនឯង",
"Favorites": "ចំណូលចិត្ត",
"HeaderFavoriteEpisodes": "ភាគដែលចូលចិត្ត",
"Forced": "បង្ខំ",
"Genres": "ប្រភេទ",
- "HeaderFavoriteArtists": "សិល្បករដែលចូលចិត្ត",
"NotificationOptionApplicationUpdateAvailable": "កម្មវិធី យើងអាចអាប់ដេតបាន",
"NotificationOptionApplicationUpdateInstalled": "កម្មវិធី ដែលបានដំឡើងរួច",
"NotificationOptionAudioPlaybackStopped": "ការ​ចាក់សម្លេងបានផ្អាក",
"HeaderContinueWatching": "បន្តមើល",
- "HeaderFavoriteAlbums": "អាល់ប៊ុមដែលចូលចិត្ត",
"HeaderFavoriteShows": "រឿងភាគដែលចូលចិត្ត",
"NewVersionIsAvailable": "មានជំនាន់ថ្មី ម៉ាស៊ីនមេJellyfin អាចទាញយកបាន.",
- "HeaderAlbumArtists": "សិល្បករអាល់ប៊ុម",
"NotificationOptionCameraImageUploaded": "រូបភាពពីកាំមេរ៉ាបានអាប់ឡូតរួច",
- "HeaderFavoriteSongs": "ចម្រៀងដែលចូលចិត្ត",
"HeaderNextUp": "បន្ទាប់",
"HeaderLiveTV": "ទូរទស្សន៍ផ្សាយផ្ទាល់",
"Movies": "រឿង",
- "HeaderRecordingGroups": "ក្រុមនៃការថត",
"Music": "តន្ត្រី",
"Inherit": "មរតក",
"MusicVideos": "វីដេអូតន្ត្រី",
"NameInstallFailed": "{0} ការដំឡើងបានបរាជ័យ",
"NotificationOptionNewLibraryContent": "មាតិកាថ្មីៗត្រូវបានបន្ថែម",
- "ItemAddedWithName": "{0} ត្រូវបានបន្ថែមទៅបណ្ណាល័យ",
"NameSeasonUnknown": "រដូវកាលមិនច្បាស់លាស់",
- "ItemRemovedWithName": "{0} ត្រូវបានដកចេញពីបណ្ណាល័យ",
"LabelIpAddressValue": "លេខ IP: {0}",
"LabelRunningTimeValue": "ពេលវេលាកំពុងដំណើរការ: {0}",
"Latest": "ចុងក្រោយ",
"NotificationOptionAudioPlayback": "ការ​ចាក់​សំឡេង​បាន​ចាប់ផ្ដើម",
"NotificationOptionPluginError": "Plugin មិនដំណើរការ",
"NotificationOptionPluginUninstalled": "Plugin បានលុបចេញរួច",
- "MessageApplicationUpdated": "ម៉ាស៊ីនមេនៃJellyfinត្រូវបានអាប់ដេត",
"NotificationOptionPluginUpdateInstalled": "Plugin អាប់ដេតបានដំឡើងរួច",
"NotificationOptionUserLockedOut": "អ្នកប្រើប្រាស់ត្រូវបានជាប់គាំង",
"NotificationOptionServerRestartRequired": "តម្រូវឱ្យចាប់ផ្ដើមម៉ាស៊ីនមេឡើងវិញ",
"Photos": "រូបថត",
- "Playlists": "បញ្ជីចាក់",
- "Plugin": "Plugin",
"PluginInstalledWithName": "{0} ត្រូវបានដំឡើង",
"NotificationOptionTaskFailed": "កិច្ចការដែលបានគ្រោងទុកបានបរាជ័យ",
"PluginUpdatedWithName": "{0} ត្រូវបានអាប់ដេត",
"NotificationOptionVideoPlayback": "ការចាក់វីដេអូបានចាប់ផ្តើម",
- "Songs": "ចម្រៀង",
- "ScheduledTaskStartedWithName": "{0} បានចាប់ផ្តើម",
"NotificationOptionVideoPlaybackStopped": "ការ​ចាក់​វីដេអូ​បាន​បញ្ឈប់",
"PluginUninstalledWithName": "{0} ត្រូវបានលុបចេញ",
"Shows": "រឿងភាគ",
- "ProviderValue": "អ្នកផ្តល់សេវា: {0}",
"SubtitleDownloadFailureFromForItem": "សាប់ថាយថលបានបរាជ័យក្នុងការទាញយកពី {0} នៃ {1}",
- "Sync": "ធ្វើអោយដំណាលគ្នា",
- "System": "ប្រព័ន្ធ",
"TvShows": "កម្មវិធីទូរទស្សន៍",
"ScheduledTaskFailedWithName": "{0} បានបរាជ័យ",
"Undefined": "មិនបានកំណត់",
- "User": "អ្នកប្រើប្រាស់",
"UserCreatedWithName": "អ្នកប្រើប្រាស់ {0} ត្រូវបានបង្កើតឡើង",
- "ServerNameNeedsToBeRestarted": "{0} ចាំបាច់ត្រូវចាប់ផ្តើមឡើងវិញ",
"StartupEmbyServerIsLoading": "ម៉ាស៊ីនមេJellyfin កំពុងដំណើរការ. សូមព្យាយាមម្តងទៀតក្នុងពេលឆាប់ៗនេះ.",
"UserDeletedWithName": "អ្នកប្រើប្រាស់ {0} ត្រូវបានលុបចេញ",
"UserOnlineFromDevice": "{0} បានឃើញអនឡានពី {1}",
@@ -98,10 +72,7 @@
"UserPasswordChangedWithName": "ពាក្យសម្ងាត់ត្រូវបានផ្លាស់ប្តូរសម្រាប់អ្នកប្រើប្រាស់ {0}",
"TaskCleanCache": "សម្អាតបញ្ជីឃ្លាំងសម្ងាត់",
"TaskRefreshChapterImages": "ដកស្រង់រូបភាពតាមជំពូក",
- "UserPolicyUpdatedWithName": "គោលការណ៍អ្នកប្រើប្រាស់ត្រូវបានធ្វើបច្ចុប្បន្នភាពសម្រាប់ {0}",
"UserStoppedPlayingItemWithValues": "{0} បានបញ្ចប់ការចាក់ {1} នៅលើ {2}",
- "ValueHasBeenAddedToLibrary": "{0} ត្រូវបានបញ្ចូលទៅក្នុងបណ្ណាល័យរឿងរបស់អ្នក",
- "ValueSpecialEpisodeName": "ពិសេស - {0}",
"TasksChannelsCategory": "ប៉ុស្តតាមអ៊ីនធឺណិត",
"TaskAudioNormalization": "ធ្វើឱ្យមានតន្ត្រីមានសម្លេងស្មើគ្នា",
"TaskCleanActivityLogDescription": "លុបកំណត់ហេតុសកម្មភាពចាស់ជាងអាយុដែលបានកំណត់រចនាសម្ព័ន្ធ.",
diff --git a/Emby.Server.Implementations/Localization/Core/kn.json b/Emby.Server.Implementations/Localization/Core/kn.json
index 0850600588..f053619a7a 100644
--- a/Emby.Server.Implementations/Localization/Core/kn.json
+++ b/Emby.Server.Implementations/Localization/Core/kn.json
@@ -4,8 +4,6 @@
"TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.",
"TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್‌ಟ್ರಾಕ್ಟರ್",
"TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು.",
- "ValueHasBeenAddedToLibrary": "{0} ಅನ್ನು ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಗೆ ಸೇರಿಸಲಾಗಿದೆ",
- "ValueSpecialEpisodeName": "ವಿಶೇಷ - {0}",
"TasksLibraryCategory": "ಸಮೊಹ",
"TasksApplicationCategory": "ಅಪ್ಲಿಕೇಶನ್",
"TasksChannelsCategory": "ಇಂಟರ್ನೆಟ್ ಚಾನೆಲ್ಗಳು",
@@ -13,8 +11,6 @@
"TaskCleanCacheDescription": "ಸಿಸ್ಟಮ್‌ಗೆ ಇನ್ನು ಮುಂದೆ ಅಗತ್ಯವಿಲ್ಲದ ಸಂಗ್ರಹ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
"TaskRefreshLibrary": "ಸ್ಕ್ಯಾನ್ ಮೀಡಿಯಾ ಲೈಬ್ರರಿ",
"UserOfflineFromDevice": "{1} ನಿಂದ {0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
- "Albums": "ಸಂಪುಟ",
- "Application": "ಅಪ್ಲಿಕೇಶನ್",
"AppDeviceValues": "ಅಪ್ಲಿಕೇಶನ್: {0}, ಸಾಧನ: {1}",
"Artists": "ಕಲಾವಿದರು",
"AuthenticationSucceededWithUserName": "{0} ಯಶಸ್ವಿಯಾಗಿ ದೃಢೀಕರಿಸಲಾಗಿದೆ",
@@ -22,8 +18,6 @@
"ChapterNameValue": "ಅಧ್ಯಾಯ {0}",
"Collections": "ಸಂಗ್ರಹಣೆಗಳು",
"Default": "ಪೂರ್ವನಿಯೋಜಿತ",
- "DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
- "DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ",
"External": "ಹೊರಗಿನ",
"FailedLoginAttemptWithUserName": "ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ ಸಂಖ್ಯೆ {0}",
"Favorites": "ಮೆಚ್ಚಿನವುಗಳು",
@@ -31,22 +25,11 @@
"Forced": "ಬಲವಂತವಾಗಿ",
"Genres": "ಪ್ರಕಾರಗಳು",
"HeaderContinueWatching": "ನೋಡುವುದನ್ನು ಮುಂದುವರಿಸಿ",
- "HeaderFavoriteAlbums": "ಮೆಚ್ಚಿನ ಸಂಪುಟಗಳು",
- "HeaderFavoriteArtists": "ಮೆಚ್ಚಿನ ಕಲಾವಿದರು",
"HeaderFavoriteShows": "ಮೆಚ್ಚಿನ ಪ್ರದರ್ಶನಗಳು",
- "HeaderFavoriteSongs": "ಮೆಚ್ಚಿನ ಹಾಡುಗಳು",
"HeaderLiveTV": "ನೇರ ದೂರದರ್ಶನ",
"HeaderNextUp": "ಮುಂದೆ",
- "HeaderRecordingGroups": "ರೆಕಾರ್ಡಿಂಗ್ ಗುಂಪುಗಳು",
- "MessageApplicationUpdated": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
- "CameraImageUploadedFrom": "ಹೊಸ ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು {0} ನಿಂದ ಅಪ್‌ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
- "Channels": "ಮೂಲಗಳು",
- "HeaderAlbumArtists": "ಸಂಪುಟ ಕಲಾವಿದರು",
"HeaderFavoriteEpisodes": "ಮೆಚ್ಚಿನ ಸಂಚಿಕೆಗಳು",
"HearingImpaired": "ಮೂಗ",
- "ItemAddedWithName": "{0} ಅನ್ನು ಸಂಕಲನಕ್ಕೆ ಸೇರಿಸಲಾಗಿದೆ",
- "MessageApplicationUpdatedTo": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
- "MessageNamedServerConfigurationUpdatedWithValue": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ವಿಭಾಗ {0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
"NewVersionIsAvailable": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್‌ನ ಹೊಸ ಆವೃತ್ತಿಯು ಡೌನ್‌ಲೋಡ್‌ಗೆ ಲಭ್ಯವಿದೆ.",
"NotificationOptionAudioPlayback": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
"NotificationOptionCameraImageUploaded": "ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
@@ -55,13 +38,10 @@
"NotificationOptionVideoPlaybackStopped": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
"PluginUninstalledWithName": "{0} ಅನ್ನು ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
"ScheduledTaskFailedWithName": "{0} ವಿಫಲವಾಗಿದೆ",
- "ScheduledTaskStartedWithName": "{0} ಪ್ರಾರಂಭವಾಯಿತು",
- "ServerNameNeedsToBeRestarted": "{0} ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಬೇಕಾಗಿದೆ",
"UserCreatedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ರಚಿಸಲಾಗಿದೆ",
"UserLockedOutWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಲಾಕ್ ಮಾಡಲಾಗಿದೆ",
"UserOnlineFromDevice": "{1} ನಿಂದ {0} ಆನ್‌ಲೈನ್‌ನಲ್ಲಿದೆ",
"UserPasswordChangedWithName": "{0} ಬಳಕೆದಾರರಿಗಾಗಿ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ",
- "UserPolicyUpdatedWithName": "ಬಳಕೆದಾರರ ನೀತಿಯನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
"UserStartedPlayingItemWithValues": "{2} ರಂದು {0} ಆಡುತ್ತಿದೆ {1}",
"UserStoppedPlayingItemWithValues": "{0} ಅವರು {1} ಅನ್ನು {2} ನಲ್ಲಿ ಆಡುವುದನ್ನು ಮುಗಿಸಿದ್ದಾರೆ",
"VersionNumber": "ಆವೃತ್ತಿ {0}",
@@ -76,23 +56,17 @@
"TaskCleanTranscodeDescription": "ಒಂದು ದಿನಕ್ಕಿಂತ ಹಳೆಯದಾದ ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
"TaskDownloadMissingSubtitles": "ಕಾಣೆಯಾದ ಉಪಶೀರ್ಷಿಕೆಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ",
"Shows": "ಧಾರವಾಹಿಗಳು",
- "Songs": "ಹಾಡುಗಳು",
"StartupEmbyServerIsLoading": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಲೋಡ್ ಆಗುತ್ತಿದೆ. ದಯವಿಟ್ಟು ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.",
"UserDeletedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಅಳಿಸಲಾಗಿದೆ",
"UserDownloadingItemWithValues": "{0} ಡೌನ್‌ಲೋಡ್ ಆಗುತ್ತಿದೆ {1}",
"SubtitleDownloadFailureFromForItem": "ಉಪಶೀರ್ಷಿಕೆಗಳು {0} ನಿಂದ {1} ಗಾಗಿ ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ವಿಫಲವಾಗಿವೆ",
- "Sync": "ಹೊಂದಿಕೆ",
- "System": "ವ್ಯವಸ್ಥೆ",
"TvShows": "ದೂರದರ್ಶನ ಕಾರ್ಯಕ್ರಮಗಳು",
"Undefined": "ವ್ಯಾಖ್ಯಾನಿಸಲಾಗಿಲ್ಲ",
- "User": "ಬಳಕೆದಾರ",
"HomeVideos": "ಮುಖಪುಟ ವೀಡಿಯೊಗಳು",
"Inherit": "ಪಾರಂಪರ್ಯವಾಗಿ",
- "ItemRemovedWithName": "{0} ಅನ್ನು ಸಂಕಲನದಿಂದ ತೆಗೆದುಹಾಕಲಾಗಿದೆ",
"LabelIpAddressValue": "IP ವಿಳಾಸ: {0}",
"LabelRunningTimeValue": "ಅವಧಿ: {0}",
"Latest": "ಹೊಸದಾದ",
- "MessageServerConfigurationUpdated": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
"MixedContent": "ಮಿಶ್ರ ವಿಷಯ",
"Movies": "ಚಲನಚಿತ್ರಗಳು",
"Music": "ಸಂಗೀತ",
@@ -112,11 +86,8 @@
"NotificationOptionTaskFailed": "ನಿಗದಿತ ಕಾರ್ಯ ವೈಫಲ್ಯ",
"NotificationOptionVideoPlayback": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
"Photos": "ಚಿತ್ರಗಳು",
- "Playlists": "ಪ್ಲೇಪಟ್ಟಿಗಳು",
- "Plugin": "ಪ್ಲಗಿನ್",
"PluginInstalledWithName": "{0} ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
"PluginUpdatedWithName": "{0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
- "ProviderValue": "ಒದಗಿಸುವವರು: {0}",
"TaskCleanLogs": "ಕ್ಲೀನ್ ಲಾಗ್ ಡೈರೆಕ್ಟರಿ",
"TaskRefreshPeople": "ಜನರನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
"TaskRefreshPeopleDescription": "ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಯಲ್ಲಿ ನಟರು ಮತ್ತು ನಿರ್ದೇಶಕರಿಗಾಗಿ ಮೆಟಾಡೇಟಾವನ್ನು ನವೀಕರಿಸಿ.",
diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json
index 0451dcc9f0..5d64405d19 100644
--- a/Emby.Server.Implementations/Localization/Core/ko.json
+++ b/Emby.Server.Implementations/Localization/Core/ko.json
@@ -1,41 +1,24 @@
{
- "Albums": "앨범",
"AppDeviceValues": "앱: {0}, 장치: {1}",
- "Application": "애플리케이션",
"Artists": "아티스트",
"AuthenticationSucceededWithUserName": "{0} 사용자가 성공적으로 인증됨",
"Books": "도서",
- "CameraImageUploadedFrom": "{0}에서 새로운 카메라 이미지가 업로드됨",
- "Channels": "채널",
"ChapterNameValue": "챕터 {0}",
"Collections": "컬렉션",
- "DeviceOfflineWithName": "{0}의 연결 끊김",
- "DeviceOnlineWithName": "{0}이(가) 연결됨",
"FailedLoginAttemptWithUserName": "{0}에서 로그인 실패",
"Favorites": "즐겨찾기",
"Folders": "폴더",
"Genres": "장르",
- "HeaderAlbumArtists": "앨범 음악가",
"HeaderContinueWatching": "계속 시청하기",
- "HeaderFavoriteAlbums": "즐겨찾는 앨범",
- "HeaderFavoriteArtists": "즐겨찾는 아티스트",
"HeaderFavoriteEpisodes": "즐겨찾는 에피소드",
"HeaderFavoriteShows": "즐겨찾는 쇼",
- "HeaderFavoriteSongs": "즐겨찾는 노래",
"HeaderLiveTV": "실시간 TV",
"HeaderNextUp": "다음으로",
- "HeaderRecordingGroups": "녹화 그룹",
"HomeVideos": "홈 비디오",
"Inherit": "상속",
- "ItemAddedWithName": "{0}가 라이브러리에 추가되었습니다",
- "ItemRemovedWithName": "{0}가 라이브러리에서 제거됨",
"LabelIpAddressValue": "IP 주소: {0}",
"LabelRunningTimeValue": "상영 시간: {0}",
"Latest": "최근",
- "MessageApplicationUpdated": "Jellyfin 서버가 업데이트되었습니다",
- "MessageApplicationUpdatedTo": "Jellyfin 서버가 {0}로 업데이트되었습니다",
- "MessageNamedServerConfigurationUpdatedWithValue": "서버 환경 설정 {0} 섹션이 업데이트되었습니다",
- "MessageServerConfigurationUpdated": "서버 환경 설정이 업데이트되었습니다",
"MixedContent": "혼합 콘텐츠",
"Movies": "영화",
"Music": "음악",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "비디오 재생 시작됨",
"NotificationOptionVideoPlaybackStopped": "비디오 재생 중지됨",
"Photos": "사진",
- "Playlists": "재생목록",
- "Plugin": "플러그인",
"PluginInstalledWithName": "{0} 설치됨",
"PluginUninstalledWithName": "{0} 제거됨",
"PluginUpdatedWithName": "{0} 업데이트됨",
- "ProviderValue": "제공자: {0}",
"ScheduledTaskFailedWithName": "{0} 실패",
- "ScheduledTaskStartedWithName": "{0} 시작",
- "ServerNameNeedsToBeRestarted": "{0}를 재시작해야합니다",
"Shows": "시리즈",
- "Songs": "노래",
"StartupEmbyServerIsLoading": "Jellyfin 서버를 불러오고 있습니다. 잠시 후에 다시 시도하십시오.",
"SubtitleDownloadFailureFromForItem": "{0}에서 {1} 자막 다운로드에 실패했습니다",
- "Sync": "동기화",
- "System": "시스템",
"TvShows": "TV 쇼",
- "User": "사용자",
"UserCreatedWithName": "사용자 {0} 생성됨",
"UserDeletedWithName": "사용자 {0} 삭제됨",
"UserDownloadingItemWithValues": "{0} 사용자가 {1} 다운로드 중",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} 사용자의 {1}에서 연결이 끊김",
"UserOnlineFromDevice": "{0} 사용자가 {1}에서 접속함",
"UserPasswordChangedWithName": "{0} 사용자 비밀번호 변경됨",
- "UserPolicyUpdatedWithName": "{0} 사용자 정책 업데이트됨",
"UserStartedPlayingItemWithValues": "{0} 사용자의 {2}에서 {1} 재생 중",
"UserStoppedPlayingItemWithValues": "{0} 사용자의 {2}에서 {1} 재생을 마침",
- "ValueHasBeenAddedToLibrary": "{0}가 미디어 라이브러리에 추가되었습니다",
- "ValueSpecialEpisodeName": "스페셜 - {0}",
"VersionNumber": "버전 {0}",
"TasksApplicationCategory": "어플리케이션",
"TasksMaintenanceCategory": "유지 보수",
diff --git a/Emby.Server.Implementations/Localization/Core/kw.json b/Emby.Server.Implementations/Localization/Core/kw.json
index 613d531103..fc2e189e7f 100644
--- a/Emby.Server.Implementations/Localization/Core/kw.json
+++ b/Emby.Server.Implementations/Localization/Core/kw.json
@@ -1,21 +1,13 @@
{
"Collections": "Kuntellow",
- "DeviceOfflineWithName": "{0} re anjunyas",
"External": "A-ves",
"Folders": "Plegellow",
- "HeaderFavoriteAlbums": "Albomow Drudh",
- "HeaderFavoriteArtists": "Artydhyon Drudh",
"HeaderFavoriteEpisodes": "Towlennow Drudh",
- "HeaderFavoriteSongs": "Kanow Drudh",
- "HeaderRecordingGroups": "Bagasow Rekordya",
"HearingImpaired": "Klewans Aperys",
"HomeVideos": "Gwydhyow Tre",
"Inherit": "Herya",
"LabelRunningTimeValue": "Prys ow ponya: {0}",
"Latest": "Diwettha",
- "MessageApplicationUpdated": "Servell Jellyfin re beu nowedhys",
- "MessageApplicationUpdatedTo": "Servell Jellyfin re beu nowedhys dhe {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Rann dewisyans servell {0} re beu nowedhys",
"MixedContent": "Dalgh kemmyskys",
"Movies": "Fylmow",
"MusicVideos": "Gwydhyow Ilow",
@@ -25,23 +17,16 @@
"NotificationOptionPluginError": "Defowt ystynnans",
"NotificationOptionPluginUninstalled": "Ystynnans anynstallys",
"NotificationOptionPluginUpdateInstalled": "Nowedheans ystynnans ynstallys",
- "Application": "Gweythres",
"Favorites": "Moyha Kerys",
"Forced": "Konstrynys",
- "Albums": "Albomow",
"Books": "Lyvrow",
- "Channels": "Kanolyow",
"AppDeviceValues": "App: {0}, Devis: {1}",
"Artists": "Artyhdyon",
- "HeaderAlbumArtists": "Albom artydhyon",
"HeaderNextUp": "Nessa",
- "CameraImageUploadedFrom": "Skeusen kamera nowydh re beu ughkargys a-dhyworth {0}",
"ChapterNameValue": "Chaptra {0}",
"FailedLoginAttemptWithUserName": "Assay omgelm fyllys a-dhyworth {0}",
"AuthenticationSucceededWithUserName": "{0} omgelmys yn sewen",
"Default": "Defowt",
- "DeviceOnlineWithName": "{0} yw junys",
- "ItemRemovedWithName": "{0} a veu dileys a-dhyworth an lyverva",
"LabelIpAddressValue": "Trigva PK: {)}",
"Music": "Ilow",
"HeaderContinueWatching": "Pesya Ow Kweles",
@@ -50,8 +35,6 @@
"NotificationOptionCameraImageUploaded": "Skeusen kamera ughkargys",
"HeaderFavoriteShows": "Diskwedhyansow Drudh",
"HeaderLiveTV": "PW Yn Fyw",
- "MessageServerConfigurationUpdated": "Dewisyans servell re beu nowedhys",
- "ItemAddedWithName": "{0} a veu keworrys dhe'n lyverva",
"NameInstallFailed": "{0} ynstallyans fyllys",
"NotificationOptionNewLibraryContent": "Dalgh nowydh keworrys",
"NewVersionIsAvailable": "Yma versyon nowydh a Servell Jellyfin neb yw kavadow rag iskarga.",
@@ -62,8 +45,6 @@
"NotificationOptionServerRestartRequired": "Dastalleth servell yw res",
"StartupEmbyServerIsLoading": "Yma Servell Jellyfin ow kargya. Assay arta yn berr mar pleg.",
"SubtitleDownloadFailureFromForItem": "Istitlow a fyllis iskarga a-dhyworth {0] rag {1}",
- "System": "Kevreyth",
- "User": "Devnydhyer",
"UserDeletedWithName": "Devnydhyer {0} re beu dileys",
"UserLockedOutWithName": "Devnydhyer {0} re beu alhwedhys yn-mes",
"UserStoppedPlayingItemWithValues": "{0} re worfennas gwari {1} war {2}",
@@ -71,21 +52,15 @@
"UserOnlineFromDevice": "{0} yw warlinen a-dhyworth {1}",
"NotificationOptionUserLockedOut": "Devnydhyer yw alhwedhys yn-mes",
"Photos": "Skeusennow",
- "Playlists": "Rolyow-gwari",
- "Plugin": "Ystynnans",
"PluginInstalledWithName": "{0} a veu ynstallys",
- "UserPolicyUpdatedWithName": "Polici devnydhyer re beu nowedhys rag {0}",
"PluginUpdatedWithName": "{0} a veu nowedhys",
"ScheduledTaskFailedWithName": "{0} a fyllis",
- "Songs": "Kanow",
- "Sync": "Kesseni",
"TvShows": "Towlennow PW",
"Undefined": "Anstyrys",
"UserCreatedWithName": "Devnydhyer {0} re beu gwruthys",
"UserDownloadingItemWithValues": "Yma {0} owth iskarga {1}",
"UserPasswordChangedWithName": "Ger-tremena re beu chanjys rag devnydhyer {0}",
"UserStartedPlayingItemWithValues": "Yma {0} ow kwari {1} war {2}",
- "ValueHasBeenAddedToLibrary": "{0} re beu keworrys dhe'th lyverva media",
"VersionNumber": "Versyon {0}",
"TasksLibraryCategory": "Lyverva",
"TaskCleanActivityLog": "Glanhe Kovlyver Gwrians",
@@ -96,10 +71,6 @@
"NotificationOptionVideoPlayback": "Gwareans gwydhyow yw dallethys",
"PluginUninstalledWithName": "{0} a veu anynstallys",
"NotificationOptionTaskFailed": "Defowt oberen towlennys",
- "ProviderValue": "Provier: {0}",
- "ScheduledTaskStartedWithName": "{0} a dhallathas",
- "ServerNameNeedsToBeRestarted": "Yma edhom dhe {0} a vos dastallathys",
- "ValueSpecialEpisodeName": "Arbennik - {0}",
"TasksMaintenanceCategory": "Mentons",
"TasksApplicationCategory": "Gweythres",
"TasksChannelsCategory": "Kanolyow Kesrosweyth",
diff --git a/Emby.Server.Implementations/Localization/Core/lb.json b/Emby.Server.Implementations/Localization/Core/lb.json
index 2afec05dbd..e94709b083 100644
--- a/Emby.Server.Implementations/Localization/Core/lb.json
+++ b/Emby.Server.Implementations/Localization/Core/lb.json
@@ -1,38 +1,24 @@
{
- "Albums": "Alben",
- "Application": "Applikatioun",
"Artists": "Kënschtler",
"Books": "Bicher",
- "Channels": "Kanäl",
"Collections": "Kollektiounen",
"Default": "Standard",
"ChapterNameValue": "Kapitel {0}",
- "DeviceOnlineWithName": "{0} ass Online",
- "DeviceOfflineWithName": "{0} ass Offline",
"External": "Extern",
"Favorites": "Favoritten",
"Folders": "Dossieren",
"Forced": "Forcéiert",
- "HeaderAlbumArtists": "Album Kënschtler",
- "HeaderFavoriteAlbums": "Léifsten Alben",
- "HeaderFavoriteArtists": "Léifsten Kënschtler",
"HeaderFavoriteEpisodes": "Léifsten Episoden",
"HeaderFavoriteShows": "Léifsten Shows",
- "HeaderFavoriteSongs": "Léifsten Lidder",
"Genres": "Generen",
"HeaderContinueWatching": "Weider kucken",
"Inherit": "Iwwerhuelen",
"HeaderNextUp": "Als Nächst",
- "HeaderRecordingGroups": "Opname Gruppen",
"HearingImpaired": "Daaf",
"HomeVideos": "Amateur Videoen",
- "ItemRemovedWithName": "Element ewech geholl: {0}",
"LabelIpAddressValue": "IP Adress: {0}",
"LabelRunningTimeValue": "Lafzäit: {0}",
"Latest": "Dat Aktuellst",
- "MessageApplicationUpdatedTo": "Jellyfin Server aktualiséiert op {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Server Konfiguratiounssektioun {0} aktualiséiert",
- "MessageServerConfigurationUpdated": "Server Konfiguratioun aktualiséiert",
"Movies": "Filmer",
"Music": "Musek",
"NameInstallFailed": "{0} Installatioun net gelongen",
@@ -55,19 +41,11 @@
"NotificationOptionUserLockedOut": "Benotzer Gesperrt",
"NotificationOptionVideoPlaybackStopped": "Video ofspillen gestoppt",
"NotificationOptionVideoPlayback": "Video ofspillen gestartet",
- "Plugin": "Plugin",
"PluginUninstalledWithName": "{0} desinstalléiert",
"PluginUpdatedWithName": "{0} aktualiséiert",
- "ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "Aufgab: {0} net gelongen",
- "Playlists": "Playlëschten",
"Shows": "Shows",
- "Songs": "Lidder",
- "ServerNameNeedsToBeRestarted": "{0} muss nei gestart ginn",
"StartupEmbyServerIsLoading": "Jellyfin Server luedt. Probéier méi spéit nach eng Kéier.",
- "Sync": "Synchroniséieren",
- "System": "System",
- "User": "Benotzer",
"TvShows": "TV Shows",
"Undefined": "Net definéiert",
"UserCreatedWithName": "Benotzer {0} erstellt",
@@ -76,13 +54,10 @@
"UserLockedOutWithName": "Benotzer {0} gesperrt",
"UserOnlineFromDevice": "{0} Benotzer Online um Gerät {1}",
"UserPasswordChangedWithName": "Benotzer Passwuert geännert fir {0}",
- "UserPolicyUpdatedWithName": "Benotzer Politik aktualiséiert fir: {0}",
"UserStartedPlayingItemWithValues": "{0} spillt {1} op {2} oof",
- "ValueHasBeenAddedToLibrary": "{0} der Bibliothéik bäigefüügt",
"VersionNumber": "Versioun {0}",
"TasksMaintenanceCategory": "Ënnerhalt",
"TasksLibraryCategory": "Bibliothéik",
- "ValueSpecialEpisodeName": "Spezial-Episodenumm",
"TasksChannelsCategory": "Internet Kanäl",
"TaskCleanActivityLog": "Aktivitéits Log botzen",
"TaskCleanActivityLogDescription": "Läscht Aktivitéitslogs méi al wéi konfiguréiert.",
@@ -106,18 +81,14 @@
"TaskKeyframeExtractor": "Schlësselbild Extrakter",
"TaskExtractMediaSegments": "Mediesegment-Scan",
"NewVersionIsAvailable": "Nei Versioun fir Jellyfin Server ass verfügbar.",
- "CameraImageUploadedFrom": "En neit Kamera Bild gouf vu {0} eropgelueden",
"PluginInstalledWithName": "{0} installéiert",
"TaskMoveTrickplayImagesDescription": "Verschëfft existent Trickplay-Dateien no de Bibliothéik-Astellungen.",
"AppDeviceValues": "App: {0}, Geräter: {1}",
"FailedLoginAttemptWithUserName": "Net Gelongen Umeldung {0}",
"HeaderLiveTV": "LiveTV",
- "ItemAddedWithName": "Element derbäi gesat: {0}",
"NotificationOptionServerRestartRequired": "Server Restart Erfuerderlech",
- "ScheduledTaskStartedWithName": "Aufgab: {0} gestart",
"AuthenticationSucceededWithUserName": "{0} Authentifikatioun gelongen",
"MixedContent": "Gemëschten Inhalt",
- "MessageApplicationUpdated": "Jellyfin Server Aktualiséiert",
"SubtitleDownloadFailureFromForItem": "Ënnertitel Download Feeler vun {0} fir {1}",
"TaskCleanLogsDescription": "Läscht Log-Dateien, déi méi al wéi {0} Deeg sinn.",
"TaskUpdatePlugins": "Plugins aktualiséieren",
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index daff719ea7..ed26004a43 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -1,41 +1,24 @@
{
- "Albums": "Albumai",
"AppDeviceValues": "Programa: {0}, Įrenginys: {1}",
- "Application": "Programėlė",
"Artists": "Atlikėjai",
"AuthenticationSucceededWithUserName": "{0} sėkmingai autentifikuota",
"Books": "Knygos",
- "CameraImageUploadedFrom": "Nauja nuotrauka įkelta iš kameros {0}",
- "Channels": "Kanalai",
"ChapterNameValue": "Scena{0}",
"Collections": "Rinkiniai",
- "DeviceOfflineWithName": "{0} buvo atjungtas",
- "DeviceOnlineWithName": "{0} prisijungęs",
"FailedLoginAttemptWithUserName": "Nesėkmingas {0} bandymas prisijungti",
"Favorites": "Mėgstami",
"Folders": "Katalogai",
"Genres": "Žanrai",
- "HeaderAlbumArtists": "Albumo atlikėjai",
"HeaderContinueWatching": "Žiūrėti toliau",
- "HeaderFavoriteAlbums": "Mėgstami albumai",
- "HeaderFavoriteArtists": "Mėgstami atlikėjai",
"HeaderFavoriteEpisodes": "Mėgstamiausios serijos",
"HeaderFavoriteShows": "Mėgstamiausios TV Laidos",
- "HeaderFavoriteSongs": "Mėgstamos Dainos",
"HeaderLiveTV": "Tiesioginė TV",
"HeaderNextUp": "Toliau",
- "HeaderRecordingGroups": "Įrašų grupės",
"HomeVideos": "Namų vaizdo įrašai",
"Inherit": "Paveldėti",
- "ItemAddedWithName": "{0} - buvo įkeltas į biblioteką",
- "ItemRemovedWithName": "{0} - buvo pašalinta iš bibliotekos",
"LabelIpAddressValue": "IP adresas: {0}",
"LabelRunningTimeValue": "Trukmė: {0}",
"Latest": "Naujausi",
- "MessageApplicationUpdated": "\"Jellyfin Server\" atnaujintas",
- "MessageApplicationUpdatedTo": "\"Jellyfin Server\" buvo atnaujinta iki {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Serverio nustatymai (skyrius {0}) buvo atnaujinti",
- "MessageServerConfigurationUpdated": "Serverio nustatymai buvo atnaujinti",
"MixedContent": "Mišrus turinys",
"Movies": "Filmai",
"Music": "Muzika",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Vaizdo įrašo atkūrimas pradėtas",
"NotificationOptionVideoPlaybackStopped": "Vaizdo įrašo atkūrimas sustabdytas",
"Photos": "Nuotraukos",
- "Playlists": "Grojaraščiai",
- "Plugin": "Įskiepis",
"PluginInstalledWithName": "{0} buvo įdiegtas",
"PluginUninstalledWithName": "{0} buvo pašalintas",
"PluginUpdatedWithName": "{0} buvo atnaujintas",
- "ProviderValue": "Paslaugos tiekėjas: {0}",
"ScheduledTaskFailedWithName": "{0} nepavyko",
- "ScheduledTaskStartedWithName": "{0} paleista",
- "ServerNameNeedsToBeRestarted": "{0} reikia iš naujo paleisti",
"Shows": "Laidos",
- "Songs": "Kūriniai",
"StartupEmbyServerIsLoading": "Jellyfin Server kraunasi. Netrukus pabandykite dar kartą.",
"SubtitleDownloadFailureFromForItem": "{1} subtitrai buvo nesėkmingai parsiųsti iš {0}",
- "Sync": "Sinchronizuoti",
- "System": "Sistema",
"TvShows": "TV laidos",
- "User": "Naudotojas",
"UserCreatedWithName": "Buvo sukurtas {0} naudotojas",
"UserDeletedWithName": "Naudotojas {0} ištrintas",
"UserDownloadingItemWithValues": "{0} siunčiasi {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} buvo atjungtas nuo {1}",
"UserOnlineFromDevice": "{0} prisijungęs iš {1}",
"UserPasswordChangedWithName": "Slaptažodis pakeistas naudotojui {0}",
- "UserPolicyUpdatedWithName": "Naudotojo {0} teisės buvo pakeistos",
"UserStartedPlayingItemWithValues": "{0} leidžia {1} į {2}",
"UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}",
- "ValueHasBeenAddedToLibrary": "{0} pridėtas į mediateką",
- "ValueSpecialEpisodeName": "Ypatingų - {0}",
"VersionNumber": "Versija {0}",
"TaskUpdatePluginsDescription": "Atsisiunčia ir įdiegia įskiepių, kurie sukonfigūruoti atnaujinti automatiškai, naujinius.",
"TaskUpdatePlugins": "Atnaujinti įskieius",
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index 1083e3c299..4a1b248e76 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -1,15 +1,10 @@
{
- "ServerNameNeedsToBeRestarted": "{0} ir vajadzīgs restarts",
"NotificationOptionTaskFailed": "Plānota uzdevuma kļūme",
- "HeaderRecordingGroups": "Ierakstu grupas",
- "UserPolicyUpdatedWithName": "Lietotāju politika atjaunota priekš {0}",
"SubtitleDownloadFailureFromForItem": "Subtitru lejupielāde no {0} priekš {1} neizdevās",
"NotificationOptionVideoPlaybackStopped": "Video atskaņošana apturēta",
"NotificationOptionVideoPlayback": "Video atskaņošana sākta",
"NotificationOptionInstallationFailed": "Instalācija neizdevās",
"AuthenticationSucceededWithUserName": "{0} veiksmīgi autentificējies",
- "ValueSpecialEpisodeName": "Speciālais - {0}",
- "ScheduledTaskStartedWithName": "{0} iesākts",
"ScheduledTaskFailedWithName": "{0} neizdevās",
"Photos": "Attēli",
"NotificationOptionUserLockedOut": "Lietotājs bloķēts",
@@ -17,7 +12,6 @@
"Inherit": "Pārmantot",
"AppDeviceValues": "Lietotne: {0}, Ierīce: {1}",
"VersionNumber": "Versija {0}",
- "ValueHasBeenAddedToLibrary": "{0} tika pievienots jūsu multvides bibliotēkai",
"UserStoppedPlayingItemWithValues": "{0} ir beidzis atskaņot {1} uz {2}",
"UserStartedPlayingItemWithValues": "{0} atskaņo {1} uz {2}",
"UserPasswordChangedWithName": "Lietotāja {0} parole tika nomainīta",
@@ -27,23 +21,16 @@
"UserDownloadingItemWithValues": "{0} lejupielādē {1}",
"UserDeletedWithName": "Lietotājs {0} ir izdzēsts",
"UserCreatedWithName": "Lietotājs {0} ir ticis izveidots",
- "User": "Lietotājs",
"TvShows": "TV raidījumi",
- "Sync": "Sinhronizācija",
- "System": "Sistēma",
"StartupEmbyServerIsLoading": "Jellyfin Serveris lādējas. Lūdzu mēģiniet vēlreiz pēc brīža.",
- "Songs": "Dziesmas",
"Shows": "Šovi",
"PluginUpdatedWithName": "{0} tika atjaunots",
"PluginUninstalledWithName": "{0} tika noņemts",
"PluginInstalledWithName": "{0} tika uzstādīts",
- "Plugin": "Paplašinājums",
- "Playlists": "Atskaņošanas saraksti",
"MixedContent": "Jaukts saturs",
"HomeVideos": "Mājas video",
"HeaderNextUp": "Nākamais",
"ChapterNameValue": "{0}. nodaļa",
- "Application": "Lietotne",
"NotificationOptionServerRestartRequired": "Nepieciešams servera restarts",
"NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts",
"NotificationOptionPluginUninstalled": "Paplašinājums noņemts",
@@ -62,35 +49,19 @@
"MusicVideos": "Mūzikas video",
"Music": "Mūzika",
"Movies": "Filmas",
- "MessageServerConfigurationUpdated": "Servera konfigurācija ir tikusi atjaunota",
- "MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} tika atjaunota",
- "MessageApplicationUpdatedTo": "Jellyfin Server ir ticis atjaunots uz {0}",
- "MessageApplicationUpdated": "Jellyfin Server ir ticis atjaunots",
"Latest": "Jaunākais",
"LabelIpAddressValue": "IP adrese: {0}",
- "ItemRemovedWithName": "{0} tika noņemts no bibliotēkas",
- "ItemAddedWithName": "{0} tika pievienots bibliotēkai",
"HeaderLiveTV": "Tiešraides TV",
"HeaderContinueWatching": "Turpini skatīties",
- "HeaderAlbumArtists": "Albumu izpildītāji",
"Genres": "Žanri",
"Folders": "Mapes",
"Favorites": "Izlase",
"FailedLoginAttemptWithUserName": "Neizdevies ieiešanas mēģinājums no {0}",
- "DeviceOnlineWithName": "Savienojums ar {0} ir izveidots",
- "DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts",
"Collections": "Kolekcijas",
- "Channels": "Kanāli",
- "CameraImageUploadedFrom": "Jauns kameras attēls tika augšupielādēts no {0}",
"Books": "Grāmatas",
"Artists": "Izpildītāji",
- "Albums": "Albumi",
- "ProviderValue": "Provider: {0}",
- "HeaderFavoriteSongs": "Dziesmu izlase",
"HeaderFavoriteShows": "Raidījumu izlase",
"HeaderFavoriteEpisodes": "Sēriju izlase",
- "HeaderFavoriteArtists": "Izpildītāju izlase",
- "HeaderFavoriteAlbums": "Albumu izlase",
"TaskCleanCacheDescription": "Nodzēš kešatmiņas datnes, kas vairs nav sistēmai vajadzīgas.",
"TaskRefreshChapterImages": "Izvilkt nodaļu attēlus",
"TasksApplicationCategory": "Lietotne",
@@ -135,5 +106,6 @@
"TaskDownloadMissingLyrics": "Lejupielādēt trūkstošos vārdus",
"TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām",
"CleanupUserDataTask": "Lietotāju datu tīrīšanas uzdevums",
- "CleanupUserDataTaskDescription": "Notīra visus lietotāja datus (skatīšanās stāvokļus, favorītu statusi utt.) no medijiem, kas vairs nav pieejami vismaz 90 dienas."
+ "CleanupUserDataTaskDescription": "Notīra visus lietotāja datus (skatīšanās stāvokļus, favorītu statusi utt.) no medijiem, kas vairs nav pieejami vismaz 90 dienas.",
+ "Original": "Oriģināls"
}
diff --git a/Emby.Server.Implementations/Localization/Core/lzh.json b/Emby.Server.Implementations/Localization/Core/lzh.json
index 9fb53e41d5..6b61755d12 100644
--- a/Emby.Server.Implementations/Localization/Core/lzh.json
+++ b/Emby.Server.Implementations/Localization/Core/lzh.json
@@ -1,10 +1,8 @@
{
- "Albums": "辑册",
"Artists": "艺人",
"AuthenticationSucceededWithUserName": "{0} 授之权矣",
"Books": "册",
"Genres": "类",
- "HeaderAlbumArtists": "辑者",
"Favorites": "至爱",
"Folders": "箧",
"HeaderContinueWatching": "接目未竟"
diff --git a/Emby.Server.Implementations/Localization/Core/mi.json b/Emby.Server.Implementations/Localization/Core/mi.json
index 3b20abb369..74fae52dca 100644
--- a/Emby.Server.Implementations/Localization/Core/mi.json
+++ b/Emby.Server.Implementations/Localization/Core/mi.json
@@ -1,9 +1,15 @@
{
- "Albums": "Pukaemi",
"AppDeviceValues": "Taupānga: {0}, Pūrere: {1}",
- "Application": "Taupānga",
"Artists": "Kaiwaiata",
"AuthenticationSucceededWithUserName": "{0} has been successfully authenticated",
"Books": "Ngā pukapuka",
- "CameraImageUploadedFrom": "Kua tuku ake he whakaahua kāmera hou mai i {0}"
+ "Default": "Taunoa",
+ "Collections": "Kohinga",
+ "External": "Waho",
+ "Folders": "Kōpaki",
+ "Forced": "Kaha",
+ "Music": "Waiata",
+ "Movies": "Kiriata",
+ "Latest": "Hou",
+ "Inherit": "Riro"
}
diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json
index efef194e14..daf8112eef 100644
--- a/Emby.Server.Implementations/Localization/Core/mk.json
+++ b/Emby.Server.Implementations/Localization/Core/mk.json
@@ -1,11 +1,8 @@
{
"ScheduledTaskFailedWithName": "{0} неуспешно",
- "ProviderValue": "Провајдер: {0}",
"PluginUpdatedWithName": "{0} беше надоградено",
"PluginUninstalledWithName": "{0} беше успешно деинсталирано",
"PluginInstalledWithName": "{0} беше успешно инсталирано",
- "Plugin": "Додатоци",
- "Playlists": "Плејлисти",
"Photos": "Слики",
"NotificationOptionVideoPlaybackStopped": "Видео стопирано",
"NotificationOptionVideoPlayback": "Видео пуштено",
@@ -31,49 +28,29 @@
"Music": "Музика",
"Movies": "Филмови",
"MixedContent": "Мешана содржина",
- "MessageServerConfigurationUpdated": "Серверската конфигурација беше надградена",
- "MessageNamedServerConfigurationUpdatedWithValue": "Секцијата на конфигурација на сервер {0} беше надоградена",
- "MessageApplicationUpdatedTo": "Jellyfin беше надограден до {0}",
- "MessageApplicationUpdated": "Jellyfin Серверот беше надограден",
"Latest": "Последно",
"LabelRunningTimeValue": "Време на работа: {0}",
"LabelIpAddressValue": "ИП Адреса: {0}",
- "ItemRemovedWithName": "{0} е избришано до Библиотеката",
- "ItemAddedWithName": "{0} беше додадено во Библиотеката",
"Inherit": "Следно",
"HomeVideos": "Домашни Видеа",
- "HeaderRecordingGroups": "Групи на снимање",
"HeaderNextUp": "Следно",
"HeaderLiveTV": "ТВ",
- "HeaderFavoriteSongs": "Омилени Песни",
"HeaderFavoriteShows": "Омилени Серии",
"HeaderFavoriteEpisodes": "Омилени Епизоди",
- "HeaderFavoriteArtists": "Омилени Изведувачи",
- "HeaderFavoriteAlbums": "Омилени Албуми",
"HeaderContinueWatching": "Продолжи со Гледање",
- "HeaderAlbumArtists": "Изведувачи од Албуми",
"Genres": "Жанрови",
"Folders": "Папки",
"Favorites": "Омилени",
"FailedLoginAttemptWithUserName": "Неуспешен обид за најавување од {0}",
- "DeviceOnlineWithName": "{0} е приклучен",
- "DeviceOfflineWithName": "{0} се исклучи",
"Collections": "Колекции",
"ChapterNameValue": "Дел {0}",
- "Channels": "Канали",
- "CameraImageUploadedFrom": "Нова слика од камера беше поставена од {0}",
"Books": "Книги",
"AuthenticationSucceededWithUserName": "{0} успешно поврзан",
"Artists": "Изведувачи",
- "Application": "Апликација",
"AppDeviceValues": "Апликација: {0}, Уред: {1}",
- "Albums": "Албуми",
"VersionNumber": "Верзија {0}",
- "ValueSpecialEpisodeName": "Специјално - {0}",
- "ValueHasBeenAddedToLibrary": "{0} е додадено во твојата библиотека",
"UserStoppedPlayingItemWithValues": "{0} заврши со репродукција {1} во {2}",
"UserStartedPlayingItemWithValues": "{0} пушти {1} на {2}",
- "UserPolicyUpdatedWithName": "Полисата на користење беше надоградена за {0}",
"UserPasswordChangedWithName": "Лозинката е сменета за корисникот {0}",
"UserOnlineFromDevice": "{0} е приклучен од {1}",
"UserOfflineFromDevice": "{0} е дисконектиран од {1}",
@@ -81,16 +58,10 @@
"UserDownloadingItemWithValues": "{0} се спушта {1}",
"UserDeletedWithName": "Корисникот {0} е избришан",
"UserCreatedWithName": "Корисникот {0} е креиран",
- "User": "Корисник",
"TvShows": "ТВ Серии",
- "System": "Систем",
- "Sync": "Синхронизација",
"SubtitleDownloadFailureFromForItem": "Преводот неуспешно се спушти од {0} за {1}",
"StartupEmbyServerIsLoading": "Jellyfin Server се пушта. Ве молиме причекајте.",
- "Songs": "Песни",
"Shows": "Серии",
- "ServerNameNeedsToBeRestarted": "{0} треба да се рестартира",
- "ScheduledTaskStartedWithName": "{0} започна",
"TaskRefreshChapterImages": "Извези Слики од Поглавје",
"TaskCleanCacheDescription": "Ги брише кешираните фајлови што не се повеќе потребни од системот.",
"TaskCleanCache": "Исчисти Го Кешот",
diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json
index 5f098bccac..dbf2ed4648 100644
--- a/Emby.Server.Implementations/Localization/Core/ml.json
+++ b/Emby.Server.Implementations/Localization/Core/ml.json
@@ -1,32 +1,18 @@
{
"AppDeviceValues": "അപ്ലിക്കേഷൻ: {0}, ഉപകരണം: {1}",
- "Application": "അപ്ലിക്കേഷൻ",
"AuthenticationSucceededWithUserName": "{0} വിജയകരമായി പ്രാമാണീകരിച്ചു",
- "CameraImageUploadedFrom": "{0} എന്നതിൽ നിന്ന് ഒരു പുതിയ ക്യാമറ ചിത്രം അപ്‌ലോഡുചെയ്‌തു",
"ChapterNameValue": "അധ്യായം {0}",
- "DeviceOfflineWithName": "{0} വിച്ഛേദിച്ചു",
- "DeviceOnlineWithName": "{0} ബന്ധിപ്പിച്ചു",
"FailedLoginAttemptWithUserName": "{0}ൽ നിന്നുള്ള പ്രവേശന ശ്രമം പരാജയപ്പെട്ടു",
"Forced": "നിർബന്ധിതമായി",
- "HeaderFavoriteAlbums": "പ്രിയപ്പെട്ട ആൽബങ്ങൾ",
- "HeaderFavoriteArtists": "പ്രിയപ്പെട്ട കലാകാരന്മാർ",
"HeaderFavoriteEpisodes": "പ്രിയപ്പെട്ട എപ്പിസോഡുകൾ",
"HeaderFavoriteShows": "പ്രിയപ്പെട്ട ഷോകൾ",
- "HeaderFavoriteSongs": "പ്രിയപ്പെട്ട ഗാനങ്ങൾ",
"HeaderLiveTV": "തത്സമയ ടിവി",
"HeaderNextUp": "അടുത്തത്",
- "HeaderRecordingGroups": "ഗ്രൂപ്പുകൾ റെക്കോർഡുചെയ്യുന്നു",
"HomeVideos": "ഹോം വീഡിയോകൾ",
"Inherit": "അനന്തരാവകാശം",
- "ItemAddedWithName": "{0} ലൈബ്രറിയിൽ ചേർത്തു",
- "ItemRemovedWithName": "{0} ലൈബ്രറിയിൽ നിന്ന് നീക്കംചെയ്തു",
"LabelIpAddressValue": "IP വിലാസം: {0}",
"LabelRunningTimeValue": "പ്രവർത്തന സമയം: {0}",
"Latest": "ഏറ്റവും പുതിയ",
- "MessageApplicationUpdated": "ജെല്ലിഫിൻ സെർവർ അപ്‌ഡേറ്റുചെയ്‌തു",
- "MessageApplicationUpdatedTo": "ജെല്ലിഫിൻ സെർവർ {0 to ലേക്ക് അപ്‌ഡേറ്റുചെയ്‌തു",
- "MessageNamedServerConfigurationUpdatedWithValue": "സെർവർ കോൺഫിഗറേഷൻ വിഭാഗം {0 അപ്‌ഡേറ്റുചെയ്‌തു",
- "MessageServerConfigurationUpdated": "സെർവർ കോൺഫിഗറേഷൻ അപ്‌ഡേറ്റുചെയ്‌തു",
"MixedContent": "മിശ്രിത ഉള്ളടക്കം",
"Music": "സംഗീതം",
"MusicVideos": "സംഗീത വീഡിയോകൾ",
@@ -50,20 +36,14 @@
"NotificationOptionUserLockedOut": "ഉപയോക്താവ് ലോക്ക് out ട്ട് ചെയ്‌തു",
"NotificationOptionVideoPlayback": "വീഡിയോ പ്ലേബാക്ക് ആരംഭിച്ചു",
"NotificationOptionVideoPlaybackStopped": "വീഡിയോ പ്ലേബാക്ക് നിർത്തി",
- "Plugin": "പ്ലഗിൻ",
"PluginInstalledWithName": "{0} ഇൻസ്റ്റാളുചെയ്‌തു",
"PluginUninstalledWithName": "{0 un അൺഇൻസ്റ്റാൾ ചെയ്തു",
"PluginUpdatedWithName": "{0} അപ്‌ഡേറ്റുചെയ്‌തു",
- "ProviderValue": "ദാതാവ്: {0}",
"ScheduledTaskFailedWithName": "{0} പരാജയപ്പെട്ടു",
- "ScheduledTaskStartedWithName": "{0} ആരംഭിച്ചു",
- "ServerNameNeedsToBeRestarted": "{0} പുനരാരംഭിക്കേണ്ടതുണ്ട്",
"StartupEmbyServerIsLoading": "ജെല്ലിഫിൻ സെർവർ ലോഡുചെയ്യുന്നു. ഉടൻ തന്നെ വീണ്ടും ശ്രമിക്കുക.",
"SubtitleDownloadFailureFromForItem": "സബ്ടൈറ്റിലുകൾ {1} ന് {0 from ൽ നിന്ന് ഡ download ൺലോഡ് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു",
- "System": "സിസ്റ്റം",
"TvShows": "ടിവി ഷോകൾ",
"Undefined": "നിർവചിച്ചിട്ടില്ല",
- "User": "ഉപയോക്താവ്",
"UserCreatedWithName": "ഉപയോക്താവ് {0 created സൃഷ്ടിച്ചു",
"UserDeletedWithName": "ഉപയോക്താവ് {0 deleted ഇല്ലാതാക്കി",
"UserDownloadingItemWithValues": "{0} ഡൗൺലോഡുചെയ്യുന്നു {1}",
@@ -71,10 +51,8 @@
"UserOfflineFromDevice": "{0} {1} ൽ നിന്ന് വിച്ഛേദിച്ചു",
"UserOnlineFromDevice": "{0} {1} മുതൽ ഓൺ‌ലൈനിലാണ്",
"UserPasswordChangedWithName": "{0} ഉപയോക്താവിനായി പാസ്‌വേഡ് മാറ്റി",
- "UserPolicyUpdatedWithName": "{0} എന്നതിനായി ഉപയോക്തൃ നയം അപ്‌ഡേറ്റുചെയ്‌തു",
"UserStartedPlayingItemWithValues": "{0} {2} ൽ {1} പ്ലേ ചെയ്യുന്നു",
"UserStoppedPlayingItemWithValues": "{0} {2} ൽ {1 play കളിക്കുന്നത് പൂർത്തിയാക്കി",
- "ValueHasBeenAddedToLibrary": "Media 0 your നിങ്ങളുടെ മീഡിയ ലൈബ്രറിയിലേക്ക് ചേർത്തു",
"VersionNumber": "പതിപ്പ് {0}",
"TasksMaintenanceCategory": "പരിപാലനം",
"TasksLibraryCategory": "പുസ്തകശാല",
@@ -100,16 +78,10 @@
"TaskRefreshChannelsDescription": "ഇന്റർനെറ്റ് ചാനൽ വിവരങ്ങൾ പുതുക്കുന്നു.",
"TaskDownloadMissingSubtitles": "നഷ്‌ടമായ സബ്‌ടൈറ്റിലുകൾ ഡൗൺലോഡുചെയ്യുക",
"TaskDownloadMissingSubtitlesDescription": "മെറ്റാഡാറ്റ കോൺഫിഗറേഷനെ അടിസ്ഥാനമാക്കി നഷ്‌ടമായ സബ്‌ടൈറ്റിലുകൾക്കായി ഇന്റർനെറ്റ് തിരയുന്നു.",
- "ValueSpecialEpisodeName": "പ്രത്യേക - {0}",
"Collections": "ശേഖരങ്ങൾ",
"Folders": "ഫോൾഡറുകൾ",
- "HeaderAlbumArtists": "കലാകാരന്റെ ആൽബം",
- "Sync": "സമന്വയിപ്പിക്കുക",
"Movies": "സിനിമകൾ",
"Photos": "ഫോട്ടോകൾ",
- "Albums": "ആൽബങ്ങൾ",
- "Playlists": "പ്ലേലിസ്റ്റുകൾ",
- "Songs": "ഗാനങ്ങൾ",
"HeaderContinueWatching": "കാണുന്നത് തുടരുക",
"Artists": "കലാകാരന്മാർ",
"Shows": "ഷോകൾ",
@@ -117,7 +89,6 @@
"Favorites": "പ്രിയപ്പെട്ടവ",
"Books": "പുസ്തകങ്ങൾ",
"Genres": "വിഭാഗങ്ങൾ",
- "Channels": "ചാനലുകൾ",
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
"HearingImpaired": "കേൾവി തകരാറുകൾ",
@@ -127,5 +98,10 @@
"TaskAudioNormalization": "സാധാരണ ശബ്ദ നിലയിലെത്തിലെത്തിക്കുക",
"TaskAudioNormalizationDescription": "സാധാരണ ശബ്ദ നിലയിലെത്തിലെത്തിക്കുന്ന ഡാറ്റയ്ക്കായി ഫയലുകൾ സ്കാൻ ചെയ്യുക.",
"TaskRefreshTrickplayImages": "ട്രിക്ക് പ്ലേ ചിത്രങ്ങൾ സൃഷ്ടിക്കുക",
- "TaskRefreshTrickplayImagesDescription": "പ്രവർത്തനക്ഷമമാക്കിയ ലൈബ്രറികളിൽ വീഡിയോകൾക്കായി ട്രിക്ക്പ്ലേ പ്രിവ്യൂകൾ സൃഷ്ടിക്കുന്നു."
+ "TaskRefreshTrickplayImagesDescription": "പ്രവർത്തനക്ഷമമാക്കിയ ലൈബ്രറികളിൽ വീഡിയോകൾക്കായി ട്രിക്ക്പ്ലേ പ്രിവ്യൂകൾ സൃഷ്ടിക്കുന്നു.",
+ "Original": "ഓറിജിനൽ",
+ "TaskDownloadMissingLyrics": "ഇല്ലാത്ത വരികൾ ഡൗൺലോഡ് ചെയ്യുക",
+ "TaskDownloadMissingLyricsDescription": "പാട്ടുകളുടെ വരികൾ ഡൗൺലോഡ് ചെയ്യുന്നു",
+ "TaskExtractMediaSegments": "മീഡിയ സെഗ്‌മെന്റ് സ്‌കാൻ",
+ "TaskExtractMediaSegmentsDescription": "മീഡിയസെഗ്മെന്റ് പ്രാപ്തമാക്കിയ പ്ലഗിനുകളിൽ നിന്ന് മീഡിയ സെഗ്‌മെന്റുകൾ എക്‌സ്‌ട്രാക്റ്റുചെയ്യുന്നു അല്ലെങ്കിൽ നേടുന്നു."
}
diff --git a/Emby.Server.Implementations/Localization/Core/mn.json b/Emby.Server.Implementations/Localization/Core/mn.json
index 63f4d0cef3..83caaf346f 100644
--- a/Emby.Server.Implementations/Localization/Core/mn.json
+++ b/Emby.Server.Implementations/Localization/Core/mn.json
@@ -2,15 +2,12 @@
"Books": "Номнууд",
"HeaderNextUp": "Дараа нь",
"HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
- "Songs": "Дуунууд",
- "Playlists": "Тоглуулах жагсаалтууд",
"Movies": "Кинонууд",
"Latest": "Сүүлийн үеийн",
"Genres": "Төрлүүд",
"Favorites": "Дуртай",
"Collections": "Цуглуулгууд",
"Artists": "Уран бүтээлчид",
- "Albums": "Дуут цомгууд",
"TaskExtractMediaSegments": "Медиа сегмент шалга",
"TaskExtractMediaSegmentsDescription": "MediaSegment идэвхжүүлсэн залгаасуудаас медиа сегментүүдийг задлах эсвэл олж авах.",
"TaskMoveTrickplayImages": "Трикплэй зургуудын байршлыг шилжүүлэх",
@@ -21,7 +18,6 @@
"TaskKeyframeExtractor": "Түлхүүр кадр гаргагч",
"TaskCleanCache": "Кэш санг цэвэрлэх",
"NewVersionIsAvailable": "Jellyfin Server-н шинэ хувилбар татаж авахад нээлттэй боллоо.",
- "MessageNamedServerConfigurationUpdatedWithValue": "Server-н {0}-р хэсгийн тохиргоо шинэчлэгдлээ",
"NotificationOptionAudioPlaybackStopped": "Дууг зогсоов",
"NotificationOptionNewLibraryContent": "Шинэ агуулга орлоо",
"NotificationOptionServerRestartRequired": "Server-г дахин асаана уу",
@@ -33,7 +29,6 @@
"SubtitleDownloadFailureFromForItem": "{0}-г {1}-д зориулсан хадмал орчуулгыг татаж авч чадсангүй",
"TaskRefreshLibraryDescription": "Таны медиа санг шинэ файлуудын хувьд шалгаж, мета мэдээллийг шинэчилнэ.",
"UserOfflineFromDevice": "{0}-г {1}-с салгалаа",
- "ValueHasBeenAddedToLibrary": "{0}-г медиа сан руу нэмэгдлээ",
"TaskRefreshPeopleDescription": "Таны медиа санд байгаа жүжигчид болон найруулагчдын мета мэдээллийг шинэчилнэ.",
"TaskCleanTranscodeDescription": "Нэг өдрөөс илүү настай транскодлох файлуудыг устгана.",
"TaskRefreshChannelsDescription": "Интернет сувгуудын мэдээллийг шинэчлэх.",
@@ -51,36 +46,21 @@
"TaskRefreshChannels": "Сувгуудыг шинэчлэх",
"TaskDownloadMissingSubtitles": "Алга болсон хадмал орчуулгыг татах",
"External": "Гадны",
- "HeaderFavoriteArtists": "Дуртай уран бүтээлчид",
"HeaderFavoriteEpisodes": "Дуртай ангиуд",
"HeaderFavoriteShows": "Дуртай нэвтрүүлэг",
- "HeaderFavoriteSongs": "Дуртай дуу",
"AppDeviceValues": "Aпп: {0}, Төхөөрөмж: {1}",
- "Application": "Aпп",
"AuthenticationSucceededWithUserName": "{0} амжилттай нэвтэрлээ",
- "CameraImageUploadedFrom": "{0}-с шинэ зураг байршуулагдлаа",
- "Channels": "Сувгууд",
"ChapterNameValue": "{0}-р бүлэг",
"Default": "Анхдагч",
- "DeviceOfflineWithName": "{0}-н холболт саллаа",
- "DeviceOnlineWithName": "{0} холбогдлоо",
"FailedLoginAttemptWithUserName": "{0}-н нэвтрэх оролдлого амжилтгүй",
"Folders": "Хавтасууд",
"Forced": "Хүчээр",
- "HeaderAlbumArtists": "Цомгийн уран бүтээлчид",
- "HeaderFavoriteAlbums": "Дуртай цомгууд",
"HeaderLiveTV": "Шууд ТВ",
- "HeaderRecordingGroups": "Бичлэгийн бүлгүүд",
"HearingImpaired": "Сонсголын бэрхшээлтэй",
"HomeVideos": "Үндсэн дүрсүүд",
"Inherit": "Уламжлах",
- "ItemAddedWithName": "{0}-г санд нэмлээ",
- "ItemRemovedWithName": "{0}-с сангаас хаслаа",
"LabelIpAddressValue": "IP хаяг: {0}",
"LabelRunningTimeValue": "Үргэлжлэх хугацаа: {0}",
- "MessageApplicationUpdated": "Jellyfin Server шинэчлэгдлээ",
- "MessageApplicationUpdatedTo": "Jellyfin Server {0} болж шинэчлэгдлээ",
- "MessageServerConfigurationUpdated": "Server-н тохиргоо шинэчлэгдлээ",
"MixedContent": "Холимог агуулга",
"Music": "Хөгжим",
"MusicVideos": "Дууны клипүүд",
@@ -99,28 +79,19 @@
"NotificationOptionUserLockedOut": "Хэрэглэгчийг түгжив",
"NotificationOptionVideoPlayback": "Бичлэгийг тоглуулж эхлэв",
"Photos": "Зургууд",
- "Plugin": "Плагин",
"PluginInstalledWithName": "{0}-г суулгалаа",
"PluginUninstalledWithName": "{0}-г устгалаа",
"PluginUpdatedWithName": "{0}-г шинэчиллээ",
- "ProviderValue": "Нийлүүлэгч: {0}",
- "ScheduledTaskStartedWithName": "{0}-г эхлүүлэв",
- "ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу",
"Shows": "Шоу",
- "Sync": "Синхрончлох",
- "System": "Систем",
"TvShows": "ТВ нэвтрүүлгүүд",
"Undefined": "Танисангүй",
- "User": "Хэрэглэгч",
"UserCreatedWithName": "Хэрэглэгч {0}-г үүсгэлээ",
"UserDeletedWithName": "Хэрэглэгч {0}-г устгалаа",
"UserDownloadingItemWithValues": "{0} нь {1}-г татаж байна",
"UserLockedOutWithName": "Хэрэглэгч {0}-г түгжлээ",
"UserOnlineFromDevice": "{0} нь {1}-тэй холбоотой байна",
- "UserPolicyUpdatedWithName": "Хэрэглэгчийн журмыг {0}-д зориулан шинэчиллээ",
"UserStartedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж байна",
"UserStoppedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж дуусгалаа",
- "ValueSpecialEpisodeName": "Онцгой - {0}",
"VersionNumber": "Хувилбар {0}",
"TasksMaintenanceCategory": "Засвар",
"TasksLibraryCategory": "Сан",
diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json
index 267222ecbe..f169d085d5 100644
--- a/Emby.Server.Implementations/Localization/Core/mr.json
+++ b/Emby.Server.Implementations/Localization/Core/mr.json
@@ -1,22 +1,13 @@
{
"Books": "पुस्तकं",
"Artists": "संगीतकार",
- "Albums": "अल्बम",
- "Playlists": "प्लेलिस्ट",
- "HeaderAlbumArtists": "अल्बम संगीतकार",
"Folders": "फोल्डर",
"HeaderFavoriteEpisodes": "आवडते भाग",
- "HeaderFavoriteSongs": "आवडती गाणी",
"Movies": "चित्रपट",
- "HeaderFavoriteArtists": "आवडते संगीतकार",
"Shows": "कार्यक्रम",
- "HeaderFavoriteAlbums": "आवडते अल्बम",
- "Channels": "वाहिन्या",
- "ValueSpecialEpisodeName": "विशेष - {0}",
"HeaderFavoriteShows": "आवडते कार्यक्रम",
"Favorites": "आवडीचे",
"HeaderNextUp": "यानंतर",
- "Songs": "गाणी",
"HeaderLiveTV": "लाइव्ह टीव्ही",
"Genres": "जाँनरे",
"Photos": "चित्र",
@@ -34,10 +25,8 @@
"UserOnlineFromDevice": "{0} हे {1} येथून ऑनलाइन आहेत",
"UserDeletedWithName": "प्रयोक्ता {0} काढून टाकण्यात आले आहे",
"UserCreatedWithName": "प्रयोक्ता {0} बनवण्यात आले आहे",
- "User": "प्रयोक्ता",
"TvShows": "टीव्ही कार्यक्रम",
"StartupEmbyServerIsLoading": "जेलिफिन सर्व्हर लोड होत आहे. कृपया थोड्या वेळात पुन्हा प्रयत्न करा.",
- "Plugin": "प्लगइन",
"NotificationOptionCameraImageUploaded": "कॅमेरा चित्र अपलोड केले आहे",
"NotificationOptionApplicationUpdateInstalled": "अ‍ॅप्लिकेशन अपडेट इन्स्टॉल केले आहे",
"NotificationOptionApplicationUpdateAvailable": "अ‍ॅप्लिकेशन अपडेट उपलब्ध आहे",
@@ -46,16 +35,9 @@
"NameSeasonNumber": "सीझन {0}",
"MusicVideos": "संगीत व्हिडीयो",
"Music": "संगीत",
- "MessageApplicationUpdatedTo": "जेलिफिन सर्व्हर अपडेट होऊन {0} आवृत्तीवर पोहोचला आहे",
- "MessageApplicationUpdated": "जेलिफिन सर्व्हर अपडेट केला गेला आहे",
"Latest": "नवीनतम",
"LabelIpAddressValue": "आयपी पत्ता: {0}",
- "ItemRemovedWithName": "{0} हे संग्रहालयातून काढून टाकण्यात आले",
- "ItemAddedWithName": "{0} हे संग्रहालयात जोडले गेले",
"HomeVideos": "घरचे व्हिडीयो",
- "HeaderRecordingGroups": "रेकॉर्डिंग गट",
- "CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे",
- "Application": "अ‍ॅप्लिकेशन",
"AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}",
"Collections": "संग्रह",
"ChapterNameValue": "धडा {0}",
@@ -70,18 +52,12 @@
"TaskRefreshChapterImagesDescription": "अध्याय असलेल्या व्हिडियोंसाठी थंबनेल चित्र बनवतो.",
"TaskRefreshChapterImages": "अध्याय चित्र काढून घ्या",
"TasksMaintenanceCategory": "देखरेख",
- "ValueHasBeenAddedToLibrary": "{0} हे तुमच्या माध्यम संग्रहात जोडण्यात आले आहे",
"UserStoppedPlayingItemWithValues": "{0} यांचं {2} वर {1} पूर्णपणे प्ले करून झालं आहे",
"UserStartedPlayingItemWithValues": "{0} हे {2} वर {1} प्ले करत आहे",
"UserDownloadingItemWithValues": "{0} हे {1} डाउनलोड करत आहे",
- "System": "प्रणाली",
"Undefined": "अव्याख्यात",
- "Sync": "सिंक",
- "ServerNameNeedsToBeRestarted": "{0} याला बंद करून पुन्हा सुरू करायची गरज आहे",
"SubtitleDownloadFailureFromForItem": "{0} येथून {1} यासाठी उपशिर्षक डाउनलोड करण्यात अपयश",
- "ScheduledTaskStartedWithName": "{0} सुरू झाले",
"ScheduledTaskFailedWithName": "{0} अपयशी झाले",
- "ProviderValue": "पुरवणारा: {0}",
"PluginUpdatedWithName": "{0} अपडेट केले",
"PluginUninstalledWithName": "{0} अनिन्स्टॉल केले",
"PluginInstalledWithName": "{0} इन्स्टॉल केले",
@@ -109,19 +85,14 @@
"TaskCleanCacheDescription": "सिस्टमला यापुढे आवश्यक नसलेल्या कॅशे फाइल्स हटवा.",
"TaskCleanActivityLogDescription": "कॉन्फिगर केलेल्या वयापेक्षा जुन्या क्रियाकलाप लॉग एंट्री हटवा.",
"TaskCleanActivityLog": "क्रियाकलाप लॉग साफ करा",
- "UserPolicyUpdatedWithName": "{0} साठी वापरकर्ता धोरण अपडेट केले गेले आहे",
"UserOfflineFromDevice": "{0} {1} वरून डिस्कनेक्ट झाला आहे",
"UserLockedOutWithName": "वापरकर्ता {0} लॉक केले गेले आहे",
"NotificationOptionUserLockedOut": "वापरकर्ता लॉक आउट",
"NameInstallFailed": "{0} स्थापना अयशस्वी",
- "MessageServerConfigurationUpdated": "सर्व्हर कॉन्फिगरेशन अद्यतनित केले आहे",
- "MessageNamedServerConfigurationUpdatedWithValue": "सर्व्हर कॉन्फिगरेशन विभाग {0} अद्यतनित केला गेला आहे",
"Inherit": "वारसा",
"Forced": "सक्ती केली आहे",
"FailedLoginAttemptWithUserName": "{0} कडून लॉगिन करण्याचा प्रयत्न अयशस्वी झाला",
"External": "बाहेरचा",
- "DeviceOnlineWithName": "{0} कनेक्ट झाले",
- "DeviceOfflineWithName": "{0} डिस्कनेक्ट झाला आहे",
"AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत",
"HearingImpaired": "कर्णबधीर",
"TaskRefreshTrickplayImages": "ट्रिकप्ले प्रतिमा तयार करा",
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index 743c14ac10..9fc61fadd5 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -1,41 +1,24 @@
{
- "Albums": "Album",
"AppDeviceValues": "Aplikasi: {0}, Peranti: {1}",
- "Application": "Aplikasi",
"Artists": "Artis",
"AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
"Books": "Buku",
- "CameraImageUploadedFrom": "Gambar baharu telah dimuat naik melalui {0}",
- "Channels": "Saluran",
"ChapterNameValue": "Bab {0}",
"Collections": "Koleksi",
- "DeviceOfflineWithName": "{0} telah dinyahsambung",
- "DeviceOnlineWithName": "{0} telah disambung",
"FailedLoginAttemptWithUserName": "Percubaan gagal log masuk daripada {0}",
"Favorites": "Kegemaran",
"Folders": "Folder-folder",
"Genres": "Genre-genre",
- "HeaderAlbumArtists": "Album artis-artis",
"HeaderContinueWatching": "Teruskan Menonton",
- "HeaderFavoriteAlbums": "Album-album Kegemaran",
- "HeaderFavoriteArtists": "Artis-artis Kegemaran",
"HeaderFavoriteEpisodes": "Episod-episod Kegemaran",
"HeaderFavoriteShows": "Rancangan-rancangan Kegemaran",
- "HeaderFavoriteSongs": "Lagu-lagu Kegemaran",
"HeaderLiveTV": "TV Siaran Langsung",
"HeaderNextUp": "Seterusnya",
- "HeaderRecordingGroups": "Kumpulan-kumpulan Rakaman",
"HomeVideos": "Video Peribadi",
"Inherit": "Warisi",
- "ItemAddedWithName": "{0} telah ditambah ke dalam pustaka",
- "ItemRemovedWithName": "{0} telah dibuang daripada pustaka",
"LabelIpAddressValue": "Alamat IP: {0}",
"LabelRunningTimeValue": "Masa berjalan: {0}",
"Latest": "Terbaharu",
- "MessageApplicationUpdated": "Pelayan Jellyfin telah dikemas kini",
- "MessageApplicationUpdatedTo": "Pelayan Jellyfin telah dikemas kini ke {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan bahagian {0} telah dikemas kini",
- "MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini",
"MixedContent": "Kandungan campuran",
"Movies": "Filem-filem",
"Music": "Muzik",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Ulangmain video bermula",
"NotificationOptionVideoPlaybackStopped": "Ulangmain video dihentikan",
"Photos": "Gambar-gambar",
- "Playlists": "Senarai ulangmain",
- "Plugin": "Plugin",
"PluginInstalledWithName": "{0} telah dipasang",
"PluginUninstalledWithName": "{0} telah dinyahpasang",
"PluginUpdatedWithName": "{0} telah dikemaskini",
- "ProviderValue": "Pembekal: {0}",
"ScheduledTaskFailedWithName": "{0} gagal",
- "ScheduledTaskStartedWithName": "{0} bermula",
- "ServerNameNeedsToBeRestarted": "{0} perlu di ulangmula",
"Shows": "Tayangan",
- "Songs": "Lagu-lagu",
"StartupEmbyServerIsLoading": "Pelayan Jellyfin sedang dimuatkan. Sila cuba sebentar lagi.",
"SubtitleDownloadFailureFromForItem": "Muat turun sarikata gagal dari {0} untuk {1}",
- "Sync": "Segerak",
- "System": "Sistem",
"TvShows": "Tayangan TV",
- "User": "Pengguna",
"UserCreatedWithName": "Pengguna {0} telah diwujudkan",
"UserDeletedWithName": "Pengguna {0} telah dipadamkan",
"UserDownloadingItemWithValues": "{0} sedang memuat turun {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} telah terputus dari {1}",
"UserOnlineFromDevice": "{0} berada dalam talian dari {1}",
"UserPasswordChangedWithName": "Kata laluan telah ditukar bagi pengguna {0}",
- "UserPolicyUpdatedWithName": "Dasar pengguna telah dikemas kini untuk {0}",
"UserStartedPlayingItemWithValues": "{0} sedang dimainkan {1} pada {2}",
"UserStoppedPlayingItemWithValues": "{0} telah tamat dimainkan {1} pada {2}",
- "ValueHasBeenAddedToLibrary": "{0} telah ditambah ke media library anda",
- "ValueSpecialEpisodeName": "Khas - {0}",
"VersionNumber": "Versi {0}",
"TaskCleanActivityLog": "Log Aktiviti Bersih",
"TasksChannelsCategory": "Saluran Internet",
diff --git a/Emby.Server.Implementations/Localization/Core/mt.json b/Emby.Server.Implementations/Localization/Core/mt.json
index aa3029a262..e237a80b70 100644
--- a/Emby.Server.Implementations/Localization/Core/mt.json
+++ b/Emby.Server.Implementations/Localization/Core/mt.json
@@ -1,28 +1,18 @@
{
- "Albums": "Albums",
"AppDeviceValues": "Applikazzjoni: {0}, Device: {1}",
- "Application": "Applikazzjoni",
"Artists": "Artisti",
"AuthenticationSucceededWithUserName": "{1} awtentikat b'suċċess",
"Books": "Kotba",
- "CameraImageUploadedFrom": "Ttella' ritratt ġdid tal-kamera minn {1}",
- "Channels": "Stazzjonijiet",
"ChapterNameValue": "Kapitlu {0}",
"Collections": "Kollezzjonijiet",
- "DeviceOfflineWithName": "{0} tneħħa",
- "DeviceOnlineWithName": "{0} tqabbad",
"External": "Estern",
"FailedLoginAttemptWithUserName": "Attentat fallut ta' login minn {0}",
"Favorites": "Favoriti",
"Forced": "Sfurzat",
"Genres": "Ġeneri",
- "HeaderAlbumArtists": "Artisti tal-album",
"HeaderContinueWatching": "Kompli Ara",
- "HeaderFavoriteAlbums": "Albums Favoriti",
- "HeaderFavoriteArtists": "Artisti Favoriti",
"HeaderFavoriteEpisodes": "Episodji Favoriti",
"HeaderFavoriteShows": "Programmi Favoriti",
- "HeaderFavoriteSongs": "Kanzunetti Favoriti",
"HeaderNextUp": "Li Jmiss",
"SubtitleDownloadFailureFromForItem": "Is-sottotitli ma setgħux jitniżżlu minn {0} għal {1}",
"UserPasswordChangedWithName": "Il-password għall-utent {0} inbidlet",
@@ -32,18 +22,11 @@
"Default": "Standard",
"Folders": "Folders",
"HeaderLiveTV": "TV Dirett",
- "HeaderRecordingGroups": "Gruppi ta' Rikordjar",
"HearingImpaired": "Nuqqas ta' Smigħ",
"HomeVideos": "Filmati Personali",
"Inherit": "Jiret",
- "ItemAddedWithName": "{0} żdied fil-librerija",
- "ItemRemovedWithName": "{0} tneħħa mil-librerija",
"LabelIpAddressValue": "Indirizz tal-IP: {0}",
"Latest": "Tal-Aħħar",
- "MessageApplicationUpdated": "Il-Jellyfin Server ġie aġġornat",
- "MessageApplicationUpdatedTo": "Il-JellyFin Server ġie aġġornat għal {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Is-sezzjoni {0} tal-konfigurazzjoni tas-server ġiet aġġornata",
- "MessageServerConfigurationUpdated": "Il-konfigurazzjoni tas-server ġiet aġġornata",
"MixedContent": "Kontenut imħallat",
"Movies": "Films",
"Music": "Mużika",
@@ -67,21 +50,12 @@
"NotificationOptionTaskFailed": "Falliment tat-task skedat",
"NotificationOptionUserLockedOut": "Utent imsakkar",
"Photos": "Ritratti",
- "Playlists": "Playlists",
- "Plugin": "Plugin",
"PluginInstalledWithName": "{0} ġie installat",
"PluginUninstalledWithName": "{0} tneħħa",
"PluginUpdatedWithName": "{0} ġie aġġornat",
- "ProviderValue": "Fornitur: {0}",
"ScheduledTaskFailedWithName": "{0} falla",
- "ScheduledTaskStartedWithName": "{0} beda",
- "ServerNameNeedsToBeRestarted": "{0} jeħtieġ restart",
- "Songs": "Kanzunetti",
"StartupEmbyServerIsLoading": "Jellyfin Server qed jillowdja. Jekk jogħġbok erġa' pprova ftit tal-ħin oħra.",
- "Sync": "Sinkronizza",
- "System": "Sistema",
"Undefined": "Bla Definizzjoni",
- "User": "Utent",
"UserCreatedWithName": "L-utent {0} inħoloq",
"UserDeletedWithName": "L-utent {0} tħassar",
"UserDownloadingItemWithValues": "{0} qed iniżżel {1}",
@@ -93,11 +67,8 @@
"NotificationOptionVideoPlaybackStopped": "Il-playback tal-filmat twaqqaf",
"Shows": "Serje",
"TvShows": "Serje Televiżivi",
- "UserPolicyUpdatedWithName": "Il-politka tal-utent ġiet aġġornata għal {0}",
"UserStartedPlayingItemWithValues": "{0} qed jara {1} fuq {2}",
"UserStoppedPlayingItemWithValues": "{0} waqaf jara {1} fuq {2}",
- "ValueHasBeenAddedToLibrary": "{0} ġie miżjud mal-librerija tal-midja tiegħek",
- "ValueSpecialEpisodeName": "Speċjali - {0}",
"VersionNumber": "Verżjoni {0}",
"TasksMaintenanceCategory": "Manutenzjoni",
"TasksLibraryCategory": "Librerija",
diff --git a/Emby.Server.Implementations/Localization/Core/my.json b/Emby.Server.Implementations/Localization/Core/my.json
index f2cd501076..47728adfc5 100644
--- a/Emby.Server.Implementations/Localization/Core/my.json
+++ b/Emby.Server.Implementations/Localization/Core/my.json
@@ -1,10 +1,8 @@
{
"Default": "ပုံသေ",
"Collections": "စုစည်းမှုများ",
- "Channels": "တီဗွီလိုင်းများ",
"Books": "စာအုပ်များ",
"Artists": "အနုပညာရှင်များ",
- "Albums": "သီချင်းအခွေများ",
"TaskOptimizeDatabaseDescription": "ဒေတာဘေ့စ်ကို ကျစ်လစ်စေပြီး နေရာလွတ်များကို ဖြတ်တောက်ပေးသည်။ စာကြည့်တိုက်ကို စကင်န်ဖတ်ပြီးနောက် ဤလုပ်ငန်းကို လုပ်ဆောင်ခြင်း သို့မဟုတ် ဒေတာဘေ့စ်မွမ်းမံမှုများ စွမ်းဆောင်ရည်ကို မြှင့်တင်ပေးနိုင်သည်ဟု ရည်ညွှန်းသော အခြားပြောင်းလဲမှုများကို လုပ်ဆောင်ခြင်း။.",
"TaskOptimizeDatabase": "ဒေတာဘေ့စ်ကို အကောင်းဆုံးဖြစ်အောင်လုပ်ပါ",
"TaskDownloadMissingSubtitlesDescription": "မက်တာဒေတာ ဖွဲ့စည်းမှုပုံစံအပေါ် အခြေခံ၍ ပျောက်ဆုံးနေသော စာတန်းထိုးများအတွက် အင်တာနက်ကို ရှာဖွေသည်။",
@@ -32,11 +30,8 @@
"TasksLibraryCategory": "မီဒီယာတိုက်",
"TasksMaintenanceCategory": "ပြုပြင် ထိန်းသိမ်းခြင်း",
"VersionNumber": "ဗားရှင်း {0}",
- "ValueSpecialEpisodeName": "အထူး- {0}",
- "ValueHasBeenAddedToLibrary": "{0} ကို သင့်မီဒီယာဒစ်ဂျစ်တိုက်သို့ ပေါင်းထည့်လိုက်ပါပြီ",
"UserStoppedPlayingItemWithValues": "{0} သည် {1} ကို {2} တွင် ဖွင့်ပြီးပါပြီ",
"UserStartedPlayingItemWithValues": "{0} သည် {1} ကို {2} တွင် ပြသနေသည်",
- "UserPolicyUpdatedWithName": "{0} အတွက် အသုံးပြုသူမူဝါဒကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
"UserPasswordChangedWithName": "အသုံးပြုသူ {0} အတွက် စကားဝှက်ကို ပြောင်းထားသည်",
"UserOnlineFromDevice": "{0} သည် {1} မှ အွန်လိုင်းဖြစ်သည်",
"UserOfflineFromDevice": "{0} သည် {1} မှ ချိတ်ဆက်မှုပြတ်တောက်သွားသည်",
@@ -44,24 +39,15 @@
"UserDownloadingItemWithValues": "{0} သည် {1} ကို ဒေါင်းလုဒ်လုပ်နေသည်",
"UserDeletedWithName": "အသုံးပြုသူ {0} ကို ဖျက်လိုက်ပါပြီ",
"UserCreatedWithName": "အသုံးပြုသူ {0} ကို ဖန်တီးပြီးပါပြီ",
- "User": "အသုံးပြုသူ",
"Undefined": "သတ်မှတ်မထားသော",
"TvShows": "တီဗီ ဇာတ်လမ်းတွဲများ",
- "System": "စနစ်",
- "Sync": "ချိန်ကိုက်မည်",
"SubtitleDownloadFailureFromForItem": "{1} အတွက် {0} မှ စာတန်းထိုးများ ဒေါင်းလုဒ်လုပ်ခြင်း မအောင်မြင်ပါ",
"StartupEmbyServerIsLoading": "Jellyfin ဆာဗာကို အသင့်ပြင်နေပါသည်။ ခဏနေ ထပ်စမ်းကြည့်ပါ။",
- "Songs": "သီချင်းများ",
"Shows": "ဇာတ်လမ်းတွဲများ",
- "ServerNameNeedsToBeRestarted": "{0} ကို ပြန်လည်စတင်ရန် လိုအပ်သည်",
- "ScheduledTaskStartedWithName": "{0} စတင်ခဲ့သည်",
"ScheduledTaskFailedWithName": "{0} မအောင်မြင်ပါ",
- "ProviderValue": "ဝန်ဆောင်မှုပေးသူ- {0}",
"PluginUpdatedWithName": "ပလပ်ခ်အင် {0} ကို အပ်ဒိတ်လုပ်ထားသည်",
"PluginUninstalledWithName": "ပလပ်ခ်အင် {0} ကို ဖြုတ်လိုက်ပါပြီ",
"PluginInstalledWithName": "ပလပ်ခ်အင် {0} ကို ထည့်သွင်းခဲ့သည်",
- "Plugin": "ပလပ်အင်",
- "Playlists": "အစီအစဉ်များ",
"Photos": "ဓာတ်ပုံများ",
"NotificationOptionVideoPlaybackStopped": "ဗီဒီယိုဖွင့်ခြင်း ရပ်သွားသည်",
"NotificationOptionVideoPlayback": "ဗီဒီယိုဖွင့်ခြင်း စတင်ပါပြီ",
@@ -87,38 +73,23 @@
"Music": "တေးဂီတ",
"Movies": "ရုပ်ရှင်များ",
"MixedContent": "ရောနှောပါဝင်မှု",
- "MessageServerConfigurationUpdated": "ဆာဗာဖွဲ့စည်းပုံကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
- "MessageNamedServerConfigurationUpdatedWithValue": "ဆာဗာဖွဲ့စည်းပုံကဏ္ဍ {0} ကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
- "MessageApplicationUpdatedTo": "Jellyfin ဆာဗာကို {0} သို့ အပ်ဒိတ်လုပ်ထားသည်",
- "MessageApplicationUpdated": "Jellyfin ဆာဗာကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
"Latest": "နောက်ဆုံး",
"LabelRunningTimeValue": "ကြာချိန် - {0}",
"LabelIpAddressValue": "IP လိပ်စာ- {0}",
- "ItemRemovedWithName": "{0} ကို ဒစ်ဂျစ်တိုက်မှ ဖယ်ရှားခဲ့သည်",
- "ItemAddedWithName": "{0} ကို စာကြည့်တိုက်သို့ ထည့်ထားသည်",
"Inherit": "ဆက်ခံ၍ လုပ်ဆောင်သည်",
"HomeVideos": "ကိုယ်တိုင်ရိုက် ဗီဒီယိုများ",
- "HeaderRecordingGroups": "အသံဖမ်းအဖွဲ့များ",
"HeaderNextUp": "နောက်ထပ်",
"HeaderLiveTV": "တီဗွီတိုက်ရိုက်",
- "HeaderFavoriteSongs": "အကြိုက်ဆုံးသီချင်းများ",
"HeaderFavoriteShows": "အကြိုက်ဆုံး ဇာတ်လမ်းတွဲများ",
"HeaderFavoriteEpisodes": "အကြိုက်ဆုံး ဇာတ်လမ်းအပိုင်းများ",
- "HeaderFavoriteArtists": "အကြိုက်ဆုံး အနုပညာရှင်များ",
- "HeaderFavoriteAlbums": "အကြိုက်ဆုံး အယ်လ်ဘမ်များ",
"HeaderContinueWatching": "ဆက်လက်ကြည့်ရှုပါ",
- "HeaderAlbumArtists": "အယ်လ်ဘမ်အနုပညာရှင်များ",
"Genres": "အမျိုးအစားများ",
"Forced": "အတင်းအကြပ်",
"Folders": "ဖိုလ်ဒါများ",
"Favorites": "အကြိုက်ဆုံးများ",
"FailedLoginAttemptWithUserName": "{0} မှ အကောင့်ဝင်ရန် မအောင်မြင်ပါ",
- "DeviceOnlineWithName": "{0} ကို ချိတ်ဆက်ထားသည်",
- "DeviceOfflineWithName": "{0} နှင့် အဆက်ပြတ်သွားပါပြီ",
"ChapterNameValue": "အခန်း {0}",
- "CameraImageUploadedFrom": "ကင်မရာပုံအသစ်ကို {0} မှ ထည့်သွင်းလိုက်သည်",
"AuthenticationSucceededWithUserName": "{0} အောင်မြင်စွာ စစ်မှန်ကြောင်း အတည်ပြုပြီးပါပြီ",
- "Application": "အပလီကေးရှင်း",
"AppDeviceValues": "အက်ပ်- {0}၊ စက်- {1}",
"External": "ပြင်ပ",
"TaskKeyframeExtractorDescription": "ပိုမိုတိကျသည့် အိတ်ချ်အယ်လ်အက်စ် အစဉ်လိုက်ပြသမှုများ ဖန်တီးနိုင်ရန်အတွက် ဗီဒီယိုဖိုင်များမှ ကီးဖရိန်များကို ထုတ်နှုတ်ယူမည် ဖြစ်သည်။ ဤလုပ်ဆောင်မှုသည် အချိန်ကြာရှည်နိုင်သည်။",
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index 351b238f9d..752b74ec1c 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -1,41 +1,24 @@
{
- "Albums": "Album",
"AppDeviceValues": "App: {0}, Enhet: {1}",
- "Application": "Program",
"Artists": "Artister",
"AuthenticationSucceededWithUserName": "{0} har logget inn",
"Books": "Bøker",
- "CameraImageUploadedFrom": "Et nytt kamerabilde har blitt lastet opp fra {0}",
- "Channels": "Kanaler",
"ChapterNameValue": "Kapittel {0}",
"Collections": "Samlinger",
- "DeviceOfflineWithName": "{0} har koblet fra",
- "DeviceOnlineWithName": "{0} er tilkoblet",
"FailedLoginAttemptWithUserName": "Mislykket påloggingsforsøk fra {0}",
"Favorites": "Favoritter",
"Folders": "Mapper",
"Genres": "Sjangre",
- "HeaderAlbumArtists": "Albumartister",
"HeaderContinueWatching": "Fortsett å se",
- "HeaderFavoriteAlbums": "Favorittalbum",
- "HeaderFavoriteArtists": "Favorittartister",
"HeaderFavoriteEpisodes": "Favorittepisoder",
"HeaderFavoriteShows": "Favorittserier",
- "HeaderFavoriteSongs": "Favorittsanger",
"HeaderLiveTV": "Direkte-TV",
"HeaderNextUp": "Neste",
- "HeaderRecordingGroups": "Opptaksgrupper",
"HomeVideos": "Hjemmelagde filmer",
"Inherit": "Arve",
- "ItemAddedWithName": "{0} ble lagt til i biblioteket",
- "ItemRemovedWithName": "{0} ble fjernet fra biblioteket",
"LabelIpAddressValue": "IP-adresse: {0}",
"LabelRunningTimeValue": "Spilletid: {0}",
"Latest": "Siste",
- "MessageApplicationUpdated": "Jellyfin-serveren har blitt oppdatert",
- "MessageApplicationUpdatedTo": "Jellyfin-serveren ble oppdatert til {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfigurasjonsseksjon {0} har blitt oppdatert",
- "MessageServerConfigurationUpdated": "Serverkonfigurasjon har blitt oppdatert",
"MixedContent": "Blandet innhold",
"Movies": "Filmer",
"Music": "Musikk",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Videoavspilling startet",
"NotificationOptionVideoPlaybackStopped": "Videoavspilling stoppet",
"Photos": "Bilder",
- "Playlists": "Spillelister",
- "Plugin": "Programvareutvidelse",
"PluginInstalledWithName": "{0} ble installert",
"PluginUninstalledWithName": "{0} ble avinstallert",
"PluginUpdatedWithName": "{0} ble oppdatert",
- "ProviderValue": "Leverandør: {0}",
"ScheduledTaskFailedWithName": "{0} mislykkes",
- "ScheduledTaskStartedWithName": "{0} startet",
- "ServerNameNeedsToBeRestarted": "{0} må startes på nytt",
"Shows": "Serier",
- "Songs": "Sanger",
"StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.",
"SubtitleDownloadFailureFromForItem": "Kunne ikke laste ned undertekster fra {0} for {1}",
- "Sync": "Synkroniser",
- "System": "System",
"TvShows": "TV-serier",
- "User": "Bruker",
"UserCreatedWithName": "Bruker {0} er opprettet",
"UserDeletedWithName": "Bruker {0} har blitt slettet",
"UserDownloadingItemWithValues": "{0} laster ned {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} har koblet fra {1}",
"UserOnlineFromDevice": "{0} er tilkoblet fra {1}",
"UserPasswordChangedWithName": "Passordet for {0} er oppdatert",
- "UserPolicyUpdatedWithName": "Brukerretningslinjene har blitt oppdatert for {0}",
"UserStartedPlayingItemWithValues": "{0} har startet avspilling {1} på {2}",
"UserStoppedPlayingItemWithValues": "{0} har stoppet avspilling {1}",
- "ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt",
- "ValueSpecialEpisodeName": "Spesialepisode - {0}",
"VersionNumber": "Versjon {0}",
"TasksChannelsCategory": "Internettkanaler",
"TasksApplicationCategory": "Applikasjon",
diff --git a/Emby.Server.Implementations/Localization/Core/ne.json b/Emby.Server.Implementations/Localization/Core/ne.json
index 0e52e32c1b..ad6070c4c3 100644
--- a/Emby.Server.Implementations/Localization/Core/ne.json
+++ b/Emby.Server.Implementations/Localization/Core/ne.json
@@ -21,64 +21,39 @@
"Music": "संगीत",
"Movies": "चलचित्रहरू",
"MixedContent": "मिश्रित सामग्री",
- "MessageServerConfigurationUpdated": "सर्भर कन्फिगरेसन अद्यावधिक गरिएको छ",
- "MessageNamedServerConfigurationUpdatedWithValue": "सर्भर कन्फिगरेसन विभाग {0} अद्यावधिक गरिएको छ",
- "MessageApplicationUpdatedTo": "जेलीफिन सर्भर {0} मा अद्यावधिक गरिएको छ",
- "MessageApplicationUpdated": "जेलीफिन सर्भर अपडेट गरिएको छ",
"Latest": "नविनतम",
"LabelRunningTimeValue": "कुल समय: {0}",
"LabelIpAddressValue": "आईपी ठेगाना: {0}",
- "ItemRemovedWithName": "{0}लाई पुस्तकालयबाट हटाईयो",
- "ItemAddedWithName": "{0} लाईब्रेरीमा थपियो",
"Inherit": "उत्तराधिकार",
"HomeVideos": "घरेलु भिडियोहरू",
- "HeaderRecordingGroups": "रेकर्ड समूहहरू",
"HeaderNextUp": "आगामी",
"HeaderLiveTV": "प्रत्यक्ष टिभी",
- "HeaderFavoriteSongs": "मनपर्ने गीतहरू",
"HeaderFavoriteShows": "मनपर्ने कार्यक्रमहरू",
"HeaderFavoriteEpisodes": "मनपर्ने एपिसोडहरू",
- "HeaderFavoriteArtists": "मनपर्ने कलाकारहरू",
- "HeaderFavoriteAlbums": "मनपर्ने एल्बमहरू",
"HeaderContinueWatching": "हेर्न जारी राख्नुहोस्",
- "HeaderAlbumArtists": "एल्बमका कलाकारहरू",
"Genres": "विधाहरू",
"Folders": "फोल्डरहरू",
"Favorites": "मनपर्ने",
"FailedLoginAttemptWithUserName": "असफल लग इन प्रयास {0} देखि",
- "DeviceOnlineWithName": "{0}को साथ जडित",
- "DeviceOfflineWithName": "{0}बाट विच्छेदन भयो",
"Collections": "संग्रह",
"ChapterNameValue": "अध्याय {0}",
- "Channels": "च्यानलहरू",
"AppDeviceValues": "अनुप्रयोग: {0}, उपकरण: {1}",
"AuthenticationSucceededWithUserName": "{0} सफलतापूर्वक प्रमाणीकरण गरियो",
- "CameraImageUploadedFrom": "{0}बाट नयाँ क्यामेरा छवि अपलोड गरिएको छ",
"Books": "पुस्तकहरु",
"Artists": "कलाकारहरू",
- "Application": "अनुप्रयोगहरू",
- "Albums": "एल्बमहरू",
"TasksLibraryCategory": "पुस्तकालय",
"TasksApplicationCategory": "अनुप्रयोग",
"TasksMaintenanceCategory": "मर्मत",
- "UserPolicyUpdatedWithName": "प्रयोगकर्ता नीति को लागी अद्यावधिक गरिएको छ {0}",
"UserPasswordChangedWithName": "पासवर्ड प्रयोगकर्ताका लागि परिवर्तन गरिएको छ {0}",
"UserOnlineFromDevice": "{0} बाट अनलाइन छ {1}",
"UserOfflineFromDevice": "{0} बाट विच्छेदन भएको छ {1}",
"UserLockedOutWithName": "प्रयोगकर्ता {0} लक गरिएको छ",
"UserDeletedWithName": "प्रयोगकर्ता {0} हटाइएको छ",
"UserCreatedWithName": "प्रयोगकर्ता {0} सिर्जना गरिएको छ",
- "User": "प्रयोगकर्ता",
"PluginInstalledWithName": "{0} सभएको थियो",
"StartupEmbyServerIsLoading": "Jellyfin सर्भर लोड हुँदैछ। कृपया छिट्टै फेरि प्रयास गर्नुहोस्।",
- "Songs": "गीतहरू",
"Shows": "शोहरू",
- "ServerNameNeedsToBeRestarted": "{0} लाई पुन: सुरु गर्नु पर्छ",
- "ScheduledTaskStartedWithName": "{0} सुरु भयो",
"ScheduledTaskFailedWithName": "{0} असफल",
- "ProviderValue": "प्रदायक: {0}",
- "Plugin": "प्लगइनहरू",
- "Playlists": "प्लेलिस्टहरू",
"Photos": "तस्बिरहरु",
"NotificationOptionVideoPlaybackStopped": "भिडियो प्लेब्याक रोकियो",
"NotificationOptionVideoPlayback": "भिडियो प्लेब्याक सुरु भयो",
@@ -98,15 +73,11 @@
"TaskCleanActivityLog": "गतिविधि लग सफा गर्नुहोस्",
"TasksChannelsCategory": "इन्टरनेट च्यानलहरू",
"VersionNumber": "संस्करण {0}",
- "ValueSpecialEpisodeName": "विशेष - {0}",
- "ValueHasBeenAddedToLibrary": "{0} तपाईंको मिडिया लाइब्रेरीमा थपिएको छ",
"UserStoppedPlayingItemWithValues": "{2} मा {0} हेरिसकेको छ{1}",
"UserStartedPlayingItemWithValues": "{0} हेर्दै {1} मा {2}",
"UserDownloadingItemWithValues": "{0} डाउनलोड गर्दै छ {1}",
"Undefined": "अपरिभाषित",
"TvShows": "टेलिभिजन कार्यक्रमहरू",
- "System": "प्रणाली",
- "Sync": "समकालीन",
"SubtitleDownloadFailureFromForItem": "उपशीर्षकहरू {0} बाट {1} को लागि डाउनलोड गर्न असफल",
"PluginUpdatedWithName": "{0} अद्यावधिक गरिएको थियो",
"PluginUninstalledWithName": "{0} को स्थापना रद्द गरिएको थियो",
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index de4c277ce7..9aea3adc22 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -1,39 +1,23 @@
{
"AppDeviceValues": "App: {0}, Apparaat: {1}",
- "Application": "Applicatie",
"Artists": "Artiesten",
"AuthenticationSucceededWithUserName": "{0} is succesvol geauthenticeerd",
"Books": "Boeken",
- "CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}",
- "Channels": "Kanalen",
"ChapterNameValue": "Hoofdstuk {0}",
"Collections": "Collecties",
- "DeviceOfflineWithName": "Verbinding met {0} is verbroken",
- "DeviceOnlineWithName": "{0} is verbonden",
"FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}",
"Favorites": "Favorieten",
"Folders": "Mappen",
- "HeaderAlbumArtists": "Albumartiesten",
"HeaderContinueWatching": "Verderkijken",
- "HeaderFavoriteAlbums": "Favoriete albums",
- "HeaderFavoriteArtists": "Favoriete artiesten",
"HeaderFavoriteEpisodes": "Favoriete afleveringen",
"HeaderFavoriteShows": "Favoriete series",
- "HeaderFavoriteSongs": "Favoriete nummers",
"HeaderLiveTV": "Live-tv",
"HeaderNextUp": "Volgende",
- "HeaderRecordingGroups": "Opnamegroepen",
"HomeVideos": "Homevideo's",
"Inherit": "Overnemen",
- "ItemAddedWithName": "{0} is toegevoegd aan de bibliotheek",
- "ItemRemovedWithName": "{0} is verwijderd uit de bibliotheek",
"LabelIpAddressValue": "IP-adres: {0}",
"LabelRunningTimeValue": "Looptijd: {0}",
"Latest": "Nieuwste",
- "MessageApplicationUpdated": "Jellyfin Server is bijgewerkt",
- "MessageApplicationUpdatedTo": "Jellyfin Server is bijgewerkt naar {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Sectie {0} van de serverconfiguratie is bijgewerkt",
- "MessageServerConfigurationUpdated": "Serverconfiguratie is bijgewerkt",
"MixedContent": "Gemengde inhoud",
"Movies": "Films",
"Music": "Muziek",
@@ -59,23 +43,14 @@
"NotificationOptionVideoPlayback": "Afspelen van video gestart",
"NotificationOptionVideoPlaybackStopped": "Afspelen van video gestopt",
"Photos": "Foto's",
- "Playlists": "Afspeellijsten",
- "Plugin": "Plug-in",
"PluginInstalledWithName": "{0} is geïnstalleerd",
"PluginUninstalledWithName": "{0} is verwijderd",
"PluginUpdatedWithName": "{0} is bijgewerkt",
- "ProviderValue": "Aanbieder: {0}",
"ScheduledTaskFailedWithName": "{0} is mislukt",
- "ScheduledTaskStartedWithName": "{0} is gestart",
- "ServerNameNeedsToBeRestarted": "{0} moet herstart worden",
"Shows": "Series",
- "Songs": "Nummers",
"StartupEmbyServerIsLoading": "Jellyfin Server is aan het laden. Probeer het later opnieuw.",
"SubtitleDownloadFailureFromForItem": "Ondertiteling kon niet gedownload worden van {0} voor {1}",
- "Sync": "Synchronisatie",
- "System": "Systeem",
"TvShows": "Tv-series",
- "User": "Gebruiker",
"UserCreatedWithName": "Gebruiker {0} is aangemaakt",
"UserDeletedWithName": "Gebruiker {0} is verwijderd",
"UserDownloadingItemWithValues": "{0} downloadt {1}",
@@ -83,11 +58,8 @@
"UserOfflineFromDevice": "Verbinding van {0} via {1} is verbroken",
"UserOnlineFromDevice": "{0} is verbonden via {1}",
"UserPasswordChangedWithName": "Wachtwoord voor {0} is gewijzigd",
- "UserPolicyUpdatedWithName": "Gebruikersbeleid gewijzigd voor {0}",
"UserStartedPlayingItemWithValues": "{0} speelt {1} af op {2}",
"UserStoppedPlayingItemWithValues": "{0} heeft afspelen van {1} gestopt op {2}",
- "ValueHasBeenAddedToLibrary": "{0} is toegevoegd aan je mediabibliotheek",
- "ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Versie {0}",
"TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar ontbrekende ondertiteling gebaseerd op metadataconfiguratie.",
"TaskDownloadMissingSubtitles": "Ontbrekende ondertiteling downloaden",
@@ -134,7 +106,7 @@
"TaskExtractMediaSegments": "Scannen op mediasegmenten",
"CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig zijn.",
"CleanupUserDataTask": "Opruimtaak gebruikersdata",
- "Albums": "Albums",
"Genres": "Genres",
- "Original": "Oorspronkelijk"
+ "Original": "Oorspronkelijk",
+ "LyricDownloadFailureFromForItem": "Downloaden van liedteksten voor {1} van {0} mislukt"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json
index feb5fe2154..8c5da8c1de 100644
--- a/Emby.Server.Implementations/Localization/Core/nn.json
+++ b/Emby.Server.Implementations/Localization/Core/nn.json
@@ -1,41 +1,24 @@
{
- "MessageServerConfigurationUpdated": "Tenarkonfigurasjonen har blitt oppdatert",
- "MessageNamedServerConfigurationUpdatedWithValue": "Tenar konfigurasjon seksjon {0} har blitt oppdatert",
- "MessageApplicationUpdatedTo": "Jellyfin-tenaren har blitt oppdatert til {0}",
- "MessageApplicationUpdated": "Jellyfin-tenaren har blitt oppdatert",
"Latest": "Nyaste",
"LabelRunningTimeValue": "Speletid: {0}",
"LabelIpAddressValue": "IP-adresse: {0}",
- "ItemRemovedWithName": "{0} vart fjerna frå biblioteket",
- "ItemAddedWithName": "{0} vart lagt til i biblioteket",
"Inherit": "Arve",
"HomeVideos": "Heimevideoar",
- "HeaderRecordingGroups": "Innspelingsgrupper",
"HeaderNextUp": "Neste",
"HeaderLiveTV": "Direkte TV",
- "HeaderFavoriteSongs": "Favorittsongar",
"HeaderFavoriteShows": "Favorittseriar",
"HeaderFavoriteEpisodes": "Favorittepisodar",
- "HeaderFavoriteArtists": "Favorittartistar",
- "HeaderFavoriteAlbums": "Favorittalbum",
"HeaderContinueWatching": "Fortsett å sjå",
- "HeaderAlbumArtists": "Albumartist",
"Genres": "Sjangrar",
"Folders": "Mapper",
"Favorites": "Favorittar",
"FailedLoginAttemptWithUserName": "Mislukka påloggingsforsøk frå {0}",
- "DeviceOnlineWithName": "{0} er tilkopla",
- "DeviceOfflineWithName": "{0} har kopla frå",
"Collections": "Samlingar",
"ChapterNameValue": "Kapittel {0}",
- "Channels": "Kanalar",
- "CameraImageUploadedFrom": "Eit nytt kamerabilete har blitt lasta opp frå {0}",
"Books": "Bøker",
"AuthenticationSucceededWithUserName": "{0} har logga inn",
"Artists": "Artistar",
- "Application": "Program",
"AppDeviceValues": "App: {0}, Eining: {1}",
- "Albums": "Album",
"NotificationOptionServerRestartRequired": "Tenaren krev omstart",
"NotificationOptionPluginUpdateInstalled": "Programvaretilleggoppdatering vart installert",
"NotificationOptionPluginUninstalled": "Programvaretillegg avinstallert",
@@ -56,7 +39,6 @@
"Music": "Musikk",
"Movies": "Filmar",
"MixedContent": "Blanda innhald",
- "Sync": "Synkroniser",
"TaskDownloadMissingSubtitlesDescription": "Søk Internettet for manglande undertekstar basert på metadatainnstillingar.",
"TaskDownloadMissingSubtitles": "Last ned manglande undertekstar",
"TaskRefreshChannelsDescription": "Oppdater internettkanalinformasjon.",
@@ -80,11 +62,8 @@
"TasksLibraryCategory": "Bibliotek",
"TasksMaintenanceCategory": "Vedlikehald",
"VersionNumber": "Versjon {0}",
- "ValueSpecialEpisodeName": "Spesialepisode - {0}",
- "ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt",
"UserStoppedPlayingItemWithValues": "{0} har fullført avspeling {1} på {2}",
"UserStartedPlayingItemWithValues": "{0} spelar {1} på {2}",
- "UserPolicyUpdatedWithName": "Brukarreglar har blitt oppdatert for {0}",
"UserPasswordChangedWithName": "Passordet for {0} er oppdatert",
"UserOnlineFromDevice": "{0} er direktekopla frå {1}",
"UserOfflineFromDevice": "{0} har kopla frå {1}",
@@ -92,22 +71,14 @@
"UserDownloadingItemWithValues": "{0} lastar ned {1}",
"UserDeletedWithName": "Brukar {0} er sletta",
"UserCreatedWithName": "Brukar {0} er oppretta",
- "User": "Brukar",
"TvShows": "TV-seriar",
- "System": "System",
"SubtitleDownloadFailureFromForItem": "Feila å laste ned undertekstar frå {0} for {1}",
"StartupEmbyServerIsLoading": "Jellyfin-tenaren laster. Prøv igjen seinare.",
- "Songs": "Sangar",
"Shows": "Seriar",
- "ServerNameNeedsToBeRestarted": "{0} må omstartast",
- "ScheduledTaskStartedWithName": "{0} starta",
"ScheduledTaskFailedWithName": "{0} feila",
- "ProviderValue": "Leverandør: {0}",
"PluginUpdatedWithName": "{0} blei oppdatert",
"PluginUninstalledWithName": "{0} blei avinstallert",
"PluginInstalledWithName": "{0} blei installert",
- "Plugin": "Programvaretillegg",
- "Playlists": "Spelelister",
"Photos": "Bilete",
"NotificationOptionVideoPlaybackStopped": "Videoavspeling stoppa",
"NotificationOptionVideoPlayback": "Videoavspeling starta",
diff --git a/Emby.Server.Implementations/Localization/Core/oc.json b/Emby.Server.Implementations/Localization/Core/oc.json
index 0967ef424b..cad5640763 100644
--- a/Emby.Server.Implementations/Localization/Core/oc.json
+++ b/Emby.Server.Implementations/Localization/Core/oc.json
@@ -1 +1,3 @@
-{}
+{
+ "AppDeviceValues": "Aplicacion: {0}, Periferic: {1}"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/or.json b/Emby.Server.Implementations/Localization/Core/or.json
index 8251c12907..febc98d1a4 100644
--- a/Emby.Server.Implementations/Localization/Core/or.json
+++ b/Emby.Server.Implementations/Localization/Core/or.json
@@ -1,11 +1,8 @@
{
"External": "ବହିଃସ୍ଥ",
"Genres": "ଧରଣ",
- "Albums": "ଆଲବମଗୁଡ଼ିକ",
"Artists": "କଳାକାରଗୁଡ଼ିକ",
- "Application": "ଆପ୍ଲିକେସନ",
"Books": "ବହିଗୁଡ଼ିକ",
- "Channels": "ଚ୍ୟାନେଲଗୁଡ଼ିକ",
"ChapterNameValue": "ବିଭାଗ {0}",
"Collections": "ସଂଗ୍ରହଗୁଡ଼ିକ",
"Folders": "ଫୋଲ୍ଡରଗୁଡ଼ିକ"
diff --git a/Emby.Server.Implementations/Localization/Core/pa.json b/Emby.Server.Implementations/Localization/Core/pa.json
index b00291ccb4..e609ad52c8 100644
--- a/Emby.Server.Implementations/Localization/Core/pa.json
+++ b/Emby.Server.Implementations/Localization/Core/pa.json
@@ -24,11 +24,8 @@
"TasksLibraryCategory": "ਲਾਇਬ੍ਰੇਰੀ",
"TasksMaintenanceCategory": "ਰੱਖ-ਰਖਾਅ",
"VersionNumber": "ਵਰਜਨ {0}",
- "ValueSpecialEpisodeName": "ਖਾਸ - {0}",
- "ValueHasBeenAddedToLibrary": "{0} ਤੁਹਾਡੀ ਮੀਡੀਆ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਸ਼ਾਮਲ ਕੀਤਾ ਗਿਆ ਹੈ",
"UserStoppedPlayingItemWithValues": "{0} ਨੇ {2} 'ਤੇ {1} ਖੇਡਣਾ ਪੂਰਾ ਕਰ ਲਿਆ ਹੈ",
"UserStartedPlayingItemWithValues": "{0} {2} 'ਤੇ {1} ਖੇਡ ਰਿਹਾ ਹੈ",
- "UserPolicyUpdatedWithName": "ਵਰਤੋਂਕਾਰ ਨੀਤੀ ਨੂੰ {0} ਲਈ ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ",
"UserPasswordChangedWithName": "{0} ਵਰਤੋਂਕਾਰ ਲਈ ਪਾਸਵਰਡ ਬਦਲਿਆ ਗਿਆ ਸੀ",
"UserOnlineFromDevice": "{0} ਨੂੰ {1} ਤੋਂ ਆਨਲਾਈਨ ਹੈ",
"UserOfflineFromDevice": "{0} ਤੋਂ ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ {1}",
@@ -36,24 +33,15 @@
"UserDownloadingItemWithValues": "{0} {1} ਨੂੰ ਡਾਊਨਲੋਡ ਕਰ ਰਿਹਾ ਹੈ",
"UserDeletedWithName": "ਵਰਤੋਂਕਾਰ {0} ਨੂੰ ਹਟਾਇਆ ਗਿਆ",
"UserCreatedWithName": "ਵਰਤੋਂਕਾਰ {0} ਬਣਾਇਆ ਗਿਆ ਹੈ",
- "User": "ਵਰਤੋਂਕਾਰ",
"Undefined": "ਪਰਿਭਾਸ਼ਤ",
"TvShows": "ਟੀਵੀ ਸ਼ੋਅ",
- "System": "ਸਿਸਟਮ",
- "Sync": "ਸਿੰਕ",
"SubtitleDownloadFailureFromForItem": "ਉਪਸਿਰਲੇਖ {1} ਲਈ {0} ਤੋਂ ਡਾਊਨਲੋਡ ਕਰਨ ਵਿੱਚ ਅਸਫਲ ਰਹੇ",
"StartupEmbyServerIsLoading": "Jellyfin ਸਰਵਰ ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ। ਛੇਤੀ ਹੀ ਫ਼ੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ।",
- "Songs": "ਗਾਣੇ",
"Shows": "ਸ਼ੋਅ",
- "ServerNameNeedsToBeRestarted": "{0} ਮੁੜ ਚਾਲੂ ਕਰਨ ਦੀ ਲੋੜ ਹੈ",
- "ScheduledTaskStartedWithName": "{0} ਸ਼ੁਰੂ ਹੋਇਆ",
"ScheduledTaskFailedWithName": "{0} ਅਸਫਲ",
- "ProviderValue": "ਦੇਣ ਵਾਲੇ: {0}",
"PluginUpdatedWithName": "{0} ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਸੀ",
"PluginUninstalledWithName": "{0} ਅਣਇੰਸਟੌਲ ਕੀਤਾ ਗਿਆ ਸੀ",
"PluginInstalledWithName": "{0} ਲਗਾਇਆ ਗਿਆ ਸੀ",
- "Plugin": "ਪਲੱਗਇਨ",
- "Playlists": "ਪਲੇਸੂਚੀਆਂ",
"Photos": "ਫੋਟੋਆਂ",
"NotificationOptionVideoPlaybackStopped": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਰੋਕਿਆ ਗਿਆ",
"NotificationOptionVideoPlayback": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਸ਼ੁਰੂ ਹੋਇਆ",
@@ -79,45 +67,28 @@
"Music": "ਸੰਗੀਤ",
"Movies": "ਫਿਲਮਾਂ",
"MixedContent": "ਮਿਸ਼ਰਤ ਸਮੱਗਰੀ",
- "MessageServerConfigurationUpdated": "ਸਰਵਰ ਕੌਂਫਿਗਰੇਸ਼ਨ ਨੂੰ ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ",
- "MessageNamedServerConfigurationUpdatedWithValue": "ਸਰਵਰ ਕੌਂਫਿਗਰੇਸ਼ਨ ਸੈਕਸ਼ਨ {0} ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ",
- "MessageApplicationUpdatedTo": "ਜੈਲੀਫਿਨ ਸਰਵਰ ਨੂੰ ਅੱਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ {0}",
- "MessageApplicationUpdated": "ਜੈਲੀਫਿਨ ਸਰਵਰ ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ",
"Latest": "ਤਾਜ਼ਾ",
"LabelRunningTimeValue": "ਚੱਲਦਾ ਸਮਾਂ: {0}",
"LabelIpAddressValue": "IP ਪਤਾ: {0}",
- "ItemRemovedWithName": "{0} ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚੋਂ ਹਟਾ ਦਿੱਤਾ ਗਿਆ ਸੀ",
- "ItemAddedWithName": "{0} ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਸ਼ਾਮਲ ਕੀਤਾ ਗਿਆ ਸੀ",
"Inherit": "ਵਿਰਾਸਤ",
"HomeVideos": "ਘਰੇਲੂ ਵੀਡੀਓ",
- "HeaderRecordingGroups": "ਰਿਕਾਰਡਿੰਗ ਸਮੂਹ",
"HeaderNextUp": "ਅੱਗੇ",
"HeaderLiveTV": "ਲਾਈਵ ਟੀਵੀ",
- "HeaderFavoriteSongs": "ਮਨਪਸੰਦ ਗਾਣੇ",
"HeaderFavoriteShows": "ਮਨਪਸੰਦ ਸ਼ੋਅ",
"HeaderFavoriteEpisodes": "ਮਨਪਸੰਦ ਐਪੀਸੋਡ",
- "HeaderFavoriteArtists": "ਮਨਪਸੰਦ ਕਲਾਕਾਰ",
- "HeaderFavoriteAlbums": "ਮਨਪਸੰਦ ਐਲਬਮ",
"HeaderContinueWatching": "ਵੇਖਣਾ ਜਾਰੀ ਰੱਖੋ",
- "HeaderAlbumArtists": "ਐਲਬਮ ਕਲਾਕਾਰ",
"Genres": "ਸ਼ੈਲੀਆਂ",
"Forced": "ਮਜਬੂਰ",
"Folders": "ਫੋਲਡਰ",
"Favorites": "ਮਨਪਸੰਦ",
"FailedLoginAttemptWithUserName": "{0} ਤੋਂ ਲਾਗਇਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ",
- "DeviceOnlineWithName": "{0} ਜੁੜਿਆ ਹੋਇਆ ਹੈ",
- "DeviceOfflineWithName": "{0} ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ",
"Default": "ਡਿਫੌਲਟ",
"Collections": "ਸੰਗ੍ਰਹਿਣ",
"ChapterNameValue": "ਚੈਪਟਰ {0}",
- "Channels": "ਚੈਨਲ",
- "CameraImageUploadedFrom": "{0} ਤੋਂ ਇੱਕ ਨਵਾਂ ਕੈਮਰਾ ਚਿੱਤਰ ਅਪਲੋਡ ਕੀਤਾ ਗਿਆ ਹੈ",
"Books": "ਕਿਤਾਬਾਂ",
"AuthenticationSucceededWithUserName": "{0} ਸਫਲਤਾਪੂਰਕ ਪ੍ਰਮਾਣਿਤ",
"Artists": "ਕਲਾਕਾਰ",
- "Application": "ਐਪਲੀਕੇਸ਼ਨ",
"AppDeviceValues": "ਐਪ: {0}, ਜੰਤਰ: {1}",
- "Albums": "ਐਲਬਮਾਂ",
"TaskOptimizeDatabase": "ਡਾਟਾਬੇਸ ਅਨੁਕੂਲ ਬਣਾਓ",
"External": "ਬਾਹਰੀ",
"HearingImpaired": "ਸੁਨਣ ਵਿਚ ਕਮਜ਼ੋਰ",
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index e5af2c7801..c4657bdd6e 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -1,41 +1,24 @@
{
- "Albums": "Albumy",
"AppDeviceValues": "Aplikacja: {0}, Urządzenie: {1}",
- "Application": "Aplikacja",
"Artists": "Wykonawcy",
"AuthenticationSucceededWithUserName": "{0} został pomyślnie uwierzytelniony",
"Books": "Książki",
- "CameraImageUploadedFrom": "Nowy obraz został przekazany z {0}",
- "Channels": "Kanały",
"ChapterNameValue": "Rozdział {0}",
"Collections": "Kolekcje",
- "DeviceOfflineWithName": "{0} został rozłączony",
- "DeviceOnlineWithName": "{0} połączył się",
"FailedLoginAttemptWithUserName": "Nieudana próba logowania przez {0}",
"Favorites": "Ulubione",
"Folders": "Foldery",
"Genres": "Gatunki",
- "HeaderAlbumArtists": "Wykonawcy albumów",
"HeaderContinueWatching": "Kontynuuj odtwarzanie",
- "HeaderFavoriteAlbums": "Ulubione albumy",
- "HeaderFavoriteArtists": "Ulubieni wykonawcy",
"HeaderFavoriteEpisodes": "Ulubione odcinki",
"HeaderFavoriteShows": "Ulubione seriale",
- "HeaderFavoriteSongs": "Ulubione utwory",
"HeaderLiveTV": "Telewizja",
"HeaderNextUp": "Do obejrzenia",
- "HeaderRecordingGroups": "Grupy nagrań",
"HomeVideos": "Nagrania domowe",
"Inherit": "Dziedzicz",
- "ItemAddedWithName": "{0} zostało dodane do biblioteki",
- "ItemRemovedWithName": "{0} zostało usunięte z biblioteki",
"LabelIpAddressValue": "Adres IP: {0}",
"LabelRunningTimeValue": "Czas trwania: {0}",
"Latest": "Ostatnio dodane",
- "MessageApplicationUpdated": "Serwer Jellyfin został zaktualizowany",
- "MessageApplicationUpdatedTo": "Serwer Jellyfin został zaktualizowany do wersji {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Sekcja {0} konfiguracji serwera została zaktualizowana",
- "MessageServerConfigurationUpdated": "Konfiguracja serwera została zaktualizowana",
"MixedContent": "Zawartość mieszana",
"Movies": "Filmy",
"Music": "Muzyka",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Rozpoczęto odtwarzanie wideo",
"NotificationOptionVideoPlaybackStopped": "Zatrzymano odtwarzanie wideo",
"Photos": "Zdjęcia",
- "Playlists": "Listy odtwarzania",
- "Plugin": "Wtyczka",
"PluginInstalledWithName": "{0} zostało zainstalowane",
"PluginUninstalledWithName": "{0} odinstalowane",
"PluginUpdatedWithName": "{0} zaktualizowane",
- "ProviderValue": "Dostawca: {0}",
"ScheduledTaskFailedWithName": "Nieudane {0}",
- "ScheduledTaskStartedWithName": "Rozpoczęto {0}",
- "ServerNameNeedsToBeRestarted": "{0} wymaga ponownego uruchomienia",
"Shows": "Seriale",
- "Songs": "Utwory",
"StartupEmbyServerIsLoading": "Trwa wczytywanie serwera Jellyfin. Spróbuj ponownie za chwilę.",
"SubtitleDownloadFailureFromForItem": "Nieudane pobieranie napisów z {0} dla {1}",
- "Sync": "Synchronizacja",
- "System": "System",
"TvShows": "Seriale",
- "User": "Użytkownik",
"UserCreatedWithName": "Użytkownik {0} został utworzony",
"UserDeletedWithName": "Użytkownik {0} został usunięty",
"UserDownloadingItemWithValues": "{0} pobiera {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} z {1} został rozłączony",
"UserOnlineFromDevice": "{0} połączył się z {1}",
"UserPasswordChangedWithName": "Hasło użytkownika {0} zostało zmienione",
- "UserPolicyUpdatedWithName": "Zmieniono zasady użytkowania dla {0}",
"UserStartedPlayingItemWithValues": "{0} odtwarza {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} zakończył odtwarzanie {1} na {2}",
- "ValueHasBeenAddedToLibrary": "{0} został dodany do biblioteki mediów",
- "ValueSpecialEpisodeName": "Specjalne - {0}",
"VersionNumber": "Wersja {0}",
"TaskDownloadMissingSubtitlesDescription": "Przeszukuje internet w poszukiwaniu brakujących napisów w oparciu o konfigurację metadanych.",
"TaskDownloadMissingSubtitles": "Pobierz brakujące napisy",
@@ -136,5 +107,6 @@
"TaskMoveTrickplayImagesDescription": "Przenosi istniejące pliki Trickplay zgodnie z ustawieniami biblioteki.",
"CleanupUserDataTaskDescription": "Usuwa wszystkie dane użytkownika (stan oglądanych, status ulubionych itp.) z mediów, które nie są dostępne od co najmniej 90 dni.",
"CleanupUserDataTask": "Zadanie czyszczenia danych użytkownika",
- "Original": "Oryginalny"
+ "Original": "Oryginalny",
+ "LyricDownloadFailureFromForItem": "Błąd podczas pobierania tekstu piosenki z {0} dla {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json
index fee7e65f1d..912f5c876a 100644
--- a/Emby.Server.Implementations/Localization/Core/pr.json
+++ b/Emby.Server.Implementations/Localization/Core/pr.json
@@ -2,48 +2,31 @@
"Books": "Scrolls",
"AuthenticationSucceededWithUserName": "{0} passed yer trial",
"Artists": "Artistas",
- "Songs": "Shantees",
- "Albums": "Tomes",
"Photos": "Paintings",
"NotificationOptionUserLockedOut": "Crewmate sent to the brig",
"HeaderContinueWatching": "Continue Yer Journey",
"Folders": "Chests",
- "Application": "Captain",
- "DeviceOnlineWithName": "{0} joined yer crew",
- "DeviceOfflineWithName": "{0} abandoned ship",
"AppDeviceValues": "Captain: {0}, Ship: {1}",
- "CameraImageUploadedFrom": "Yer looking glass has glimpsed another painting from {0}",
"Collections": "Barrels",
- "ItemAddedWithName": "{0} is now with yer treasure",
"Default": "Normal-like",
"FailedLoginAttemptWithUserName": "Ye failed to enter from {0}",
"Favorites": "Finest Loot",
- "ItemRemovedWithName": "{0} was taken from yer treasure",
"LabelIpAddressValue": "Ship's coordinates: {0}",
"Genres": "types o' booty",
"TaskDownloadMissingSubtitlesDescription": "Scours the seven seas o' the internet for subtitles that be missin' based on the captain's map o' metadata.",
- "HeaderAlbumArtists": "Buccaneers o' the musical arts",
- "HeaderFavoriteAlbums": "Beloved booty o' musical adventures",
- "HeaderFavoriteArtists": "Treasured scallywags o' the creative seas",
- "Channels": "Channels",
"Forced": "Pressed",
"External": "Outboard",
"HeaderFavoriteEpisodes": "Treasured Tales",
"HeaderFavoriteShows": "Treasured Tales",
"ChapterNameValue": "Piece {0}",
- "HeaderFavoriteSongs": "Treasured Chimes",
"HeaderNextUp": "Incoming",
"HeaderLiveTV": "Scrying Glass",
"HearingImpaired": "Hard o' Hearing",
"LabelRunningTimeValue": "Journey duration: {0}",
- "MessageApplicationUpdated": "Yer Map of the Seas has been scribbled",
"HomeVideos": "Yer Onboard Booty",
"MixedContent": "Jumbled loot",
"Music": "Tunes",
"NameInstallFailed": "Ye couldn't bring {0} aboard yer ship",
- "MessageApplicationUpdatedTo": "Yer Map of the Seas has been scribbled with {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Yer Map Drawer has been rescribbled to {0}",
- "MessageServerConfigurationUpdated": "Yer Map drawer has been rescribbled",
"Inherit": "Carry on what be passed along",
"Latest": "Newfangled",
"Movies": "Moving pictures",
@@ -55,17 +38,13 @@
"UserOfflineFromDevice": "{0} severed ties with {1}",
"UserDownloadingItemWithValues": "{0} be haulin’ in {1}",
"UserStartedPlayingItemWithValues": "{0} be playin’ {1} aboard {2}",
- "ValueHasBeenAddedToLibrary": "{0} be stashed in yer treasure trove",
"TaskCleanCacheDescription": "Wipes away cache cargo no longer called fer.",
"TaskCleanLogsDescription": "Clears the logbook o’ entries older than {0} days.",
"TaskRefreshPeopleDescription": "Refreshes the charts fer actors an’ directors in yer Treasure Trove.",
"UserLockedOutWithName": "Matey {0} be denied boarding",
"TaskAudioNormalization": "Steadyin’ the shanties",
"TaskAudioNormalizationDescription": "Scans files fer shanty steadiyin’ data.",
- "HeaderRecordingGroups": "Loggin' Groups",
"MusicVideos": "Shanty films",
- "Playlists": "Lists o’ plunder",
- "Plugin": "Extra sail",
"NotificationOptionVideoPlaybackStopped": "Video playback dropped anchor",
"NameSeasonNumber": "Saga {0}",
"NameSeasonUnknown": "Saga be Lost",
@@ -87,23 +66,15 @@
"TaskRefreshPeople": "Freshen the Mateys",
"PluginUninstalledWithName": "{0} sent t’ Davy Jones",
"PluginUpdatedWithName": "{0} patched ‘n ready",
- "ProviderValue": "Supplier o’ goods: {0}",
- "ScheduledTaskStartedWithName": "{0} set sail",
- "ServerNameNeedsToBeRestarted": "{0} be cravin’ a restart",
"Shows": "Sagas",
"SubtitleDownloadFailureFromForItem": "Subtitles be sunk fetchin’ from {0} fer {1}",
- "Sync": "Match the tides",
- "System": "The ship’s works",
"TvShows": "TV Sagas",
"Undefined": "Uncharted",
- "User": "Matey",
"UserCreatedWithName": "Matey {0} joined the crew",
"UserDeletedWithName": "Matey {0} cast overboard",
"UserOnlineFromDevice": "{0} be aboard ship from {1}",
"UserPasswordChangedWithName": "New passphrase set fer Matey {0}",
- "UserPolicyUpdatedWithName": "Ship rules be changed fer {0}",
"UserStoppedPlayingItemWithValues": "{0} be done playin’ {1} on {2",
- "ValueSpecialEpisodeName": "Special Tale – {0}",
"VersionNumber": "Edition {0}",
"TasksMaintenanceCategory": "Hull patchin’",
"TasksLibraryCategory": "Treasure Trove",
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index 99f76c953f..1db500adf3 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -1,41 +1,24 @@
{
- "Albums": "Álbuns",
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
- "Application": "Aplicativo",
"Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
"Books": "Livros",
- "CameraImageUploadedFrom": "Uma nova imagem da câmera foi enviada de {0}",
- "Channels": "Canais",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Coleções",
- "DeviceOfflineWithName": "{0} se desconectou",
- "DeviceOnlineWithName": "{0} se conectou",
"FailedLoginAttemptWithUserName": "Falha na tentativa de login de {0}",
"Favorites": "Favoritos",
"Folders": "Pastas",
"Genres": "Gêneros",
- "HeaderAlbumArtists": "Artistas do Álbum",
"HeaderContinueWatching": "Continuar assistindo",
- "HeaderFavoriteAlbums": "Álbuns Favoritos",
- "HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Episódios favoritos",
"HeaderFavoriteShows": "Séries favoritas",
- "HeaderFavoriteSongs": "Músicas favoritas",
"HeaderLiveTV": "TV ao Vivo",
"HeaderNextUp": "A Seguir",
- "HeaderRecordingGroups": "Grupos de Gravação",
"HomeVideos": "Vídeos caseiros",
"Inherit": "Herdar",
- "ItemAddedWithName": "{0} foi adicionado à biblioteca",
- "ItemRemovedWithName": "{0} foi removido da biblioteca",
"LabelIpAddressValue": "Endereço IP: {0}",
"LabelRunningTimeValue": "Tempo de execução: {0}",
"Latest": "Recentes",
- "MessageApplicationUpdated": "Servidor Jellyfin atualizado",
- "MessageApplicationUpdatedTo": "Servidor Jellyfin atualizado para {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "A seção {0} da configuração do servidor foi atualizada",
- "MessageServerConfigurationUpdated": "A configuração do servidor foi atualizada",
"MixedContent": "Conteúdo misto",
"Movies": "Filmes",
"Music": "Música",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Reprodução de vídeo iniciada",
"NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo parada",
"Photos": "Fotos",
- "Playlists": "Listas de Reprodução",
- "Plugin": "Plugin",
"PluginInstalledWithName": "{0} foi instalado",
"PluginUninstalledWithName": "{0} foi desinstalado",
"PluginUpdatedWithName": "{0} foi atualizado",
- "ProviderValue": "Provedor: {0}",
"ScheduledTaskFailedWithName": "{0} falhou",
- "ScheduledTaskStartedWithName": "{0} iniciada",
- "ServerNameNeedsToBeRestarted": "O servidor {0} precisa ser reiniciado",
"Shows": "Séries",
- "Songs": "Músicas",
"StartupEmbyServerIsLoading": "O Servidor Jellyfin está carregando. Por favor, tente novamente mais tarde.",
"SubtitleDownloadFailureFromForItem": "Houve um problema ao baixar as legendas de {0} para {1}",
- "Sync": "Sincronizar",
- "System": "Sistema",
"TvShows": "Séries",
- "User": "Usuário",
"UserCreatedWithName": "O usuário {0} foi criado",
"UserDeletedWithName": "O usuário {0} foi excluído",
"UserDownloadingItemWithValues": "{0} está baixando {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} se desconectou de {1}",
"UserOnlineFromDevice": "{0} está online em {1}",
"UserPasswordChangedWithName": "A senha foi alterada para o usuário {0}",
- "UserPolicyUpdatedWithName": "A política de usuário foi atualizada para {0}",
"UserStartedPlayingItemWithValues": "{0} está reproduzindo {1} em {2}",
"UserStoppedPlayingItemWithValues": "{0} parou de reproduzir {1} em {2}",
- "ValueHasBeenAddedToLibrary": "{0} foi adicionado à sua biblioteca de mídia",
- "ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versão {0}",
"TaskDownloadMissingSubtitlesDescription": "Procurar na internet por legendas faltando baseado na configuração de metadados.",
"TaskDownloadMissingSubtitles": "Baixar legendas que estão faltando",
@@ -135,5 +106,7 @@
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de mídia de plug-ins habilitados para MediaSegment.",
"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."
+ "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.",
+ "LyricDownloadFailureFromForItem": "Download das Letras falharam em {0} para o item {1}",
+ "Original": "Original"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index 1d31efcdc9..dd482d1e9b 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -1,41 +1,24 @@
{
- "Albums": "Álbuns",
"AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}",
- "Application": "Aplicação",
"Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
"Books": "Livros",
- "CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}",
- "Channels": "Canais",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Coleções",
- "DeviceOfflineWithName": "{0} desligou-se",
- "DeviceOnlineWithName": "{0} ligou-se",
"FailedLoginAttemptWithUserName": "Tentativa de login falhada a partir de {0}",
"Favorites": "Favoritos",
"Folders": "Pastas",
"Genres": "Géneros",
- "HeaderAlbumArtists": "Artistas do álbum",
"HeaderContinueWatching": "Continuar a ver",
- "HeaderFavoriteAlbums": "Álbuns Favoritos",
- "HeaderFavoriteArtists": "Artistas Favoritos",
"HeaderFavoriteEpisodes": "Episódios Favoritos",
"HeaderFavoriteShows": "Séries Favoritas",
- "HeaderFavoriteSongs": "Músicas Favoritas",
"HeaderLiveTV": "TV em Direto",
"HeaderNextUp": "A Seguir",
- "HeaderRecordingGroups": "Grupos de Gravação",
"HomeVideos": "Vídeos Caseiros",
"Inherit": "Herdar",
- "ItemAddedWithName": "{0} foi adicionado à mediateca",
- "ItemRemovedWithName": "{0} foi removido da mediateca",
"LabelIpAddressValue": "Endereço IP: {0}",
"LabelRunningTimeValue": "Duração: {0}",
"Latest": "Mais Recente",
- "MessageApplicationUpdated": "O servidor Jellyfin foi atualizado",
- "MessageApplicationUpdatedTo": "O servidor Jellyfin foi atualizado para a versão {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Configurações de servidor na secção {0} foram atualizadas",
- "MessageServerConfigurationUpdated": "A configuração do servidor foi atualizada",
"MixedContent": "Conteúdo Misto",
"Movies": "Filmes",
"Music": "Música",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Reprodução do vídeo iniciada",
"NotificationOptionVideoPlaybackStopped": "Reprodução do vídeo parada",
"Photos": "Fotografias",
- "Playlists": "Playlists",
- "Plugin": "Extensão",
"PluginInstalledWithName": "{0} foi instalado",
"PluginUninstalledWithName": "{0} foi desinstalado",
"PluginUpdatedWithName": "{0} foi atualizado",
- "ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} falhou",
- "ScheduledTaskStartedWithName": "{0} iniciou",
- "ServerNameNeedsToBeRestarted": "{0} necessita de ser reiniciado",
"Shows": "Séries",
- "Songs": "Músicas",
"StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tente novamente mais tarde.",
"SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas a partir de {0} para {1}",
- "Sync": "Sincronização",
- "System": "Sistema",
"TvShows": "Séries",
- "User": "Utilizador",
"UserCreatedWithName": "Utilizador {0} criado",
"UserDeletedWithName": "Utilizador {0} apagado",
"UserDownloadingItemWithValues": "{0} está a transferir {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} desligou-se a partir de {1}",
"UserOnlineFromDevice": "{0} ligou-se a partir de {1}",
"UserPasswordChangedWithName": "Palavra-passe alterada para o utilizador {0}",
- "UserPolicyUpdatedWithName": "Política de utilizador alterada para {0}",
"UserStartedPlayingItemWithValues": "{0} está a reproduzir {1} em {2}",
"UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}",
- "ValueHasBeenAddedToLibrary": "{0} foi adicionado à sua mediateca",
- "ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versão {0}",
"TaskDownloadMissingSubtitlesDescription": "Procurar na internet por legendas em falta baseado na configuração de metadados.",
"TaskDownloadMissingSubtitles": "Transferir legendas em falta",
@@ -135,5 +106,6 @@
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de multimédia a partir de plugins com suporte para MediaSegment.",
"TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca.",
"CleanupUserDataTaskDescription": "Apaga todos os dados de utilizador (estados de reprodução, favoritos, etc) de arquivos média não presentes há 90 dias ou mais.",
- "CleanupUserDataTask": "Limpeza de dados de utilizador"
+ "CleanupUserDataTask": "Limpeza de dados de utilizador",
+ "Original": "Original"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 82da1f0aff..ce338acf34 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -3,44 +3,30 @@
"Collections": "Coleções",
"Books": "Livros",
"Artists": "Artistas",
- "Albums": "Álbuns",
"HeaderNextUp": "A Seguir",
- "HeaderFavoriteSongs": "Músicas Favoritas",
- "HeaderFavoriteArtists": "Artistas Favoritos",
- "HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteEpisodes": "Episódios Favoritos",
"HeaderFavoriteShows": "Séries Favoritas",
"HeaderContinueWatching": "Continuar a ver",
- "HeaderAlbumArtists": "Artistas do Álbum",
"Genres": "Géneros",
"Folders": "Pastas",
"Favorites": "Favoritos",
- "Channels": "Canais",
"UserDownloadingItemWithValues": "{0} está sendo baixado {1}",
"VersionNumber": "Versão {0}",
- "ValueHasBeenAddedToLibrary": "{0} foi adicionado à sua mediateca",
"UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}",
"UserStartedPlayingItemWithValues": "{0} está reproduzindo {1} em {2}",
- "UserPolicyUpdatedWithName": "A política do usuário {0} foi alterada",
"UserPasswordChangedWithName": "A senha do usuário {0} foi alterada",
"UserOnlineFromDevice": "{0} está online a partir de {1}",
"UserOfflineFromDevice": "{0} desconectou-se a partir de {1}",
"UserLockedOutWithName": "O usuário {0} foi bloqueado",
"UserDeletedWithName": "O usuário {0} foi removido",
"UserCreatedWithName": "O usuário {0} foi criado",
- "User": "Usuário",
"TvShows": "Séries",
- "System": "Sistema",
"SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas de {0} para {1}",
"StartupEmbyServerIsLoading": "O servidor Jellyfin está iniciando. Tente novamente dentro de momentos.",
- "ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciado",
- "ScheduledTaskStartedWithName": "{0} iniciou",
"ScheduledTaskFailedWithName": "{0} falhou",
- "ProviderValue": "Fornecedor: {0}",
"PluginUpdatedWithName": "{0} foi atualizado",
"PluginUninstalledWithName": "{0} foi desinstalado",
"PluginInstalledWithName": "{0} foi instalado",
- "Plugin": "Plugin",
"NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo parada",
"NotificationOptionVideoPlayback": "Reprodução de vídeo iniciada",
"NotificationOptionUserLockedOut": "Usuário bloqueado",
@@ -64,32 +50,17 @@
"MusicVideos": "Videoclipes",
"Music": "Música",
"MixedContent": "Conteúdo diverso",
- "MessageServerConfigurationUpdated": "A configuração do servidor foi atualizada",
- "MessageNamedServerConfigurationUpdatedWithValue": "As configurações do servidor na seção {0} foram atualizadas",
- "MessageApplicationUpdatedTo": "O servidor Jellyfin foi atualizado para a versão {0}",
- "MessageApplicationUpdated": "O servidor Jellyfin foi atualizado",
"Latest": "Mais Recente",
"LabelRunningTimeValue": "Duração: {0}",
"LabelIpAddressValue": "Endereço de IP: {0}",
- "ItemRemovedWithName": "{0} foi removido da mediateca",
- "ItemAddedWithName": "{0} foi adicionado à mediateca",
"Inherit": "Herdar",
"HomeVideos": "Vídeos Caseiros",
- "HeaderRecordingGroups": "Grupos de Gravação",
- "ValueSpecialEpisodeName": "Especial - {0}",
- "Sync": "Sincronização",
- "Songs": "Músicas",
"Shows": "Séries",
- "Playlists": "Playlists",
"Photos": "Fotografias",
"Movies": "Filmes",
"FailedLoginAttemptWithUserName": "Tentativa de início de sessão falhada a partir de {0}",
- "DeviceOnlineWithName": "{0} está ligado",
- "DeviceOfflineWithName": "{0} desligou-se",
"ChapterNameValue": "Capítulo {0}",
- "CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
- "Application": "Aplicação",
"AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}",
"TaskCleanCache": "Limpar Diretório de Cache",
"TasksApplicationCategory": "Aplicação",
@@ -135,5 +106,7 @@
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de multimédia a partir de plugins com suporte para MediaSegment.",
"TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay",
"CleanupUserDataTask": "Task de limpeza de dados do usuário",
- "CleanupUserDataTaskDescription": "Remove todos os dados do usuário (progresso, favoritos etc) de mídias que não estão presentes há pelo menos 90 dias."
+ "CleanupUserDataTaskDescription": "Remove todos os dados do usuário (progresso, favoritos etc) de mídias que não estão presentes há pelo menos 90 dias.",
+ "Original": "Original",
+ "LyricDownloadFailureFromForItem": "Erro ao descarregar letras de {0} para {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index 30214218f8..ea83b88951 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -1,11 +1,8 @@
{
"HeaderNextUp": "Urmează",
"VersionNumber": "Versiunea {0}",
- "ValueSpecialEpisodeName": "Special - {0}",
- "ValueHasBeenAddedToLibrary": "{0} a fost adăugat la biblioteca multimedia",
"UserStoppedPlayingItemWithValues": "{0} a terminat rularea {1} pe {2}",
"UserStartedPlayingItemWithValues": "{0} ruleaza {1} pe {2}",
- "UserPolicyUpdatedWithName": "Politica utilizatorului {0} a fost actualizată",
"UserPasswordChangedWithName": "Parola utilizatorului {0} a fost schimbată",
"UserOnlineFromDevice": "{0} este conectat de la {1}",
"UserOfflineFromDevice": "{0} s-a deconectat de la {1}",
@@ -13,23 +10,14 @@
"UserDownloadingItemWithValues": "{0} descarcă {1}",
"UserDeletedWithName": "Utilizatorul {0} a fost șters",
"UserCreatedWithName": "Utilizatorul {0} a fost creat",
- "User": "Utilizator",
"TvShows": "Seriale TV",
- "System": "Sistem",
- "Sync": "Sincronizare",
"SubtitleDownloadFailureFromForItem": "Subtitrările nu au putut fi descărcate de la {0} pentru {1}",
"StartupEmbyServerIsLoading": "Se încarcă serverul Jellyfin. Încercați din nou în scurt timp.",
- "Songs": "Melodii",
"Shows": "Seriale",
- "ServerNameNeedsToBeRestarted": "{0} trebuie să fie repornit",
- "ScheduledTaskStartedWithName": "{0} pornit/ă",
"ScheduledTaskFailedWithName": "{0} eșuat/ă",
- "ProviderValue": "Furnizor: {0}",
"PluginUpdatedWithName": "{0} a fost actualizat/ă",
"PluginUninstalledWithName": "{0} a fost dezinstalat",
"PluginInstalledWithName": "{0} a fost instalat",
- "Plugin": "Extensie",
- "Playlists": "Liste de redare",
"Photos": "Fotografii",
"NotificationOptionVideoPlaybackStopped": "Redarea video oprită",
"NotificationOptionVideoPlayback": "Redare video începută",
@@ -55,42 +43,25 @@
"Music": "Muzică",
"Movies": "Filme",
"MixedContent": "Conținut amestecat",
- "MessageServerConfigurationUpdated": "Configurarea serverului a fost actualizată",
- "MessageNamedServerConfigurationUpdatedWithValue": "Secțiunea de configurare a serverului {0} a fost acualizata",
- "MessageApplicationUpdatedTo": "Jellyfin Server a fost actualizat la {0}",
- "MessageApplicationUpdated": "Jellyfin Server a fost actualizat",
"Latest": "Cele mai recente",
"LabelRunningTimeValue": "Durată: {0}",
"LabelIpAddressValue": "Adresa IP: {0}",
- "ItemRemovedWithName": "{0} a fost eliminat din bibliotecă",
- "ItemAddedWithName": "{0} a fost adăugat în bibliotecă",
"Inherit": "Moștenit",
"HomeVideos": "Filme personale",
- "HeaderRecordingGroups": "Grupuri de înregistrare",
"HeaderLiveTV": "TV în Direct",
- "HeaderFavoriteSongs": "Melodii Favorite",
"HeaderFavoriteShows": "Seriale TV Favorite",
"HeaderFavoriteEpisodes": "Episoade Favorite",
- "HeaderFavoriteArtists": "Artiști Favoriți",
- "HeaderFavoriteAlbums": "Albume Favorite",
"HeaderContinueWatching": "Vizionează în continuare",
- "HeaderAlbumArtists": "Artiști album",
"Genres": "Genuri",
"Folders": "Dosare",
"Favorites": "Preferate",
"FailedLoginAttemptWithUserName": "Încercare de conectare eșuată pentru {0}",
- "DeviceOnlineWithName": "{0} este conectat",
- "DeviceOfflineWithName": "{0} s-a deconectat",
"Collections": "Colecții",
"ChapterNameValue": "Capitolul {0}",
- "Channels": "Canale",
- "CameraImageUploadedFrom": "O nouă fotografie a fost încărcată din {0}",
"Books": "Cărți",
"AuthenticationSucceededWithUserName": "{0} autentificare reușită",
"Artists": "Artiști",
- "Application": "Aplicație",
"AppDeviceValues": "Aplicație: {0}, Dispozitiv: {1}",
- "Albums": "Albume",
"TaskDownloadMissingSubtitlesDescription": "Caută pe internet subtitrările lipsă pe baza configurației metadatelor.",
"TaskDownloadMissingSubtitles": "Descarcă subtitrările lipsă",
"TaskRefreshChannelsDescription": "Actualizează informațiile despre canalul de internet.",
@@ -135,5 +106,7 @@
"TaskDownloadMissingLyrics": "Descarcă versurile lipsă",
"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."
+ "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.",
+ "LyricDownloadFailureFromForItem": "Versurile nu au putut fi descărcate din {0} pentru {1}",
+ "Original": "Original"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 38920b6ede..40d5e3985d 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -1,41 +1,24 @@
{
- "Albums": "Альбомы",
"AppDeviceValues": "Приложение: {0}, Устройство: {1}",
- "Application": "Приложение",
"Artists": "Исполнители",
"AuthenticationSucceededWithUserName": "{0} - авторизация успешна",
"Books": "Книги",
- "CameraImageUploadedFrom": "Новое фото загружено с камеры {0}",
- "Channels": "Каналы",
"ChapterNameValue": "Сцена {0}",
"Collections": "Коллекции",
- "DeviceOfflineWithName": "{0} - отключено",
- "DeviceOnlineWithName": "{0} - подключено",
"FailedLoginAttemptWithUserName": "Неудачная попытка входа с {0}",
"Favorites": "Избранное",
"Folders": "Папки",
"Genres": "Жанры",
- "HeaderAlbumArtists": "Исполнители альбома",
"HeaderContinueWatching": "Продолжить просмотр",
- "HeaderFavoriteAlbums": "Избранные альбомы",
- "HeaderFavoriteArtists": "Избранные исполнители",
"HeaderFavoriteEpisodes": "Избранные эпизоды",
"HeaderFavoriteShows": "Избранные сериалы",
- "HeaderFavoriteSongs": "Избранные композиции",
"HeaderLiveTV": "Эфир",
"HeaderNextUp": "Следующий",
- "HeaderRecordingGroups": "Группы записей",
"HomeVideos": "Домашние видео",
"Inherit": "Наследуемое",
- "ItemAddedWithName": "{0} - добавлено в медиатеку",
- "ItemRemovedWithName": "{0} - изъято из медиатеки",
"LabelIpAddressValue": "IP-адрес: {0}",
"LabelRunningTimeValue": "Длительность: {0}",
"Latest": "Последние",
- "MessageApplicationUpdated": "Jellyfin Server был обновлён",
- "MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена",
- "MessageServerConfigurationUpdated": "Конфигурация сервера была обновлена",
"MixedContent": "Смешанное содержание",
"Movies": "Фильмы",
"Music": "Музыка",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Воспроизведение видео запущено",
"NotificationOptionVideoPlaybackStopped": "Воспроизведение видео остановлено",
"Photos": "Фото",
- "Playlists": "Плей-листы",
- "Plugin": "Плагин",
"PluginInstalledWithName": "{0} - было установлено",
"PluginUninstalledWithName": "{0} - было удалено",
"PluginUpdatedWithName": "{0} - было обновлено",
- "ProviderValue": "Поставщик: {0}",
"ScheduledTaskFailedWithName": "{0} - неудачна",
- "ScheduledTaskStartedWithName": "{0} - запущена",
- "ServerNameNeedsToBeRestarted": "Необходим перезапуск {0}",
"Shows": "Сериалы",
- "Songs": "Композиции",
"StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.",
"SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
- "Sync": "Синхронизация",
- "System": "Система",
"TvShows": "Телесериалы",
- "User": "Пользователь",
"UserCreatedWithName": "Пользователь {0} был создан",
"UserDeletedWithName": "Пользователь {0} был удалён",
"UserDownloadingItemWithValues": "{0} загружает {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} отключился с {1}",
"UserOnlineFromDevice": "{0} подключился с {1}",
"UserPasswordChangedWithName": "Пароль пользователя {0} был изменён",
- "UserPolicyUpdatedWithName": "Политики пользователя {0} были обновлены",
"UserStartedPlayingItemWithValues": "{0} - воспроизведение «{1}» на {2}",
"UserStoppedPlayingItemWithValues": "{0} - воспроизведение остановлено «{1}» на {2}",
- "ValueHasBeenAddedToLibrary": "{0} добавлено в медиатеку",
- "ValueSpecialEpisodeName": "Спецэпизод - {0}",
"VersionNumber": "Версия {0}",
"TaskDownloadMissingSubtitles": "Загрузка отсутствующих субтитров",
"TaskRefreshChannels": "Обновление каналов",
@@ -135,5 +106,7 @@
"TaskExtractMediaSegmentsDescription": "Извлекает или получает медиасегменты из плагинов MediaSegment.",
"TaskMoveTrickplayImagesDescription": "Перемещает существующие файлы trickplay в соответствии с настройками медиатеки.",
"CleanupUserDataTask": "Задача очистки пользовательских данных",
- "CleanupUserDataTaskDescription": "Очищает все пользовательские данные (состояние просмотра, статус избранного и т.д.) с медиа, отсутствующих по меньшей мере в течение 90 дней."
+ "CleanupUserDataTaskDescription": "Очищает все пользовательские данные (состояние просмотра, статус избранного и т.д.) с медиа, отсутствующих по меньшей мере в течение 90 дней.",
+ "Original": "Оригинальный",
+ "LyricDownloadFailureFromForItem": "Не получилось скачать текст песни с {0} для {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index 184e9b0a5c..afea835bd4 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -1,41 +1,24 @@
{
- "Albums": "Albumy",
"AppDeviceValues": "Aplikácia: {0}, Zariadenie: {1}",
- "Application": "Aplikácia",
"Artists": "Interpreti",
"AuthenticationSucceededWithUserName": "{0} úspešne overený",
"Books": "Knihy",
- "CameraImageUploadedFrom": "Z {0} bola nahraná nová fotografia",
- "Channels": "Kanály",
"ChapterNameValue": "Kapitola {0}",
"Collections": "Kolekcie",
- "DeviceOfflineWithName": "{0} sa odpojil",
- "DeviceOnlineWithName": "{0} je pripojený",
"FailedLoginAttemptWithUserName": "Neúspešný pokus o prihlásenie z {0}",
"Favorites": "Obľúbené",
"Folders": "Priečinky",
"Genres": "Žánre",
- "HeaderAlbumArtists": "Interpreti albumu",
"HeaderContinueWatching": "Pokračovať v pozeraní",
- "HeaderFavoriteAlbums": "Obľúbené albumy",
- "HeaderFavoriteArtists": "Obľúbení interpreti",
"HeaderFavoriteEpisodes": "Obľúbené epizódy",
"HeaderFavoriteShows": "Obľúbené seriály",
- "HeaderFavoriteSongs": "Obľúbené skladby",
"HeaderLiveTV": "Živá TV",
"HeaderNextUp": "Nasleduje",
- "HeaderRecordingGroups": "Skupiny nahrávok",
"HomeVideos": "Domáce videá",
"Inherit": "Zdediť",
- "ItemAddedWithName": "{0} bol pridaný do knižnice",
- "ItemRemovedWithName": "{0} bol odstránený z knižnice",
"LabelIpAddressValue": "IP adresa: {0}",
"LabelRunningTimeValue": "Dĺžka: {0}",
"Latest": "Najnovšie",
- "MessageApplicationUpdated": "Jellyfin Server bol aktualizovaný",
- "MessageApplicationUpdatedTo": "Jellyfin Server bol aktualizovaný na verziu {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Sekcia {0} konfigurácie servera bola aktualizovaná",
- "MessageServerConfigurationUpdated": "Konfigurácia servera bola aktualizovaná",
"MixedContent": "Zmiešaný obsah",
"Movies": "Filmy",
"Music": "Hudba",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Spustené prehrávanie videa",
"NotificationOptionVideoPlaybackStopped": "Zastavené prehrávanie videa",
"Photos": "Fotky",
- "Playlists": "Playlisty",
- "Plugin": "Zásuvný modul",
"PluginInstalledWithName": "{0} bol nainštalovaný",
"PluginUninstalledWithName": "{0} bol odinštalovaný",
"PluginUpdatedWithName": "{0} bol aktualizovaný",
- "ProviderValue": "Poskytovateľ: {0}",
"ScheduledTaskFailedWithName": "{0} zlyhalo",
- "ScheduledTaskStartedWithName": "{0} zahájených",
- "ServerNameNeedsToBeRestarted": "{0} vyžaduje reštart",
"Shows": "Seriály",
- "Songs": "Skladby",
"StartupEmbyServerIsLoading": "Jellyfin Server sa spúšťa. Prosím, skúste to o chvíľu znova.",
"SubtitleDownloadFailureFromForItem": "Sťahovanie titulkov z {0} pre {1} zlyhalo",
- "Sync": "Synchronizácia",
- "System": "Systém",
"TvShows": "TV seriály",
- "User": "Používateľ",
"UserCreatedWithName": "Používateľ {0} bol vytvorený",
"UserDeletedWithName": "Používateľ {0} bol vymazaný",
"UserDownloadingItemWithValues": "{0} sťahuje {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} sa odpojil od {1}",
"UserOnlineFromDevice": "{0} je online z {1}",
"UserPasswordChangedWithName": "Heslo používateľa {0} bolo zmenené",
- "UserPolicyUpdatedWithName": "Používateľské zásady pre {0} boli aktualizované",
"UserStartedPlayingItemWithValues": "{0} spustil prehrávanie {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} ukončil prehrávanie {1} na {2}",
- "ValueHasBeenAddedToLibrary": "{0} bol pridaný do vašej knižnice médií",
- "ValueSpecialEpisodeName": "Špeciál - {0}",
"VersionNumber": "Verzia {0}",
"TaskDownloadMissingSubtitlesDescription": "Vyhľadá na internete chýbajúce titulky podľa toho, ako sú nakonfigurované metadáta.",
"TaskDownloadMissingSubtitles": "Stiahnuť chýbajúce titulky",
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index 35c5b4a914..8c8ed3254a 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -1,41 +1,24 @@
{
- "Albums": "Albumi",
"AppDeviceValues": "Aplikacija: {0}, Naprava: {1}",
- "Application": "Aplikacija",
"Artists": "Izvajalci",
"AuthenticationSucceededWithUserName": "{0} se je uspešno prijavil/a",
"Books": "Knjige",
- "CameraImageUploadedFrom": "Nova fotografija je bila naložena iz {0}",
- "Channels": "Kanali",
"ChapterNameValue": "Poglavje {0}",
"Collections": "Zbirke",
- "DeviceOfflineWithName": "{0} je prekinil povezavo",
- "DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
"Favorites": "Priljubljeno",
"Folders": "Mape",
"Genres": "Zvrsti",
- "HeaderAlbumArtists": "Izvajalci albuma",
"HeaderContinueWatching": "Nadaljuj ogled",
- "HeaderFavoriteAlbums": "Priljubljeni albumi",
- "HeaderFavoriteArtists": "Priljubljeni izvajalci",
"HeaderFavoriteEpisodes": "Priljubljene epizode",
"HeaderFavoriteShows": "Priljubljene serije",
- "HeaderFavoriteSongs": "Priljubljene pesmi",
"HeaderLiveTV": "TV v živo",
"HeaderNextUp": "Sledi",
- "HeaderRecordingGroups": "Zbirke posnetkov",
"HomeVideos": "Domači posnetki",
"Inherit": "Podeduj",
- "ItemAddedWithName": "{0} je dodan v knjižnico",
- "ItemRemovedWithName": "{0} je bil odstranjen iz knjižnice",
"LabelIpAddressValue": "IP naslov: {0}",
"LabelRunningTimeValue": "Čas trajanja: {0}",
"Latest": "Najnovejše",
- "MessageApplicationUpdated": "Jellyfin strežnik je bil posodobljen",
- "MessageApplicationUpdatedTo": "Jellyfin strežnik je bil posodobljen na {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitev {0} je bil posodobljen",
- "MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene",
"MixedContent": "Mešane vsebine",
"Movies": "Filmi",
"Music": "Glasba",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Predvajanje videa se je začelo",
"NotificationOptionVideoPlaybackStopped": "Predvajanje videa se je ustavilo",
"Photos": "Fotografije",
- "Playlists": "Seznami predvajanja",
- "Plugin": "Dodatek",
"PluginInstalledWithName": "{0} je bil nameščen",
"PluginUninstalledWithName": "{0} je bil odstranjen",
"PluginUpdatedWithName": "{0} je bil posodobljen",
- "ProviderValue": "Ponudnik: {0}",
"ScheduledTaskFailedWithName": "{0} ni uspelo",
- "ScheduledTaskStartedWithName": "{0} začeto",
- "ServerNameNeedsToBeRestarted": "{0} mora biti ponovno zagnan",
"Shows": "Serije",
- "Songs": "Pesmi",
"StartupEmbyServerIsLoading": "Jellyfin strežnik se zaganja. Poskusite ponovno kasneje.",
"SubtitleDownloadFailureFromForItem": "Neuspešen prenos podnapisov iz {0} za {1}",
- "Sync": "Sinhroniziraj",
- "System": "Sistem",
"TvShows": "TV serije",
- "User": "Uporabnik",
"UserCreatedWithName": "Uporabnik {0} je bil ustvarjen",
"UserDeletedWithName": "Uporabnik {0} je bil izbrisan",
"UserDownloadingItemWithValues": "{0} prenaša {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} je prekinil povezavo z {1}",
"UserOnlineFromDevice": "{0} je aktiven na {1}",
"UserPasswordChangedWithName": "Geslo za uporabnika {0} je bilo spremenjeno",
- "UserPolicyUpdatedWithName": "Pravilnik uporabe je bil posodobljen za uporabnika {0}",
"UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}",
- "ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici",
- "ValueSpecialEpisodeName": "Posebna epizoda - {0}",
"VersionNumber": "Različica {0}",
"TaskDownloadMissingSubtitles": "Prenesi manjkajoče podnapise",
"TaskRefreshChannelsDescription": "Osveži podatke spletnih kanalov.",
diff --git a/Emby.Server.Implementations/Localization/Core/sn.json b/Emby.Server.Implementations/Localization/Core/sn.json
index 74720e7646..45a459cbe1 100644
--- a/Emby.Server.Implementations/Localization/Core/sn.json
+++ b/Emby.Server.Implementations/Localization/Core/sn.json
@@ -1,28 +1,18 @@
{
- "HeaderAlbumArtists": "Vaimbi vemadambarefu",
"HeaderContinueWatching": "Simudzira kuona",
- "HeaderFavoriteSongs": "Nziyo dzaunofarira",
- "Albums": "Dambarefu",
"AppDeviceValues": "Apu: {0}, Dhivhaisi: {1}",
- "Application": "Purogiramu",
"Artists": "Vaimbi",
"AuthenticationSucceededWithUserName": "apinda",
"Books": "Mabhuku",
- "CameraImageUploadedFrom": "Mufananidzo mutsva vabva pakamera {0}",
- "Channels": "Machanewo",
"ChapterNameValue": "Chikamu {0}",
"Collections": "Akafanana",
"Default": "Zvakasarudzwa Kare",
- "DeviceOfflineWithName": "{0} haasisipo",
- "DeviceOnlineWithName": "{0} aripo",
"External": "Zvekunze",
"FailedLoginAttemptWithUserName": "Vatadza kuloga chimboedza kushandisa {0}",
"Favorites": "Zvaunofarira",
"Folders": "Mafoodha",
"Forced": "Zvekumanikidzira",
"Genres": "Mhando",
- "HeaderFavoriteAlbums": "Madambarefu aunofarira",
- "HeaderFavoriteArtists": "Vaimbi vaunofarira",
"HeaderFavoriteEpisodes": "Maepisodhi aunofarira",
"HeaderFavoriteShows": "Masirisi aunofarira"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json
index 5a284e20b9..b1f76aafbb 100644
--- a/Emby.Server.Implementations/Localization/Core/sq.json
+++ b/Emby.Server.Implementations/Localization/Core/sq.json
@@ -1,5 +1,4 @@
{
- "MessageApplicationUpdatedTo": "Serveri Jellyfin u përditesua në versionin {0}",
"Inherit": "Trashgimi",
"TaskDownloadMissingSubtitlesDescription": "Kërkon në internet për titra që mungojnë bazuar tek konfigurimi i metadata-ve.",
"TaskDownloadMissingSubtitles": "Shkarko titra që mungojnë",
@@ -24,11 +23,8 @@
"TasksLibraryCategory": "Libraria",
"TasksMaintenanceCategory": "Mirëmbajtje",
"VersionNumber": "Versioni {0}",
- "ValueSpecialEpisodeName": "Speciale - {0}",
- "ValueHasBeenAddedToLibrary": "{0} u shtua tek libraria juaj",
"UserStoppedPlayingItemWithValues": "{0} mbaroi së shikuari {1} tek {2}",
"UserStartedPlayingItemWithValues": "{0} po shikon {1} tek {2}",
- "UserPolicyUpdatedWithName": "Politika e përdoruesit u përditësua për {0}",
"UserPasswordChangedWithName": "Fjalëkalimi u ndryshua për përdoruesin {0}",
"UserOnlineFromDevice": "{0} është në linjë nga {1}",
"UserOfflineFromDevice": "{0} u shkëput nga {1}",
@@ -36,23 +32,14 @@
"UserDownloadingItemWithValues": "{0} po shkarkon {1}",
"UserDeletedWithName": "Përdoruesi {0} u fshi",
"UserCreatedWithName": "Përdoruesi {0} u krijua",
- "User": "Përdoruesi",
"TvShows": "Seriale TV",
- "System": "Sistemi",
- "Sync": "Sinkronizo",
"SubtitleDownloadFailureFromForItem": "Titrat deshtuan të shkarkohen nga {0} për {1}",
"StartupEmbyServerIsLoading": "Serveri Jellyfin po ngarkohet. Ju lutemi provoni përseri pas pak.",
- "Songs": "Këngët",
"Shows": "Serialet",
- "ServerNameNeedsToBeRestarted": "{0} duhet të ristartoj",
- "ScheduledTaskStartedWithName": "{0} filloi",
"ScheduledTaskFailedWithName": "{0} dështoi",
- "ProviderValue": "Ofruesi: {0}",
"PluginUpdatedWithName": "{0} u përditësua",
"PluginUninstalledWithName": "{0} u çinstalua",
"PluginInstalledWithName": "{0} u instalua",
- "Plugin": "Plugin",
- "Playlists": "Listat për luajtje",
"Photos": "Fotografitë",
"NotificationOptionVideoPlaybackStopped": "Luajtja e videos ndaloi",
"NotificationOptionVideoPlayback": "Luajtja e videos filloi",
@@ -78,41 +65,25 @@
"Music": "Muzikë",
"Movies": "Filmat",
"MixedContent": "Përmbajtje e përzier",
- "MessageServerConfigurationUpdated": "Konfigurimet e serverit u përditësuan",
- "MessageNamedServerConfigurationUpdatedWithValue": "Seksioni i konfigurimit të serverit {0} u përditësua",
- "MessageApplicationUpdated": "Serveri Jellyfin u përditësua",
"Latest": "Të fundit",
"LabelRunningTimeValue": "Kohëzgjatja: {0}",
"LabelIpAddressValue": "Adresa IP: {0}",
- "ItemRemovedWithName": "{0} u fshi nga libraria",
- "ItemAddedWithName": "{0} u shtua tek libraria",
"HomeVideos": "Video personale",
- "HeaderRecordingGroups": "Grupet e regjistrimit",
"HeaderNextUp": "Në vazhdim",
"HeaderLiveTV": "TV Live",
- "HeaderFavoriteSongs": "Kënget e preferuara",
"HeaderFavoriteShows": "Serialet e preferuar",
"HeaderFavoriteEpisodes": "Episodet e preferuar",
- "HeaderFavoriteArtists": "Artistët e preferuar",
- "HeaderFavoriteAlbums": "Albumet e preferuar",
"HeaderContinueWatching": "Vazhdo të shikosh",
- "HeaderAlbumArtists": "Artistët e albumeve",
"Genres": "Zhanret",
"Folders": "Skedarët",
"Favorites": "Të preferuarat",
"FailedLoginAttemptWithUserName": "Përpjekja për hyrje dështoi nga {0}",
- "DeviceOnlineWithName": "{0} u lidh",
- "DeviceOfflineWithName": "{0} u shkëput",
"Collections": "Koleksionet",
"ChapterNameValue": "Kapituj",
- "Channels": "Kanalet",
- "CameraImageUploadedFrom": "Një foto e re nga kamera u ngarkua nga {0}",
"Books": "Librat",
"AuthenticationSucceededWithUserName": "{0} u identifikua me sukses",
"Artists": "Artistët",
- "Application": "Aplikacioni",
"AppDeviceValues": "Aplikacioni: {0}, Pajisja: {1}",
- "Albums": "Albumet",
"TaskCleanActivityLogDescription": "Pastro të dhënat mbi aktivitetin më të vjetra sesa koha e përcaktuar.",
"TaskCleanActivityLog": "Pastro të dhënat mbi aktivitetin",
"Undefined": "I papërcaktuar",
diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json
index 52f4124657..56806e25c1 100644
--- a/Emby.Server.Implementations/Localization/Core/sr.json
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -1,9 +1,6 @@
{
- "UserPolicyUpdatedWithName": "Корисничке смернице ажуриране за {0}",
"NotificationOptionUserLockedOut": "Корисник закључан",
"VersionNumber": "Верзија {0}",
- "ValueSpecialEpisodeName": "Специјал - {0}",
- "ValueHasBeenAddedToLibrary": "{0} је додато у вашу медијску библиотеку",
"UserStoppedPlayingItemWithValues": "{0} завршио пуштање {1} на {2}",
"UserStartedPlayingItemWithValues": "{0} пушта {1} на {2}",
"UserPasswordChangedWithName": "Лозинка је промењена за корисника {0}",
@@ -13,23 +10,14 @@
"UserDownloadingItemWithValues": "{0} преузима {1}",
"UserDeletedWithName": "Корисник {0} је обрисан",
"UserCreatedWithName": "Корисник {0} је направљен",
- "User": "Корисник",
"TvShows": "ТВ серије",
- "System": "Систем",
- "Sync": "Усклади",
"SubtitleDownloadFailureFromForItem": "Неуспело преузимање титлова за {1} са {0}",
"StartupEmbyServerIsLoading": "Џелифин сервер се подиже. Покушајте поново убрзо.",
- "Songs": "Песме",
"Shows": "Серије",
- "ServerNameNeedsToBeRestarted": "{0} треба поново покренути",
- "ScheduledTaskStartedWithName": "{0} покренуто",
"ScheduledTaskFailedWithName": "{0} неуспело",
- "ProviderValue": "Пружалац: {0}",
"PluginUpdatedWithName": "{0} ажуриран",
"PluginUninstalledWithName": "{0} деинсталиран",
"PluginInstalledWithName": "{0} инсталиран",
- "Plugin": "Прикључак",
- "Playlists": "Листе",
"Photos": "Фотографије",
"NotificationOptionVideoPlaybackStopped": "Заустављено пуштање видеа",
"NotificationOptionVideoPlayback": "Покренуто пуштање видеа",
@@ -54,43 +42,26 @@
"Music": "Музика",
"Movies": "Филмови",
"MixedContent": "Мешовит садржај",
- "MessageServerConfigurationUpdated": "Серверска поставка је ажурирана",
- "MessageNamedServerConfigurationUpdatedWithValue": "Одељак серверске поставке {0} је ажуриран",
- "MessageApplicationUpdatedTo": "Џелифин сервер је ажуриран на {0}",
- "MessageApplicationUpdated": "Џелифин сервер је ажуриран",
"Latest": "Последње",
"LabelRunningTimeValue": "Време рада: {0}",
"LabelIpAddressValue": "ИП адреса: {0}",
- "ItemRemovedWithName": "{0} уклоњено из библиотеке",
- "ItemAddedWithName": "{0} додато у библиотеку",
"Inherit": "Наследи",
"HomeVideos": "Кућни Видео",
- "HeaderRecordingGroups": "Групе снимања",
"HeaderNextUp": "Следи",
"HeaderLiveTV": "ТВ уживо",
- "HeaderFavoriteSongs": "Омиљене песме",
"HeaderFavoriteShows": "Омиљене серије",
"HeaderFavoriteEpisodes": "Омиљене епизоде",
- "HeaderFavoriteArtists": "Омиљени извођачи",
- "HeaderFavoriteAlbums": "Омиљени албуми",
"HeaderContinueWatching": "Настави гледање",
- "HeaderAlbumArtists": "Извођачи албума",
"Genres": "Жанрови",
"Folders": "Фасцикле",
"Favorites": "Омиљено",
"FailedLoginAttemptWithUserName": "Неуспели покушај пријавe са {0}",
- "DeviceOnlineWithName": "{0} је повезан",
- "DeviceOfflineWithName": "{0} је прекинуо везу",
"Collections": "Колекције",
"ChapterNameValue": "Поглавље {0}",
- "Channels": "Канали",
- "CameraImageUploadedFrom": "Нова фотографија је учитана са {0}",
"Books": "Књиге",
"AuthenticationSucceededWithUserName": "{0} Успешна аутентификација",
"Artists": "Извођачи",
- "Application": "Апликација",
"AppDeviceValues": "Апликација: {0}, Уређај: {1}",
- "Albums": "Албуми",
"TaskDownloadMissingSubtitlesDescription": "Претражује интернет за недостајуће титлове на основу конфигурације метаподатака.",
"TaskDownloadMissingSubtitles": "Преузмите недостајуће титлове",
"TaskRefreshChannelsDescription": "Освежава информације о интернет каналу.",
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index a47ed248e9..7384967122 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -1,41 +1,24 @@
{
- "Albums": "Album",
"AppDeviceValues": "Applikation: {0}, Enhet: {1}",
- "Application": "Applikation",
"Artists": "Artister",
"AuthenticationSucceededWithUserName": "{0} har autentiserats",
"Books": "Böcker",
- "CameraImageUploadedFrom": "En ny kamerabild har laddats upp från {0}",
- "Channels": "Kanaler",
"ChapterNameValue": "Kapitel {0}",
"Collections": "Samlingar",
- "DeviceOfflineWithName": "{0} har kopplat ned",
- "DeviceOnlineWithName": "{0} är ansluten",
"FailedLoginAttemptWithUserName": "Misslyckat inloggningsförsök från {0}",
"Favorites": "Favoriter",
"Folders": "Mappar",
"Genres": "Genrer",
- "HeaderAlbumArtists": "Albumartister",
"HeaderContinueWatching": "Fortsätt titta",
- "HeaderFavoriteAlbums": "Favoritalbum",
- "HeaderFavoriteArtists": "Favoritartister",
"HeaderFavoriteEpisodes": "Favoritavsnitt",
"HeaderFavoriteShows": "Favoritserier",
- "HeaderFavoriteSongs": "Favoritlåtar",
"HeaderLiveTV": "Direktsänd TV",
"HeaderNextUp": "Nästa",
- "HeaderRecordingGroups": "Inspelningsgrupper",
"HomeVideos": "Hemmavideor",
"Inherit": "Ärv",
- "ItemAddedWithName": "{0} lades till i biblioteket",
- "ItemRemovedWithName": "{0} togs bort från biblioteket",
"LabelIpAddressValue": "IP-adress: {0}",
"LabelRunningTimeValue": "Speltid: {0}",
"Latest": "Senaste",
- "MessageApplicationUpdated": "Jellyfin Server har uppdaterats",
- "MessageApplicationUpdatedTo": "Jellyfin Server har uppdaterats till {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Serverinställningarna {0} har uppdaterats",
- "MessageServerConfigurationUpdated": "Serverkonfigurationen har uppdaterats",
"MixedContent": "Blandat innehåll",
"Movies": "Filmer",
"Music": "Musik",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Videouppspelning har påbörjats",
"NotificationOptionVideoPlaybackStopped": "Videouppspelning stoppades",
"Photos": "Bilder",
- "Playlists": "Spellistor",
- "Plugin": "Tillägg",
"PluginInstalledWithName": "{0} installerades",
"PluginUninstalledWithName": "{0} avinstallerades",
"PluginUpdatedWithName": "{0} uppdaterades",
- "ProviderValue": "Leverantör: {0}",
"ScheduledTaskFailedWithName": "{0} misslyckades",
- "ScheduledTaskStartedWithName": "{0} startades",
- "ServerNameNeedsToBeRestarted": "{0} behöver startas om",
"Shows": "Serier",
- "Songs": "Låtar",
"StartupEmbyServerIsLoading": "Jellyfin Server arbetar. Pröva igen snart.",
"SubtitleDownloadFailureFromForItem": "Undertexter kunde inte laddas ner från {0} till {1}",
- "Sync": "Synk",
- "System": "System",
"TvShows": "Tv-serier",
- "User": "Användare",
"UserCreatedWithName": "Användaren {0} har skapats",
"UserDeletedWithName": "Användaren {0} har tagits bort",
"UserDownloadingItemWithValues": "{0} laddar ner {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} har kopplat ned från {1}",
"UserOnlineFromDevice": "{0} är uppkopplad från {1}",
"UserPasswordChangedWithName": "Lösenordet för {0} har ändrats",
- "UserPolicyUpdatedWithName": "Användarpolicyn har uppdaterats för {0}",
"UserStartedPlayingItemWithValues": "{0} spelar {1} på {2}",
"UserStoppedPlayingItemWithValues": "{0} har stoppat uppspelningen av {1} på {2}",
- "ValueHasBeenAddedToLibrary": "{0} har lagts till i ditt mediebibliotek",
- "ValueSpecialEpisodeName": "Specialavsnitt - {0}",
"VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Söker på internet efter saknade undertexter baserat på metadata-konfiguration.",
"TaskDownloadMissingSubtitles": "Ladda ner saknade undertexter",
@@ -135,5 +106,7 @@
"TaskMoveTrickplayImages": "Migrera platsen för Trickplay-bilder",
"TaskMoveTrickplayImagesDescription": "Flyttar befintliga trickplay-filer enligt bibliotekets inställningar.",
"CleanupUserDataTaskDescription": "Tar bort all användardata (såsom vad du sett, favoriter med mera) för media som inte funnits på enheten på minst 90 dagar.",
- "CleanupUserDataTask": "Uppgift för rensning av användardata"
+ "CleanupUserDataTask": "Uppgift för rensning av användardata",
+ "Original": "Original",
+ "LyricDownloadFailureFromForItem": "Misslyckades att ladda ner låttexter från {0} för {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index b68af92033..f613b973db 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -1,13 +1,11 @@
{
"VersionNumber": "பதிப்பு {0}",
- "ValueSpecialEpisodeName": "சிறப்பு - {0}",
"TasksMaintenanceCategory": "பராமரிப்பு",
"TaskCleanCache": "தற்காலிக சேமிப்பு கோப்பகத்தை சுத்தம் செய்யவும்",
"TaskRefreshChapterImages": "அத்தியாயப் படங்களை பிரித்தெடுக்கவும்",
"TaskRefreshPeople": "மக்களைப் புதுப்பிக்கவும்",
"TaskCleanTranscode": "டிரான்ஸ்கோட் கோப்பகத்தை சுத்தம் செய்யவும்",
"TaskRefreshChannelsDescription": "இணையச் சேனல் தகவல்களைப் புதுப்பிக்கிறது.",
- "System": "ஒருங்கியம்",
"NotificationOptionTaskFailed": "திட்டமிடப்பட்ட பணி தோல்வியடைந்தது",
"NotificationOptionPluginUpdateInstalled": "உட்செருகி புதுப்பிக்கப்பட்டது",
"NotificationOptionPluginUninstalled": "உட்செருகி நீக்கப்பட்டது",
@@ -15,17 +13,10 @@
"NotificationOptionPluginError": "உட்செருகி செயலிழந்தது",
"NotificationOptionCameraImageUploaded": "புகைப்படம் பதிவேற்றப்பட்டது",
"MixedContent": "கலப்பு உள்ளடக்கங்கள்",
- "MessageServerConfigurationUpdated": "சேவையக அமைப்புகள் புதுப்பிக்கப்பட்டன",
- "MessageApplicationUpdatedTo": "ஜெல்லிஃபின் சேவையகம் {0} இற்கு புதுப்பிக்கப்பட்டது",
- "MessageApplicationUpdated": "ஜெல்லிஃபின் சேவையகம் புதுப்பிக்கப்பட்டது",
"Inherit": "மரபுரிமையாகப் பெறு",
- "HeaderRecordingGroups": "பதிவு குழுக்கள்",
"Folders": "கோப்புறைகள்",
"FailedLoginAttemptWithUserName": "{0} இலிருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது",
- "DeviceOnlineWithName": "{0} இணைக்கப்பட்டது",
- "DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது",
"Collections": "தொகுப்புகள்",
- "CameraImageUploadedFrom": "{0} இல் இருந்து புதிய புகைப்படம் பதிவேற்றப்பட்டது",
"AppDeviceValues": "செயலி: {0}, சாதனம்: {1}",
"TaskDownloadMissingSubtitles": "விடுபட்டுபோன வசன வரிகளைப் பதிவிறக்கு",
"TaskRefreshChannels": "சேனல்களை புதுப்பி",
@@ -34,27 +25,18 @@
"TasksChannelsCategory": "இணைய சேனல்கள்",
"TasksApplicationCategory": "செயலி",
"TasksLibraryCategory": "நூலகம்",
- "UserPolicyUpdatedWithName": "பயனர் கொள்கை {0} இற்கு புதுப்பிக்கப்பட்டுள்ளது",
"UserPasswordChangedWithName": "{0} பயனருக்கு கடவுச்சொல் மாற்றப்பட்டுள்ளது",
"UserLockedOutWithName": "பயனர் {0} முடக்கப்பட்டார்",
"UserDownloadingItemWithValues": "{0} ஆல் {1} பதிவிறக்கப்படுகிறது",
"UserDeletedWithName": "பயனர் {0} நீக்கப்பட்டார்",
"UserCreatedWithName": "பயனர் {0} உருவாக்கப்பட்டார்",
- "User": "பயனர்",
"TvShows": "தொலைக்காட்சித் தொடர்கள்",
- "Sync": "ஒத்திசைவு",
"StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.",
- "Songs": "பாடல்கள்",
"Shows": "நிகழ்ச்சிகள்",
- "ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்",
- "ScheduledTaskStartedWithName": "{0} துவங்கியது",
"ScheduledTaskFailedWithName": "{0} தோல்வியடைந்தது",
- "ProviderValue": "வழங்குநர்: {0}",
"PluginUpdatedWithName": "{0} புதுப்பிக்கப்பட்டது",
"PluginUninstalledWithName": "{0} நீக்கப்பட்டது",
"PluginInstalledWithName": "{0} நிறுவப்பட்டது",
- "Plugin": "உட்செருகி",
- "Playlists": "தொடர் பட்டியல்கள்",
"Photos": "புகைப்படங்கள்",
"NotificationOptionVideoPlaybackStopped": "நிகழ்பட ஒளிபரப்பு நிறுத்தப்பட்டது",
"NotificationOptionVideoPlayback": "நிகழ்பட ஒளிபரப்பு துவங்கியது",
@@ -75,28 +57,18 @@
"Latest": "புதியவை",
"LabelRunningTimeValue": "ஓடும் நேரம்: {0}",
"LabelIpAddressValue": "ஐபி முகவரி: {0}",
- "ItemRemovedWithName": "{0} நூலகத்திலிருந்து அகற்றப்பட்டது",
- "ItemAddedWithName": "{0} நூலகத்தில் சேர்க்கப்பட்டது",
"HeaderNextUp": "அடுத்தது",
"HeaderLiveTV": "நேரடித் தொலைக்காட்சி",
- "HeaderFavoriteSongs": "பிடித்த பாடல்கள்",
"HeaderFavoriteShows": "பிடித்த தொடர்கள்",
"HeaderFavoriteEpisodes": "பிடித்த அத்தியாயங்கள்",
- "HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்",
- "HeaderFavoriteAlbums": "பிடித்த ஆல்பங்கள்",
"HeaderContinueWatching": "தொடர்ந்து பார்",
- "HeaderAlbumArtists": "கலைஞரின் ஆல்பம்",
"Genres": "வகைகள்",
"Favorites": "பிடித்தவை",
"ChapterNameValue": "அத்தியாயம் {0}",
- "Channels": "சேனல்கள்",
"Books": "புத்தகங்கள்",
"AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது",
"Artists": "கலைஞர்கள்",
- "Application": "செயலி",
- "Albums": "ஆல்பங்கள்",
"NewVersionIsAvailable": "ஜெல்லிஃபின் சேவையகத்தின் புதிய பதிப்பு பதிவிறக்கத்திற்கு கிடைக்கிறது.",
- "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0} புதுப்பிக்கப்பட்டது",
"TaskCleanCacheDescription": "கணினிக்கு இனி தேவைப்படாத தற்காலிக கோப்புகளை நீக்கு.",
"UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது",
"SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இல் இருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
@@ -108,7 +80,6 @@
"TaskCleanLogs": "பதிவு அடைவை சுத்தம் செய்யுங்கள்",
"TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் ஊடக நூலகத்தை ஆராய்ந்து மீத்தரவை புதுப்பிக்கும்.",
"TaskRefreshChapterImagesDescription": "அத்தியாயங்களைக் கொண்ட வீடியோக்களுக்கான சிறு உருவங்களை உருவாக்குகிறது.",
- "ValueHasBeenAddedToLibrary": "உங்கள் மீடியா நூலகத்தில் {0} சேர்க்கப்பட்டது",
"UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
"HomeVideos": "முகப்பு வீடியோக்கள்",
"UserStoppedPlayingItemWithValues": "{0} {2} இல் {1} முடித்துவிட்டது",
diff --git a/Emby.Server.Implementations/Localization/Core/te.json b/Emby.Server.Implementations/Localization/Core/te.json
index ca9e345214..7ac770752e 100644
--- a/Emby.Server.Implementations/Localization/Core/te.json
+++ b/Emby.Server.Implementations/Localization/Core/te.json
@@ -1,51 +1,31 @@
{
- "ValueSpecialEpisodeName": "ప్రత్యేక - {0}",
- "Sync": "సమకాలీకరించు",
- "Songs": "పాటలు",
"Shows": "ప్రదర్శనలు",
- "Playlists": "ప్లేజాబితాలు",
"Photos": "ఫోటోలు",
"MusicVideos": "మ్యూజిక్ వీడియోలు",
"Music": "సంగీతం",
"Movies": "సినిమాలు",
"HeaderContinueWatching": "చూడటం కొనసాగించండి",
- "HeaderAlbumArtists": "ఆల్బమ్ కళాకారులు",
"Genres": "శైలులు",
"Forced": "బలవంతంగా",
"Folders": "ఫోల్డర్లు",
"Favorites": "ఇష్టమైనవి",
"Default": "డిఫాల్ట్",
"Collections": "సేకరణలు",
- "Channels": "ఛానెల్‌లు",
"Books": "పుస్తకాలు",
"Artists": "కళాకారులు",
- "Albums": "ఆల్బమ్‌లు",
"HearingImpaired": "వినికిడి లోపం",
"HomeVideos": "హోమ్ వీడియోలు",
"AppDeviceValues": "అప్లికేషన్ : {0}, పరికరం: {1}",
- "Application": "అప్లికేషన్",
"AuthenticationSucceededWithUserName": "విజయవంతంగా ఆమోదించబడింది",
- "CameraImageUploadedFrom": "{0} నుండి కొత్త కెమెరా చిత్రం అప్‌లోడ్ చేయబడింది",
"ChapterNameValue": "అధ్యాయం",
- "DeviceOfflineWithName": "{0} డిస్‌కనెక్ట్ చేయబడింది",
- "DeviceOnlineWithName": "{0} కనెక్ట్ చేయబడింది",
"External": "బాహ్య",
"FailedLoginAttemptWithUserName": "{0} నుండి విఫలమైన లాగిన్ ప్రయత్నం",
- "HeaderFavoriteAlbums": "ఇష్టమైన ఆల్బమ్‌లు",
- "HeaderFavoriteArtists": "ఇష్టమైన కళాకారులు",
"HeaderFavoriteEpisodes": "ఇష్టమైన ఎపిసోడ్‌లు",
"HeaderFavoriteShows": "ఇష్టమైన ప్రదర్శనలు",
- "HeaderFavoriteSongs": "ఇష్టమైన పాటలు",
"HeaderLiveTV": "ప్రత్యక్ష TV",
"HeaderNextUp": "తదుపరి",
- "HeaderRecordingGroups": "రికార్డింగ్ గుంపులు",
- "MessageApplicationUpdated": "జెల్లీఫిన్ సర్వర్ అప్‌డేట్ చేయడం పూర్తి అయ్యింది",
- "MessageApplicationUpdatedTo": "జెల్లీఫిన్ సర్వర్ {0} వెర్షన్ కి అప్‌డేట్ చెయ్యబడింది",
- "MessageServerConfigurationUpdated": "సర్వర్ కన్ఫిగరేషన్ అప్డేట్ చేయబడింది",
"NewVersionIsAvailable": "జెల్లీఫిన్ సర్వర్ యొక్క కొత్త వెర్షన్ డౌన్‌లోడ్ చేసుకోవడానికి అందుబాటులో ఉంది.",
"NotificationOptionApplicationUpdateInstalled": "అప్లికేషన్ అప్‌డేట్ ఇన్‌స్టాల్ చేయబడింది",
- "ItemAddedWithName": "{0} లైబ్రరీకి జోడించబడింది",
- "ItemRemovedWithName": "లైబ్రరీ నుండి {0} తీసివేయబడింది",
"LabelIpAddressValue": "ఐపీ చిరునామా: {0}",
"LabelRunningTimeValue": "నడుస్తున్న సమయం: {0}",
"Latest": "తాజా",
diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json
index f0a62646f7..89c2c26748 100644
--- a/Emby.Server.Implementations/Localization/Core/th.json
+++ b/Emby.Server.Implementations/Localization/Core/th.json
@@ -1,10 +1,7 @@
{
- "ProviderValue": "ผู้ให้บริการ: {0}",
"PluginUpdatedWithName": "อัปเดต {0} แล้ว",
"PluginUninstalledWithName": "ถอนการติดตั้ง {0} แล้ว",
"PluginInstalledWithName": "ติดตั้ง {0} แล้ว",
- "Plugin": "ปลั๊กอิน",
- "Playlists": "เพลย์ลิสต์",
"Photos": "รูปภาพ",
"NotificationOptionVideoPlaybackStopped": "หยุดเล่นวิดีโอ",
"NotificationOptionVideoPlayback": "เริ่มเล่นวิดีโอ",
@@ -30,48 +27,28 @@
"Music": "ดนตรี",
"Movies": "ภาพยนตร์",
"MixedContent": "เนื้อหาผสม",
- "MessageServerConfigurationUpdated": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์แล้ว",
- "MessageNamedServerConfigurationUpdatedWithValue": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์ในส่วน {0} แล้ว",
- "MessageApplicationUpdatedTo": "เซิร์ฟเวอร์ Jellyfin ได้รับการอัปเดตเป็น {0}",
- "MessageApplicationUpdated": "อัพเดตเซิร์ฟเวอร์ Jellyfin แล้ว",
"Latest": "ล่าสุด",
"LabelRunningTimeValue": "ผ่านไปแล้ว: {0}",
"LabelIpAddressValue": "ที่อยู่ IP: {0}",
- "ItemRemovedWithName": "{0} ถูกลบออกจากไลบรารี",
- "ItemAddedWithName": "{0} ถูกเพิ่มลงในไลบรารีแล้ว",
"Inherit": "สืบทอด",
"HomeVideos": "โฮมวิดีโอ",
- "HeaderRecordingGroups": "กลุ่มการบันทึก",
"HeaderNextUp": "ถัดไป",
"HeaderLiveTV": "ทีวีสด",
- "HeaderFavoriteSongs": "เพลงที่ชื่นชอบ",
"HeaderFavoriteShows": "รายการที่ชื่นชอบ",
"HeaderFavoriteEpisodes": "ตอนที่ชื่นชอบ",
- "HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ",
- "HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ",
"HeaderContinueWatching": "ดูต่อ",
- "HeaderAlbumArtists": "ศิลปินอัลบั้ม",
"Genres": "ประเภท",
"Folders": "โฟลเดอร์",
"Favorites": "รายการโปรด",
"FailedLoginAttemptWithUserName": "ความพยายามในการเข้าสู่ระบบล้มเหลวจาก {0}",
- "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จแล้ว",
- "DeviceOfflineWithName": "{0} ยกเลิกการเชื่อมต่อแล้ว",
"Collections": "คอลเลกชัน",
"ChapterNameValue": "บทที่ {0}",
- "Channels": "ช่อง",
- "CameraImageUploadedFrom": "ภาพถ่ายใหม่ได้ถูกอัปโหลดมาจาก {0}",
"Books": "หนังสือ",
"AuthenticationSucceededWithUserName": "{0} ยืนยันตัวตนสำเร็จแล้ว",
"Artists": "ศิลปิน",
- "Application": "แอปพลิเคชัน",
"AppDeviceValues": "แอป: {0}, อุปกรณ์: {1}",
- "Albums": "อัลบั้ม",
- "ScheduledTaskStartedWithName": "{0} เริ่มต้น",
"ScheduledTaskFailedWithName": "{0} ล้มเหลว",
- "Songs": "เพลง",
"Shows": "รายการ",
- "ServerNameNeedsToBeRestarted": "{0} ต้องการการรีสตาร์ท",
"TaskDownloadMissingSubtitlesDescription": "ค้นหาคำบรรยายที่หายไปในอินเทอร์เน็ตตามค่ากำหนดในข้อมูลเมตา",
"TaskDownloadMissingSubtitles": "ดาวน์โหลดคำบรรยายที่ขาดหายไป",
"TaskRefreshChannelsDescription": "รีเฟรชข้อมูลช่องอินเทอร์เน็ต",
@@ -95,11 +72,8 @@
"TasksLibraryCategory": "ไลบรารี",
"TasksMaintenanceCategory": "ปิดซ่อมบำรุง",
"VersionNumber": "เวอร์ชัน {0}",
- "ValueSpecialEpisodeName": "พิเศษ - {0}",
- "ValueHasBeenAddedToLibrary": "เพิ่ม {0} ลงในไลบรารีสื่อของคุณแล้ว",
"UserStoppedPlayingItemWithValues": "{0} เล่นเสร็จแล้ว {1} บน {2}",
"UserStartedPlayingItemWithValues": "{0} กำลังเล่น {1} บน {2}",
- "UserPolicyUpdatedWithName": "มีการอัปเดตนโยบายผู้ใช้ของ {0}",
"UserPasswordChangedWithName": "มีการเปลี่ยนรหัสผ่านของผู้ใช้ {0}",
"UserOnlineFromDevice": "{0} ออนไลน์จาก {1}",
"UserOfflineFromDevice": "{0} ได้ยกเลิกการเชื่อมต่อจาก {1}",
@@ -107,10 +81,7 @@
"UserDownloadingItemWithValues": "{0} กำลังดาวน์โหลด {1}",
"UserDeletedWithName": "ลบผู้ใช้ {0} แล้ว",
"UserCreatedWithName": "สร้างผู้ใช้ {0} แล้ว",
- "User": "ผู้ใช้งาน",
"TvShows": "รายการทีวี",
- "System": "ระบบ",
- "Sync": "ซิงค์",
"SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้",
"StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่",
"Default": "ค่าเริ่มต้น",
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index 3789466868..0c42d4a55f 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -1,41 +1,24 @@
{
- "Albums": "Albümler",
"AppDeviceValues": "Uygulama: {0}, Aygıt: {1}",
- "Application": "Uygulama",
"Artists": "Sanatçılar",
"AuthenticationSucceededWithUserName": "{0} kimliği başarıyla doğrulandı",
"Books": "Kitaplar",
- "CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
- "Channels": "Kanallar",
"ChapterNameValue": "{0}. Bölüm",
"Collections": "Koleksiyonlar",
- "DeviceOfflineWithName": "{0} bağlantısı kesildi",
- "DeviceOnlineWithName": "{0} bağlı",
"FailedLoginAttemptWithUserName": "{0} kullanıcısının başarısız oturum açma girişimi",
"Favorites": "Favoriler",
"Folders": "Klasörler",
"Genres": "Türler",
- "HeaderAlbumArtists": "Albüm sanatçıları",
"HeaderContinueWatching": "İzlemeye Devam Et",
- "HeaderFavoriteAlbums": "Favori Albümler",
- "HeaderFavoriteArtists": "Favori Sanatçılar",
"HeaderFavoriteEpisodes": "Favori Bölümler",
"HeaderFavoriteShows": "Favori Diziler",
- "HeaderFavoriteSongs": "Favori Şarkılar",
"HeaderLiveTV": "Canlı TV",
"HeaderNextUp": "Sıradaki Bölümler",
- "HeaderRecordingGroups": "Kayıt Grupları",
"HomeVideos": "Ana Ekran Videoları",
"Inherit": "Devral",
- "ItemAddedWithName": "{0} kütüphaneye eklendi",
- "ItemRemovedWithName": "{0} kütüphaneden silindi",
"LabelIpAddressValue": "IP adresi: {0}",
"LabelRunningTimeValue": "Oynatma süresi: {0}",
"Latest": "En son",
- "MessageApplicationUpdated": "Jellyfin Sunucusu güncellendi",
- "MessageApplicationUpdatedTo": "Jellyfin Sunucusu {0} sürümüne güncellendi",
- "MessageNamedServerConfigurationUpdatedWithValue": "Sunucu yapılandırma bölümü {0} güncellendi",
- "MessageServerConfigurationUpdated": "Sunucu yapılandırması güncellendi",
"MixedContent": "Karışık içerik",
"Movies": "Filmler",
"Music": "Müzik",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "Video oynatma başladı",
"NotificationOptionVideoPlaybackStopped": "Video oynatma durduruldu",
"Photos": "Fotoğraflar",
- "Playlists": "Çalma listeleri",
- "Plugin": "Eklenti",
"PluginInstalledWithName": "{0} yüklendi",
"PluginUninstalledWithName": "{0} kaldırıldı",
"PluginUpdatedWithName": "{0} güncellendi",
- "ProviderValue": "Sağlayıcı: {0}",
"ScheduledTaskFailedWithName": "{0} başarısız oldu",
- "ScheduledTaskStartedWithName": "{0} başladı",
- "ServerNameNeedsToBeRestarted": "{0} yeniden başlatılması gerekiyor",
"Shows": "Diziler",
- "Songs": "Şarkılar",
"StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.",
"SubtitleDownloadFailureFromForItem": "{1} için altyazılar {0} sağlayıcısından indirilemedi",
- "Sync": "Eşzamanlama",
- "System": "Sistem",
"TvShows": "Diziler",
- "User": "Kullanıcı",
"UserCreatedWithName": "{0} kullanıcısı oluşturuldu",
"UserDeletedWithName": "{0} kullanıcısı silindi",
"UserDownloadingItemWithValues": "{0} kullanıcısı {1} medyasını indiriyor",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} kullanıcısının {1} ile bağlantısı kesildi",
"UserOnlineFromDevice": "{0} kullanıcısı {1} ile çevrimiçi",
"UserPasswordChangedWithName": "{0} kullanıcısının parolası değiştirildi",
- "UserPolicyUpdatedWithName": "{0} için kullanıcı politikası güncellendi",
"UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor",
"UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi",
- "ValueHasBeenAddedToLibrary": "{0} medya kütüphanenize eklendi",
- "ValueSpecialEpisodeName": "Özel - {0}",
"VersionNumber": "Sürüm {0}",
"TaskCleanCache": "Önbellek Dizinini Temizle",
"TasksChannelsCategory": "İnternet Kanalları",
@@ -135,5 +106,7 @@
"TaskDownloadMissingLyricsDescription": "Şarkı sözlerini indirir",
"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."
+ "CleanupUserDataTaskDescription": "En az 90 gün boyunca artık mevcut olmayan medyadaki tüm kullanıcı verilerini (İzleme durumu, favori durumu vb.) temizler.",
+ "LyricDownloadFailureFromForItem": "{1} şarkı sözleri {0} adresinden indirilemedi",
+ "Original": "Orijinal"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ug.json b/Emby.Server.Implementations/Localization/Core/ug.json
index 0bcbffb41a..1d5adecb26 100644
--- a/Emby.Server.Implementations/Localization/Core/ug.json
+++ b/Emby.Server.Implementations/Localization/Core/ug.json
@@ -1,22 +1,15 @@
{
"ChapterNameValue": "باب {0}",
- "Channels": "قانال",
- "CameraImageUploadedFrom": "{0} ئورۇندىن يېڭى سۈرەت چىقىرىلدى",
"Books": "كىتاب",
"AuthenticationSucceededWithUserName": "{0} تەستىقلاش مۇۋاپىقىيەتلىك بولدى",
"Artists": "سەنئەتكار",
- "Albums": "پىلاستىنكا",
- "DeviceOnlineWithName": "{0} ئۇلاندى",
- "DeviceOfflineWithName": "{0} ئۈزۈلدى",
"Collections": "توپلام",
- "Application": "ئەپ",
"AppDeviceValues": "ئەپ: {0}، ئۈسكۈنە: {1}",
"HeaderLiveTV": "تور تېلېۋىزىيەسى",
"Default": "سۈكۈتتىكى",
"Folders": "ھۆججەت خالتىسى",
"Favorites": "ساقلىغۇچ",
"LabelRunningTimeValue": "ئىجرا بولغان ۋاقتى:{0}",
- "HeaderRecordingGroups": "خاتىرلەش گۇرۇپىسى",
"Forced": "ئەڭ",
"TaskKeyframeExtractor": "ھالقىلىق رامكا ئاجراتقۇچ",
"TaskKeyframeExtractorDescription": "سىن ھۆججەتلىرىدىن رامكا ئاجرىتىپ، تېخىمۇ ئېنىق بولغان HLS قويۇلۇش تىزىملىكىنى قۇرۇلىدۇ. بۇ ۋەزىپە ئۇزۇن داۋام قىلىشى مۇمكىن.",
@@ -46,35 +39,23 @@
"TasksLibraryCategory": "مېدىيا ئامبىرى",
"TasksMaintenanceCategory": "ئاسراش",
"VersionNumber": "نەشرى {0}",
- "ValueSpecialEpisodeName": "خاسلىق - {0}",
- "ValueHasBeenAddedToLibrary": "{0} مېدىيا ئامبىرىڭىزغا قوشۇلدى",
"UserStoppedPlayingItemWithValues": "{0}،{1} نى {2} دە قويۇنشتىن توختىدى",
"UserStartedPlayingItemWithValues": "{0}،{1} نى {2} دە قويۇۋاتىدۇ",
- "UserPolicyUpdatedWithName": "ئابونتلار سىياسىتى {0} غا يېڭىلاندى",
"UserPasswordChangedWithName": "ئابونت{0} ئۈچۈن پارول ئۆزگەرتىلدى",
"UserOfflineFromDevice": "{0} بىلەن {1} نىڭ ئالاقىسى ئۈزۈلدى",
"UserLockedOutWithName": "ئابونت {0} قۇلۇپلاندى",
"UserDownloadingItemWithValues": "{0} چۈشۈرۈۋاتىدۇ {1}",
"UserDeletedWithName": "{0} ئابونت ئۆچۈرۈلدى",
"UserCreatedWithName": "{0} ئابونت يېڭىدىن قوشۇلدى",
- "User": "ئابونت",
"Undefined": "بېكىتىلمىگەن",
"TvShows": "تىياتىرلار",
- "System": "سىستېما",
- "Sync": "ماس قەدەمدەش",
"SubtitleDownloadFailureFromForItem": "{0} دىن {0} نىڭ فىلىم خېتىنى چۈشۈرگىلى بولمىدى",
"StartupEmbyServerIsLoading": "Jellyfin مۇلازىمىتېرى يۈكلىنىۋاتىدۇ. سەل تۇرۇپ قايتا سىناڭ.",
- "Songs": "ناخشىلار",
"Shows": "پروگراممىلار",
- "ServerNameNeedsToBeRestarted": "{0} قايتا قوزغىتىلىشى كېرەك",
- "ScheduledTaskStartedWithName": "{0} باشلاندى",
"ScheduledTaskFailedWithName": "{0} مەغلۇپ بولدى",
- "ProviderValue": "تەمىنلىگۈچى: {0}",
"PluginUpdatedWithName": "{0} يېڭىلاندى",
"PluginUninstalledWithName": "{0} ئۆچۈرۈلدى",
"PluginInstalledWithName": "{0} قاچىلاندى",
- "Plugin": "قىستۇرما",
- "Playlists": "قويۇش تىزىملىكى",
"Photos": "رەسىملەر",
"NotificationOptionVideoPlaybackStopped": "سىن قويۇلۇش توختىدى",
"NotificationOptionVideoPlayback": "سىن قويۇلدى",
@@ -100,24 +81,14 @@
"Music": "مۇزىكا",
"Movies": "فىلىملەر",
"MixedContent": "ئارىلاشما مەزمۇن",
- "MessageNamedServerConfigurationUpdatedWithValue": "مۇلازىمىتېر تەڭشىكىنىڭ {0} قىسمى يېڭىلىنىپ بولدى",
- "MessageServerConfigurationUpdated": "مۇلازىمىتېر يېڭىلىنىپ بولدى",
- "MessageApplicationUpdated": "Jellyfin مۇلازىمىتېرى يېڭىلاندى",
- "MessageApplicationUpdatedTo": "Jellyfin مۇلازىمىتېر نەشرى {0} گە يېڭىلاندى",
"Latest": "ئەڭ يېڭى",
"LabelIpAddressValue": "{0}: IP ئادرىسى",
- "ItemRemovedWithName": "{0} ئامباردىن چىقىرىلدى",
- "ItemAddedWithName": "{0} ئامبارغا قوشۇلدى",
"Inherit": "داۋاملاشتۇرۇش",
"HomeVideos": "ئائىلە سىنلىرى",
"HeaderNextUp": "كېيىنكىسى",
- "HeaderFavoriteSongs": "ئەڭ ياقتۇرىدىغان ناخشىلار",
"HeaderFavoriteShows": "ئەڭ ياقتۇرىدىغان پروگراممىلار",
"HeaderFavoriteEpisodes": "ئەڭ ياقتۇرىدىغان تىياتېرلار",
- "HeaderFavoriteArtists": "ئەڭ ياقتۇرىدىغان سەنئەتكارلار",
- "HeaderFavoriteAlbums": "ياقتۇرىدىغان پىلاستىنكىلار",
"HeaderContinueWatching": "داۋاملىق كۆرۈش",
- "HeaderAlbumArtists": "پىلاستىنكا سەنئەتكارلىرى",
"Genres": "ئۇسلۇبلار",
"FailedLoginAttemptWithUserName": "{0} كىرىش ئوڭۇشلۇق بولمىدى",
"External": "سىرتقى"
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index 9246d9de20..ccb9d915d1 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -2,36 +2,22 @@
"MusicVideos": "Відеокліпи",
"Music": "Музика",
"Movies": "Фільми",
- "MessageApplicationUpdatedTo": "Jellyfin Server оновлено до версії {0}",
- "MessageApplicationUpdated": "Jellyfin Server оновлено",
"Latest": "Останні",
"LabelIpAddressValue": "IP-адреса: {0}",
- "ItemRemovedWithName": "{0} видалено з медіатеки",
- "ItemAddedWithName": "{0} додано до медіатеки",
"HeaderNextUp": "Наступний",
"HeaderLiveTV": "Ефірне ТБ",
- "HeaderFavoriteSongs": "Обрані пісні",
"HeaderFavoriteShows": "Обрані шоу",
"HeaderFavoriteEpisodes": "Обрані епізоди",
- "HeaderFavoriteArtists": "Обрані виконавці",
- "HeaderFavoriteAlbums": "Обрані альбоми",
"HeaderContinueWatching": "Продовжити перегляд",
- "HeaderAlbumArtists": "Виконавці альбому",
"Genres": "Жанри",
"Folders": "Теки",
"Favorites": "Обрані",
- "DeviceOnlineWithName": "Пристрій {0} підключився",
- "DeviceOfflineWithName": "Пристрій {0} відключився",
"Collections": "Колекції",
"ChapterNameValue": "Сцена {0}",
- "Channels": "Канали",
- "CameraImageUploadedFrom": "Нову фотографію завантажено з {0}",
"Books": "Книги",
"AuthenticationSucceededWithUserName": "{0} успішно авторизовано",
"Artists": "Виконавці",
- "Application": "Додаток",
"AppDeviceValues": "Додаток: {0}, Пристрій: {1}",
- "Albums": "Альбоми",
"NotificationOptionServerRestartRequired": "Необхідно перезапустити сервер",
"NotificationOptionPluginUpdateInstalled": "Встановлено оновлення плагіна",
"NotificationOptionPluginUninstalled": "Плагін видалено",
@@ -64,11 +50,8 @@
"TasksLibraryCategory": "Медіатека",
"TasksMaintenanceCategory": "Обслуговування",
"VersionNumber": "Версія {0}",
- "ValueSpecialEpisodeName": "Спецепізод - {0}",
- "ValueHasBeenAddedToLibrary": "{0} додано до медіатеки",
"UserStoppedPlayingItemWithValues": "{0} закінчив відтворення {1} на {2}",
"UserStartedPlayingItemWithValues": "{0} відтворює {1} на {2}",
- "UserPolicyUpdatedWithName": "Політика користувача оновлена для {0}",
"UserPasswordChangedWithName": "Пароль змінено для користувача {0}",
"UserOnlineFromDevice": "{0} підключився з {1}",
"UserOfflineFromDevice": "{0} відключився від {1}",
@@ -76,23 +59,14 @@
"UserDownloadingItemWithValues": "{0} завантажує {1}",
"UserDeletedWithName": "Користувача {0} видалено",
"UserCreatedWithName": "Користувача {0} створено",
- "User": "Користувач",
"TvShows": "ТВ-шоу",
- "System": "Система",
- "Sync": "Синхронізація",
"SubtitleDownloadFailureFromForItem": "Не вдалося завантажити субтитри з {0} для {1}",
"StartupEmbyServerIsLoading": "Jellyfin Server завантажується. Будь ласка, спробуйте трішки пізніше.",
- "Songs": "Пісні",
"Shows": "Серіали",
- "ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити",
- "ScheduledTaskStartedWithName": "{0} розпочато",
"ScheduledTaskFailedWithName": "{0} незавершено, збій",
- "ProviderValue": "Постачальник: {0}",
"PluginUpdatedWithName": "{0} оновлено",
"PluginUninstalledWithName": "{0} видалено",
"PluginInstalledWithName": "{0} встановлено",
- "Plugin": "Плагін",
- "Playlists": "Плейлисти",
"Photos": "Фотографії",
"NotificationOptionVideoPlaybackStopped": "Відтворення відео зупинено",
"NotificationOptionVideoPlayback": "Розпочато відтворення відео",
@@ -109,10 +83,7 @@
"NameSeasonNumber": "Сезон {0}",
"NameInstallFailed": "Не вдалося встановити {0}",
"MixedContent": "Змішаний контент",
- "MessageServerConfigurationUpdated": "Конфігурація сервера оновлена",
- "MessageNamedServerConfigurationUpdatedWithValue": "Розділ конфігурації сервера {0} оновлено",
"Inherit": "Успадкувати",
- "HeaderRecordingGroups": "Групи запису",
"Forced": "Форсовані",
"TaskCleanActivityLogDescription": "Видаляє старші за встановлений термін записи з журналу активності.",
"TaskCleanActivityLog": "Очистити журнал активності",
@@ -135,5 +106,7 @@
"TaskMoveTrickplayImages": "Змінити місце розташування прев'ю-зображень",
"TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment.",
"CleanupUserDataTask": "Завдання очищення даних користувача",
- "CleanupUserDataTaskDescription": "Очищає всі дані користувача (стан перегляду, статус обраного тощо) з медіа, які перестали бути доступними щонайменше 90 днів тому."
+ "CleanupUserDataTaskDescription": "Очищає всі дані користувача (стан перегляду, статус обраного тощо) з медіа, які перестали бути доступними щонайменше 90 днів тому.",
+ "Original": "Оригінал",
+ "LyricDownloadFailureFromForItem": "Не вдалося завантажити текст пісні з {0} для {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ur.json b/Emby.Server.Implementations/Localization/Core/ur.json
index 94d9c8541e..07d309c270 100644
--- a/Emby.Server.Implementations/Localization/Core/ur.json
+++ b/Emby.Server.Implementations/Localization/Core/ur.json
@@ -1,16 +1,10 @@
{
"Books": "کتابیں",
"AppDeviceValues": "ایپ: {0}، ڈیوائس: {1}",
- "Albums": "البمز",
- "Application": "ایپلی کیشن",
"Artists": "فنکار",
"AuthenticationSucceededWithUserName": "{0} کی کامیابی سے تصدیق ہو چکی ہے",
- "CameraImageUploadedFrom": "ایک نئی کیمرے کی تصویر {0} سے اپ لوڈ کی گئی ہے",
- "Channels": "چینلز",
"ChapterNameValue": "باب {0}",
"Collections": "مجموعے",
"Default": "ڈیفالٹ",
- "DeviceOfflineWithName": "{0} نے رابطہ منقطع کر دیا ہے",
- "DeviceOnlineWithName": "{0} منسلک ہے",
"External": "بیرونی"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ur_PK.json b/Emby.Server.Implementations/Localization/Core/ur_PK.json
index f6539adff3..b3f24a31e3 100644
--- a/Emby.Server.Implementations/Localization/Core/ur_PK.json
+++ b/Emby.Server.Implementations/Localization/Core/ur_PK.json
@@ -1,27 +1,17 @@
{
- "HeaderFavoriteAlbums": "پسندیدہ البمز",
"HeaderNextUp": "اگلا",
- "HeaderFavoriteArtists": "پسندیدہ فنکار",
- "HeaderAlbumArtists": "البم کے فنکار",
"Movies": "فلمیں",
"HeaderFavoriteEpisodes": "پسندیدہ اقساط",
"Collections": "مجموعے",
"Folders": "فولڈرز",
"HeaderLiveTV": "براہ راست ٹی وی",
- "Channels": "چینلز",
"HeaderContinueWatching": "دیکھنا جاری رکھیں",
- "Playlists": "پلے لسٹس",
- "ValueSpecialEpisodeName": "خصوصی - {0}",
"Shows": "دکھاتا ہے",
"Genres": "انواع",
"Artists": "فنکار",
- "Sync": "مطابقت پذیری",
"Photos": "تصاویر",
- "Albums": "البمز",
"Favorites": "پسندیدہ",
- "Songs": "گانے",
"Books": "کتابیں",
- "HeaderFavoriteSongs": "پسندیدہ گانے",
"HeaderFavoriteShows": "پسندیدہ شوز",
"TaskDownloadMissingSubtitlesDescription": "میٹا ڈیٹا کی تشکیل پر مبنی ذیلی عنوانات کے غائب عنوانات انٹرنیٹ پے تلاش کرتا ہے۔",
"TaskDownloadMissingSubtitles": "غائب سب ٹائٹلز ڈاؤن لوڈ کریں",
@@ -46,10 +36,8 @@
"TasksLibraryCategory": "لآیبریری",
"TasksMaintenanceCategory": "مرمت",
"VersionNumber": "ورژن {0}",
- "ValueHasBeenAddedToLibrary": "{0} آپ کی میڈیا لائبریری میں شامل کر دیا گیا ہے",
"UserStoppedPlayingItemWithValues": "{0} نے {1} چلانا ختم کر دیا ھے {2} پے",
"UserStartedPlayingItemWithValues": "{0} چلا رہا ہے {1} {2} پے",
- "UserPolicyUpdatedWithName": "صارف {0} کی پالیسی کیلئے تازہ کاری کی گئی ہے",
"UserPasswordChangedWithName": "صارف {0} کے لئے پاس ورڈ تبدیل کر دیا گیا ہے",
"UserOnlineFromDevice": "{0} آن لائن ہے {1} سے",
"UserOfflineFromDevice": "{0} سے منقطع ہوگیا ہے {1}",
@@ -57,19 +45,13 @@
"UserDownloadingItemWithValues": "{0} ڈاؤن لوڈ کر رھا ھے {1}",
"UserDeletedWithName": "صارف {0} کو ہٹا دیا گیا ہے",
"UserCreatedWithName": "صارف {0} تشکیل دیا گیا ہے",
- "User": "صارف",
"TvShows": "ٹی وی کے پروگرام",
- "System": "نظام",
"SubtitleDownloadFailureFromForItem": "ذیلی عنوانات {0} سے ڈاؤن لوڈ کرنے میں ناکام {1} کے لیے",
"StartupEmbyServerIsLoading": "جیلیفن سرور لوڈ ہورہا ہے۔ براہ کرم جلد ہی دوبارہ کوشش کریں۔",
- "ServerNameNeedsToBeRestarted": "{0} دوبارہ چلانے کرنے کی ضرورت ہے",
- "ScheduledTaskStartedWithName": "{0} شروع",
"ScheduledTaskFailedWithName": "{0} ناکام",
- "ProviderValue": "فراہم کرنے والا: {0}",
"PluginUpdatedWithName": "{0} تازہ کاری کی گئی تھی",
"PluginUninstalledWithName": "[0} ہٹا دیا گیا تھا",
"PluginInstalledWithName": "{0} انسٹال کیا گیا تھا",
- "Plugin": "پلگن",
"NotificationOptionVideoPlaybackStopped": "ویڈیو پلے بیک رک گیا",
"NotificationOptionVideoPlayback": "ویڈیو پلے بیک شروع ہوا",
"NotificationOptionUserLockedOut": "صارف کو لاک آؤٹ کیا گیا",
@@ -93,25 +75,14 @@
"MusicVideos": "میوزک ویڈیوز",
"Music": "موسیقی",
"MixedContent": "مخلوط مواد",
- "MessageServerConfigurationUpdated": "سرور کو اپ ڈیٹ کر دیا گیا ہے",
- "MessageNamedServerConfigurationUpdatedWithValue": "سرور ضمن {0} کو ترتیب دے دیا گیا ھے",
- "MessageApplicationUpdatedTo": "جیلیفن سرور کو اپ ڈیٹ کیا ہے {0}",
- "MessageApplicationUpdated": "جیلیفن سرور کو اپ ڈیٹ کر دیا گیا ہے",
"Latest": "تازہ ترین",
"LabelRunningTimeValue": "چلانے کی مدت",
"LabelIpAddressValue": "آئ پی ایڈریس {0}",
- "ItemRemovedWithName": "لائبریری سے ہٹا دیا گیا ھے",
- "ItemAddedWithName": "[0} لائبریری میں شامل کیا گیا ھے",
"Inherit": "وراثت",
"HomeVideos": "ہوم ویڈیوز",
- "HeaderRecordingGroups": "ریکارڈنگ گروپس",
"FailedLoginAttemptWithUserName": "{0} سے لاگ ان کی ناکام کوشش",
- "DeviceOnlineWithName": "{0} متصل ھو چکا ھے",
- "DeviceOfflineWithName": "{0} منقطع ھو چکا ھے",
"ChapterNameValue": "باب",
"AuthenticationSucceededWithUserName": "{0} کامیابی کے ساتھ تصدیق ھوچکی ھے",
- "CameraImageUploadedFrom": "ایک نئی کیمرہ تصویر اپ لوڈ کی گئی ہے {0}",
- "Application": "پروگرام",
"AppDeviceValues": "پروگرام:{0}, ڈیوائس:{1}",
"Forced": "جَبری",
"Undefined": "غير وضاحتى",
diff --git a/Emby.Server.Implementations/Localization/Core/uz.json b/Emby.Server.Implementations/Localization/Core/uz.json
index e44b3f5167..3215733c1a 100644
--- a/Emby.Server.Implementations/Localization/Core/uz.json
+++ b/Emby.Server.Implementations/Localization/Core/uz.json
@@ -1,47 +1,32 @@
{
"HeaderContinueWatching": "Ko‘rishda davom etish",
- "HeaderAlbumArtists": "Albom ijrochilari",
"Genres": "Janrlar",
"Folders": "Jildlar",
"Favorites": "Sevimlilar",
"Collections": "To'plamlar",
- "Channels": "Kanallar",
"Books": "Kitoblar",
"Artists": "Ijrochilar",
- "Albums": "Albomlar",
"AuthenticationSucceededWithUserName": "{0} muvaffaqiyatli tasdiqlandi",
"AppDeviceValues": "Ilova: {0}, Qurilma: {1}",
- "Application": "Ilova",
- "CameraImageUploadedFrom": "{0}dan yangi kamera rasmi yuklandi",
- "DeviceOnlineWithName": "{0} ulangan",
- "ItemRemovedWithName": "{0} kutbxonadan o'chirildi",
"External": "Tashqi",
"FailedLoginAttemptWithUserName": "Muvafaqiyatsiz kirishlar soni {0}",
"Forced": "Majburiy",
"ChapterNameValue": "{0}chi bo'lim",
- "DeviceOfflineWithName": "{0} aloqa uzildi",
"HeaderLiveTV": "Jonli TV",
"HeaderNextUp": "Keyingisi",
- "ItemAddedWithName": "{0} kutbxonaga qo'shildi",
"LabelIpAddressValue": "IP manzil: {0}",
"SubtitleDownloadFailureFromForItem": "{0} dan {1} uchun taglavhalarni yuklab boʻlmadi",
"UserPasswordChangedWithName": "Foydalanuvchi {0} paroli oʻzgartirildi",
- "ValueHasBeenAddedToLibrary": "{0} kutubxonaga qoʻshildi",
"TaskCleanActivityLogDescription": "Belgilangan yoshdan kattaroq faoliyat jurnali yozuvlarini oʻchiradi.",
"TaskAudioNormalization": "Ovozni normallashtirish",
"TaskRefreshLibraryDescription": "Media kutubxonasi yangi fayllar uchun skanerlanmoqda va metama'lumotlar yangilanmoqda.",
"Default": "Joriy",
- "HeaderFavoriteAlbums": "Tanlangan albomlar",
- "HeaderFavoriteArtists": "Tanlangan artistlar",
"HeaderFavoriteEpisodes": "Tanlangan epizodlar",
"HeaderFavoriteShows": "Tanlangan shoular",
- "HeaderFavoriteSongs": "Tanlangan qo'shiqlar",
- "HeaderRecordingGroups": "Yozuvlar guruhi",
"HomeVideos": "Uy videolari",
"NotificationOptionVideoPlaybackStopped": "Video ijrosi toʻxtatildi",
"TvShows": "TV seriallar",
"Undefined": "Belgilanmagan",
- "User": "Foydalanuvchi",
"UserCreatedWithName": "{0} foydalanuvchi yaratildi",
"TaskCleanCacheDescription": "Tizimga kerak bo'lmagan kesh fayllari o'chiriladi.",
"TaskAudioNormalizationDescription": "Ovozni normallashtirish ma'lumotlari uchun fayllarni skanerlaydi.",
@@ -67,10 +52,6 @@
"NotificationOptionVideoPlayback": "Video ijrosi boshlandi",
"Photos": "Surat",
"Latest": "So'ngi",
- "MessageApplicationUpdated": "Jellyfin Server yangilandi",
- "MessageApplicationUpdatedTo": "Jellyfin Server {0} gacha yangilandi",
- "MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguratsiyasi ({0}-boʻlim) yangilandi",
- "MessageServerConfigurationUpdated": "Server konfiguratsiyasi yangilandi",
"MixedContent": "Aralashgan tarkib",
"Movies": "Kinolar",
"Music": "Qo'shiqlar",
@@ -78,29 +59,19 @@
"NameInstallFailed": "Omadsiz ornatish {0}",
"NameSeasonNumber": "{0} Fasl",
"NameSeasonUnknown": "Fasl aniqlanmagan",
- "Playlists": "Pleylistlar",
"NewVersionIsAvailable": "Yuklab olish uchun Jellyfin Server ning yangi versiyasi mavjud",
- "Plugin": "Plagin",
"TaskCleanLogs": "Jurnallar katalogini tozalash",
"PluginUpdatedWithName": "{0} - yangilandi",
- "ProviderValue": "Yetkazib beruvchi: {0}",
"ScheduledTaskFailedWithName": "{0} - omadsiz",
- "ScheduledTaskStartedWithName": "{0} - ishga tushirildi",
- "ServerNameNeedsToBeRestarted": "Qayta yuklash kerak {0}",
"Shows": "Teleko'rsatuv",
- "Songs": "Kompozitsiyalar",
"StartupEmbyServerIsLoading": "Jellyfin Server yuklanmoqda. Tez orada qayta urinib koʻring.",
- "Sync": "Sinxronizatsiya",
- "System": "Tizim",
"UserDeletedWithName": "{0} foydalanuvchisi oʻchirib tashlandi",
"UserDownloadingItemWithValues": "{0} yuklanmoqda {1}",
"UserLockedOutWithName": "{0} foydalanuvchisi bloklandi",
"UserOfflineFromDevice": "{0} {1}dan uzildi",
"UserOnlineFromDevice": "{0} {1} dan ulandi",
- "UserPolicyUpdatedWithName": "{0} foydalanuvchisining siyosatlari yangilandi",
"UserStartedPlayingItemWithValues": "{0} - {2} da \"{1}\" ijrosi",
"UserStoppedPlayingItemWithValues": "{0} - ijro etish to‘xtatildi {1} {2}",
- "ValueSpecialEpisodeName": "Maxsus qism – {0}",
"VersionNumber": "Versiya {0}",
"TasksMaintenanceCategory": "Xizmat ko'rsatish",
"TasksLibraryCategory": "Media kutubxona",
@@ -111,5 +82,7 @@
"TaskRefreshChapterImages": "Sahnadan tasvirini chiqarish",
"TaskRefreshChapterImagesDescription": "Sahnalarni o'z ichiga olgan videolar uchun eskizlarni yaratadi.",
"TaskRefreshLibrary": "Media kutubxonangizni skanerlash",
- "TaskCleanLogsDescription": "{0} kundan eski log fayllarni o'chiradi."
+ "TaskCleanLogsDescription": "{0} kundan eski log fayllarni o'chiradi.",
+ "Original": "Original",
+ "LyricDownloadFailureFromForItem": "{0} dan {1} gacha qo'shiq matninin yuklab olishda xatolik ketdi"
}
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index 947a2c80de..2ba665e2ff 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -3,17 +3,11 @@
"Favorites": "Yêu Thích",
"Folders": "Thư Mục",
"Genres": "Thể Loại",
- "HeaderAlbumArtists": "Album nghệ sĩ",
"HeaderContinueWatching": "Xem Tiếp",
"HeaderLiveTV": "TV Trực Tiếp",
"Movies": "Phim",
"Photos": "Ảnh",
- "Playlists": "Danh sách phát",
"Shows": "Chương Trình TV",
- "Songs": "Bài Hát",
- "Sync": "Đồng Bộ",
- "ValueSpecialEpisodeName": "Đặc Biệt - {0}",
- "Albums": "Album",
"Artists": "Ca Sĩ",
"TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.",
"TaskDownloadMissingSubtitles": "Tải Xuống Phụ Đề Bị Thiếu",
@@ -38,10 +32,8 @@
"TasksLibraryCategory": "Thư Viện",
"TasksMaintenanceCategory": "Bảo Trì",
"VersionNumber": "Phiên Bản {0}",
- "ValueHasBeenAddedToLibrary": "{0} đã được thêm vào thư viện của bạn",
"UserStoppedPlayingItemWithValues": "{0} đã kết thúc phát {1} trên {2}",
"UserStartedPlayingItemWithValues": "{0} đang phát {1} trên {2}",
- "UserPolicyUpdatedWithName": "Chính sách người dùng đã được cập nhật cho {0}",
"UserPasswordChangedWithName": "Mật khẩu đã được thay đổi cho người dùng {0}",
"UserOnlineFromDevice": "{0} trực tuyến từ {1}",
"UserOfflineFromDevice": "{0} đã ngắt kết nối từ {1}",
@@ -49,19 +41,13 @@
"UserDownloadingItemWithValues": "{0} đang tải xuống {1}",
"UserDeletedWithName": "Người Dùng {0} đã được xóa",
"UserCreatedWithName": "Người Dùng {0} đã được tạo",
- "User": "Người Dùng",
"TvShows": "Chương Trình TV",
- "System": "Hệ Thống",
"SubtitleDownloadFailureFromForItem": "Không thể tải xuống phụ đề từ {0} cho {1}",
"StartupEmbyServerIsLoading": "Jellyfin Server đang tải. Vui lòng thử lại trong thời gian ngắn.",
- "ServerNameNeedsToBeRestarted": "{0} cần được khởi động lại",
- "ScheduledTaskStartedWithName": "{0} đã bắt đầu",
"ScheduledTaskFailedWithName": "{0} đã thất bại",
- "ProviderValue": "Provider: {0}",
"PluginUpdatedWithName": "{0} đã cập nhật",
"PluginUninstalledWithName": "{0} đã được gỡ bỏ",
"PluginInstalledWithName": "{0} đã được cài đặt",
- "Plugin": "Plugin",
"NotificationOptionVideoPlaybackStopped": "Đã dừng phát lại video",
"NotificationOptionVideoPlayback": "Đã bắt đầu phát lại video",
"NotificationOptionUserLockedOut": "Người dùng bị khóa",
@@ -85,33 +71,18 @@
"MusicVideos": "Videos Nhạc",
"Music": "Nhạc",
"MixedContent": "Nội dung hỗn hợp",
- "MessageServerConfigurationUpdated": "Cấu hình máy chủ đã được cập nhật",
- "MessageNamedServerConfigurationUpdatedWithValue": "Phần cấu hình máy chủ {0} đã được cập nhật",
- "MessageApplicationUpdatedTo": "Jellyfin Server đã được cập nhật lên {0}",
- "MessageApplicationUpdated": "Jellyfin Server đã được cập nhật",
"Latest": "Gần Nhất",
"LabelRunningTimeValue": "Thời Gian Chạy: {0}",
"LabelIpAddressValue": "Địa chỉ IP: {0}",
- "ItemRemovedWithName": "{0} đã xóa khỏi thư viện",
- "ItemAddedWithName": "{0} được thêm vào thư viện",
"Inherit": "Thừa hưởng",
"HomeVideos": "Video Nhà",
- "HeaderRecordingGroups": "Nhóm Ghi Video",
"HeaderNextUp": "Tiếp Theo",
- "HeaderFavoriteSongs": "Bài Hát Yêu Thích",
"HeaderFavoriteShows": "Chương Trình Yêu Thích",
"HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
- "HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
- "HeaderFavoriteAlbums": "Album Ưa Thích",
"FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập không thành công từ {0}",
- "DeviceOnlineWithName": "{0} đã kết nối",
- "DeviceOfflineWithName": "{0} đã ngắt kết nối",
"ChapterNameValue": "Phân Cảnh {0}",
- "Channels": "Kênh",
- "CameraImageUploadedFrom": "Một hình ảnh máy ảnh mới đã được tải lên từ {0}",
"Books": "Sách",
"AuthenticationSucceededWithUserName": "{0} xác thực thành công",
- "Application": "Ứng Dụng",
"AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}",
"TaskCleanActivityLogDescription": "Xóa các mục nhật ký hoạt động cũ hơn độ tuổi đã cài đặt.",
"TaskCleanActivityLog": "Xóa Nhật Ký Hoạt Động",
@@ -135,5 +106,7 @@
"TaskMoveTrickplayImagesDescription": "Di chuyển các tập tin trickplay hiện có theo cài đặt thư viện.",
"TaskExtractMediaSegments": "Quét Phân Đoạn Phương Tiện",
"CleanupUserDataTask": "Tác vụ dọn dẹp dữ liệu người dùng",
- "CleanupUserDataTaskDescription": "Làm sạch tất cả dữ liệu người dùng (trạng thái xem, trạng thái yêu thích, v.v.) từ phương tiện không còn có mặt trong ít nhất 90 ngày."
+ "CleanupUserDataTaskDescription": "Làm sạch tất cả dữ liệu người dùng (trạng thái xem, trạng thái yêu thích, v.v.) từ phương tiện không còn có mặt trong ít nhất 90 ngày.",
+ "Original": "Gốc",
+ "LyricDownloadFailureFromForItem": "Lời bài hát không tải xuống được từ {0} cho {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index 6a7b7fb4e6..18418ae0bc 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -1,41 +1,24 @@
{
- "Albums": "专辑",
"AppDeviceValues": "应用:{0},设备:{1}",
- "Application": "应用程序",
"Artists": "艺术家",
"AuthenticationSucceededWithUserName": "{0} 认证成功",
"Books": "书籍",
- "CameraImageUploadedFrom": "已从 {0} 上传新的相机照片",
- "Channels": "频道",
"ChapterNameValue": "章节 {0}",
"Collections": "合集",
- "DeviceOfflineWithName": "{0} 已断开连接",
- "DeviceOnlineWithName": "{0} 已连接",
"FailedLoginAttemptWithUserName": "来自 {0} 的登录失败",
"Favorites": "收藏夹",
"Folders": "文件夹",
"Genres": "类型",
- "HeaderAlbumArtists": "专辑艺术家",
"HeaderContinueWatching": "继续观看",
- "HeaderFavoriteAlbums": "收藏的专辑",
- "HeaderFavoriteArtists": "收藏的艺术家",
"HeaderFavoriteEpisodes": "收藏的剧集",
"HeaderFavoriteShows": "收藏的节目",
- "HeaderFavoriteSongs": "收藏的歌曲",
"HeaderLiveTV": "电视直播",
"HeaderNextUp": "接下来播放",
- "HeaderRecordingGroups": "录制组",
"HomeVideos": "家庭视频",
"Inherit": "继承",
- "ItemAddedWithName": "{0} 已添加到媒体库",
- "ItemRemovedWithName": "{0} 已从媒体库移除",
"LabelIpAddressValue": "IP 地址:{0}",
"LabelRunningTimeValue": "运行时间:{0}",
"Latest": "最新",
- "MessageApplicationUpdated": "Jellyfin 服务器已更新",
- "MessageApplicationUpdatedTo": "Jellyfin 服务器版本已更新到 {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "服务器配置 {0} 部分已更新",
- "MessageServerConfigurationUpdated": "服务器配置已更新",
"MixedContent": "混合内容",
"Movies": "电影",
"Music": "音乐",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "视频已开始播放",
"NotificationOptionVideoPlaybackStopped": "视频播放已停止",
"Photos": "照片",
- "Playlists": "播放列表",
- "Plugin": "插件",
"PluginInstalledWithName": "{0} 已安装",
"PluginUninstalledWithName": "{0} 已卸载",
"PluginUpdatedWithName": "{0} 已更新",
- "ProviderValue": "提供商:{0}",
"ScheduledTaskFailedWithName": "{0} 已失败",
- "ScheduledTaskStartedWithName": "{0} 已开始",
- "ServerNameNeedsToBeRestarted": "{0} 需要重新启动",
"Shows": "节目",
- "Songs": "歌曲",
"StartupEmbyServerIsLoading": "Jellyfin 服务器正在启动,请稍后再试。",
"SubtitleDownloadFailureFromForItem": "无法从 {0} 下载 {1} 的字幕",
- "Sync": "同步",
- "System": "系统",
"TvShows": "电视剧",
- "User": "用户",
"UserCreatedWithName": "已创建用户 {0}",
"UserDeletedWithName": "已删除用户 {0}",
"UserDownloadingItemWithValues": "{0} 正在下载 {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} 已从 {1} 断开",
"UserOnlineFromDevice": "{0} 已在 {1} 上线",
"UserPasswordChangedWithName": "用户 {0} 的密码已更改",
- "UserPolicyUpdatedWithName": "用户协议已更新为 {0}",
"UserStartedPlayingItemWithValues": "{0} 在 {2} 上开始播放 {1}",
"UserStoppedPlayingItemWithValues": "{0} 在 {2} 上停止播放 {1}",
- "ValueHasBeenAddedToLibrary": "{0} 已添加至您的媒体库中",
- "ValueSpecialEpisodeName": "特典 - {0}",
"VersionNumber": "版本 {0}",
"TaskUpdatePluginsDescription": "为已设置为自动更新的插件下载和安装更新。",
"TaskRefreshPeople": "刷新演职人员",
@@ -135,5 +106,7 @@
"TaskExtractMediaSegmentsDescription": "从支持 MediaSegment 的插件中提取或获取媒体分段。",
"TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的进度条预览图文件。",
"CleanupUserDataTask": "用户数据清理任务",
- "CleanupUserDataTaskDescription": "清理已被删除超过90天的媒体中的所有用户数据(观看状态、收藏夹状态等)。"
+ "CleanupUserDataTaskDescription": "清理已被删除超过90天的媒体中的所有用户数据(观看状态、收藏夹状态等)。",
+ "LyricDownloadFailureFromForItem": "无法从 {0} 下载 {1} 的歌词",
+ "Original": "原始"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index f3ad8be2a8..8b9665cf9a 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -1,41 +1,24 @@
{
- "Albums": "專輯",
"AppDeviceValues": "程式:{0},裝置:{1}",
- "Application": "應用程式",
"Artists": "藝人",
"AuthenticationSucceededWithUserName": "{0} 成功通過驗證",
"Books": "書籍",
- "CameraImageUploadedFrom": "{0} 已經成功上載咗一張新相",
- "Channels": "頻道",
"ChapterNameValue": "第 {0} 章",
"Collections": "系列",
- "DeviceOfflineWithName": "{0} 斷開咗連線",
- "DeviceOnlineWithName": "{0} 連線咗",
"FailedLoginAttemptWithUserName": "來自 {0} 嘅登入嘗試失敗咗",
"Favorites": "心水",
"Folders": "資料夾",
"Genres": "風格",
- "HeaderAlbumArtists": "專輯歌手",
"HeaderContinueWatching": "繼續睇返",
- "HeaderFavoriteAlbums": "心水嘅專輯",
- "HeaderFavoriteArtists": "心水嘅藝人",
"HeaderFavoriteEpisodes": "心水嘅劇集",
"HeaderFavoriteShows": "心水嘅節目",
- "HeaderFavoriteSongs": "心水嘅歌曲",
"HeaderLiveTV": "電視直播",
"HeaderNextUp": "跟住落嚟",
- "HeaderRecordingGroups": "錄製組",
"HomeVideos": "家庭影片",
"Inherit": "繼承",
- "ItemAddedWithName": "{0} 經已加咗入媒體櫃",
- "ItemRemovedWithName": "{0} 經已由媒體櫃移除咗",
"LabelIpAddressValue": "IP 位址:{0}",
"LabelRunningTimeValue": "運行時間:{0}",
"Latest": "最新",
- "MessageApplicationUpdated": "Jellyfin 經已更新咗",
- "MessageApplicationUpdatedTo": "Jellyfin 已經更新到 {0} 版本",
- "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定「{0}」經已更新咗",
- "MessageServerConfigurationUpdated": "伺服器設定經已更新咗",
"MixedContent": "混合內容",
"Movies": "電影",
"Music": "音樂",
@@ -61,23 +44,14 @@
"NotificationOptionVideoPlayback": "開始播放影片",
"NotificationOptionVideoPlaybackStopped": "停咗播放影片",
"Photos": "相片",
- "Playlists": "播放清單",
- "Plugin": "外掛程式",
"PluginInstalledWithName": "裝好咗 {0}",
"PluginUninstalledWithName": "剷走咗 {0}",
"PluginUpdatedWithName": "更新好咗 {0}",
- "ProviderValue": "提供者:{0}",
"ScheduledTaskFailedWithName": "{0} 執行失敗",
- "ScheduledTaskStartedWithName": "開始執行 {0}",
- "ServerNameNeedsToBeRestarted": "{0} 需要重新啟動",
"Shows": "節目",
- "Songs": "歌曲",
"StartupEmbyServerIsLoading": "Jellyfin 伺服器載入緊,唔該稍後再試。",
"SubtitleDownloadFailureFromForItem": "經 {0} 下載 {1} 嘅字幕失敗咗",
- "Sync": "同步",
- "System": "系統",
"TvShows": "電視節目",
- "User": "使用者",
"UserCreatedWithName": "經已建立咗新使用者 {0}",
"UserDeletedWithName": "使用者 {0} 經已被刪走",
"UserDownloadingItemWithValues": "{0} 下載緊 {1}",
@@ -85,11 +59,8 @@
"UserOfflineFromDevice": "{0} 經已由 {1} 斷開咗連線",
"UserOnlineFromDevice": "{0} 正喺 {1} 連線",
"UserPasswordChangedWithName": "使用者 {0} 嘅密碼經已更改咗",
- "UserPolicyUpdatedWithName": "使用者 {0} 嘅權限經已更新咗",
"UserStartedPlayingItemWithValues": "{0} 正喺 {2} 播緊 {1}",
"UserStoppedPlayingItemWithValues": "{0} 已經喺 {2} 停止播放 {1}",
- "ValueHasBeenAddedToLibrary": "{0} 已經成功加入咗你嘅媒體櫃",
- "ValueSpecialEpisodeName": "特輯 - {0}",
"VersionNumber": "版本 {0}",
"TaskDownloadMissingSubtitles": "下載漏咗嘅字幕",
"TaskUpdatePlugins": "更新外掛程式",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index 1caf887094..5dace3b0b7 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -1,39 +1,23 @@
{
- "Albums": "專輯",
"AppDeviceValues": "應用程式:{0},裝置:{1}",
- "Application": "應用程式",
"Artists": "藝人",
"AuthenticationSucceededWithUserName": "成功授權 {0}",
"Books": "書籍",
- "CameraImageUploadedFrom": "已從 {0} 成功上傳一張照片",
- "Channels": "頻道",
"ChapterNameValue": "章節 {0}",
"Collections": "系列作",
- "DeviceOfflineWithName": "{0} 已中斷連接",
- "DeviceOnlineWithName": "{0} 已連接",
"FailedLoginAttemptWithUserName": "來自 {0} 的登入失敗嘗試",
"Favorites": "我的最愛",
"Folders": "資料夾",
"Genres": "風格",
- "HeaderAlbumArtists": "專輯演出者",
"HeaderContinueWatching": "繼續觀看",
- "HeaderFavoriteAlbums": "最愛專輯",
- "HeaderFavoriteArtists": "最愛的藝人",
"HeaderFavoriteEpisodes": "最愛的劇集",
"HeaderFavoriteShows": "最愛的節目",
- "HeaderFavoriteSongs": "最愛的歌曲",
"HeaderLiveTV": "電視直播",
"HeaderNextUp": "接下來",
"HomeVideos": "家庭影片",
- "ItemAddedWithName": "{0} 已新增至媒體庫",
- "ItemRemovedWithName": "{0} 已從媒體庫移除",
"LabelIpAddressValue": "IP 位址:{0}",
"LabelRunningTimeValue": "運行時間:{0}",
"Latest": "最新",
- "MessageApplicationUpdated": "Jellyfin 伺服器已經更新",
- "MessageApplicationUpdatedTo": "Jellyfin 伺服器已經更新至 {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 部分已經更新",
- "MessageServerConfigurationUpdated": "伺服器設定已經更新",
"MixedContent": "混合內容",
"Movies": "電影",
"Music": "音樂",
@@ -59,22 +43,13 @@
"NotificationOptionVideoPlayback": "影片播放已開始",
"NotificationOptionVideoPlaybackStopped": "影片播放已停止",
"Photos": "相片",
- "Playlists": "播放清單",
- "Plugin": "擴充功能",
"PluginInstalledWithName": "已安裝 {0}",
"PluginUninstalledWithName": "已移除 {0}",
"PluginUpdatedWithName": "已更新 {0}",
- "ProviderValue": "提供者:{0}",
"ScheduledTaskFailedWithName": "排程任務 {0} 執行失敗",
- "ScheduledTaskStartedWithName": "排程任務 {0} 已開始",
- "ServerNameNeedsToBeRestarted": "伺服器 {0} 需要重新啟動",
"Shows": "節目",
- "Songs": "歌曲",
"StartupEmbyServerIsLoading": "Jellyfin 伺服器載入中,請稍後再試。",
- "Sync": "同步",
- "System": "系統",
"TvShows": "電視節目",
- "User": "使用者",
"UserCreatedWithName": "已建立使用者 {0}",
"UserDeletedWithName": "已刪除使用者 {0}",
"UserDownloadingItemWithValues": "使用者 {0} 正在下載 {1}",
@@ -82,13 +57,9 @@
"UserOfflineFromDevice": "使用者 {0} 已從 {1} 斷線",
"UserOnlineFromDevice": "使用者 {0} 已從 {1} 連線",
"UserPasswordChangedWithName": "使用者 {0} 的密碼已變更",
- "UserPolicyUpdatedWithName": "使用者權限已更新為 {0}",
"UserStartedPlayingItemWithValues": "{0} 正在 {2} 上播放 {1}",
"UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}",
- "ValueHasBeenAddedToLibrary": "{0} 已新增至您的媒體庫",
- "ValueSpecialEpisodeName": "特輯 - {0}",
"VersionNumber": "版本 {0}",
- "HeaderRecordingGroups": "錄製組",
"Inherit": "繼承",
"SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕",
"TaskDownloadMissingSubtitlesDescription": "透過媒體資訊從網路上搜尋遺失的字幕。",
diff --git a/Emby.Server.Implementations/Localization/Core/zu.json b/Emby.Server.Implementations/Localization/Core/zu.json
index aa056d4498..669ea0372d 100644
--- a/Emby.Server.Implementations/Localization/Core/zu.json
+++ b/Emby.Server.Implementations/Localization/Core/zu.json
@@ -2,37 +2,24 @@
"TasksApplicationCategory": "Ukusetshenziswa",
"TasksLibraryCategory": "Umtapo",
"TasksMaintenanceCategory": "Ukunakekela",
- "User": "Umsebenzisi",
"Undefined": "Akuchaziwe",
- "System": "Isistimu",
- "Sync": "Vumelanisa",
- "Songs": "Amaculo",
"Shows": "Izinhlelo",
- "Plugin": "Isijobelelo",
- "Playlists": "Izinhla Zokudlalayo",
"Photos": "Izithombe",
"Music": "Umculo",
"Movies": "Amamuvi",
"Latest": "lwakamuva",
"Inherit": "Ngefa",
"Forced": "Kuphoqiwe",
- "Application": "Ukusetshenziswa",
"Genres": "Izinhlobo",
"Folders": "Izikhwama",
"Favorites": "Izintandokazi",
"Default": "Okumisiwe",
"Collections": "Amaqoqo",
- "Channels": "Amashaneli",
"Books": "Izincwadi",
"Artists": "Abadlali",
- "Albums": "Ama-albhamu",
- "CameraImageUploadedFrom": "Kulandelayo lwesithonjana sekhamera selithunyelwe kusuka ku {0}",
- "HeaderFavoriteArtists": "Abasethi Abathandekayo",
"HeaderFavoriteEpisodes": "Izilimi Ezithandekayo",
"HeaderFavoriteShows": "Izisho Ezithandekayo",
"External": "Kwezifungo",
"FailedLoginAttemptWithUserName": "Ukushayiswa kwesithombe sokungena okungekho {0}",
- "HeaderContinueWatching": "Buyela Ukubona",
- "HeaderFavoriteAlbums": "Izimpahla Ezithandwayo",
- "HeaderAlbumArtists": "Abasethi wenkulumo"
+ "HeaderContinueWatching": "Buyela Ukubona"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index d8797e612b..843e35afcc 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -26,6 +27,7 @@ namespace Emby.Server.Implementations.Localization
private const string RatingsPath = "Emby.Server.Implementations.Localization.Ratings.";
private const string CulturesPath = "Emby.Server.Implementations.Localization.iso6392.txt";
private const string CountriesPath = "Emby.Server.Implementations.Localization.countries.json";
+ private const string CoreResourcePrefix = "Emby.Server.Implementations.Localization.Core.";
private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly;
private static readonly string[] _unratedValues = ["n/a", "unrated", "not rated", "nr"];
@@ -34,13 +36,21 @@ namespace Emby.Server.Implementations.Localization
private readonly Dictionary<string, Dictionary<string, ParentalRatingScore?>> _allParentalRatings = new(StringComparer.OrdinalIgnoreCase);
- private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries = new(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, Dictionary<string, string>> _cultureOnlyDictionaries = new(StringComparer.OrdinalIgnoreCase);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private readonly ConcurrentDictionary<string, CultureDto?> _cultureCache = new(StringComparer.OrdinalIgnoreCase);
private List<CultureDto> _cultures = [];
+ private static readonly (IReadOnlyList<LocalizationOption> Options, FrozenDictionary<string, string> Bcp47ToJellyfinMap) _localizationData = BuildLocalizationData();
+ private static readonly IReadOnlyList<LocalizationOption> _localizationOptions = _localizationData.Options;
+
+ // Maps BCP-47 hyphenated culture codes (set by ASP.NET Core's RequestLocalizationMiddleware
+ // and used as CurrentUICulture.Name) to Jellyfin's underscore-based resource file codes.
+ // Built reflexively from the resource file scan so both directions stay in sync.
+ private static readonly FrozenDictionary<string, string> _bcp47ToJellyfinMap = _localizationData.Bcp47ToJellyfinMap;
+
private FrozenDictionary<string, string> _iso6392BtoT = null!;
/// <summary>
@@ -54,6 +64,59 @@ namespace Emby.Server.Implementations.Localization
{
_configurationManager = configurationManager;
_logger = logger;
+
+ _configurationManager.ConfigurationUpdated += OnConfigurationUpdated;
+ }
+
+ /// <summary>
+ /// Gets the supported UI cultures.
+ /// </summary>
+ /// <returns>A list of <see cref="CultureInfo"/> objects covering every embedded translation.</returns>
+ public static IList<CultureInfo> GetSupportedUICultures()
+ {
+ var cultures = new List<CultureInfo>();
+ foreach (var option in _localizationOptions)
+ {
+ // Skip novelty codes (e.g. "pr" Pirate, "jbo" Lojban) that .NET cannot resolve.
+ if (TryGetCultureInfo(option.Value, out var cultureInfo))
+ {
+ cultures.Add(cultureInfo);
+ }
+ }
+
+ return cultures;
+ }
+
+ /// <summary>
+ /// Resolves a Jellyfin resource culture code (which may use underscores, e.g. <c>es_419</c>)
+ /// to a <see cref="CultureInfo"/>. Returns <see langword="false"/> for codes .NET cannot resolve.
+ /// </summary>
+ private static bool TryGetCultureInfo(string cultureCode, [NotNullWhen(true)] out CultureInfo? cultureInfo)
+ {
+ try
+ {
+ // Resource files use underscores for some variants (e.g. es_419);
+ // CultureInfo only accepts hyphenated BCP-47 codes.
+ cultureInfo = CultureInfo.GetCultureInfo(cultureCode.Replace('_', '-'));
+ return true;
+ }
+ catch (CultureNotFoundException)
+ {
+ cultureInfo = null;
+ return false;
+ }
+ }
+
+ private static void OnConfigurationUpdated(object? sender, EventArgs e)
+ {
+ if (sender is IServerConfigurationManager configManager)
+ {
+ var uiCulture = configManager.Configuration.UICulture;
+ if (!string.IsNullOrEmpty(uiCulture))
+ {
+ CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(uiCulture);
+ }
+ }
}
/// <summary>
@@ -255,13 +318,13 @@ namespace Emby.Server.Implementations.Localization
// A lot of countries don't explicitly have a separate rating for adult content
if (ratings.All(x => x.RatingScore?.Score != 1000))
{
- ratings.Add(new ParentalRating("XXX", new(1000, null)));
+ ratings.Add(new ParentalRating("XXX", new(1000, null)));
}
// A lot of countries don't explicitly have a separate rating for banned content
if (ratings.All(x => x.RatingScore?.Score != 1001))
{
- ratings.Add(new ParentalRating("Banned", new(1001, null)));
+ ratings.Add(new ParentalRating("Banned", new(1001, null)));
}
return [.. ratings.OrderBy(r => r.RatingScore?.Score).ThenBy(r => r.RatingScore?.SubScore)];
@@ -420,6 +483,12 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc />
public string GetLocalizedString(string phrase)
{
+ return GetLocalizedString(phrase, CultureInfo.CurrentUICulture.Name);
+ }
+
+ /// <inheritdoc />
+ public string GetServerLocalizedString(string phrase)
+ {
return GetLocalizedString(phrase, _configurationManager.Configuration.UICulture);
}
@@ -436,6 +505,12 @@ namespace Emby.Server.Implementations.Localization
culture = DefaultCulture;
}
+ // Normalize BCP-47 hyphenated codes to Jellyfin's underscore-based codes
+ if (_bcp47ToJellyfinMap.TryGetValue(culture, out var mapped))
+ {
+ culture = mapped;
+ }
+
var dictionary = GetLocalizationDictionary(culture);
if (dictionary.TryGetValue(phrase, out var value))
@@ -443,6 +518,15 @@ namespace Emby.Server.Implementations.Localization
return value;
}
+ if (!string.Equals(culture, DefaultCulture, StringComparison.OrdinalIgnoreCase))
+ {
+ var fallback = GetLocalizationDictionary(DefaultCulture);
+ if (fallback.TryGetValue(phrase, out var fallbackValue))
+ {
+ return fallbackValue;
+ }
+ }
+
return phrase;
}
@@ -450,26 +534,17 @@ namespace Emby.Server.Implementations.Localization
{
ArgumentException.ThrowIfNullOrEmpty(culture);
- const string Prefix = "Core";
-
- return _dictionaries.GetOrAdd(
+ return _cultureOnlyDictionaries.GetOrAdd(
culture,
- static (key, localizationManager) => localizationManager.GetDictionary(Prefix, key, DefaultCulture + ".json").GetAwaiter().GetResult(),
- this);
- }
-
- private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename)
- {
- ArgumentException.ThrowIfNullOrEmpty(culture);
-
- var dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
- var namespaceName = GetType().Namespace + "." + prefix;
-
- await CopyInto(dictionary, namespaceName + "." + baseFilename).ConfigureAwait(false);
- await CopyInto(dictionary, namespaceName + "." + GetResourceFilename(culture)).ConfigureAwait(false);
+ static (key, localizationManager) =>
+ {
+ var dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ var namespaceName = localizationManager.GetType().Namespace + ".Core";
+ localizationManager.CopyInto(dictionary, namespaceName + "." + GetResourceFilename(key)).GetAwaiter().GetResult();
- return dictionary;
+ return dictionary;
+ },
+ this);
}
private async Task CopyInto(IDictionary<string, string> dictionary, string resourcePath)
@@ -508,77 +583,55 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc />
public IEnumerable<LocalizationOption> GetLocalizationOptions()
{
- yield return new LocalizationOption("Afrikaans", "af");
- yield return new LocalizationOption("العربية", "ar");
- yield return new LocalizationOption("Беларуская", "be");
- yield return new LocalizationOption("Български", "bg-BG");
- yield return new LocalizationOption("বাংলা (বাংলাদেশ)", "bn");
- yield return new LocalizationOption("Català", "ca");
- yield return new LocalizationOption("Čeština", "cs");
- yield return new LocalizationOption("Cymraeg", "cy");
- yield return new LocalizationOption("Dansk", "da");
- yield return new LocalizationOption("Deutsch", "de");
- yield return new LocalizationOption("English (United Kingdom)", "en-GB");
- yield return new LocalizationOption("English", "en-US");
- yield return new LocalizationOption("Ελληνικά", "el");
- yield return new LocalizationOption("Esperanto", "eo");
- yield return new LocalizationOption("Español", "es");
- yield return new LocalizationOption("Español americano", "es_419");
- yield return new LocalizationOption("Español (Argentina)", "es-AR");
- yield return new LocalizationOption("Español (Dominicana)", "es_DO");
- yield return new LocalizationOption("Español (México)", "es-MX");
- yield return new LocalizationOption("Eesti", "et");
- yield return new LocalizationOption("Basque", "eu");
- yield return new LocalizationOption("فارسی", "fa");
- yield return new LocalizationOption("Suomi", "fi");
- yield return new LocalizationOption("Filipino", "fil");
- yield return new LocalizationOption("Français", "fr");
- yield return new LocalizationOption("Français (Canada)", "fr-CA");
- yield return new LocalizationOption("Galego", "gl");
- yield return new LocalizationOption("Schwiizerdütsch", "gsw");
- yield return new LocalizationOption("עִבְרִית", "he");
- yield return new LocalizationOption("हिन्दी", "hi");
- yield return new LocalizationOption("Hrvatski", "hr");
- yield return new LocalizationOption("Magyar", "hu");
- yield return new LocalizationOption("Bahasa Indonesia", "id");
- yield return new LocalizationOption("Íslenska", "is");
- yield return new LocalizationOption("Italiano", "it");
- yield return new LocalizationOption("日本語", "ja");
- yield return new LocalizationOption("Qazaqşa", "kk");
- yield return new LocalizationOption("한국어", "ko");
- yield return new LocalizationOption("Lietuvių", "lt");
- yield return new LocalizationOption("Latviešu", "lv");
- yield return new LocalizationOption("Македонски", "mk");
- yield return new LocalizationOption("മലയാളം", "ml");
- yield return new LocalizationOption("मराठी", "mr");
- yield return new LocalizationOption("Bahasa Melayu", "ms");
- yield return new LocalizationOption("Norsk bokmål", "nb");
- yield return new LocalizationOption("नेपाली", "ne");
- yield return new LocalizationOption("Nederlands", "nl");
- yield return new LocalizationOption("Norsk nynorsk", "nn");
- yield return new LocalizationOption("ਪੰਜਾਬੀ", "pa");
- yield return new LocalizationOption("Polski", "pl");
- yield return new LocalizationOption("Pirate", "pr");
- yield return new LocalizationOption("Português", "pt");
- yield return new LocalizationOption("Português (Brasil)", "pt-BR");
- yield return new LocalizationOption("Português (Portugal)", "pt-PT");
- yield return new LocalizationOption("Românește", "ro");
- yield return new LocalizationOption("Русский", "ru");
- yield return new LocalizationOption("Slovenčina", "sk");
- yield return new LocalizationOption("Slovenščina", "sl-SI");
- yield return new LocalizationOption("Shqip", "sq");
- yield return new LocalizationOption("Српски", "sr");
- yield return new LocalizationOption("Svenska", "sv");
- yield return new LocalizationOption("தமிழ்", "ta");
- yield return new LocalizationOption("తెలుగు", "te");
- yield return new LocalizationOption("ภาษาไทย", "th");
- yield return new LocalizationOption("Türkçe", "tr");
- yield return new LocalizationOption("Українська", "uk");
- yield return new LocalizationOption("اُردُو", "ur_PK");
- yield return new LocalizationOption("Tiếng Việt", "vi");
- yield return new LocalizationOption("汉语 (简体字)", "zh-CN");
- yield return new LocalizationOption("漢語 (繁體字)", "zh-TW");
- yield return new LocalizationOption("廣東話 (香港)", "zh-HK");
+ return _localizationOptions;
+ }
+
+ private static (IReadOnlyList<LocalizationOption> Options, FrozenDictionary<string, string> Bcp47ToJellyfinMap) BuildLocalizationData()
+ {
+ var options = new List<LocalizationOption>();
+ var bcp47Map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ var prefix = CoreResourcePrefix;
+
+ foreach (var resource in _assembly.GetManifestResourceNames())
+ {
+ if (!resource.StartsWith(prefix, StringComparison.Ordinal)
+ || !resource.EndsWith(".json", StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ // Extract culture code from resource name: "...Core.de.json" -> "de", "...Core.pt-BR.json" -> "pt-BR"
+ var code = resource[prefix.Length..^5];
+
+ // Record the BCP-47 → Jellyfin mapping for any resource file using underscores.
+ if (code.Contains('_', StringComparison.Ordinal))
+ {
+ bcp47Map[code.Replace('_', '-')] = code;
+ }
+
+ // Skip the base language file — en-US is added explicitly below
+ if (code.Equals(DefaultCulture, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ var displayName = GetDisplayName(code);
+ options.Add(new LocalizationOption(displayName, code));
+ }
+
+ // Ensure en-US is always present
+ options.Add(new LocalizationOption("English", DefaultCulture));
+
+ options.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
+ return (options, bcp47Map.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase));
+ }
+
+ private static string GetDisplayName(string cultureCode)
+ {
+ // Custom/novelty codes like "pr" (Pirate) — fall back to code itself
+ return TryGetCultureInfo(cultureCode, out var cultureInfo)
+ ? cultureInfo.NativeName
+ : cultureCode;
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index 91ccb16ef9..f699c99d85 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -564,7 +564,8 @@ namespace Emby.Server.Implementations.Plugins
Id = instance.Id,
Status = PluginStatus.Active,
Name = instance.Name,
- Version = instance.Version.ToString()
+ Version = instance.Version.ToString(),
+ ImageResourceName = (instance as IHasEmbeddedImage)?.ImageResourceName
})
{
Instance = instance
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs
index 51920c5b14..5e92808f78 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 1782b53e10..18811ef3a9 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -271,9 +271,9 @@ namespace Emby.Server.Implementations.Session
user.LastActivityDate = activityDate;
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
}
- catch (DbUpdateConcurrencyException e)
+ catch (DbUpdateConcurrencyException)
{
- _logger.LogDebug(e, "Error updating user's last activity date.");
+ _logger.LogDebug("Error updating user's last activity date due to concurrency conflict. This is an expected event.");
}
}
}
@@ -386,7 +386,7 @@ namespace Emby.Server.Implementations.Session
{
if (session is null)
{
- return;
+ return;
}
if (string.IsNullOrEmpty(info.MediaSourceId))
@@ -453,18 +453,6 @@ namespace Emby.Server.Implementations.Session
session.PlayState.RepeatMode = info.RepeatMode;
session.PlayState.PlaybackOrder = info.PlaybackOrder;
session.PlaylistItemId = info.PlaylistItemId;
-
- var nowPlayingQueue = info.NowPlayingQueue;
-
- if (nowPlayingQueue?.Length > 0 && !nowPlayingQueue.SequenceEqual(session.NowPlayingQueue))
- {
- session.NowPlayingQueue = nowPlayingQueue;
-
- var itemIds = Array.ConvertAll(nowPlayingQueue, queue => queue.Id);
- session.NowPlayingQueueFullItems = _dtoService.GetBaseItemDtos(
- _libraryManager.GetItemList(new InternalItemsQuery { ItemIds = itemIds }),
- new DtoOptions(true));
- }
}
/// <summary>
@@ -1217,7 +1205,6 @@ namespace Emby.Server.Implementations.Session
SupportsMediaControl = sessionInfo.SupportsMediaControl,
SupportsRemoteControl = sessionInfo.SupportsRemoteControl,
NowPlayingQueue = sessionInfo.NowPlayingQueue,
- NowPlayingQueueFullItems = sessionInfo.NowPlayingQueueFullItems,
HasCustomDeviceName = sessionInfo.HasCustomDeviceName,
PlaylistItemId = sessionInfo.PlaylistItemId,
ServerId = sessionInfo.ServerId,
diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs
index d140426ddf..11a94648f8 100644
--- a/Emby.Server.Implementations/SystemManager.cs
+++ b/Emby.Server.Implementations/SystemManager.cs
@@ -89,11 +89,11 @@ public class SystemManager : ISystemManager
.GetVirtualFolders()
.Where(e => !string.IsNullOrWhiteSpace(e.ItemId)) // this should not be null but for some users it is.
.Select(e => new LibraryStorageInfo()
- {
- Id = Guid.Parse(e.ItemId),
- Name = e.Name,
- Folders = e.Locations.Select(f => StorageHelper.GetFreeSpaceOf(f)).ToArray()
- });
+ {
+ Id = Guid.Parse(e.ItemId),
+ Name = e.Name,
+ Folders = e.Locations.Select(f => StorageHelper.GetFreeSpaceOf(f)).ToArray()
+ });
return new SystemStorageInfo()
{
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 67b77a112d..ef53e3b326 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -527,42 +527,44 @@ namespace Emby.Server.Implementations.Updates
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-
- // CA5351: Do Not Use Broken Cryptographic Algorithms
+ Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using (stream.ConfigureAwait(false))
+ {
+ // CA5351: Do Not Use Broken Cryptographic Algorithms
#pragma warning disable CA5351
- cancellationToken.ThrowIfCancellationRequested();
+ cancellationToken.ThrowIfCancellationRequested();
- var hash = Convert.ToHexString(await MD5.HashDataAsync(stream, cancellationToken).ConfigureAwait(false));
- if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
- {
- _logger.LogError(
- "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}",
- package.Name,
- package.Checksum,
- hash);
- throw new InvalidDataException("The checksum of the received data doesn't match.");
- }
+ var hash = Convert.ToHexString(await MD5.HashDataAsync(stream, cancellationToken).ConfigureAwait(false));
+ if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogError(
+ "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}",
+ package.Name,
+ package.Checksum,
+ hash);
+ throw new InvalidDataException("The checksum of the received data doesn't match.");
+ }
- // Version folder as they cannot be overwritten in Windows.
- targetDir += "_" + package.Version;
+ // Version folder as they cannot be overwritten in Windows.
+ targetDir += "_" + package.Version;
- if (Directory.Exists(targetDir))
- {
- try
+ if (Directory.Exists(targetDir))
{
- Directory.Delete(targetDir, true);
- }
+ try
+ {
+ Directory.Delete(targetDir, true);
+ }
#pragma warning disable CA1031 // Do not catch general exception types
- catch
+ catch
#pragma warning restore CA1031 // Do not catch general exception types
- {
- // Ignore any exceptions.
+ {
+ // Ignore any exceptions.
+ }
}
- }
- stream.Position = 0;
- await ZipFile.ExtractToDirectoryAsync(stream, targetDir, true, cancellationToken);
+ stream.Position = 0;
+ await ZipFile.ExtractToDirectoryAsync(stream, targetDir, true, cancellationToken).ConfigureAwait(false);
+ }
// Ensure we create one or populate existing ones with missing data.
await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index f97ab414ce..f19ca77818 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -87,6 +87,7 @@ public class ArtistsController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the artists.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Obsolete("Use GetPersons")]
public ActionResult<QueryResult<BaseItemDto>> GetArtists(
[FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
@@ -258,6 +259,7 @@ public class ArtistsController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the album artists.</returns>
[HttpGet("AlbumArtists")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Obsolete("Use GetPersons")]
public ActionResult<QueryResult<BaseItemDto>> GetAlbumArtists(
[FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
@@ -399,6 +401,7 @@ public class ArtistsController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the artist.</returns>
[HttpGet("{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Obsolete("Use GetPerson")]
public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId)
{
userId = RequestHelpers.GetUserId(User, userId);
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 590bd05da4..77bb6ee7e7 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -91,18 +91,18 @@ public class AudioController : BaseJellyfinApiController
[ProducesAudioFile]
public async Task<ActionResult> GetAudioStream(
[FromRoute, Required] Guid itemId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -112,7 +112,7 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
+ [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -131,8 +131,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -255,18 +255,18 @@ public class AudioController : BaseJellyfinApiController
[ProducesAudioFile]
public async Task<ActionResult> GetAudioStreamByContainer(
[FromRoute, Required] Guid itemId,
- [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
+ [FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -276,7 +276,7 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
+ [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -295,8 +295,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index 227487b390..aa2b24c1e7 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -88,7 +88,7 @@ public class CollectionController : BaseJellyfinApiController
[FromRoute, Required] Guid collectionId,
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids)
{
- await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true);
+ await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(false);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs
index ee912a9be8..b9958867e7 100644
--- a/Jellyfin.Api/Controllers/DashboardController.cs
+++ b/Jellyfin.Api/Controllers/DashboardController.cs
@@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers;
/// The dashboard controller.
/// </summary>
[Route("")]
+[Tags("Plugin")]
public class DashboardController : BaseJellyfinApiController
{
private readonly ILogger<DashboardController> _logger;
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index eadb8c9855..2bbfeb40b8 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -1,7 +1,11 @@
using System;
+using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
+using System.Linq;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Dtos;
using Jellyfin.Data.Queries;
using MediaBrowser.Common.Api;
@@ -112,28 +116,31 @@ public class DevicesController : BaseJellyfinApiController
}
/// <summary>
- /// Deletes a device.
+ /// Deletes devices.
/// </summary>
- /// <param name="id">Device Id.</param>
+ /// <param name="id">Device Ids.</param>
/// <response code="204">Device deleted.</response>
- /// <response code="404">Device not found.</response>
- /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
+ /// <response code="400">A requested device is invalid.</response>
+ /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="BadRequestResult"/> if a requested device is invalid.</returns>
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task<ActionResult> DeleteDevice([FromQuery] string[] id)
{
- var existingDevice = _deviceManager.GetDevice(id);
- if (existingDevice is null)
+ var devices = id.Select(_deviceManager.GetDevice).ToArray();
+ if (devices.Any(f => f is null))
{
- return NotFound();
+ return BadRequest();
}
- var sessions = _deviceManager.GetDevices(new DeviceQuery { DeviceId = id });
-
- foreach (var session in sessions.Items)
+ foreach (var device in devices)
{
- await _sessionManager.Logout(session).ConfigureAwait(false);
+ var sessions = _deviceManager.GetDevices(new DeviceQuery { DeviceId = device!.Id });
+
+ foreach (var session in sessions.Items)
+ {
+ await _sessionManager.Logout(session).ConfigureAwait(false);
+ }
}
return NoContent();
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index c059f5880d..838f48949d 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -167,18 +167,18 @@ public class DynamicHlsController : BaseJellyfinApiController
[ProducesPlaylistFile]
public async Task<ActionResult> GetLiveHlsStream(
[FromRoute, Required] Guid itemId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -188,7 +188,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
+ [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -207,8 +207,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -413,12 +413,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -428,7 +428,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
+ [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -449,8 +449,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -586,12 +586,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -602,7 +602,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
+ [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -621,8 +621,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -753,12 +753,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -768,7 +768,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
+ [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -789,8 +789,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -922,12 +922,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -938,7 +938,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
+ [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -957,8 +957,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -1092,7 +1092,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId,
- [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
+ [FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
[FromQuery, Required] long runtimeTicks,
[FromQuery, Required] long actualSegmentLengthTicks,
[FromQuery] bool? @static,
@@ -1100,12 +1100,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -1115,7 +1115,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
+ [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -1136,8 +1136,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -1274,7 +1274,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId,
- [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
+ [FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
[FromQuery, Required] long runtimeTicks,
[FromQuery, Required] long actualSegmentLengthTicks,
[FromQuery] bool? @static,
@@ -1282,12 +1282,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -1298,7 +1298,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
+ [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -1317,8 +1317,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 2f53784db1..cfc8be28ae 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -8,6 +8,8 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -24,16 +26,19 @@ public class FilterController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
+ private readonly ILocalizationManager _localization;
/// <summary>
/// Initializes a new instance of the <see cref="FilterController"/> class.
/// </summary>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- public FilterController(ILibraryManager libraryManager, IUserManager userManager)
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ public FilterController(ILibraryManager libraryManager, IUserManager userManager, ILocalizationManager localization)
{
_libraryManager = libraryManager;
_userManager = userManager;
+ _localization = localization;
}
/// <summary>
@@ -183,6 +188,36 @@ public class FilterController : BaseJellyfinApiController
}).ToArray();
}
+ if (includeItemTypes.Contains(BaseItemKind.Movie) || includeItemTypes.Contains(BaseItemKind.Series))
+ {
+ filters.AudioLanguages = _libraryManager
+ .GetMediaStreamLanguages(MediaStreamType.Audio)
+ .Select(language =>
+ {
+ var culture = _localization.FindLanguageInfo(language);
+ return new NameValuePair
+ {
+ Name = culture is null ? language : $"{culture.DisplayName} ({language})",
+ Value = language
+ };
+ })
+ .OrderBy(l => l.Name)
+ .ToArray();
+ filters.SubtitleLanguages = _libraryManager
+ .GetMediaStreamLanguages(MediaStreamType.Subtitle)
+ .Select(language =>
+ {
+ var culture = _localization.FindLanguageInfo(language);
+ return new NameValuePair
+ {
+ Name = culture is null ? language : $"{culture.DisplayName} ({language})",
+ Value = language
+ };
+ })
+ .OrderBy(l => l.Name)
+ .ToArray();
+ }
+
return filters;
}
}
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index f80d32d149..8cd79645a8 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -196,6 +196,7 @@ public class InstantMixController : BaseJellyfinApiController
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("MusicGenres/{name}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Obsolete("Use GetInstantMixFromItem")]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreByName(
[FromRoute, Required] string name,
[FromQuery] Guid? userId,
@@ -359,7 +360,7 @@ public class InstantMixController : BaseJellyfinApiController
[HttpGet("MusicGenres/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- [Obsolete("Use GetInstantMixFromMusicGenreByName")]
+ [Obsolete("Use GetInstantMixFromItem")]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
[FromQuery, Required] Guid id,
[FromQuery] Guid? userId,
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index 7effe61e49..5fc4ad88b6 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers;
/// </summary>
[Route("Items")]
[Authorize(Policy = Policies.RequiresElevation)]
+[Tags("Library")]
public class ItemRefreshController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 4d697ab854..d560ee8238 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -288,7 +288,7 @@ public class ItemUpdateController : BaseJellyfinApiController
item.CustomRating = request.CustomRating;
var currentTags = item.Tags;
- var newTags = request.Tags.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+ var newTags = request.Tags.Select(t => t.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
var removedTags = currentTags.Except(newTags).ToList();
var addedTags = newTags.Except(currentTags).ToList();
item.Tags = newTags;
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 53656186c8..5705284cfb 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -14,6 +14,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -31,7 +32,7 @@ namespace Jellyfin.Api.Controllers;
/// </summary>
[Route("")]
[Authorize]
-[Tags("Item")]
+[Tags("Library")]
public class ItemsController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
@@ -157,6 +158,8 @@ public class ItemsController : BaseJellyfinApiController
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+ /// <param name="audioLanguages">Optional. If specified, results will be filtered based on audio language. This allows multiple, comma delimited values.</param>
+ /// <param name="subtitleLanguages">Optional. If specified, results will be filtered based on subtitle language. This allows multiple, comma delimited values.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
@@ -247,6 +250,8 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] string? nameLessThan,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] audioLanguages,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] subtitleLanguages,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
@@ -267,7 +272,7 @@ public class ItemsController : BaseJellyfinApiController
&& user.GetPreference(PreferenceKind.AllowedTags).Length != 0
&& !fields.Contains(ItemFields.Tags))
{
- fields = [..fields, ItemFields.Tags];
+ fields = [.. fields, ItemFields.Tags];
}
var dtoOptions = new DtoOptions { Fields = fields }
@@ -276,10 +281,21 @@ public class ItemsController : BaseJellyfinApiController
var item = _libraryManager.GetParentItem(parentId, userId);
QueryResult<BaseItem> result;
+ Guid[] linkedChildAncestorIds = [];
if (includeItemTypes.Length == 1
- && includeItemTypes[0] == BaseItemKind.BoxSet
- && item is not BoxSet)
+ && (includeItemTypes[0] == BaseItemKind.BoxSet || includeItemTypes[0] == BaseItemKind.Playlist)
+ && item is not BoxSet
+ && item is not Playlist)
{
+ var itemCollectionType = item is IHasCollectionType hct ? hct.CollectionType : null;
+ var targetCollectionType = includeItemTypes[0] == BaseItemKind.BoxSet
+ ? CollectionType.boxsets
+ : CollectionType.playlists;
+ if (parentId.HasValue && item is not UserRootFolder && itemCollectionType != targetCollectionType)
+ {
+ linkedChildAncestorIds = [parentId.Value];
+ }
+
parentId = null;
item = _libraryManager.GetUserRootFolder();
}
@@ -302,9 +318,6 @@ public class ItemsController : BaseJellyfinApiController
}
else if (folder is ICollectionFolder)
{
- // When the client doesn't specify recursive/includeItemTypes, force the query
- // through the database path where all filters (IsHD, genres, etc.) are applied.
- recursive ??= true;
if (includeItemTypes.Length == 0)
{
includeItemTypes = collectionType switch
@@ -314,6 +327,13 @@ public class ItemsController : BaseJellyfinApiController
_ => []
};
}
+
+ // When the client doesn't specify recursive/includeItemTypes, force the query
+ // through the database path where all filters (IsHD, genres, etc.) are applied.
+ if (includeItemTypes.Length > 0)
+ {
+ recursive ??= true;
+ }
}
if (item is not UserRootFolder
@@ -399,6 +419,9 @@ public class ItemsController : BaseJellyfinApiController
MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
MinPremiereDate = minPremiereDate?.ToUniversalTime(),
MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
+ AudioLanguages = audioLanguages,
+ SubtitleLanguages = subtitleLanguages,
+ LinkedChildAncestorIds = linkedChildAncestorIds,
};
if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
@@ -406,6 +429,33 @@ public class ItemsController : BaseJellyfinApiController
query.CollapseBoxSetItems = false;
}
+ if (query.SubtitleLanguages.Count > 0 && query.HasSubtitles.HasValue)
+ {
+ if (query.HasSubtitles.Value)
+ {
+ // if we check for specific subtitles we don't need a separate check for subtitle existence
+ query.HasSubtitles = null;
+ }
+ else
+ {
+ // if we search for items without subtitles, we don't need to check for subtitles of a specific language
+ query.SubtitleLanguages = [];
+ }
+ }
+
+ // for filter values that rely on media streams, we need to include alternative and linked versions
+ if (query.HasSubtitles.HasValue
+ || query.SubtitleLanguages.Count > 0
+ || query.AudioLanguages.Count > 0
+ || query.Is3D.HasValue
+ || query.IsHD.HasValue
+ || query.Is4K.HasValue
+ || query.VideoTypes.Length > 0
+ )
+ {
+ query.IncludeOwnedItems = true;
+ }
+
query.ApplyFilters(filters);
// Filter by Series Status
@@ -785,6 +835,8 @@ public class ItemsController : BaseJellyfinApiController
nameLessThan,
studioIds,
genreIds,
+ [],
+ [],
enableTotalRecordCount,
enableImages).ConfigureAwait(false);
@@ -955,6 +1007,7 @@ public class ItemsController : BaseJellyfinApiController
[HttpGet("UserItems/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Tags("UserData")]
public ActionResult<UserItemDataDto?> GetItemUserData(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
@@ -1010,6 +1063,7 @@ public class ItemsController : BaseJellyfinApiController
[HttpPost("UserItems/{itemId}/UserData")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Tags("UserData")]
public ActionResult<UserItemDataDto?> UpdateItemUserData(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId,
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 69c17f2486..39a6fbace8 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -17,6 +17,7 @@ using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -47,8 +48,10 @@ namespace Jellyfin.Api.Controllers;
public class LibraryController : BaseJellyfinApiController
{
private readonly IProviderManager _providerManager;
+ private readonly ISimilarItemsManager _similarItemsManager;
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
+ private readonly ICollectionManager _collectionManager;
private readonly IDtoService _dtoService;
private readonly IActivityManager _activityManager;
private readonly ILocalizationManager _localization;
@@ -60,8 +63,10 @@ public class LibraryController : BaseJellyfinApiController
/// Initializes a new instance of the <see cref="LibraryController"/> class.
/// </summary>
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="similarItemsManager">Instance of the <see cref="ISimilarItemsManager"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="collectionManager">Instance of the <see cref="ICollectionManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
@@ -70,8 +75,10 @@ public class LibraryController : BaseJellyfinApiController
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
public LibraryController(
IProviderManager providerManager,
+ ISimilarItemsManager similarItemsManager,
ILibraryManager libraryManager,
IUserManager userManager,
+ ICollectionManager collectionManager,
IDtoService dtoService,
IActivityManager activityManager,
ILocalizationManager localization,
@@ -80,8 +87,10 @@ public class LibraryController : BaseJellyfinApiController
IServerConfigurationManager serverConfigurationManager)
{
_providerManager = providerManager;
+ _similarItemsManager = similarItemsManager;
_libraryManager = libraryManager;
_userManager = userManager;
+ _collectionManager = collectionManager;
_dtoService = dtoService;
_activityManager = activityManager;
_localization = localization;
@@ -110,7 +119,18 @@ public class LibraryController : BaseJellyfinApiController
return NotFound();
}
- return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true);
+ var filePath = item.Path;
+ if (item.IsFileProtocol)
+ {
+ // PhysicalFile does not work well with symlinks at the moment.
+ var resolved = FileSystemHelper.ResolveLinkTarget(filePath, returnFinalTarget: true);
+ if (resolved is not null && resolved.Exists)
+ {
+ filePath = resolved.FullName;
+ }
+ }
+
+ return PhysicalFile(filePath, MimeTypes.GetMimeType(filePath), true);
}
/// <summary>
@@ -701,6 +721,72 @@ public class LibraryController : BaseJellyfinApiController
}
/// <summary>
+ /// Gets the collections that include the specified item.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+ /// <param name="startIndex">Optional. The index of the first record in the output.</param>
+ /// <param name="limit">Optional. The maximum number of records to return.</param>
+ /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+ /// <response code="200">Collections returned.</response>
+ /// <response code="401">User context missing.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>The collections that contain the requested item.</returns>
+ [HttpGet("Items/{itemId}/Collections")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public ActionResult<QueryResult<BaseItemDto>> GetItemCollections(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.IsNullOrEmpty()
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ if (user is null)
+ {
+ return Unauthorized();
+ }
+
+ var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
+ var dtoOptions = new DtoOptions { Fields = fields };
+
+ var visibleCollections = _collectionManager
+ .GetCollectionsContainingItem(user, item.Id)
+ .OrderBy(i => i.SortName, StringComparer.OrdinalIgnoreCase)
+ .ThenBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ IEnumerable<BaseItem> pagedCollections = visibleCollections;
+ if (startIndex.HasValue)
+ {
+ pagedCollections = pagedCollections.Skip(startIndex.Value);
+ }
+
+ if (limit.HasValue)
+ {
+ pagedCollections = pagedCollections.Take(limit.Value);
+ }
+
+ var dtos = _dtoService.GetBaseItemDtos(pagedCollections.ToList(), dtoOptions, user);
+
+ return new QueryResult<BaseItemDto>(
+ startIndex,
+ visibleCollections.Count,
+ dtos);
+ }
+
+ /// <summary>
/// Gets similar items.
/// </summary>
/// <param name="itemId">The item id.</param>
@@ -708,6 +794,7 @@ public class LibraryController : BaseJellyfinApiController
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <response code="200">Similar items returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
[HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists")]
@@ -718,12 +805,13 @@ public class LibraryController : BaseJellyfinApiController
[HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
+ public async Task<ActionResult<QueryResult<BaseItemDto>>> GetSimilarItems(
[FromRoute, Required] Guid itemId,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] excludeArtistIds,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
+ CancellationToken cancellationToken)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@@ -746,57 +834,22 @@ public class LibraryController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields };
- var program = item as IHasProgramAttributes;
- bool? isMovie = item is Movie || (program is not null && program.IsMovie) || item is Trailer;
- bool? isSeries = item is Series || (program is not null && program.IsSeries);
-
- var includeItemTypes = new List<BaseItemKind>();
- if (isMovie.Value)
- {
- includeItemTypes.Add(BaseItemKind.Movie);
- if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- includeItemTypes.Add(BaseItemKind.Trailer);
- includeItemTypes.Add(BaseItemKind.LiveTvProgram);
- }
- }
- else if (isSeries.Value)
- {
- includeItemTypes.Add(BaseItemKind.Series);
- }
- else
- {
- // For non series and movie types these columns are typically null
- // isSeries = null;
- isMovie = null;
- includeItemTypes.Add(item.GetBaseItemKind());
- }
-
- var query = new InternalItemsQuery(user)
- {
- Genres = item.Genres,
- Tags = item.Tags,
- Limit = limit,
- IncludeItemTypes = includeItemTypes.ToArray(),
- DtoOptions = dtoOptions,
- EnableTotalRecordCount = !isMovie ?? true,
- EnableGroupByMetadataKey = isMovie ?? false,
- ExcludeItemIds = [itemId],
- OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
- };
-
- // ExcludeArtistIds
- if (excludeArtistIds.Length != 0)
- {
- query.ExcludeArtistIds = excludeArtistIds;
- }
+ // Get library options for provider configuration
+ var libraryOptions = _libraryManager.GetLibraryOptions(item);
- var itemsResult = _libraryManager.GetItemList(query);
+ var itemsResult = await _similarItemsManager.GetSimilarItemsAsync(
+ item,
+ excludeArtistIds,
+ user,
+ dtoOptions,
+ limit,
+ libraryOptions,
+ cancellationToken).ConfigureAwait(false);
var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
return new QueryResult<BaseItemDto>(
- query.StartIndex,
+ 0,
itemsResult.Count,
returnList);
}
@@ -907,6 +960,17 @@ public class LibraryController : BaseJellyfinApiController
.DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.ToArray(),
+ SimilarItemProviders = plugins
+ .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
+ .SelectMany(i => i.Plugins.Where(p => p.Type == MetadataPluginType.LocalSimilarityProvider || p.Type == MetadataPluginType.SimilarityProvider))
+ .Select(i => new LibraryOptionInfoDto
+ {
+ Name = i.Name,
+ DefaultEnabled = i.Type == MetadataPluginType.LocalSimilarityProvider
+ })
+ .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
+ .ToArray(),
+
SupportedImageTypes = plugins
.Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => i.SupportedImageTypes ?? Array.Empty<ImageType>())
@@ -935,11 +999,11 @@ public class LibraryController : BaseJellyfinApiController
try
{
await _activityManager.CreateAsync(new ActivityLog(
- string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
+ string.Format(CultureInfo.InvariantCulture, _localization.GetServerLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
"UserDownloadingContent",
User.GetUserId())
{
- ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), User.GetClient(), User.GetDevice()),
+ ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetServerLocalizedString("AppDeviceValues"), User.GetClient(), User.GetDevice()),
ItemId = item.Id.ToString("N", CultureInfo.InvariantCulture)
}).ConfigureAwait(false);
}
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 074cdb24e0..113298c251 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -744,10 +744,12 @@ public class LiveTvController : BaseJellyfinApiController
/// <param name="programId">Program id.</param>
/// <param name="userId">Optional. Attach user data.</param>
/// <response code="200">Program returned.</response>
+ /// <response code="404">Program not found.</response>
/// <returns>An <see cref="OkResult"/> containing the livetv program.</returns>
[HttpGet("Programs/{programId}")]
[Authorize(Policy = Policies.LiveTvAccess)]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<BaseItemDto>> GetProgram(
[FromRoute, Required] string programId,
[FromQuery] Guid? userId)
@@ -756,8 +758,14 @@ public class LiveTvController : BaseJellyfinApiController
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
+ var result = await _liveTvManager.GetProgram(programId, CancellationToken.None, user).ConfigureAwait(false);
+
+ if (result is null)
+ {
+ return NotFound();
+ }
- return await _liveTvManager.GetProgram(programId, CancellationToken.None, user).ConfigureAwait(false);
+ return Ok(result);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 50d34d0656..a1f2fe7ce7 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -1,17 +1,13 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
-using Jellyfin.Data.Enums;
-using Jellyfin.Database.Implementations.Entities;
-using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -30,27 +26,23 @@ namespace Jellyfin.Api.Controllers;
public class MoviesController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
- private readonly IServerConfigurationManager _serverConfigurationManager;
+ private readonly ISimilarItemsManager _similarItemsManager;
/// <summary>
/// Initializes a new instance of the <see cref="MoviesController"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
- /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
- /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ /// <param name="similarItemsManager">Instance of the <see cref="ISimilarItemsManager"/> interface.</param>
public MoviesController(
IUserManager userManager,
- ILibraryManager libraryManager,
IDtoService dtoService,
- IServerConfigurationManager serverConfigurationManager)
+ ISimilarItemsManager similarItemsManager)
{
_userManager = userManager;
- _libraryManager = libraryManager;
_dtoService = dtoService;
- _serverConfigurationManager = serverConfigurationManager;
+ _similarItemsManager = similarItemsManager;
}
/// <summary>
@@ -61,15 +53,17 @@ public class MoviesController : BaseJellyfinApiController
/// <param name="fields">Optional. The fields to return.</param>
/// <param name="categoryLimit">The max number of categories to return.</param>
/// <param name="itemLimit">The max number of items to return per category.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
/// <response code="200">Movie recommendations returned.</response>
/// <returns>The list of movie recommendations.</returns>
[HttpGet("Recommendations")]
- public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations(
+ public async Task<ActionResult<IEnumerable<RecommendationDto>>> GetMovieRecommendations(
[FromQuery] Guid? userId,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery] int categoryLimit = 5,
- [FromQuery] int itemLimit = 8)
+ [FromQuery] int itemLimit = 8,
+ CancellationToken cancellationToken = default)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
@@ -77,251 +71,16 @@ public class MoviesController : BaseJellyfinApiController
: _userManager.GetUserById(userId.Value);
var dtoOptions = new DtoOptions { Fields = fields };
- var categories = new List<RecommendationDto>();
+ var recommendations = await _similarItemsManager
+ .GetMovieRecommendationsAsync(user, parentId ?? Guid.Empty, categoryLimit, itemLimit, dtoOptions, cancellationToken)
+ .ConfigureAwait(false);
- var parentIdGuid = parentId ?? Guid.Empty;
-
- var query = new InternalItemsQuery(user)
- {
- IncludeItemTypes = new[]
- {
- BaseItemKind.Movie,
- // nameof(Trailer),
- // nameof(LiveTvProgram)
- },
- // IsMovie = true
- OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.Random, SortOrder.Descending) },
- Limit = 7,
- ParentId = parentIdGuid,
- Recursive = true,
- IsPlayed = true,
- DtoOptions = dtoOptions
- };
-
- var recentlyPlayedMovies = _libraryManager.GetItemList(query);
-
- var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
- if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- itemTypes.Add(BaseItemKind.Trailer);
- itemTypes.Add(BaseItemKind.LiveTvProgram);
- }
-
- var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
- {
- IncludeItemTypes = itemTypes.ToArray(),
- IsMovie = true,
- OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) },
- Limit = 10,
- IsFavoriteOrLiked = true,
- ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(),
- EnableGroupByMetadataKey = true,
- ParentId = parentIdGuid,
- Recursive = true,
- DtoOptions = dtoOptions
- });
-
- var mostRecentMovies = recentlyPlayedMovies.Take(Math.Min(recentlyPlayedMovies.Count, 6)).ToList();
- // Get recently played directors
- var recentDirectors = GetDirectors(mostRecentMovies)
- .ToList();
-
- // Get recently played actors
- var recentActors = GetActors(mostRecentMovies)
- .ToList();
-
- var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator();
- var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator();
-
- var hasDirectorFromRecentlyPlayed = GetWithDirector(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator();
- var hasActorFromRecentlyPlayed = GetWithActor(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator();
-
- var categoryTypes = new List<IEnumerator<RecommendationDto>>
- {
- // Give this extra weight
- similarToRecentlyPlayed,
- similarToRecentlyPlayed,
-
- // Give this extra weight
- similarToLiked,
- similarToLiked,
- hasDirectorFromRecentlyPlayed,
- hasActorFromRecentlyPlayed
- };
-
- while (categories.Count < categoryLimit)
+ return Ok(recommendations.Select(r => new RecommendationDto
{
- var allEmpty = true;
-
- foreach (var category in categoryTypes)
- {
- if (category.MoveNext())
- {
- categories.Add(category.Current);
- allEmpty = false;
-
- if (categories.Count >= categoryLimit)
- {
- break;
- }
- }
- }
-
- if (allEmpty)
- {
- break;
- }
- }
-
- return Ok(categories.OrderBy(i => i.RecommendationType).AsEnumerable());
- }
-
- private IEnumerable<RecommendationDto> GetWithDirector(
- User? user,
- IEnumerable<string> names,
- int itemLimit,
- DtoOptions dtoOptions,
- RecommendationType type)
- {
- var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
- if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- itemTypes.Add(BaseItemKind.Trailer);
- itemTypes.Add(BaseItemKind.LiveTvProgram);
- }
-
- foreach (var name in names)
- {
- var items = _libraryManager.GetItemList(
- new InternalItemsQuery(user)
- {
- Person = name,
- // Account for duplicates by IMDb id, since the database doesn't support this yet
- Limit = itemLimit + 2,
- PersonTypes = new[] { PersonType.Director },
- IncludeItemTypes = itemTypes.ToArray(),
- IsMovie = true,
- EnableGroupByMetadataKey = true,
- DtoOptions = dtoOptions
- }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
- .Take(itemLimit)
- .ToList();
-
- if (items.Count > 0)
- {
- var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
-
- yield return new RecommendationDto
- {
- BaselineItemName = name,
- CategoryId = name.GetMD5(),
- RecommendationType = type,
- Items = returnItems
- };
- }
- }
- }
-
- private IEnumerable<RecommendationDto> GetWithActor(User? user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
- {
- var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
- if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- itemTypes.Add(BaseItemKind.Trailer);
- itemTypes.Add(BaseItemKind.LiveTvProgram);
- }
-
- foreach (var name in names)
- {
- var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
- {
- Person = name,
- // Account for duplicates by IMDb id, since the database doesn't support this yet
- Limit = itemLimit + 2,
- IncludeItemTypes = itemTypes.ToArray(),
- IsMovie = true,
- EnableGroupByMetadataKey = true,
- DtoOptions = dtoOptions
- }).DistinctBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
- .Take(itemLimit)
- .ToList();
-
- if (items.Count > 0)
- {
- var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
-
- yield return new RecommendationDto
- {
- BaselineItemName = name,
- CategoryId = name.GetMD5(),
- RecommendationType = type,
- Items = returnItems
- };
- }
- }
- }
-
- private IEnumerable<RecommendationDto> GetSimilarTo(User? user, IEnumerable<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
- {
- var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
- if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
- {
- itemTypes.Add(BaseItemKind.Trailer);
- itemTypes.Add(BaseItemKind.LiveTvProgram);
- }
-
- foreach (var item in baselineItems)
- {
- var similar = _libraryManager.GetItemList(new InternalItemsQuery(user)
- {
- Limit = itemLimit,
- IncludeItemTypes = itemTypes.ToArray(),
- IsMovie = true,
- EnableGroupByMetadataKey = true,
- DtoOptions = dtoOptions
- });
-
- if (similar.Count > 0)
- {
- var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user);
-
- yield return new RecommendationDto
- {
- BaselineItemName = item.Name,
- CategoryId = item.Id,
- RecommendationType = type,
- Items = returnItems
- };
- }
- }
- }
-
- private IEnumerable<string> GetActors(IEnumerable<BaseItem> items)
- {
- var people = _libraryManager.GetPeople(new InternalPeopleQuery(Array.Empty<string>(), new[] { PersonType.Director })
- {
- MaxListOrder = 3
- });
-
- var itemIds = items.Select(i => i.Id).ToList();
-
- return people
- .Where(i => itemIds.Contains(i.ItemId))
- .Select(i => i.Name)
- .DistinctNames();
- }
-
- private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items)
- {
- var people = _libraryManager.GetPeople(new InternalPeopleQuery(
- new[] { PersonType.Director },
- Array.Empty<string>()));
-
- var itemIds = items.Select(i => i.Id).ToList();
-
- return people
- .Where(i => itemIds.Contains(i.ItemId))
- .Select(i => i.Name)
- .DistinctNames();
+ BaselineItemName = r.BaselineItemName,
+ CategoryId = r.CategoryId,
+ RecommendationType = r.RecommendationType,
+ Items = _dtoService.GetBaseItemDtos(r.Items, dtoOptions, user)
+ }));
}
}
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index aa22bdf6af..4ff1eef413 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -72,6 +72,7 @@ public class PlaystateController : BaseJellyfinApiController
[HttpPost("UserPlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Tags("UserData")]
public async Task<ActionResult<UserItemDataDto?>> MarkPlayedItem(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId,
@@ -138,6 +139,7 @@ public class PlaystateController : BaseJellyfinApiController
[HttpDelete("UserPlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Tags("UserData")]
public async Task<ActionResult<UserItemDataDto?>> MarkUnplayedItem(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 79e6536fb6..0105ecf7a7 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -226,16 +226,32 @@ public class PluginsController : BaseJellyfinApiController
return NotFound();
}
- var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty);
- if (plugin.Manifest.ImagePath is null || !System.IO.File.Exists(imagePath))
+ if (!string.IsNullOrEmpty(plugin.Manifest.ImagePath))
{
- return NotFound();
+ var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
+ if (!System.IO.File.Exists(imagePath))
+ {
+ return NotFound();
+ }
+
+ Response.Headers.ContentDisposition = "attachment";
+ return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
}
- Response.Headers.ContentDisposition = "attachment";
+ var resourceName = plugin.Manifest.ImageResourceName;
+ if (!string.IsNullOrEmpty(resourceName) && plugin.Instance is not null)
+ {
+ var stream = plugin.Instance.GetType().Assembly.GetManifestResourceStream(resourceName);
+ if (stream is null)
+ {
+ return NotFound();
+ }
+
+ Response.Headers.ContentDisposition = "attachment";
+ return File(stream, MimeTypes.GetMimeType(resourceName));
+ }
- imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
- return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
+ return NotFound();
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 9886d03dee..a144961d74 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -432,6 +432,7 @@ public class SessionController : BaseJellyfinApiController
/// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the auth providers.</returns>
[HttpGet("Auth/Providers")]
[Authorize(Policy = Policies.RequiresElevation)]
+ [Tags("Authentication")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<NameIdPair>> GetAuthProviders()
{
@@ -444,6 +445,7 @@ public class SessionController : BaseJellyfinApiController
/// <response code="200">Password reset providers retrieved.</response>
/// <returns>An <see cref="IEnumerable{NameIdPair}"/> with the password reset providers.</returns>
[HttpGet("Auth/PasswordResetProviders")]
+ [Tags("Authentication")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.RequiresElevation)]
public ActionResult<IEnumerable<NameIdPair>> GetPasswordResetProviders()
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index 9378cfedb6..fa6d9efe36 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -1,7 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.StartupDtos;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Net;
@@ -54,6 +53,7 @@ public class StartupController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the initial startup wizard configuration.</returns>
[HttpGet("Configuration")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Obsolete("Use configuration endpoints")]
public ActionResult<StartupConfigurationDto> GetStartupConfiguration()
{
return new StartupConfigurationDto
@@ -73,6 +73,7 @@ public class StartupController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Configuration")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Obsolete("Use configuration endpoints")]
public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration)
{
_config.Configuration.ServerName = startupConfiguration.ServerName ?? string.Empty;
@@ -91,6 +92,7 @@ public class StartupController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("RemoteAccess")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Obsolete("Use configuration endpoints")]
public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto)
{
NetworkConfiguration settings = _config.GetNetworkConfiguration();
@@ -107,6 +109,7 @@ public class StartupController : BaseJellyfinApiController
[HttpGet("User")]
[HttpGet("FirstUser", Name = "GetFirstUser_2")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Obsolete("Use authentication endpoints")]
public async Task<StartupUserDto> GetFirstUser()
{
// TODO: Remove this method when startup wizard no longer requires an existing user.
@@ -142,12 +145,14 @@ public class StartupController : BaseJellyfinApiController
return BadRequest("Password must not be empty");
}
- if (startupUserDto.Name is not null)
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+
+#pragma warning disable CA1309 // Use ordinal string comparison
+ if (startupUserDto.Name is not null && !startupUserDto.Name.Equals(user.Username, StringComparison.InvariantCultureIgnoreCase))
{
- user.Username = startupUserDto.Name;
+ await _userManager.RenameUser(user.Id, user.Username, startupUserDto.Name).ConfigureAwait(false);
}
-
- await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
+#pragma warning restore CA1309 // Use ordinal string comparison
if (!string.IsNullOrEmpty(startupUserDto.Password))
{
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index e2075c2b8d..121db66858 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -115,6 +115,8 @@ public class TrailersController : BaseJellyfinApiController
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
+ /// <param name="audioLanguages">Optional. If specified, results will be filtered based on audio language. This allows multiple, comma delimited values.</param>
+ /// <param name="subtitleLanguages">Optional. If specified, results will be filtered based on subtitale language. This allows multiple, comma delimited values.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns>
@@ -203,6 +205,8 @@ public class TrailersController : BaseJellyfinApiController
[FromQuery] string? nameLessThan,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] genreIds,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] audioLanguages,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] subtitleLanguages,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
@@ -294,6 +298,8 @@ public class TrailersController : BaseJellyfinApiController
nameLessThan,
studioIds,
genreIds,
+ audioLanguages,
+ subtitleLanguages,
enableTotalRecordCount,
enableImages).ConfigureAwait(false);
}
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index e45a100b77..340a54e13b 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -232,7 +232,7 @@ public class TvShowsController : BaseJellyfinApiController
if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
{
- var item = _libraryManager.GetItemById<BaseItem>(seasonId.Value);
+ var item = _libraryManager.GetItemById<BaseItem>(seasonId.Value, user);
if (item is not Season seasonItem)
{
return NotFound("No season exists with Id " + seasonId);
@@ -242,7 +242,7 @@ public class TvShowsController : BaseJellyfinApiController
}
else if (season.HasValue) // Season number was supplied. Get episodes by season number
{
- var series = _libraryManager.GetItemById<Series>(seriesId);
+ var series = _libraryManager.GetItemById<Series>(seriesId, user);
if (series is null)
{
return NotFound("Series not found");
@@ -258,7 +258,7 @@ public class TvShowsController : BaseJellyfinApiController
}
else // No season number or season id was supplied. Returning all episodes.
{
- if (_libraryManager.GetItemById<BaseItem>(seriesId) is not Series series)
+ if (_libraryManager.GetItemById<BaseItem>(seriesId, user) is not Series series)
{
return NotFound("Series not found");
}
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index d4e9b234c5..2f5ed327c0 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -102,13 +102,13 @@ public class UniversalAudioController : BaseJellyfinApiController
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] Guid? userId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] int? maxAudioChannels,
[FromQuery] int? transcodingAudioChannels,
[FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] long? startTimeTicks,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? transcodingContainer,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? transcodingContainer,
[FromQuery] MediaStreamProtocol? transcodingProtocol,
[FromQuery] int? maxAudioSampleRate,
[FromQuery] int? maxAudioBitDepth,
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 55cc66f79f..657bda4d15 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -208,6 +208,7 @@ public class UserController : BaseJellyfinApiController
/// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
[HttpPost("AuthenticateByName")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Tags("Authentication")]
public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request)
{
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
@@ -243,6 +244,7 @@ public class UserController : BaseJellyfinApiController
/// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns>
[HttpPost("AuthenticateWithQuickConnect")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Tags("Authentication")]
public ActionResult<AuthenticationResult> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
{
try
@@ -538,6 +540,7 @@ public class UserController : BaseJellyfinApiController
/// <returns>A <see cref="Task"/> containing a <see cref="ForgotPasswordResult"/>.</returns>
[HttpPost("ForgotPassword")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Tags("Authentication")]
public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest)
{
var ip = HttpContext.GetNormalizedRemoteIP();
@@ -562,6 +565,7 @@ public class UserController : BaseJellyfinApiController
/// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns>
[HttpPost("ForgotPassword/Pin")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Tags("Authentication")]
public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody, Required] ForgotPasswordPinDto forgotPasswordPinRequest)
{
var result = await _userManager.RedeemPasswordResetPin(forgotPasswordPinRequest.Pin).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index b908f92be6..25f781e496 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -31,6 +31,7 @@ namespace Jellyfin.Api.Controllers;
/// </summary>
[Route("")]
[Authorize]
+[Tags("Library")]
public class UserLibraryController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
@@ -212,6 +213,7 @@ public class UserLibraryController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("UserFavoriteItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Tags("UserData")]
public ActionResult<UserItemDataDto> MarkFavoriteItem(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
@@ -259,6 +261,7 @@ public class UserLibraryController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("UserFavoriteItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Tags("UserData")]
public ActionResult<UserItemDataDto> UnmarkFavoriteItem(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
@@ -306,6 +309,7 @@ public class UserLibraryController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("UserItems/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Tags("UserData")]
public ActionResult<UserItemDataDto?> DeleteUserItemRating(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId)
@@ -354,6 +358,7 @@ public class UserLibraryController : BaseJellyfinApiController
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("UserItems/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [Tags("UserData")]
public ActionResult<UserItemDataDto?> UpdateUserItemRating(
[FromQuery] Guid? userId,
[FromRoute, Required] Guid itemId,
@@ -424,14 +429,8 @@ public class UserLibraryController : BaseJellyfinApiController
}
var dtoOptions = new DtoOptions();
- if (item is IHasTrailers hasTrailers)
- {
- var trailers = hasTrailers.LocalTrailers;
- return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item).AsEnumerable());
- }
- return Ok(item.GetExtras()
- .Where(e => e.ExtraType == ExtraType.Trailer)
+ return Ok(item.GetExtras([ExtraType.Trailer], user)
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
}
@@ -482,7 +481,7 @@ public class UserLibraryController : BaseJellyfinApiController
var dtoOptions = new DtoOptions();
return Ok(item
- .GetExtras()
+ .GetExtras(user)
.Where(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contains(i.ExtraType.Value))
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
}
@@ -552,6 +551,8 @@ public class UserLibraryController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields }
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ dtoOptions.PreferEpisodeParentPoster = true;
+
var list = _userViewManager.GetLatestItems(
new LatestItemsQuery
{
@@ -572,7 +573,7 @@ public class UserLibraryController : BaseJellyfinApiController
var item = tuple.Item2[0];
var childCount = 0;
- if (tuple.Item1 is not null && (tuple.Item2.Count > 1 || tuple.Item1 is MusicAlbum || tuple.Item1 is Series))
+ if (tuple.Item1 is not null && (tuple.Item2.Count > 1 || tuple.Item1 is MusicAlbum))
{
item = tuple.Item1;
childCount = tuple.Item2.Count;
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index c1d06bad36..8b359c48af 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -88,7 +88,7 @@ public class UserViewsController : BaseJellyfinApiController
var folders = _userViewManager.GetUserViews(query);
var dtoOptions = new DtoOptions();
- dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId];
+ dtoOptions.Fields = [.. dtoOptions.Fields, ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId];
var dtos = Array.ConvertAll(folders, i => _dtoService.GetBaseItemDto(i, dtoOptions, user));
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 2c2cbf1ec6..29a92cdb90 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -116,7 +116,7 @@ public class VideosController : BaseJellyfinApiController
BaseItemDto[] items;
if (item is Video video)
{
- items = video.GetAdditionalParts()
+ items = video.GetAdditionalParts(user)
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, video))
.ToArray();
}
@@ -317,18 +317,18 @@ public class VideosController : BaseJellyfinApiController
[ProducesVideoFile]
public async Task<ActionResult> GetVideoStream(
[FromRoute, Required] Guid itemId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -338,7 +338,7 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
+ [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -359,8 +359,8 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -555,18 +555,18 @@ public class VideosController : BaseJellyfinApiController
[ProducesVideoFile]
public Task<ActionResult> GetVideoStreamByContainer(
[FromRoute, Required] Guid itemId,
- [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
+ [FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery] string? deviceProfileId,
[FromQuery] string? playSessionId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -576,7 +576,7 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
- [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
+ [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -597,8 +597,8 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
- [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec,
+ [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index 4034a80887..d123dbc82e 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -62,12 +62,12 @@ public static class FileStreamResponseHelpers
if (response.Headers.TryGetValues(HeaderNames.AcceptRanges, out var acceptRangesHeaders))
{
// Prefer upstream server's Accept-Ranges header if available
- acceptRangesValue = string.Join(", ", acceptRangesHeaders);
- upstreamSupportsRange |= acceptRangesValue.Contains("bytes", StringComparison.OrdinalIgnoreCase);
+ acceptRangesValue = string.Join(", ", acceptRangesHeaders);
+ upstreamSupportsRange |= acceptRangesValue.Contains("bytes", StringComparison.OrdinalIgnoreCase);
}
else if (upstreamSupportsRange) // If we got 206 but no Accept-Ranges header, assume bytes
{
- acceptRangesValue = "bytes";
+ acceptRangesValue = "bytes";
}
// Set Accept-Ranges header for the client based on upstream support
@@ -76,13 +76,13 @@ public static class FileStreamResponseHelpers
// Set Content-Range header if upstream provided it (implies partial content)
if (response.Content.Headers.ContentRange is not null)
{
- httpContext.Response.Headers[HeaderNames.ContentRange] = response.Content.Headers.ContentRange.ToString();
+ httpContext.Response.Headers[HeaderNames.ContentRange] = response.Content.Headers.ContentRange.ToString();
}
// Set Content-Length header. For partial content, this is the length of the partial segment.
if (response.Content.Headers.ContentLength.HasValue)
{
- httpContext.Response.ContentLength = response.Content.Headers.ContentLength.Value;
+ httpContext.Response.ContentLength = response.Content.Headers.ContentLength.Value;
}
// Set Content-Type header
diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs
index f76c4a9678..98da6c8f44 100644
--- a/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs
+++ b/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs
@@ -1,4 +1,3 @@
-using System;
using System.Collections.Generic;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
@@ -18,20 +17,25 @@ public class LibraryTypeOptionsDto
/// <summary>
/// Gets or sets the metadata fetchers.
/// </summary>
- public IReadOnlyList<LibraryOptionInfoDto> MetadataFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
+ public IReadOnlyList<LibraryOptionInfoDto> MetadataFetchers { get; set; } = [];
/// <summary>
/// Gets or sets the image fetchers.
/// </summary>
- public IReadOnlyList<LibraryOptionInfoDto> ImageFetchers { get; set; } = Array.Empty<LibraryOptionInfoDto>();
+ public IReadOnlyList<LibraryOptionInfoDto> ImageFetchers { get; set; } = [];
+
+ /// <summary>
+ /// Gets or sets the similar item providers.
+ /// </summary>
+ public IReadOnlyList<LibraryOptionInfoDto> SimilarItemProviders { get; set; } = [];
/// <summary>
/// Gets or sets the supported image types.
/// </summary>
- public IReadOnlyList<ImageType> SupportedImageTypes { get; set; } = Array.Empty<ImageType>();
+ public IReadOnlyList<ImageType> SupportedImageTypes { get; set; } = [];
/// <summary>
/// Gets or sets the default image options.
/// </summary>
- public IReadOnlyList<ImageOption> DefaultImageOptions { get; set; } = Array.Empty<ImageOption>();
+ public IReadOnlyList<ImageOption> DefaultImageOptions { get; set; } = [];
}
diff --git a/Jellyfin.Api/Models/SystemInfoDtos/LibraryStorageDto.cs b/Jellyfin.Api/Models/SystemInfoDtos/LibraryStorageDto.cs
index c138324d2e..6e4ba91133 100644
--- a/Jellyfin.Api/Models/SystemInfoDtos/LibraryStorageDto.cs
+++ b/Jellyfin.Api/Models/SystemInfoDtos/LibraryStorageDto.cs
@@ -10,7 +10,7 @@ namespace Jellyfin.Api.Models.SystemInfoDtos;
/// </summary>
public record LibraryStorageDto
{
- /// <summary>
+ /// <summary>
/// Gets or sets the Library Id.
/// </summary>
public required Guid Id { get; set; }
diff --git a/Jellyfin.Data/Enums/ActivityLogSortBy.cs b/Jellyfin.Data/Enums/ActivityLogSortBy.cs
index d6d44e8c07..a24185e365 100644
--- a/Jellyfin.Data/Enums/ActivityLogSortBy.cs
+++ b/Jellyfin.Data/Enums/ActivityLogSortBy.cs
@@ -1,4 +1,4 @@
-namespace Jellyfin.Data.Enums;
+namespace Jellyfin.Data.Enums;
/// <summary>
/// Activity log sorting options.
diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
index fe987b9d86..ba24dc3864 100644
--- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
+++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
@@ -58,9 +58,9 @@ public class ActivityManager : IActivityManager
{
// TODO switch to LeftJoin in .NET 10.
var entries = from a in dbContext.ActivityLogs
- join u in dbContext.Users on a.UserId equals u.Id into ugj
- from u in ugj.DefaultIfEmpty()
- select new ExpandedActivityLog { ActivityLog = a, Username = u.Username };
+ join u in dbContext.Users on a.UserId equals u.Id into ugj
+ from u in ugj.DefaultIfEmpty()
+ select new ExpandedActivityLog { ActivityLog = a, Username = u.Username };
if (query.HasUserId is not null)
{
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Library/LyricDownloadFailureLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Library/LyricDownloadFailureLogger.cs
index 5f4864e953..cfd1cbe05b 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Library/LyricDownloadFailureLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Library/LyricDownloadFailureLogger.cs
@@ -37,7 +37,7 @@ public class LyricDownloadFailureLogger : IEventConsumer<LyricDownloadFailureEve
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("LyricDownloadFailureFromForItem"),
+ _localizationManager.GetServerLocalizedString("LyricDownloadFailureFromForItem"),
eventArgs.Provider,
GetItemName(eventArgs.Item)),
"LyricDownloadFailure",
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs
index 8fe380e4f4..24146210c6 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs
@@ -37,7 +37,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Library
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
+ _localizationManager.GetServerLocalizedString("SubtitleDownloadFailureFromForItem"),
eventArgs.Provider,
GetItemName(eventArgs.Item)),
"SubtitleDownloadFailure",
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs
index 1a8931a6dc..df526977a2 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs
@@ -35,7 +35,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("FailedLoginAttemptWithUserName"),
+ _localizationManager.GetServerLocalizedString("FailedLoginAttemptWithUserName"),
eventArgs.Username),
"AuthenticationFailed",
Guid.Empty)
@@ -43,7 +43,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
LogSeverity = LogLevel.Error,
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("LabelIpAddressValue"),
+ _localizationManager.GetServerLocalizedString("LabelIpAddressValue"),
eventArgs.RemoteEndPoint),
}).ConfigureAwait(false);
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
index 584d559e44..fa9ce21170 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
@@ -33,14 +33,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("AuthenticationSucceededWithUserName"),
+ _localizationManager.GetServerLocalizedString("AuthenticationSucceededWithUserName"),
eventArgs.User.Name),
"AuthenticationSucceeded",
eventArgs.User.Id)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("LabelIpAddressValue"),
+ _localizationManager.GetServerLocalizedString("LabelIpAddressValue"),
eventArgs.SessionInfo?.RemoteEndPoint),
}).ConfigureAwait(false);
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
index 73323acb37..8f71966b83 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
@@ -61,7 +61,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("UserStartedPlayingItemWithValues"),
+ _localizationManager.GetServerLocalizedString("UserStartedPlayingItemWithValues"),
user.Username,
GetItemName(eventArgs.MediaInfo),
eventArgs.DeviceName),
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
index b75567539c..a88904c727 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
@@ -69,15 +69,15 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("UserStoppedPlayingItemWithValues"),
+ _localizationManager.GetServerLocalizedString("UserStoppedPlayingItemWithValues"),
user.Username,
GetItemName(item),
eventArgs.DeviceName),
notificationType,
user.Id)
- {
- ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture),
- })
+ {
+ ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture),
+ })
.ConfigureAwait(false);
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs
index b90708a2f2..74dfeebba6 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs
@@ -38,7 +38,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("UserOfflineFromDevice"),
+ _localizationManager.GetServerLocalizedString("UserOfflineFromDevice"),
eventArgs.Argument.UserName,
eventArgs.Argument.DeviceName),
"SessionEnded",
@@ -46,7 +46,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("LabelIpAddressValue"),
+ _localizationManager.GetServerLocalizedString("LabelIpAddressValue"),
eventArgs.Argument.RemoteEndPoint),
}).ConfigureAwait(false);
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs
index 139c2e2acb..4028522838 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs
@@ -38,7 +38,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("UserOnlineFromDevice"),
+ _localizationManager.GetServerLocalizedString("UserOnlineFromDevice"),
eventArgs.Argument.UserName,
eventArgs.Argument.DeviceName),
"SessionStarted",
@@ -46,7 +46,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("LabelIpAddressValue"),
+ _localizationManager.GetServerLocalizedString("LabelIpAddressValue"),
eventArgs.Argument.RemoteEndPoint)
}).ConfigureAwait(false);
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs
index da82a3b30f..1e3dc7c92e 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs
@@ -47,7 +47,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.System
var time = result.EndTimeUtc - result.StartTimeUtc;
var runningTime = string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("LabelRunningTimeValue"),
+ _localizationManager.GetServerLocalizedString("LabelRunningTimeValue"),
ToUserFriendlyString(time));
if (result.Status == TaskCompletionStatus.Failed)
@@ -65,7 +65,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.System
}
await _activityManager.CreateAsync(new ActivityLog(
- string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
+ string.Format(CultureInfo.InvariantCulture, _localizationManager.GetServerLocalizedString("ScheduledTaskFailedWithName"), task.Name),
NotificationType.TaskFailed.ToString(),
Guid.Empty)
{
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs
index 632f30c7ad..9fb007aca7 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs
@@ -35,14 +35,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("NameInstallFailed"),
+ _localizationManager.GetServerLocalizedString("NameInstallFailed"),
eventArgs.InstallationInfo.Name),
NotificationType.InstallationFailed.ToString(),
Guid.Empty)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("VersionNumber"),
+ _localizationManager.GetServerLocalizedString("VersionNumber"),
eventArgs.InstallationInfo.Version),
Overview = eventArgs.Exception.Message
}).ConfigureAwait(false);
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs
index 4b49b714cf..2aa738c153 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs
@@ -35,14 +35,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("PluginInstalledWithName"),
+ _localizationManager.GetServerLocalizedString("PluginInstalledWithName"),
eventArgs.Argument.Name),
NotificationType.PluginInstalled.ToString(),
Guid.Empty)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("VersionNumber"),
+ _localizationManager.GetServerLocalizedString("VersionNumber"),
eventArgs.Argument.Version)
}).ConfigureAwait(false);
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs
index 2d24de7fc6..f7e651173d 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs
@@ -35,7 +35,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("PluginUninstalledWithName"),
+ _localizationManager.GetServerLocalizedString("PluginUninstalledWithName"),
eventArgs.Argument.Name),
NotificationType.PluginUninstalled.ToString(),
Guid.Empty))
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs
index e892d3dd9a..bca9662839 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs
@@ -35,14 +35,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("PluginUpdatedWithName"),
+ _localizationManager.GetServerLocalizedString("PluginUpdatedWithName"),
eventArgs.Argument.Name),
NotificationType.PluginUpdateInstalled.ToString(),
Guid.Empty)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("VersionNumber"),
+ _localizationManager.GetServerLocalizedString("VersionNumber"),
eventArgs.Argument.Version),
Overview = eventArgs.Argument.Changelog
}).ConfigureAwait(false);
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs
index 4f063f6a1b..cf5c81b981 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs
@@ -33,7 +33,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("UserCreatedWithName"),
+ _localizationManager.GetServerLocalizedString("UserCreatedWithName"),
eventArgs.Argument.Username),
"UserCreated",
eventArgs.Argument.Id))
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs
index ba4a072e84..720480c28f 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs
@@ -34,7 +34,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("UserDeletedWithName"),
+ _localizationManager.GetServerLocalizedString("UserDeletedWithName"),
eventArgs.Argument.Username),
"UserDeleted",
Guid.Empty))
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs
index bbc00567d1..efaf19397f 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs
@@ -35,7 +35,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("UserLockedOutWithName"),
+ _localizationManager.GetServerLocalizedString("UserLockedOutWithName"),
eventArgs.Argument.Username),
NotificationType.UserLockedOut.ToString(),
eventArgs.Argument.Id)
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs
index 7219704ec6..cc9efa7061 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs
@@ -33,7 +33,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("UserPasswordChangedWithName"),
+ _localizationManager.GetServerLocalizedString("UserPasswordChangedWithName"),
eventArgs.Argument.Username),
"UserPasswordChanged",
eventArgs.Argument.Id))
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs b/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs
index 736388e9eb..c64e6ac068 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs
@@ -26,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Item;
/// <summary>
/// Handles mapping between BaseItemEntity (database) and BaseItemDto (domain) objects.
/// </summary>
-internal static class BaseItemMapper
+public static class BaseItemMapper
{
/// <summary>
/// This holds all the types in the running assemblies
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs
index 380c6e582c..c5b5fbf6d8 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs
@@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
-using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
@@ -170,92 +169,50 @@ public sealed partial class BaseItemRepository
ExcludeItemIds = filter.ExcludeItemIds
};
- // Build the master query and collapse rows that share a PresentationUniqueKey
- // (e.g. alternate versions) by picking the lowest Id per group.
+ // Collapse rows that share a PresentationUniqueKey (e.g. alternate versions) by picking
+ // the lowest Id per group. For MusicArtist, prefer the entity from a library the user
+ // can actually access,since the same artist can have a folder in multiple libraries.
+ // Keep as an IQueryable sub-select so paging is applied AFTER
+ // ApplyOrder runs the caller's actual sort.
var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter);
-
- var orderedMasterQuery = ApplyOrder(masterQuery, filter, context)
- .GroupBy(e => e.PresentationUniqueKey)
- .Select(g => g.Min(e => e.Id));
+ var isMusicArtist = returnType == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
+ var representativeIds = isMusicArtist
+ ? masterQuery
+ .GroupBy(e => e.PresentationUniqueKey)
+ .Select(g => g
+ .OrderBy(e => filter.TopParentIds.Contains(e.TopParentId ?? Guid.Empty) ? 0 : 1)
+ .ThenBy(e => e.Id)
+ .First().Id)
+ : masterQuery
+ .GroupBy(e => e.PresentationUniqueKey)
+ .Select(g => g.Min(e => e.Id));
var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
if (filter.EnableTotalRecordCount)
{
- result.TotalRecordCount = orderedMasterQuery.Count();
+ result.TotalRecordCount = representativeIds.Count();
}
+ var query = ApplyNavigations(
+ context.BaseItems.AsNoTracking().AsSingleQuery().Where(e => representativeIds.Contains(e.Id)),
+ filter);
+
+ query = ApplyOrder(query, filter, context);
+
if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0)
{
- orderedMasterQuery = orderedMasterQuery.Skip(filter.StartIndex.Value);
+ query = query.Skip(filter.StartIndex.Value);
}
if (filter.Limit.HasValue)
{
- orderedMasterQuery = orderedMasterQuery.Take(filter.Limit.Value);
+ query = query.Take(filter.Limit.Value);
}
- var masterIds = orderedMasterQuery.ToList();
-
- var query = ApplyNavigations(
- context.BaseItems.AsNoTracking().AsSingleQuery().Where(e => masterIds.Contains(e.Id)),
- filter);
-
- query = ApplyOrder(query, filter, context);
-
+ result.StartIndex = filter.StartIndex ?? 0;
if (filter.IncludeItemTypes.Length > 0)
{
- var typeSubQuery = new InternalItemsQuery(filter.User)
- {
- ExcludeItemTypes = filter.ExcludeItemTypes,
- IncludeItemTypes = filter.IncludeItemTypes,
- MediaTypes = filter.MediaTypes,
- AncestorIds = filter.AncestorIds,
- ExcludeItemIds = filter.ExcludeItemIds,
- ItemIds = filter.ItemIds,
- TopParentIds = filter.TopParentIds,
- ParentId = filter.ParentId,
- IsPlayed = filter.IsPlayed
- };
-
- var itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context, typeSubQuery)
- .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
-
- var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
- var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
- var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
- var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
- var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
- var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
- var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
- var itemIds = itemCountQuery.Select(e => e.Id);
-
- // Rewrite query to avoid SelectMany on navigation properties (which requires SQL APPLY, not supported on SQLite)
- // Instead, start from ItemValueMaps and join with BaseItems
- var countsByCleanName = context.ItemValuesMap
- .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type))
- .Where(ivm => itemIds.Contains(ivm.ItemId))
- .Join(
- context.BaseItems,
- ivm => ivm.ItemId,
- e => e.Id,
- (ivm, e) => new { CleanName = ivm.ItemValue.CleanValue, e.Type })
- .GroupBy(x => new { x.CleanName, x.Type })
- .Select(g => new { g.Key.CleanName, g.Key.Type, Count = g.Count() })
- .GroupBy(x => x.CleanName)
- .ToDictionary(
- g => g.Key,
- g => new ItemCounts
- {
- SeriesCount = g.Where(x => x.Type == seriesTypeName).Sum(x => x.Count),
- EpisodeCount = g.Where(x => x.Type == episodeTypeName).Sum(x => x.Count),
- MovieCount = g.Where(x => x.Type == movieTypeName).Sum(x => x.Count),
- AlbumCount = g.Where(x => x.Type == musicAlbumTypeName).Sum(x => x.Count),
- ArtistCount = g.Where(x => x.Type == musicArtistTypeName).Sum(x => x.Count),
- SongCount = g.Where(x => x.Type == audioTypeName).Sum(x => x.Count),
- TrailerCount = g.Where(x => x.Type == trailerTypeName).Sum(x => x.Count),
- });
-
- result.StartIndex = filter.StartIndex ?? 0;
+ var countsByCleanName = BuildItemCountsByCleanName(context, filter, itemValueTypes);
result.Items =
[
.. query
@@ -273,7 +230,6 @@ public sealed partial class BaseItemRepository
}
else
{
- result.StartIndex = filter.StartIndex ?? 0;
result.Items =
[
.. query
@@ -287,4 +243,61 @@ public sealed partial class BaseItemRepository
return result;
}
+
+ private Dictionary<string, ItemCounts> BuildItemCountsByCleanName(
+ Database.Implementations.JellyfinDbContext context,
+ InternalItemsQuery filter,
+ IReadOnlyList<ItemValueType> itemValueTypes)
+ {
+ var typeSubQuery = new InternalItemsQuery(filter.User)
+ {
+ ExcludeItemTypes = filter.ExcludeItemTypes,
+ IncludeItemTypes = filter.IncludeItemTypes,
+ MediaTypes = filter.MediaTypes,
+ AncestorIds = filter.AncestorIds,
+ ExcludeItemIds = filter.ExcludeItemIds,
+ ItemIds = filter.ItemIds,
+ TopParentIds = filter.TopParentIds,
+ ParentId = filter.ParentId,
+ IsPlayed = filter.IsPlayed
+ };
+
+ var itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context, typeSubQuery)
+ .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
+
+ var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
+ var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
+ var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
+ var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
+ var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
+ var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
+ var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
+ var itemIds = itemCountQuery.Select(e => e.Id);
+
+ // Rewrite query to avoid SelectMany on navigation properties (which requires SQL APPLY, not supported on SQLite)
+ // Instead, start from ItemValueMaps and join with BaseItems
+ return context.ItemValuesMap
+ .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type))
+ .Where(ivm => itemIds.Contains(ivm.ItemId))
+ .Join(
+ context.BaseItems,
+ ivm => ivm.ItemId,
+ e => e.Id,
+ (ivm, e) => new { CleanName = ivm.ItemValue.CleanValue, e.Type })
+ .GroupBy(x => new { x.CleanName, x.Type })
+ .Select(g => new { g.Key.CleanName, g.Key.Type, Count = g.Count() })
+ .GroupBy(x => x.CleanName)
+ .ToDictionary(
+ g => g.Key,
+ g => new ItemCounts
+ {
+ SeriesCount = g.Where(x => x.Type == seriesTypeName).Sum(x => x.Count),
+ EpisodeCount = g.Where(x => x.Type == episodeTypeName).Sum(x => x.Count),
+ MovieCount = g.Where(x => x.Type == movieTypeName).Sum(x => x.Count),
+ AlbumCount = g.Where(x => x.Type == musicAlbumTypeName).Sum(x => x.Count),
+ ArtistCount = g.Where(x => x.Type == musicArtistTypeName).Sum(x => x.Count),
+ SongCount = g.Where(x => x.Type == audioTypeName).Sum(x => x.Count),
+ TrailerCount = g.Where(x => x.Type == trailerTypeName).Sum(x => x.Count),
+ });
+ }
}
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
index 0abe981af8..f33a65a703 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
@@ -390,7 +390,8 @@ public sealed partial class BaseItemRepository
{
if (filter.UseRawName == true)
{
- baseQuery = baseQuery.Where(e => e.Name == filter.Name);
+ var nameLower = filter.Name.ToLowerInvariant();
+ baseQuery = baseQuery.Where(e => e.Name!.ToLower() == nameLower);
}
else
{
@@ -823,6 +824,26 @@ public sealed partial class BaseItemRepository
}
}
+ if (filter.SubtitleLanguages.Count > 0)
+ {
+ var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, filter.SubtitleLanguages));
+ baseQuery = baseQuery
+ .Where(e =>
+ (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle
+ && (filter.SubtitleLanguages.Contains(f.Language) || (filter.SubtitleLanguages.Contains("und") && string.IsNullOrEmpty(f.Language)))))
+ || (e.IsFolder && foldersWithSubtitles.Contains(e.Id)));
+ }
+
+ if (filter.AudioLanguages.Count > 0)
+ {
+ var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStreamTypeEntity.Audio, filter.AudioLanguages));
+ baseQuery = baseQuery
+ .Where(e =>
+ (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio
+ && (filter.AudioLanguages.Contains(f.Language) || (filter.AudioLanguages.Contains("und") && string.IsNullOrEmpty(f.Language)))))
+ || (e.IsFolder && foldersWithAudio.Contains(e.Id)));
+ }
+
if (filter.HasChapterImages.HasValue)
{
var hasChapterImages = filter.HasChapterImages.Value;
@@ -952,6 +973,17 @@ public sealed partial class BaseItemRepository
}
}
+ if (filter.HasAnyProviderIds is not null && filter.HasAnyProviderIds.Count > 0)
+ {
+ var includeAny = filter.HasAnyProviderIds
+ .SelectMany(kvp => kvp.Value.Select(v => $"{kvp.Key}:{v}"))
+ .ToArray();
+ if (includeAny.Length > 0)
+ {
+ baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f => includeAny.Contains(f)));
+ }
+ }
+
if (filter.HasImdbId.HasValue)
{
baseQuery = filter.HasImdbId.Value
@@ -995,6 +1027,15 @@ public sealed partial class BaseItemRepository
baseQuery = baseQuery.Where(e => e.Parents!.AsQueryable().Any(ancestorFilter));
}
+ if (filter.LinkedChildAncestorIds.Length > 0)
+ {
+ // Keep folder-like items (BoxSets, Playlists) whose linked children descend from any of the requested ancestor ids.
+ var linkedChildAncestorIds = filter.LinkedChildAncestorIds;
+ baseQuery = baseQuery.Where(e => context.LinkedChildren.Any(lc =>
+ lc.ParentId == e.Id
+ && lc.Child!.Parents!.Any(a => linkedChildAncestorIds.Contains(a.ParentItemId))));
+ }
+
if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
{
baseQuery = baseQuery
@@ -1056,8 +1097,12 @@ public sealed partial class BaseItemRepository
if (filter.VideoTypes.Length > 0)
{
+ // Dvds and Blu-rays can either be stored in a folder structure or as an iso file
+ // => to find all matches we need to check both: VideoType and IsoType
+ // alternatively, we could provide specific IsoType filters
var videoTypeBs = filter.VideoTypes.Select(vt => $"\"VideoType\":\"{vt}\"").ToArray();
- Expression<Func<BaseItemEntity, bool>> hasVideoType = e => videoTypeBs.Any(f => e.Data!.Contains(f));
+ var isoTypeBs = filter.VideoTypes.Select(vt => $"\"IsoType\":\"{vt}\"").ToArray();
+ Expression<Func<BaseItemEntity, bool>> hasVideoType = e => videoTypeBs.Any(f => e.Data!.Contains(f)) || isoTypeBs.Any(f => e.Data!.Contains(f));
baseQuery = baseQuery.WhereItemOrDescendantMatches(context, hasVideoType);
}
diff --git a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs
index 98700f3224..f7d76517e1 100644
--- a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs
@@ -55,6 +55,7 @@ public class ChapterRepository : IChapterRepository
{
using var context = _dbProvider.CreateDbContext();
return context.Chapters.AsNoTracking().Where(e => e.ItemId.Equals(baseItemId))
+ .OrderBy(e => e.StartPositionTicks)
.Select(e => new
{
chapter = e,
@@ -69,18 +70,16 @@ public class ChapterRepository : IChapterRepository
public void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters)
{
using var context = _dbProvider.CreateDbContext();
- using (var transaction = context.Database.BeginTransaction())
+ using var transaction = context.Database.BeginTransaction();
+ context.Chapters.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete();
+ for (var i = 0; i < chapters.Count; i++)
{
- context.Chapters.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete();
- for (var i = 0; i < chapters.Count; i++)
- {
- var chapter = chapters[i];
- context.Chapters.Add(Map(chapter, i, itemId));
- }
-
- context.SaveChanges();
- transaction.Commit();
+ var chapter = chapters[i];
+ context.Chapters.Add(Map(chapter, i, itemId));
}
+
+ context.SaveChanges();
+ transaction.Commit();
}
/// <inheritdoc />
diff --git a/Jellyfin.Server.Implementations/Item/LinkedChildrenService.cs b/Jellyfin.Server.Implementations/Item/LinkedChildrenService.cs
index 415510b2f4..5e5ce320a5 100644
--- a/Jellyfin.Server.Implementations/Item/LinkedChildrenService.cs
+++ b/Jellyfin.Server.Implementations/Item/LinkedChildrenService.cs
@@ -1,4 +1,6 @@
#pragma warning disable RS0030 // Do not use banned APIs
+#pragma warning disable CA1304 // Specify CultureInfo
+#pragma warning disable CA1311 // Specify a culture or use an invariant version
using System;
using System.Collections.Generic;
@@ -62,17 +64,19 @@ public class LinkedChildrenService : ILinkedChildrenService
{
using var dbContext = _dbProvider.CreateDbContext();
+ var lowerNames = artistNames.Select(n => n.ToLowerInvariant()).ToArray();
var artists = dbContext.BaseItems
.AsNoTracking()
.Where(e => e.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!)
- .Where(e => artistNames.Contains(e.Name))
+ .Where(e => lowerNames.Contains(e.Name!.ToLower()))
.ToArray();
var lookup = artists
- .GroupBy(e => e.Name!)
+ .GroupBy(e => e.Name!, StringComparer.OrdinalIgnoreCase)
.ToDictionary(
g => g.Key,
- g => g.Select(f => _queryHelpers.DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray());
+ g => g.Select(f => _queryHelpers.DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray(),
+ StringComparer.OrdinalIgnoreCase);
var result = new Dictionary<string, MusicArtist[]>(artistNames.Count);
foreach (var name in artistNames)
@@ -87,14 +91,25 @@ public class LinkedChildrenService : ILinkedChildrenService
}
/// <inheritdoc/>
- public IReadOnlyList<Guid> GetManualLinkedParentIds(Guid childId)
+ public IReadOnlyList<Guid> GetManualLinkedParentIds(Guid childId, BaseItemKind? parentType = null)
{
using var context = _dbProvider.CreateDbContext();
- return context.LinkedChildren
- .Where(lc => lc.ChildId == childId && lc.ChildType == DbLinkedChildType.Manual)
- .Select(lc => lc.ParentId)
- .Distinct()
- .ToList();
+
+ var query = context.LinkedChildren
+ .Where(lc => lc.ChildId == childId && lc.ChildType == DbLinkedChildType.Manual);
+
+ if (parentType.HasValue)
+ {
+ var parentTypeName = _itemTypeLookup.BaseItemKindNames[parentType.Value];
+ query = query.Join(
+ context.BaseItems
+ .Where(item => item.Type == parentTypeName),
+ lc => lc.ParentId,
+ item => item.Id,
+ (lc, _) => lc);
+ }
+
+ return query.Select(lc => lc.ParentId).Distinct().ToList();
}
/// <inheritdoc/>
diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
index dd0446f49a..7fa33c8639 100644
--- a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
@@ -55,6 +55,17 @@ public class MediaStreamRepository : IMediaStreamRepository
return TranslateQuery(context.MediaStreamInfos.AsNoTracking(), filter).AsEnumerable().Select(Map).ToArray();
}
+ /// <inheritdoc />
+ public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType)
+ {
+ using var context = _dbProvider.CreateDbContext();
+ return context.MediaStreamInfos
+ .Where(e => e.StreamType == (MediaStreamTypeEntity)mediaStreamType)
+ .Select(s => string.IsNullOrEmpty(s.Language) ? "und" : s.Language) // und = undetermined
+ .Distinct()
+ .ToArray();
+ }
+
private string? GetPathToSave(string? path)
{
if (path is null)
diff --git a/Jellyfin.Server.Implementations/Item/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs
index ada86c8b87..d327b218a9 100644
--- a/Jellyfin.Server.Implementations/Item/OrderMapper.cs
+++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs
@@ -48,9 +48,9 @@ public static class OrderMapper
(ItemSortBy.SeriesSortName, _) => e => e.SeriesName,
(ItemSortBy.Album, _) => e => e.Album,
(ItemSortBy.DateCreated, _) => e => e.DateCreated,
- (ItemSortBy.PremiereDate, _) => e => (e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)),
+ (ItemSortBy.PremiereDate, _) => e => e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null),
(ItemSortBy.StartDate, _) => e => e.StartDate,
- (ItemSortBy.Name, _) => e => e.CleanName,
+ (ItemSortBy.Name, _) => e => e.SortName,
(ItemSortBy.CommunityRating, _) => e => e.CommunityRating,
(ItemSortBy.ProductionYear, _) => e => e.ProductionYear,
(ItemSortBy.CriticRating, _) => e => e.CriticRating,
diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
index 6cc9729bbe..eb87b525fe 100644
--- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
@@ -46,9 +46,10 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
{
// The Peoples table has one row per (Name, PersonType), so the same person can
// appear multiple times (e.g. as Actor and GuestStar). Collapse to one row per
- // name so /Persons doesn't return the same BaseItem id repeatedly.
+ // name so /Persons doesn't return the same BaseItem id repeatedly. Lowercase the
+ // grouping key so case-only duplicates collapse together.
var representativeIds = dbQuery
- .GroupBy(e => e.Name)
+ .GroupBy(e => e.Name.ToLower())
.Select(g => g.Min(e => e.Id));
dbQuery = context.Peoples.AsNoTracking()
.Where(p => representativeIds.Contains(p.Id))
@@ -102,24 +103,23 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
person.Role = person.Role?.Trim() ?? string.Empty;
}
- // multiple metadata providers can provide the _same_ person
- people = people.DistinctBy(e => e.Name + "-" + e.Type).ToArray();
- var personKeys = people.Select(e => e.Name + "-" + e.Type).ToArray();
+ // multiple metadata providers can provide the _same_ person; dedupe case-insensitively.
+ people = people.DistinctBy(e => e.Name.ToLowerInvariant() + "-" + e.Type).ToArray();
+ var personKeys = people.Select(e => e.Name.ToLowerInvariant() + "-" + e.Type).ToArray();
using var context = _dbProvider.CreateDbContext();
using var transaction = context.Database.BeginTransaction();
var existingPersons = context.Peoples.Select(e => new
- {
- item = e,
- SelectionKey = e.Name + "-" + e.PersonType
- })
+ {
+ item = e,
+ SelectionKey = e.Name.ToLower() + "-" + e.PersonType
+ })
.Where(p => personKeys.Contains(p.SelectionKey))
.Select(f => f.item)
.ToArray();
var toAdd = people
- .Where(e => e.Type is not PersonKind.Artist && e.Type is not PersonKind.AlbumArtist)
- .Where(e => !existingPersons.Any(f => f.Name == e.Name && f.PersonType == e.Type.ToString()))
+ .Where(e => !existingPersons.Any(f => string.Equals(f.Name, e.Name, StringComparison.OrdinalIgnoreCase) && f.PersonType == e.Type.ToString()))
.Select(Map);
context.Peoples.AddRange(toAdd);
context.SaveChanges();
@@ -132,13 +132,8 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
foreach (var person in people)
{
- if (person.Type == PersonKind.Artist || person.Type == PersonKind.AlbumArtist)
- {
- continue;
- }
-
- var entityPerson = personsEntities.First(e => e.Name == person.Name && e.PersonType == person.Type.ToString());
- var existingMap = existingMaps.FirstOrDefault(e => e.People.Name == person.Name && e.People.PersonType == person.Type.ToString() && e.Role == person.Role);
+ var entityPerson = personsEntities.First(e => string.Equals(e.Name, person.Name, StringComparison.OrdinalIgnoreCase) && e.PersonType == person.Type.ToString());
+ var existingMap = existingMaps.FirstOrDefault(e => string.Equals(e.People.Name, person.Name, StringComparison.OrdinalIgnoreCase) && e.People.PersonType == person.Type.ToString() && e.Role == person.Role);
if (existingMap is null)
{
context.PeopleBaseItemMap.Add(new PeopleBaseItemMap()
@@ -170,6 +165,42 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
transaction.Commit();
}
+ /// <inheritdoc/>
+ public IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IReadOnlyList<string> personTypes)
+ {
+ using var context = _dbProvider.CreateDbContext();
+ var query = context.PeopleBaseItemMap
+ .AsNoTracking()
+ .Where(m => itemIds.Contains(m.ItemId));
+
+ if (personTypes.Count > 0)
+ {
+ query = query.Where(m => personTypes.Contains(m.People.PersonType));
+ }
+
+ var rows = query
+ .OrderBy(m => m.ListOrder)
+ .Select(m => new { m.ItemId, m.People.Name })
+ .ToList();
+
+ var result = new Dictionary<Guid, IReadOnlyList<string>>();
+ foreach (var group in rows.GroupBy(r => r.ItemId))
+ {
+ var names = group
+ .Select(r => r.Name)
+ .Where(name => !string.IsNullOrEmpty(name))
+ .Distinct()
+ .ToArray();
+
+ if (names.Length > 0)
+ {
+ result[group.Key] = names;
+ }
+ }
+
+ return result;
+ }
+
private PersonInfo Map(People people)
{
var mapping = people.BaseItems?.FirstOrDefault();
@@ -244,7 +275,7 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
if (filter.MaxListOrder.HasValue && !filter.ItemId.IsEmpty())
{
- query = query.Where(e => e.BaseItems!.Where(w => w.ItemId == filter.ItemId).OrderBy(w => w.ListOrder).First().ListOrder <= filter.MaxListOrder.Value);
+ query = query.Where(e => e.BaseItems!.Any(w => w.ItemId == filter.ItemId && w.ListOrder <= filter.MaxListOrder.Value));
}
if (!string.IsNullOrWhiteSpace(filter.NameContains))
diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
index c514735688..be98f93dab 100644
--- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
+++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
@@ -54,7 +54,7 @@ public class MediaSegmentManager : IMediaSegmentManager
public async Task RunSegmentPluginProviders(BaseItem baseItem, LibraryOptions libraryOptions, bool forceOverwrite, CancellationToken cancellationToken)
{
var providers = _segmentProviders
- .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
+ .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(e.Name, StringComparer.OrdinalIgnoreCase))
.OrderBy(i =>
{
var index = libraryOptions.MediaSegmentProviderOrder.IndexOf(i.Name);
@@ -81,6 +81,8 @@ public class MediaSegmentManager : IMediaSegmentManager
foreach (var provider in providers)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
if (!await provider.Supports(baseItem).ConfigureAwait(false))
{
_logger.LogDebug("Media Segment provider {ProviderName} does not support item with path {MediaPath}", provider.Name, baseItem.Path);
@@ -146,6 +148,15 @@ public class MediaSegmentManager : IMediaSegmentManager
await CreateSegmentAsync(segment, providerId).ConfigureAwait(false);
}
}
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+ {
+ throw;
+ }
+ catch (Exception ex) when (cancellationToken.IsCancellationRequested)
+ {
+ _logger.LogDebug(ex, "Provider {ProviderName} aborted segment extraction for {MediaPath} due to shutdown", provider.Name, baseItem.Path);
+ break;
+ }
catch (Exception ex)
{
_logger.LogError(ex, "Provider {ProviderName} failed to extract segments from {MediaPath}", provider.Name, baseItem.Path);
@@ -224,7 +235,7 @@ public class MediaSegmentManager : IMediaSegmentManager
if (filterByProvider)
{
var providerIds = _segmentProviders
- .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
+ .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(e.Name, StringComparer.OrdinalIgnoreCase))
.Select(f => GetProviderId(f.Name))
.ToArray();
if (providerIds.Length == 0)
diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index e3fe517c49..8657cb7dbb 100644
--- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -302,7 +302,7 @@ namespace Jellyfin.Server.Implementations.Security
}
else if (!escaped && token == '=')
{
- key = authorizationHeader[start.. i].Trim().ToString();
+ key = authorizationHeader[start..i].Trim().ToString();
start = i + 1;
}
}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 8c0cbbd448..37c4106496 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -1,4 +1,3 @@
-#pragma warning disable CA1307
#pragma warning disable RS0030 // Do not use banned APIs
using System;
@@ -161,12 +160,8 @@ namespace Jellyfin.Server.Implementations.Users
using var dbContext = _dbProvider.CreateDbContext();
#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
-#pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
-#pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
return UserQuery(dbContext)
- .FirstOrDefault(u => u.Username.ToUpper() == name.ToUpper());
-#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
-#pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
+ .FirstOrDefault(u => u.NormalizedUsername == name.ToUpperInvariant());
#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
}
@@ -187,10 +182,8 @@ namespace Jellyfin.Server.Implementations.Users
await using (dbContext.ConfigureAwait(false))
{
#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
-#pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
-#pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
if (await dbContext.Users
- .AnyAsync(u => u.Username.ToUpper() == newName.ToUpper() && u.Id != userId)
+ .AnyAsync(u => u.NormalizedUsername == newName.ToUpperInvariant() && u.Id != userId)
.ConfigureAwait(false))
{
throw new ArgumentException(string.Format(
@@ -198,8 +191,6 @@ namespace Jellyfin.Server.Implementations.Users
"A user with the name '{0}' already exists.",
newName));
}
-#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
-#pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
user = await UserQuery(dbContext)
@@ -208,6 +199,7 @@ namespace Jellyfin.Server.Implementations.Users
.ConfigureAwait(false)
?? throw new ResourceNotFoundException(nameof(userId));
user.Username = newName;
+ user.NormalizedUsername = newName.ToUpperInvariant();
await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
}
}
@@ -257,10 +249,8 @@ namespace Jellyfin.Server.Implementations.Users
await using (dbContext.ConfigureAwait(false))
{
#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
-#pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
-#pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
if (await dbContext.Users
- .AnyAsync(u => u.Username.ToUpper() == name.ToUpper())
+ .AnyAsync(u => u.NormalizedUsername == name.ToUpperInvariant())
.ConfigureAwait(false))
{
throw new ArgumentException(string.Format(
@@ -268,8 +258,6 @@ namespace Jellyfin.Server.Implementations.Users
"A user with the name '{0}' already exists.",
name));
}
-#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings
-#pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture
#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
diff --git a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs
index fdc49a9840..c9fd031ef9 100644
--- a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs
+++ b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs
@@ -68,7 +68,7 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider
try
{
- openApiDocument = _swaggerGenerator.GetSwagger(documentName);
+ openApiDocument = _swaggerGenerator.GetSwagger(documentName);
}
catch (Exception ex)
{
diff --git a/Jellyfin.Server/GlobalSuppressions.cs b/Jellyfin.Server/GlobalSuppressions.cs
new file mode 100644
index 0000000000..676747e29f
--- /dev/null
+++ b/Jellyfin.Server/GlobalSuppressions.cs
@@ -0,0 +1,8 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Migration files should follow the EFCore standard in regards to naming.", Scope = "namespaceanddescendants", Target = "~N:Jellyfin.Server.Migrations.Routines")]
diff --git a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
index d664b718bc..9bf927bb95 100644
--- a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
+++ b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
@@ -193,84 +193,89 @@ internal class JellyfinMigrationService
{
var historyRepository = dbContext.GetService<IHistoryRepository>();
var migrationsAssembly = dbContext.GetService<IMigrationsAssembly>();
- var appliedMigrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
- var pendingCodeMigrations = migrationStage
- .Where(e => appliedMigrations.All(f => f.MigrationId != e.BuildCodeMigrationId()))
- .Select(e => (Key: e.BuildCodeMigrationId(), Migration: new InternalCodeMigration(e, serviceProvider, dbContext)))
- .ToArray();
-
- (string Key, InternalDatabaseMigration Migration)[] pendingDatabaseMigrations = [];
- if (stage is JellyfinMigrationStageTypes.CoreInitialisation)
- {
- pendingDatabaseMigrations = migrationsAssembly.Migrations.Where(f => appliedMigrations.All(e => e.MigrationId != f.Key))
- .Select(e => (Key: e.Key, Migration: new InternalDatabaseMigration(e, dbContext)))
- .ToArray();
- }
-
- (string Key, IInternalMigration Migration)[] pendingMigrations = [.. pendingCodeMigrations, .. pendingDatabaseMigrations];
- logger.LogInformation("There are {Pending} migrations for stage {Stage}.", pendingCodeMigrations.Length, stage);
- var migrations = pendingMigrations.OrderBy(e => e.Key).ToArray();
+ (string Key, IInternalMigration Migration)[] migrations = [];
+
+ do
+ { // migrations may alter the migration state. Reevaluate the applicable migrations after every stage ran until there are no more to apply.
+ var appliedMigrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
+ var pendingCodeMigrations = migrationStage
+ .Where(e => appliedMigrations.All(f => f.MigrationId != e.BuildCodeMigrationId()))
+ .Select(e => (Key: e.BuildCodeMigrationId(), Migration: new InternalCodeMigration(e, serviceProvider, dbContext)))
+ .ToArray();
- foreach (var item in migrations)
- {
- var migrationLogger = logger.With(_loggerFactory.CreateLogger(item.Migration.GetType().Name)).BeginGroup($"{item.Key}");
- try
+ (string Key, InternalDatabaseMigration Migration)[] pendingDatabaseMigrations = [];
+ if (stage is JellyfinMigrationStageTypes.CoreInitialisation)
{
- migrationLogger.LogInformation("Perform migration {Name}", item.Key);
- await item.Migration.PerformAsync(migrationLogger).ConfigureAwait(false);
- migrationLogger.LogInformation("Migration {Name} was successfully applied", item.Key);
+ pendingDatabaseMigrations = migrationsAssembly.Migrations.Where(f => appliedMigrations.All(e => e.MigrationId != f.Key))
+ .Select(e => (Key: e.Key, Migration: new InternalDatabaseMigration(e, dbContext)))
+ .ToArray();
}
- catch (Exception ex)
- {
- migrationLogger.LogCritical("Error: {Error}", ex.Message);
- migrationLogger.LogError(ex, "Migration {Name} failed", item.Key);
- if (_backupKey != default && _backupService is not null && _jellyfinDatabaseProvider is not null)
+ (string Key, IInternalMigration Migration)[] pendingMigrations = [.. pendingCodeMigrations, .. pendingDatabaseMigrations];
+ logger.LogInformation("There are {Pending} migrations for stage {Stage}.", pendingCodeMigrations.Length, stage);
+ migrations = pendingMigrations.OrderBy(e => e.Key).ToArray();
+
+ foreach (var item in migrations)
+ {
+ var migrationLogger = logger.With(_loggerFactory.CreateLogger(item.Migration.GetType().Name)).BeginGroup($"{item.Key}");
+ try
{
- if (_backupKey.LibraryDb is not null)
- {
- migrationLogger.LogInformation("Attempt to rollback librarydb.");
- try
- {
- var libraryDbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
- File.Move(_backupKey.LibraryDb, libraryDbPath, true);
- }
- catch (Exception inner)
- {
- migrationLogger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual intervention might be required to restore a operational state.", _backupKey.LibraryDb);
- }
- }
+ migrationLogger.LogInformation("Perform migration {Name}", item.Key);
+ await item.Migration.PerformAsync(migrationLogger).ConfigureAwait(false);
+ migrationLogger.LogInformation("Migration {Name} was successfully applied", item.Key);
+ }
+ catch (Exception ex)
+ {
+ migrationLogger.LogCritical("Error: {Error}", ex.Message);
+ migrationLogger.LogError(ex, "Migration {Name} failed", item.Key);
- if (_backupKey.JellyfinDb is not null)
+ if (_backupKey != default && _backupService is not null && _jellyfinDatabaseProvider is not null)
{
- migrationLogger.LogInformation("Attempt to rollback JellyfinDb.");
- try
+ if (_backupKey.LibraryDb is not null)
{
- await _jellyfinDatabaseProvider.RestoreBackupFast(_backupKey.JellyfinDb, CancellationToken.None).ConfigureAwait(false);
+ migrationLogger.LogInformation("Attempt to rollback librarydb.");
+ try
+ {
+ var libraryDbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
+ File.Move(_backupKey.LibraryDb, libraryDbPath, true);
+ }
+ catch (Exception inner)
+ {
+ migrationLogger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual intervention might be required to restore a operational state.", _backupKey.LibraryDb);
+ }
}
- catch (Exception inner)
- {
- migrationLogger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual intervention might be required to restore a operational state.", _backupKey.JellyfinDb);
- }
- }
- if (_backupKey.FullBackup is not null)
- {
- migrationLogger.LogInformation("Attempt to rollback from backup.");
- try
+ if (_backupKey.JellyfinDb is not null)
{
- await _backupService.RestoreBackupAsync(_backupKey.FullBackup.Path).ConfigureAwait(false);
+ migrationLogger.LogInformation("Attempt to rollback JellyfinDb.");
+ try
+ {
+ await _jellyfinDatabaseProvider.RestoreBackupFast(_backupKey.JellyfinDb, CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception inner)
+ {
+ migrationLogger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual intervention might be required to restore a operational state.", _backupKey.JellyfinDb);
+ }
}
- catch (Exception inner)
+
+ if (_backupKey.FullBackup is not null)
{
- migrationLogger.LogCritical(inner, "Could not rollback from backup {Backup}. Manual intervention might be required to restore a operational state.", _backupKey.FullBackup.Path);
+ migrationLogger.LogInformation("Attempt to rollback from backup.");
+ try
+ {
+ await _backupService.RestoreBackupAsync(_backupKey.FullBackup.Path).ConfigureAwait(false);
+ }
+ catch (Exception inner)
+ {
+ migrationLogger.LogCritical(inner, "Could not rollback from backup {Backup}. Manual intervention might be required to restore a operational state.", _backupKey.FullBackup.Path);
+ }
}
}
- }
- throw;
+ throw;
+ }
}
- }
+ } while (migrations.Length != 0);
}
}
diff --git a/Jellyfin.Server/Migrations/MigrationOptions.cs b/Jellyfin.Server/Migrations/MigrationOptions.cs
index c9710f1fd1..cd1b74a613 100644
--- a/Jellyfin.Server/Migrations/MigrationOptions.cs
+++ b/Jellyfin.Server/Migrations/MigrationOptions.cs
@@ -16,7 +16,7 @@ namespace Jellyfin.Server.Migrations
Applied = new List<(Guid Id, string Name)>();
}
-// .Net xml serializer can't handle interfaces
+ // .Net xml serializer can't handle interfaces
#pragma warning disable CA1002 // Do not expose generic lists
/// <summary>
/// Gets the list of applied migration routine names.
diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/20250420050000_DisableTranscodingThrottling.cs
index acf2835fe0..acf2835fe0 100644
--- a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420050000_DisableTranscodingThrottling.cs
diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/20250420060000_CreateUserLoggingConfigFile.cs
index 1326a6dc8d..1326a6dc8d 100644
--- a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420060000_CreateUserLoggingConfigFile.cs
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/20250420070000_MigrateActivityLogDb.cs
index 8c8563190d..8c8563190d 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420070000_MigrateActivityLogDb.cs
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/20250420080000_RemoveDuplicateExtras.cs
index c9e66d0cfe..c9e66d0cfe 100644
--- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420080000_RemoveDuplicateExtras.cs
diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/20250420090000_AddDefaultPluginRepository.cs
index 8c8398a161..8c8398a161 100644
--- a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420090000_AddDefaultPluginRepository.cs
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/20250420100000_MigrateUserDb.cs
index 8c3361ee16..8c3361ee16 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420100000_MigrateUserDb.cs
diff --git a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/20250420110000_ReaddDefaultPluginRepository.cs
index ebf4a2780e..ebf4a2780e 100644
--- a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420110000_ReaddDefaultPluginRepository.cs
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/20250420120000_MigrateDisplayPreferencesDb.cs
index ffd06fea0d..ffd06fea0d 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420120000_MigrateDisplayPreferencesDb.cs
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs b/Jellyfin.Server/Migrations/Routines/20250420130000_RemoveDownloadImagesInAdvance.cs
index b626c473e3..b626c473e3 100644
--- a/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420130000_RemoveDownloadImagesInAdvance.cs
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/20250420140000_MigrateAuthenticationDb.cs
index 0de775e03a..0de775e03a 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420140000_MigrateAuthenticationDb.cs
diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/20250420150000_FixPlaylistOwner.cs
index 56614ece3c..56614ece3c 100644
--- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420150000_FixPlaylistOwner.cs
diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs b/Jellyfin.Server/Migrations/Routines/20250420160000_AddDefaultCastReceivers.cs
index 00d152b4b8..00d152b4b8 100644
--- a/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420160000_AddDefaultCastReceivers.cs
diff --git a/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/20250420170000_UpdateDefaultPluginRepository.cs
index f58cf27413..f58cf27413 100644
--- a/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420170000_UpdateDefaultPluginRepository.cs
diff --git a/Jellyfin.Server/Migrations/Routines/FixAudioData.cs b/Jellyfin.Server/Migrations/Routines/20250420180000_FixAudioData.cs
index d102e24b91..d102e24b91 100644
--- a/Jellyfin.Server/Migrations/Routines/FixAudioData.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420180000_FixAudioData.cs
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs b/Jellyfin.Server/Migrations/Routines/20250420190000_RemoveDuplicatePlaylistChildren.cs
index 1545ebdc8e..1545ebdc8e 100644
--- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420190000_RemoveDuplicatePlaylistChildren.cs
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs b/Jellyfin.Server/Migrations/Routines/20250420193000_MigrateLibraryDbCompatibilityCheck.cs
index d4cc9bbeed..d4cc9bbeed 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420193000_MigrateLibraryDbCompatibilityCheck.cs
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/20250420200000_MigrateLibraryDb.cs
index 3e4205547a..3e4205547a 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420200000_MigrateLibraryDb.cs
diff --git a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs b/Jellyfin.Server/Migrations/Routines/20250420210000_MoveExtractedFiles.cs
index fbf9c16377..cfc1628782 100644
--- a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420210000_MoveExtractedFiles.cs
@@ -144,6 +144,11 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine
}
var newSubtitleCachePath = _pathManager.GetSubtitlePath(itemIdString, mediaStreamIndex, extension);
+ if (newSubtitleCachePath is null)
+ {
+ continue;
+ }
+
if (File.Exists(newSubtitleCachePath))
{
File.Delete(oldSubtitleCachePath);
@@ -182,6 +187,11 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine
}
var newAttachmentPath = _pathManager.GetAttachmentPath(itemIdString, attachment.Filename ?? attachmentIndex.ToString(CultureInfo.InvariantCulture));
+ if (newAttachmentPath is null)
+ {
+ continue;
+ }
+
if (File.Exists(newAttachmentPath))
{
File.Delete(oldAttachmentPath);
diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/20250420230000_MoveTrickplayFiles.cs
index 79a8f9577c..79a8f9577c 100644
--- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420230000_MoveTrickplayFiles.cs
diff --git a/Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs b/Jellyfin.Server/Migrations/Routines/20250420230000_RefreshInternalDateModified.cs
index b23a7dbc42..b23a7dbc42 100644
--- a/Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250420230000_RefreshInternalDateModified.cs
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/20250421000000_MigrateKeyframeData.cs
index aa55309264..aa55309264 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250421000000_MigrateKeyframeData.cs
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs b/Jellyfin.Server/Migrations/Routines/20250618010000_MigrateLibraryUserData.cs
index 8a0a1741f1..8a0a1741f1 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250618010000_MigrateLibraryUserData.cs
diff --git a/Jellyfin.Server/Migrations/Routines/FixDates.cs b/Jellyfin.Server/Migrations/Routines/20250620180000_FixDates.cs
index a5b11b11d0..a5b11b11d0 100644
--- a/Jellyfin.Server/Migrations/Routines/FixDates.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250620180000_FixDates.cs
diff --git a/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs b/Jellyfin.Server/Migrations/Routines/20250730215000_ReseedFolderFlag.cs
index 502763ac09..502763ac09 100644
--- a/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs
+++ b/Jellyfin.Server/Migrations/Routines/20250730215000_ReseedFolderFlag.cs
diff --git a/Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs b/Jellyfin.Server/Migrations/Routines/20251008120000_RefreshCleanNames.cs
index eca50ac100..eca50ac100 100644
--- a/Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs
+++ b/Jellyfin.Server/Migrations/Routines/20251008120000_RefreshCleanNames.cs
diff --git a/Jellyfin.Server/Migrations/Routines/CleanMusicArtist.cs b/Jellyfin.Server/Migrations/Routines/20251009200000_CleanMusicArtist.cs
index d5c5f3d929..d5c5f3d929 100644
--- a/Jellyfin.Server/Migrations/Routines/CleanMusicArtist.cs
+++ b/Jellyfin.Server/Migrations/Routines/20251009200000_CleanMusicArtist.cs
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs b/Jellyfin.Server/Migrations/Routines/20260113120000_MigrateLinkedChildren.cs
index 14ae535531..74f03f5107 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs
+++ b/Jellyfin.Server/Migrations/Routines/20260113120000_MigrateLinkedChildren.cs
@@ -7,6 +7,7 @@ using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@@ -283,9 +284,9 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine
.Select(id => _libraryManager.GetItemById(id))
.Where(item => item is not null)
.ToList();
- _libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
+ var deleted = DeleteItems(itemsToDelete!);
- _logger.LogInformation("Removed {Count} wrong-type alternate version items. They will be recreated with the correct type on next library scan.", itemsToDelete.Count);
+ _logger.LogInformation("Removed {Count} wrong-type alternate version items. They will be recreated with the correct type on next library scan.", deleted);
}
private void CleanupOrphanedAlternateVersionBaseItems(JellyfinDbContext context)
@@ -314,9 +315,9 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine
.Select(id => _libraryManager.GetItemById(id))
.Where(item => item is not null)
.ToList();
- _libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
+ var deleted = DeleteItems(itemsToDelete!);
- _logger.LogInformation("Removed {Count} orphaned alternate version BaseItems.", itemsToDelete.Count);
+ _logger.LogInformation("Removed {Count} orphaned alternate version BaseItems.", deleted);
}
private void CleanupItemsFromDeletedLibraries(JellyfinDbContext context)
@@ -343,9 +344,9 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine
.Select(id => _libraryManager.GetItemById(id))
.Where(item => item is not null)
.ToList();
- _libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
+ var deleted = DeleteItems(itemsToDelete!);
- _logger.LogInformation("Removed {Count} items from deleted libraries.", itemsToDelete.Count);
+ _logger.LogInformation("Removed {Count} items from deleted libraries.", deleted);
}
private void CleanupStaleFileEntries(JellyfinDbContext context)
@@ -431,9 +432,34 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine
.Select(id => _libraryManager.GetItemById(id))
.Where(item => item is not null)
.ToList();
- _libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
+ var deleted = DeleteItems(itemsToDelete!);
- _logger.LogInformation("Removed {Count} stale items.", itemsToDelete.Count);
+ _logger.LogInformation("Removed {Count} stale items.", deleted);
+ }
+
+ private int DeleteItems(IReadOnlyCollection<BaseItem> items)
+ {
+ if (items.Count == 0)
+ {
+ return 0;
+ }
+
+ var options = new DeleteOptions { DeleteFileLocation = false, DeleteFromExternalProvider = false };
+ var deleted = 0;
+ foreach (var item in items)
+ {
+ try
+ {
+ _libraryManager.DeleteItem(item, options);
+ deleted++;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Skipping item {ItemId} ({ItemName}): delete failed.", item.Id, item.Name ?? "Unknown");
+ }
+ }
+
+ return deleted;
}
private void CleanupOrphanedLinkedChildren(JellyfinDbContext context)
diff --git a/Jellyfin.Server/Migrations/Routines/CleanupOrphanedExtras.cs b/Jellyfin.Server/Migrations/Routines/20260113230000_CleanupOrphanedExtras.cs
index 14abaa7317..f4dfa49068 100644
--- a/Jellyfin.Server/Migrations/Routines/CleanupOrphanedExtras.cs
+++ b/Jellyfin.Server/Migrations/Routines/20260113230000_CleanupOrphanedExtras.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
+using Jellyfin.Server.Implementations.Item;
using Jellyfin.Server.Migrations.Stages;
using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Controller.Channels;
@@ -23,7 +24,7 @@ namespace Jellyfin.Server.Migrations.Routines;
/// Removes orphaned extras (items with OwnerId pointing to non-existent items).
/// Must run before EF migrations that add FK constraints on OwnerId.
/// </summary>
-[JellyfinMigration("2026-01-13T23:00:00", nameof(CleanupOrphanedExtras), Stage = JellyfinMigrationStageTypes.CoreInitialisation)]
+[JellyfinMigration("2026-01-13T23:00:00", nameof(CleanupOrphanedExtras), Stage = JellyfinMigrationStageTypes.AppInitialisation)]
[JellyfinMigrationBackup(JellyfinDb = true)]
public class CleanupOrphanedExtras : IAsyncMigrationRoutine
{
@@ -37,39 +38,14 @@ public class CleanupOrphanedExtras : IAsyncMigrationRoutine
/// <param name="logger">The startup logger.</param>
/// <param name="dbContextFactory">The database context factory.</param>
/// <param name="libraryManager">The library manager.</param>
- /// <param name="itemRepository">The item repository.</param>
- /// <param name="itemCountService">The item count service.</param>
- /// <param name="channelManager">The channel manager.</param>
- /// <param name="recordingsManager">The recordings manager.</param>
- /// <param name="mediaSourceManager">The media source manager.</param>
- /// <param name="mediaSegmentManager">The media segments manager.</param>
- /// <param name="configurationManager">The configuration manager.</param>
- /// <param name="fileSystem">The file system.</param>
public CleanupOrphanedExtras(
IStartupLogger<CleanupOrphanedExtras> logger,
IDbContextFactory<JellyfinDbContext> dbContextFactory,
- ILibraryManager libraryManager,
- IItemRepository itemRepository,
- IItemCountService itemCountService,
- IChannelManager channelManager,
- IRecordingsManager recordingsManager,
- IMediaSourceManager mediaSourceManager,
- IMediaSegmentManager mediaSegmentManager,
- IServerConfigurationManager configurationManager,
- IFileSystem fileSystem)
+ ILibraryManager libraryManager)
{
_logger = logger;
_dbContextFactory = dbContextFactory;
_libraryManager = libraryManager;
- BaseItem.LibraryManager ??= libraryManager;
- BaseItem.ItemRepository ??= itemRepository;
- BaseItem.ItemCountService ??= itemCountService;
- BaseItem.ChannelManager ??= channelManager;
- BaseItem.MediaSourceManager ??= mediaSourceManager;
- BaseItem.MediaSegmentManager ??= mediaSegmentManager;
- BaseItem.ConfigurationManager ??= configurationManager;
- BaseItem.FileSystem ??= fileSystem;
- Video.RecordingsManager ??= recordingsManager;
}
/// <inheritdoc/>
@@ -78,12 +54,19 @@ public class CleanupOrphanedExtras : IAsyncMigrationRoutine
var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
+ var placeholderOwner = Guid.Parse("00000000-0000-0000-0000-000000000001");
+#pragma warning disable RS0030 // Do not use banned APIs
var orphanedItemIds = await context.BaseItems
- .Where(b => b.OwnerId.HasValue && !b.OwnerId.Value.Equals(Guid.Empty))
- .Where(b => !context.BaseItems.Any(parent => parent.Id.Equals(b.OwnerId!.Value)))
- .Select(b => b.Id)
+ .Where(b => b.OwnerId.HasValue && b.OwnerId == placeholderOwner)
+ .Select(b => new
+ {
+ b.Id,
+ b.Path,
+ b.Type
+ })
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
+#pragma warning restore RS0030 // Do not use banned APIs
if (orphanedItemIds.Count == 0)
{
@@ -97,11 +80,16 @@ public class CleanupOrphanedExtras : IAsyncMigrationRoutine
var itemsToDelete = new List<BaseItem>();
foreach (var itemId in orphanedItemIds)
{
- var item = _libraryManager.GetItemById(itemId);
- if (item is not null)
- {
- itemsToDelete.Add(item);
- }
+ itemsToDelete.Add(BaseItemMapper.DeserializeBaseItem(
+ new Database.Implementations.Entities.BaseItemEntity()
+ {
+ Id = itemId.Id,
+ Path = itemId.Path,
+ Type = itemId.Type
+ },
+ _logger,
+ null,
+ true)!);
}
_libraryManager.DeleteItemsUnsafeFast(itemsToDelete);
diff --git a/Jellyfin.Server/Migrations/Routines/FixIncorrectOwnerIdRelationships.cs b/Jellyfin.Server/Migrations/Routines/20260115120000_FixIncorrectOwnerIdRelationships.cs
index 0baf261a2e..0baf261a2e 100644
--- a/Jellyfin.Server/Migrations/Routines/FixIncorrectOwnerIdRelationships.cs
+++ b/Jellyfin.Server/Migrations/Routines/20260115120000_FixIncorrectOwnerIdRelationships.cs
diff --git a/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs b/Jellyfin.Server/Migrations/Routines/20260206200000_FixLibrarySubtitleDownloadLanguages.cs
index 2b1f549940..2b1f549940 100644
--- a/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs
+++ b/Jellyfin.Server/Migrations/Routines/20260206200000_FixLibrarySubtitleDownloadLanguages.cs
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/20260302090000_MigrateRatingLevels.cs
index ed92c34aa3..ed92c34aa3 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
+++ b/Jellyfin.Server/Migrations/Routines/20260302090000_MigrateRatingLevels.cs
diff --git a/Jellyfin.Server/Migrations/Routines/20260508120000_MergeDuplicateMusicArtists.cs b/Jellyfin.Server/Migrations/Routines/20260508120000_MergeDuplicateMusicArtists.cs
new file mode 100644
index 0000000000..f598848465
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/20260508120000_MergeDuplicateMusicArtists.cs
@@ -0,0 +1,204 @@
+#pragma warning disable RS0030 // Do not use banned APIs
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Server.ServerSetupApp;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Merges MusicArtist records that differ only by Name casing. Prior to the case-insensitive
+/// dedup lookup added alongside this migration, the artist validator would create a second
+/// MusicArtist whenever a track tagged the artist with a different casing than the
+/// resolver-created one (e.g. "Thirty Seconds To Mars" vs. "Thirty Seconds to Mars").
+/// </summary>
+[JellyfinMigration("2026-05-08T12:00:00", nameof(MergeDuplicateMusicArtists))]
+[JellyfinMigrationBackup(JellyfinDb = true)]
+public class MergeDuplicateMusicArtists : IAsyncMigrationRoutine
+{
+ private const string MusicArtistType = "MediaBrowser.Controller.Entities.Audio.MusicArtist";
+
+ private readonly IStartupLogger<MergeDuplicateMusicArtists> _logger;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IItemPersistenceService _persistenceService;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MergeDuplicateMusicArtists"/> class.
+ /// </summary>
+ /// <param name="logger">The startup logger.</param>
+ /// <param name="dbContextFactory">The database context factory.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="persistenceService">The item persistence service.</param>
+ public MergeDuplicateMusicArtists(
+ IStartupLogger<MergeDuplicateMusicArtists> logger,
+ IDbContextFactory<JellyfinDbContext> dbContextFactory,
+ ILibraryManager libraryManager,
+ IItemPersistenceService persistenceService)
+ {
+ _logger = logger;
+ _dbContextFactory = dbContextFactory;
+ _libraryManager = libraryManager;
+ _persistenceService = persistenceService;
+ }
+
+ /// <inheritdoc/>
+ public async Task PerformAsync(CancellationToken cancellationToken)
+ {
+ var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (context.ConfigureAwait(false))
+ {
+ var artists = await context.BaseItems
+ .Where(b => b.Type == MusicArtistType && b.Name != null)
+ .Select(b => new { b.Id, b.Name, b.DateCreated })
+ .ToListAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ var groups = artists
+ .GroupBy(a => a.Name!.ToLowerInvariant())
+ .Where(g => g.Count() > 1)
+ .ToList();
+
+ if (groups.Count == 0)
+ {
+ _logger.LogInformation("No case-only duplicate MusicArtist records found.");
+ return;
+ }
+
+ _logger.LogInformation("Found {Count} groups of case-only duplicate MusicArtist records.", groups.Count);
+
+ var idsToDelete = new List<Guid>();
+ foreach (var group in groups)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var groupIds = group.Select(g => g.Id).ToArray();
+
+ // Pick the keeper: the artist with the most child references is the "real" one
+ // (the resolver-created artist with a filesystem path); the duplicates are usually
+ // empty stubs created by the validator's case-sensitive miss.
+ var stats = await context.BaseItems
+ .Where(b => groupIds.Contains(b.Id))
+ .Select(b => new
+ {
+ b.Id,
+ b.Name,
+ b.DateCreated,
+ ChildCount = context.BaseItems.Count(c => c.ParentId == b.Id),
+ AncestorCount = context.AncestorIds.Count(a => a.ParentItemId == b.Id),
+ LinkedCount = context.LinkedChildren.Count(l => l.ParentId == b.Id || l.ChildId == b.Id),
+ })
+ .ToListAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ var keeper = stats
+ .OrderByDescending(s => s.ChildCount)
+ .ThenByDescending(s => s.AncestorCount)
+ .ThenByDescending(s => s.LinkedCount)
+ .ThenBy(s => s.DateCreated)
+ .First();
+
+ foreach (var dup in stats.Where(s => s.Id != keeper.Id))
+ {
+ var keeperId = keeper.Id;
+ var dupId = dup.Id;
+
+ await context.BaseItems
+ .Where(b => b.ParentId == dupId)
+ .ExecuteUpdateAsync(s => s.SetProperty(b => b.ParentId, keeperId), cancellationToken)
+ .ConfigureAwait(false);
+
+ await context.BaseItems
+ .Where(b => b.OwnerId == dupId)
+ .ExecuteUpdateAsync(s => s.SetProperty(b => b.OwnerId, keeperId), cancellationToken)
+ .ConfigureAwait(false);
+
+ // AncestorIds PK is (ItemId, ParentItemId); drop rows that would collide before redirecting.
+ await context.AncestorIds
+ .Where(a => a.ParentItemId == dupId
+ && context.AncestorIds.Any(k => k.ParentItemId == keeperId && k.ItemId == a.ItemId))
+ .ExecuteDeleteAsync(cancellationToken)
+ .ConfigureAwait(false);
+ await context.AncestorIds
+ .Where(a => a.ParentItemId == dupId)
+ .ExecuteUpdateAsync(s => s.SetProperty(a => a.ParentItemId, keeperId), cancellationToken)
+ .ConfigureAwait(false);
+
+ // LinkedChildren PK is (ParentId, ChildId); drop colliding rows in both directions.
+ await context.LinkedChildren
+ .Where(l => l.ParentId == dupId
+ && context.LinkedChildren.Any(k => k.ParentId == keeperId && k.ChildId == l.ChildId))
+ .ExecuteDeleteAsync(cancellationToken)
+ .ConfigureAwait(false);
+ await context.LinkedChildren
+ .Where(l => l.ParentId == dupId)
+ .ExecuteUpdateAsync(s => s.SetProperty(l => l.ParentId, keeperId), cancellationToken)
+ .ConfigureAwait(false);
+ await context.LinkedChildren
+ .Where(l => l.ChildId == dupId
+ && context.LinkedChildren.Any(k => k.ChildId == keeperId && k.ParentId == l.ParentId))
+ .ExecuteDeleteAsync(cancellationToken)
+ .ConfigureAwait(false);
+ await context.LinkedChildren
+ .Where(l => l.ChildId == dupId)
+ .ExecuteUpdateAsync(s => s.SetProperty(l => l.ChildId, keeperId), cancellationToken)
+ .ConfigureAwait(false);
+
+ // UserData has UNIQUE(UserId, CustomDataKey); keep the dup's row only when the
+ // keeper has no equivalent row, otherwise the keeper's value wins.
+ await context.UserData
+ .Where(u => u.ItemId == dupId
+ && context.UserData.Any(k => k.ItemId == keeperId && k.UserId == u.UserId && k.CustomDataKey == u.CustomDataKey))
+ .ExecuteDeleteAsync(cancellationToken)
+ .ConfigureAwait(false);
+ await context.UserData
+ .Where(u => u.ItemId == dupId)
+ .ExecuteUpdateAsync(s => s.SetProperty(u => u.ItemId, keeperId), cancellationToken)
+ .ConfigureAwait(false);
+
+ idsToDelete.Add(dupId);
+ }
+
+ _logger.LogDebug(
+ "Merged duplicates for '{Name}' into {KeeperId} ({Removed} removed).",
+ keeper.Name,
+ keeper.Id,
+ stats.Count - 1);
+ }
+
+ if (idsToDelete.Count == 0)
+ {
+ return;
+ }
+
+ // Resolve via LibraryManager so DeleteItemsUnsafeFast can also remove the
+ // %MetadataPath%/artists/<Name> directories that the duplicate stubs left behind.
+ // Fall back to the persistence service for any items the LibraryManager can't resolve.
+ var itemsToDelete = idsToDelete
+ .Select(id => _libraryManager.GetItemById(id))
+ .Where(item => item is not null)
+ .ToList();
+ if (itemsToDelete.Count > 0)
+ {
+ _libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
+ }
+
+ var deletedIds = itemsToDelete.Select(i => i!.Id).ToHashSet();
+ var unresolvedIds = idsToDelete.Where(id => !deletedIds.Contains(id)).ToList();
+ if (unresolvedIds.Count > 0)
+ {
+ _persistenceService.DeleteItem(unresolvedIds);
+ }
+
+ _logger.LogInformation("Removed {Count} duplicate MusicArtist records.", idsToDelete.Count);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/20260508130000_MergeDuplicatePeople.cs b/Jellyfin.Server/Migrations/Routines/20260508130000_MergeDuplicatePeople.cs
new file mode 100644
index 0000000000..10433599fa
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/20260508130000_MergeDuplicatePeople.cs
@@ -0,0 +1,300 @@
+#pragma warning disable RS0030 // Do not use banned APIs
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Server.ServerSetupApp;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Merges case-only duplicate people. Two passes:
+/// 1) Person BaseItems whose Name differs only by casing — Person.GetPath hashes the name
+/// verbatim, so two casings produce two distinct Person rows in BaseItems.
+/// 2) Peoples lookup rows whose Name differs only by casing within the same PersonType —
+/// UpdatePeople used to insert a second Peoples row when a metadata provider returned
+/// a different casing than the row already in the table.
+/// Both bugs cause the /Persons endpoint to list the same person twice.
+/// </summary>
+[JellyfinMigration("2026-05-08T13:00:00", nameof(MergeDuplicatePeople))]
+[JellyfinMigrationBackup(JellyfinDb = true)]
+public class MergeDuplicatePeople : IAsyncMigrationRoutine
+{
+ private const string PersonType = "MediaBrowser.Controller.Entities.Person";
+
+ private readonly IStartupLogger<MergeDuplicatePeople> _logger;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IItemPersistenceService _persistenceService;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MergeDuplicatePeople"/> class.
+ /// </summary>
+ /// <param name="logger">The startup logger.</param>
+ /// <param name="dbContextFactory">The database context factory.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="persistenceService">The item persistence service.</param>
+ public MergeDuplicatePeople(
+ IStartupLogger<MergeDuplicatePeople> logger,
+ IDbContextFactory<JellyfinDbContext> dbContextFactory,
+ ILibraryManager libraryManager,
+ IItemPersistenceService persistenceService)
+ {
+ _logger = logger;
+ _dbContextFactory = dbContextFactory;
+ _libraryManager = libraryManager;
+ _persistenceService = persistenceService;
+ }
+
+ /// <inheritdoc/>
+ public async Task PerformAsync(CancellationToken cancellationToken)
+ {
+ var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (context.ConfigureAwait(false))
+ {
+ await MergePersonBaseItemsAsync(context, cancellationToken).ConfigureAwait(false);
+ await MergePeoplesRowsAsync(context, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private async Task MergePersonBaseItemsAsync(JellyfinDbContext context, CancellationToken cancellationToken)
+ {
+ var persons = await context.BaseItems
+ .Where(b => b.Type == PersonType && b.Name != null)
+ .Select(b => new { b.Id, b.Name, b.DateCreated })
+ .ToListAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ var groups = persons
+ .GroupBy(p => p.Name!.ToLowerInvariant())
+ .Where(g => g.Count() > 1)
+ .ToList();
+
+ if (groups.Count == 0)
+ {
+ _logger.LogInformation("No case-only duplicate Person BaseItems found.");
+ return;
+ }
+
+ _logger.LogInformation("Found {Count} groups of case-only duplicate Person BaseItems.", groups.Count);
+
+ var idsToDelete = new List<Guid>();
+ foreach (var group in groups)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var groupIds = group.Select(g => g.Id).ToArray();
+
+ // Pick the keeper: the Person with the most UserData rows (favorites, image
+ // refresh state) is the one users have actually interacted with.
+ var stats = await context.BaseItems
+ .Where(b => groupIds.Contains(b.Id))
+ .Select(b => new
+ {
+ b.Id,
+ b.Name,
+ b.DateCreated,
+ UserDataCount = context.UserData.Count(u => u.ItemId == b.Id),
+ LinkedCount = context.LinkedChildren.Count(l => l.ParentId == b.Id || l.ChildId == b.Id),
+ })
+ .ToListAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ var keeper = stats
+ .OrderByDescending(s => s.UserDataCount)
+ .ThenByDescending(s => s.LinkedCount)
+ .ThenBy(s => s.DateCreated)
+ .First();
+
+ foreach (var dup in stats.Where(s => s.Id != keeper.Id))
+ {
+ var keeperId = keeper.Id;
+ var dupId = dup.Id;
+
+ await context.BaseItems
+ .Where(b => b.ParentId == dupId)
+ .ExecuteUpdateAsync(s => s.SetProperty(b => b.ParentId, keeperId), cancellationToken)
+ .ConfigureAwait(false);
+
+ await context.BaseItems
+ .Where(b => b.OwnerId == dupId)
+ .ExecuteUpdateAsync(s => s.SetProperty(b => b.OwnerId, keeperId), cancellationToken)
+ .ConfigureAwait(false);
+
+ await context.AncestorIds
+ .Where(a => a.ParentItemId == dupId
+ && context.AncestorIds.Any(k => k.ParentItemId == keeperId && k.ItemId == a.ItemId))
+ .ExecuteDeleteAsync(cancellationToken)
+ .ConfigureAwait(false);
+ await context.AncestorIds
+ .Where(a => a.ParentItemId == dupId)
+ .ExecuteUpdateAsync(s => s.SetProperty(a => a.ParentItemId, keeperId), cancellationToken)
+ .ConfigureAwait(false);
+
+ await context.LinkedChildren
+ .Where(l => l.ParentId == dupId
+ && context.LinkedChildren.Any(k => k.ParentId == keeperId && k.ChildId == l.ChildId))
+ .ExecuteDeleteAsync(cancellationToken)
+ .ConfigureAwait(false);
+ await context.LinkedChildren
+ .Where(l => l.ParentId == dupId)
+ .ExecuteUpdateAsync(s => s.SetProperty(l => l.ParentId, keeperId), cancellationToken)
+ .ConfigureAwait(false);
+ await context.LinkedChildren
+ .Where(l => l.ChildId == dupId
+ && context.LinkedChildren.Any(k => k.ChildId == keeperId && k.ParentId == l.ParentId))
+ .ExecuteDeleteAsync(cancellationToken)
+ .ConfigureAwait(false);
+ await context.LinkedChildren
+ .Where(l => l.ChildId == dupId)
+ .ExecuteUpdateAsync(s => s.SetProperty(l => l.ChildId, keeperId), cancellationToken)
+ .ConfigureAwait(false);
+
+ await context.UserData
+ .Where(u => u.ItemId == dupId
+ && context.UserData.Any(k => k.ItemId == keeperId && k.UserId == u.UserId && k.CustomDataKey == u.CustomDataKey))
+ .ExecuteDeleteAsync(cancellationToken)
+ .ConfigureAwait(false);
+ await context.UserData
+ .Where(u => u.ItemId == dupId)
+ .ExecuteUpdateAsync(s => s.SetProperty(u => u.ItemId, keeperId), cancellationToken)
+ .ConfigureAwait(false);
+
+ idsToDelete.Add(dupId);
+ }
+
+ _logger.LogDebug(
+ "Merged Person BaseItems for '{Name}' into {KeeperId} ({Removed} removed).",
+ keeper.Name,
+ keeper.Id,
+ stats.Count - 1);
+ }
+
+ if (idsToDelete.Count == 0)
+ {
+ return;
+ }
+
+ // Resolve via LibraryManager so DeleteItemsUnsafeFast can also remove the
+ // %MetadataPath%/People/<Letter>/<Name> directories the duplicate stubs left behind.
+ var itemsToDelete = idsToDelete
+ .Select(id => _libraryManager.GetItemById(id))
+ .Where(item => item is not null)
+ .ToList();
+ if (itemsToDelete.Count > 0)
+ {
+ _libraryManager.DeleteItemsUnsafeFast(itemsToDelete!);
+ }
+
+ var deletedIds = itemsToDelete.Select(i => i!.Id).ToHashSet();
+ var unresolvedIds = idsToDelete.Where(id => !deletedIds.Contains(id)).ToList();
+ if (unresolvedIds.Count > 0)
+ {
+ _persistenceService.DeleteItem(unresolvedIds);
+ }
+
+ _logger.LogInformation("Removed {Count} duplicate Person BaseItems.", idsToDelete.Count);
+ }
+
+ private async Task MergePeoplesRowsAsync(JellyfinDbContext context, CancellationToken cancellationToken)
+ {
+ var people = await context.Peoples
+ .Select(p => new { p.Id, p.Name, p.PersonType })
+ .ToListAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ var groups = people
+ .GroupBy(p => (Name: p.Name.ToLowerInvariant(), p.PersonType))
+ .Where(g => g.Count() > 1)
+ .ToList();
+
+ if (groups.Count == 0)
+ {
+ _logger.LogInformation("No case-only duplicate Peoples rows found.");
+ return;
+ }
+
+ _logger.LogInformation("Found {Count} groups of case-only duplicate Peoples rows.", groups.Count);
+
+ var idsToDelete = new List<Guid>();
+ foreach (var group in groups)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var groupIds = group.Select(g => g.Id).ToArray();
+
+ // Pick the keeper: the row referenced by the most BaseItems is the one most
+ // tracks/movies already point at; the duplicates are usually orphan stubs left
+ // by a casing-mismatched insert.
+ var stats = await context.Peoples
+ .Where(p => groupIds.Contains(p.Id))
+ .Select(p => new
+ {
+ p.Id,
+ p.Name,
+ MapCount = context.PeopleBaseItemMap.Count(m => m.PeopleId == p.Id),
+ })
+ .ToListAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ var keeper = stats
+ .OrderByDescending(s => s.MapCount)
+ .ThenBy(s => s.Id)
+ .First();
+
+ foreach (var dup in stats.Where(s => s.Id != keeper.Id))
+ {
+ var keeperId = keeper.Id;
+ var dupId = dup.Id;
+
+ // PeopleBaseItemMap PK is (ItemId, PeopleId, Role); drop dup rows that would
+ // collide on (ItemId, Role) before redirecting PeopleId. Role is nullable, so
+ // match nulls explicitly.
+ await context.PeopleBaseItemMap
+ .Where(m => m.PeopleId == dupId
+ && context.PeopleBaseItemMap.Any(k => k.PeopleId == keeperId
+ && k.ItemId == m.ItemId
+ && (k.Role == m.Role || (k.Role == null && m.Role == null))))
+ .ExecuteDeleteAsync(cancellationToken)
+ .ConfigureAwait(false);
+ await context.PeopleBaseItemMap
+ .Where(m => m.PeopleId == dupId)
+ .ExecuteUpdateAsync(s => s.SetProperty(m => m.PeopleId, keeperId), cancellationToken)
+ .ConfigureAwait(false);
+
+ idsToDelete.Add(dupId);
+ }
+
+ _logger.LogDebug(
+ "Merged Peoples rows for '{Name}' into {KeeperId} ({Removed} removed).",
+ keeper.Name,
+ keeper.Id,
+ stats.Count - 1);
+ }
+
+ if (idsToDelete.Count == 0)
+ {
+ return;
+ }
+
+ var idx = 0;
+ foreach (var item in idsToDelete.Chunk(200))
+ {
+ idx++; // humans count at one
+ _logger.LogInformation("Remove batch {BatchNo}/{MaxBatches} duplicate Peoples.", idx, idsToDelete.Count / 200);
+ await context.Peoples
+ .Where(p => item.Contains(p.Id))
+ .ExecuteDeleteAsync(cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ _logger.LogInformation("Removed {Count} duplicate Peoples rows.", idsToDelete.Count);
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/20260522092304_UpdateNormalizedUsername.cs b/Jellyfin.Server/Migrations/Routines/20260522092304_UpdateNormalizedUsername.cs
new file mode 100644
index 0000000000..8100d4759e
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/20260522092304_UpdateNormalizedUsername.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Database.Implementations;
+using MediaBrowser.Controller.Configuration;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Part 2 Migration for NormalisedUsername.
+/// </summary>
+[JellyfinMigration("2026-05-22T09:23:04", nameof(UpdateNormalizedUsername), Stage = Stages.JellyfinMigrationStageTypes.CoreInitialisation)]
+#pragma warning disable SA1649 // File name should match first type name
+public class UpdateNormalizedUsername : IAsyncMigrationRoutine
+#pragma warning restore SA1649 // File name should match first type name
+{
+ private readonly IDbContextFactory<JellyfinDbContext> _contextFactory;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UpdateNormalizedUsername"/> class.
+ /// </summary>
+ /// <param name="contextFactory">Db Context factory.</param>
+ public UpdateNormalizedUsername(IDbContextFactory<JellyfinDbContext> contextFactory)
+ {
+ _contextFactory = contextFactory;
+ }
+
+ /// <inheritdoc/>
+ public async Task PerformAsync(CancellationToken cancellationToken)
+ {
+ var dbContext = await _contextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ var users = await dbContext.Users.ToListAsync(cancellationToken).ConfigureAwait(false);
+ foreach (var user in users)
+ {
+ user.NormalizedUsername = user.Username.ToUpperInvariant();
+ }
+
+ await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/20260531160000_DisableLegacyAuthorization.cs b/Jellyfin.Server/Migrations/Routines/20260531160000_DisableLegacyAuthorization.cs
new file mode 100644
index 0000000000..4b8ced90ac
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/20260531160000_DisableLegacyAuthorization.cs
@@ -0,0 +1,32 @@
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Migration to disable legacy authorization in the system config.
+/// </summary>
+[JellyfinMigration("2026-05-31T16:00:00", nameof(DisableLegacyAuthorization), Stage = Stages.JellyfinMigrationStageTypes.CoreInitialisation)]
+public class DisableLegacyAuthorization : IAsyncMigrationRoutine
+{
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DisableLegacyAuthorization"/> class.
+ /// </summary>
+ /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+ public DisableLegacyAuthorization(IServerConfigurationManager serverConfigurationManager)
+ {
+ _serverConfigurationManager = serverConfigurationManager;
+ }
+
+ /// <inheritdoc />
+ public Task PerformAsync(CancellationToken cancellationToken)
+ {
+ _serverConfigurationManager.Configuration.EnableLegacyAuthorization = false;
+ _serverConfigurationManager.SaveConfiguration();
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
index 05975929db..37bb1abe71 100644
--- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs
+++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
@@ -91,6 +91,13 @@ public sealed class SetupServer : IDisposable
_startupUiRenderer = (await ParserOptionsBuilder.New()
.WithTemplate(fileTemplate)
.WithFormatter(
+ (Version version, int arg) =>
+ {
+ // version type does not for some stupid reason implement IFormattable which morestachio relies on for ToString support therefor we need to do it manually.
+ return version.ToString(arg);
+ },
+ "ToString")
+ .WithFormatter(
(StartupLogTopic logEntry, IEnumerable<StartupLogTopic> children) =>
{
if (children.Any())
@@ -237,6 +244,7 @@ public sealed class SetupServer : IDisposable
});
});
+ var version = typeof(Emby.Server.Implementations.ApplicationHost).Assembly.GetName().Version!;
app.Run(async (context) =>
{
context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
@@ -250,7 +258,7 @@ public sealed class SetupServer : IDisposable
{
{ "isInReportingMode", _isUnhealthy },
{ "retryValue", retryAfterValue },
- { "version", typeof(Emby.Server.Implementations.ApplicationHost).Assembly.GetName().Version! },
+ { "version", version },
{ "logs", startupLogEntries },
{ "networkManagerReady", networkManager is not null },
{ "localNetworkRequest", networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress) }
diff --git a/Jellyfin.Server/ServerSetupApp/index.mstemplate.html b/Jellyfin.Server/ServerSetupApp/index.mstemplate.html
index 890a77619d..5706ce1fac 100644
--- a/Jellyfin.Server/ServerSetupApp/index.mstemplate.html
+++ b/Jellyfin.Server/ServerSetupApp/index.mstemplate.html
@@ -173,9 +173,9 @@
<header class="flex-row">
{{^IF isInReportingMode}}
- <p>Jellyfin Server {{version}} still starting. Please wait.</p>
+ <p>Jellyfin Server {{version.ToString(2)}} still starting. Please wait.</p>
{{#ELSE}}
- <p>Jellyfin Server has encountered an error and was not able to start.</p>
+ <p>Jellyfin Server {{version.ToString(2)}} has encountered an error and was not able to start.</p>
{{/ELSE}}
{{/IF}}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index f6a4ae7d6e..1802440dc4 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -1,4 +1,5 @@
using System;
+using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
@@ -6,6 +7,7 @@ using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
using Emby.Server.Implementations.EntryPoints;
+using Emby.Server.Implementations.Localization;
using Jellyfin.Api.Middleware;
using Jellyfin.Database.Implementations;
using Jellyfin.LiveTv.Extensions;
@@ -22,6 +24,7 @@ using MediaBrowser.Controller.Extensions;
using MediaBrowser.XbmcMetadata;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -127,6 +130,25 @@ namespace Jellyfin.Server
services.AddHlsPlaylistGenerator();
services.AddLiveTvServices();
+ var serverUICulture = _serverConfigurationManager.Configuration.UICulture;
+ if (string.IsNullOrEmpty(serverUICulture))
+ {
+ serverUICulture = "en-US";
+ }
+
+ CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(serverUICulture);
+
+ services.Configure<RequestLocalizationOptions>(options =>
+ {
+ var supportedUICultures = LocalizationManager.GetSupportedUICultures();
+ options.SupportedCultures = supportedUICultures;
+ options.SupportedUICultures = supportedUICultures;
+ options.DefaultRequestCulture = new RequestCulture(serverUICulture);
+ options.ApplyCurrentCultureToResponseHeaders = true;
+ options.FallBackToParentCultures = true;
+ options.FallBackToParentUICultures = true;
+ });
+
services.AddHostedService<RecordingsHost>();
services.AddHostedService<AutoDiscoveryHost>();
services.AddHostedService<NfoUserDataSaver>();
@@ -168,6 +190,8 @@ namespace Jellyfin.Server
mainApp.UseCors();
+ mainApp.UseRequestLocalization();
+
if (config.RequireHttps && _serverApplicationHost.ListenWithHttps)
{
mainApp.UseHttpsRedirection();
diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs
index 5c854b39d5..25a1022d4e 100644
--- a/MediaBrowser.Common/Net/NetworkUtils.cs
+++ b/MediaBrowser.Common/Net/NetworkUtils.cs
@@ -7,6 +7,7 @@ using System.Net.Sockets;
using System.Text.RegularExpressions;
using Jellyfin.Extensions;
using MediaBrowser.Model.Net;
+using Microsoft.Extensions.Logging;
namespace MediaBrowser.Common.Net;
@@ -166,8 +167,9 @@ public static partial class NetworkUtils
/// <param name="values">Input string array to be parsed.</param>
/// <param name="result">Collection of <see cref="IPNetwork"/>.</param>
/// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param>
+ /// <param name="logger">Optional logger used to warn about entries that fail to parse.</param>
/// <returns><c>True</c> if parsing was successful.</returns>
- public static bool TryParseToSubnets(string[] values, [NotNullWhen(true)] out IReadOnlyList<IPData>? result, bool negated = false)
+ public static bool TryParseToSubnets(string[] values, [NotNullWhen(true)] out IReadOnlyList<IPData>? result, bool negated = false, ILogger? logger = null)
{
if (values is null || values.Length == 0)
{
@@ -178,9 +180,20 @@ public static partial class NetworkUtils
List<IPData>? tmpResult = null;
for (int a = 0; a < values.Length; a++)
{
+ // Skip entries whose '!' polarity doesn't match this pass
+ var trimmed = values[a].AsSpan().Trim();
+ if (trimmed.StartsWith('!') != negated)
+ {
+ continue;
+ }
+
if (TryParseToSubnet(values[a], out var innerResult, negated))
{
- (tmpResult ??= new()).Add(innerResult);
+ (tmpResult ??= []).Add(innerResult);
+ }
+ else
+ {
+ LogInvalidSubnet(logger, values[a]);
}
}
@@ -188,6 +201,35 @@ public static partial class NetworkUtils
return result is not null;
}
+ private static void LogInvalidSubnet(ILogger? logger, string value)
+ {
+ if (logger is null)
+ {
+ return;
+ }
+
+ var trimmed = value.AsSpan().Trim();
+ if (trimmed.StartsWith('!'))
+ {
+ trimmed = trimmed[1..];
+ }
+
+ var slash = trimmed.IndexOf('/');
+ if (slash != -1
+ && trimmed.Contains(':')
+ && trimmed.IndexOf("::", StringComparison.Ordinal) == -1)
+ {
+ logger.LogWarning(
+ "Invalid IPv6 subnet '{Subnet}': IPv6 prefix-only notation is not supported. Use the full notation including '::' (e.g. '{Example}::/{Prefix}').",
+ value,
+ trimmed[..slash].ToString(),
+ trimmed[(slash + 1)..].ToString());
+ return;
+ }
+
+ logger.LogWarning("Invalid subnet '{Subnet}' will be ignored.", value);
+ }
+
/// <summary>
/// Try parsing a string into an <see cref="IPData"/>, respecting exclusions.
/// Inputs without a subnet mask will be represented as <see cref="IPData"/> with a single IP.
diff --git a/MediaBrowser.Common/Plugins/LocalPlugin.cs b/MediaBrowser.Common/Plugins/LocalPlugin.cs
index 96af423cc3..4723be1001 100644
--- a/MediaBrowser.Common/Plugins/LocalPlugin.cs
+++ b/MediaBrowser.Common/Plugins/LocalPlugin.cs
@@ -109,7 +109,7 @@ namespace MediaBrowser.Common.Plugins
{
var inst = Instance?.GetPluginInfo() ?? new PluginInfo(Manifest.Name, Version, Manifest.Description, Manifest.Id, true);
inst.Status = Manifest.Status;
- inst.HasImage = !string.IsNullOrEmpty(Manifest.ImagePath);
+ inst.HasImage = !string.IsNullOrEmpty(Manifest.ImagePath) || !string.IsNullOrEmpty(Manifest.ImageResourceName);
return inst;
}
diff --git a/MediaBrowser.Common/Plugins/PluginManifest.cs b/MediaBrowser.Common/Plugins/PluginManifest.cs
index e0847ccea4..e749e85899 100644
--- a/MediaBrowser.Common/Plugins/PluginManifest.cs
+++ b/MediaBrowser.Common/Plugins/PluginManifest.cs
@@ -108,6 +108,15 @@ namespace MediaBrowser.Common.Plugins
public string? ImagePath { get; set; }
/// <summary>
+ /// Gets or sets the name of an embedded resource in the plugin's assembly
+ /// that should be served as the plugin image.
+ /// Used by bundled/integrated plugins whose images are shipped inside the assembly
+ /// rather than on disk. Ignored when <see cref="ImagePath"/> is set.
+ /// </summary>
+ [JsonIgnore]
+ public string? ImageResourceName { get; set; }
+
+ /// <summary>
/// Gets or sets the collection of assemblies that should be loaded.
/// Paths are considered relative to the plugin folder.
/// </summary>
diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs
index 206b5ac426..8d5d54ffd9 100644
--- a/MediaBrowser.Controller/Collections/ICollectionManager.cs
+++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs
@@ -58,6 +58,14 @@ namespace MediaBrowser.Controller.Collections
IEnumerable<BaseItem> CollapseItemsWithinBoxSets(IEnumerable<BaseItem> items, User user);
/// <summary>
+ /// Gets the collections accessible to the supplied user that contain the provided item.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="itemId">The item identifier.</param>
+ /// <returns>The collections containing the item.</returns>
+ IEnumerable<BoxSet> GetCollectionsContainingItem(User user, Guid itemId);
+
+ /// <summary>
/// Gets the folder where collections are stored.
/// </summary>
/// <param name="createIfNeeded">Will create the collection folder on the storage if set to true.</param>
diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs
index a71cdbd62c..d319feb6b2 100644
--- a/MediaBrowser.Controller/Dto/DtoOptions.cs
+++ b/MediaBrowser.Controller/Dto/DtoOptions.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -8,13 +6,16 @@ using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Dto
{
+ /// <summary>
+ /// Options that control which fields and images are populated when building a <see cref="MediaBrowser.Model.Dto.BaseItemDto"/>.
+ /// </summary>
public class DtoOptions
{
- private static readonly ItemFields[] DefaultExcludedFields = new[]
- {
+ private static readonly ItemFields[] DefaultExcludedFields =
+ [
ItemFields.SeasonUserData,
ItemFields.RefreshState
- };
+ ];
private static readonly ImageType[] AllImageTypes = Enum.GetValues<ImageType>();
@@ -22,11 +23,18 @@ namespace MediaBrowser.Controller.Dto
.Except(DefaultExcludedFields)
.ToArray();
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DtoOptions"/> class with all fields enabled.
+ /// </summary>
public DtoOptions()
: this(true)
{
}
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DtoOptions"/> class.
+ /// </summary>
+ /// <param name="allFields">Whether to populate all available fields.</param>
public DtoOptions(bool allFields)
{
ImageTypeLimit = int.MaxValue;
@@ -38,23 +46,61 @@ namespace MediaBrowser.Controller.Dto
ImageTypes = AllImageTypes;
}
+ /// <summary>
+ /// Gets or sets the fields to populate on the DTO.
+ /// </summary>
public IReadOnlyList<ItemFields> Fields { get; set; }
+ /// <summary>
+ /// Gets or sets the image types to populate on the DTO.
+ /// </summary>
public IReadOnlyList<ImageType> ImageTypes { get; set; }
+ /// <summary>
+ /// Gets or sets the maximum number of images to return per image type.
+ /// </summary>
public int ImageTypeLimit { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether image information is populated.
+ /// </summary>
public bool EnableImages { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether program recording information is populated.
+ /// </summary>
public bool AddProgramRecordingInfo { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether user data is populated.
+ /// </summary>
public bool EnableUserData { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether the currently airing program is populated.
+ /// </summary>
public bool AddCurrentProgram { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether an episode's portrait poster (its season's primary
+ /// image, falling back to the series') should replace the episode's own (16:9) primary image.
+ /// Used by views that render episodes as poster cards, e.g. "Latest".
+ /// </summary>
+ public bool PreferEpisodeParentPoster { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether the specified field is populated.
+ /// </summary>
+ /// <param name="field">The field to check.</param>
+ /// <returns><c>true</c> if the field is populated; otherwise, <c>false</c>.</returns>
public bool ContainsField(ItemFields field)
=> Fields.Contains(field);
+ /// <summary>
+ /// Gets the number of images to return for the specified image type.
+ /// </summary>
+ /// <param name="type">The image type.</param>
+ /// <returns>The image limit for the type, or 0 if the type is not enabled.</returns>
public int GetImageLimit(ImageType type)
{
if (EnableImages && ImageTypes.Contains(type))
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 4cdcaabbb1..21304768bd 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -23,7 +23,6 @@ using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Persistence;
@@ -94,6 +93,8 @@ namespace MediaBrowser.Controller.Entities
private string _name;
+ private string _originalLanguage;
+
public const char SlugChar = '-';
protected BaseItem()
@@ -217,7 +218,11 @@ namespace MediaBrowser.Controller.Entities
public string OriginalTitle { get; set; }
[JsonIgnore]
- public string OriginalLanguage { get; set; }
+ public string OriginalLanguage
+ {
+ get => _originalLanguage;
+ set => _originalLanguage = LocalizationManager?.FindLanguageInfo(value)?.TwoLetterISOLanguageName ?? value;
+ }
/// <summary>
/// Gets or sets the id.
@@ -1128,15 +1133,7 @@ namespace MediaBrowser.Controller.Entities
ArgumentNullException.ThrowIfNull(item);
var protocol = item.PathProtocol;
-
- // Resolve the item path so everywhere we use the media source it will always point to
- // the correct path even if symlinks are in use. Calling ResolveLinkTarget on a non-link
- // path will return null, so it's safe to check for all paths.
var itemPath = item.Path;
- if (protocol is MediaProtocol.File && FileSystemHelper.ResolveLinkTarget(itemPath, returnFinalTarget: true) is { Exists: true } linkInfo)
- {
- itemPath = linkInfo.FullName;
- }
var info = new MediaSourceInfo
{
@@ -1564,7 +1561,7 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// Gets the preferred metadata language.
+ /// Gets the preferred metadata country code.
/// </summary>
/// <returns>System.String.</returns>
public string GetPreferredMetadataCountryCode()
@@ -1598,6 +1595,15 @@ namespace MediaBrowser.Controller.Entities
return lang;
}
+ /// <summary>
+ /// Gets the original language of the item, inheriting from parent items if necessary.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ public virtual string GetInheritedOriginalLanguage()
+ {
+ return OriginalLanguage;
+ }
+
public virtual bool IsSaveLocalMetadataEnabled()
{
if (SourceType == SourceType.Channel)
@@ -2712,7 +2718,7 @@ namespace MediaBrowser.Controller.Entities
public IReadOnlyList<BaseItem> GetThemeSongs(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> orderBy)
{
- return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong), user, orderBy).ToArray();
+ return LibraryManager.Sort(GetExtras(user).Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeSong), user, orderBy).ToArray();
}
public IReadOnlyList<BaseItem> GetThemeVideos(User user = null)
@@ -2722,16 +2728,17 @@ namespace MediaBrowser.Controller.Entities
public IReadOnlyList<BaseItem> GetThemeVideos(User user, IEnumerable<(ItemSortBy SortBy, SortOrder SortOrder)> orderBy)
{
- return LibraryManager.Sort(GetExtras().Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo), user, orderBy).ToArray();
+ return LibraryManager.Sort(GetExtras(user).Where(e => e.ExtraType == Model.Entities.ExtraType.ThemeVideo), user, orderBy).ToArray();
}
/// <summary>
/// Get all extras associated with this item, sorted by <see cref="SortName"/>.
/// </summary>
+ /// <param name="user">The user to apply parental restrictions for, or <c>null</c> to skip restriction checks.</param>
/// <returns>An enumerable containing the items.</returns>
- public IEnumerable<BaseItem> GetExtras()
+ public IEnumerable<BaseItem> GetExtras(User user = null)
{
- return LibraryManager.GetItemList(new InternalItemsQuery()
+ return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
OwnerIds = [Id],
OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)]
@@ -2742,10 +2749,11 @@ namespace MediaBrowser.Controller.Entities
/// Get all extras with specific types that are associated with this item.
/// </summary>
/// <param name="extraTypes">The types of extras to retrieve.</param>
+ /// <param name="user">The user to apply parental restrictions for, or <c>null</c> to skip restriction checks.</param>
/// <returns>An enumerable containing the extras.</returns>
- public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes)
+ public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes, User user = null)
{
- return LibraryManager.GetItemList(new InternalItemsQuery()
+ return LibraryManager.GetItemList(new InternalItemsQuery(user)
{
OwnerIds = [Id],
ExtraTypes = extraTypes.ToArray(),
diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs
index c56603a3eb..380041af84 100644
--- a/MediaBrowser.Controller/Entities/Extensions.cs
+++ b/MediaBrowser.Controller/Entities/Extensions.cs
@@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Entities
}
else
{
- item.RemoteTrailers = [..item.RemoteTrailers, mediaUrl];
+ item.RemoteTrailers = [.. item.RemoteTrailers, mediaUrl];
}
}
}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 5fa1213db3..25cbcedc5f 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -906,7 +906,10 @@ namespace MediaBrowser.Controller.Entities
query.Parent = this;
}
- if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.BoxSet)
+ // BoxSets and Playlists can have per-user visibility (shares/open access) that is stored in the
+ // serialized item data and cannot be evaluated by the database query, so filter them in memory.
+ if (query.IncludeItemTypes.Length > 0
+ && query.IncludeItemTypes.All(t => t == BaseItemKind.BoxSet || t == BaseItemKind.Playlist))
{
return QueryWithPostFiltering(query);
}
@@ -927,7 +930,7 @@ namespace MediaBrowser.Controller.Entities
if (user is not null)
{
- // needed for boxsets
+ // needed for boxsets and playlists
itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User));
}
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index fa82ea8663..422c40ce5d 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -21,6 +21,7 @@ namespace MediaBrowser.Controller.Entities
AlbumArtistIds = [];
AlbumIds = [];
AncestorIds = [];
+ LinkedChildAncestorIds = [];
ArtistIds = [];
BlockUnratedItems = [];
BoxSetLibraryFolders = [];
@@ -58,6 +59,8 @@ namespace MediaBrowser.Controller.Entities
VideoTypes = [];
Years = [];
SkipDeserialization = false;
+ AudioLanguages = [];
+ SubtitleLanguages = [];
}
public InternalItemsQuery(User? user)
@@ -263,6 +266,12 @@ namespace MediaBrowser.Controller.Entities
public Guid[] AncestorIds { get; set; }
+ /// <summary>
+ /// Gets or sets a list of ancestor ids that the item's linked children must descend from.
+ /// Useful for filtering BoxSets/Playlists to only those that contain items from a specific library.
+ /// </summary>
+ public Guid[] LinkedChildAncestorIds { get; set; }
+
public Guid[] TopParentIds { get; set; }
public CollectionType?[] PresetViews { get; set; }
@@ -351,6 +360,8 @@ namespace MediaBrowser.Controller.Entities
public Dictionary<string, string>? HasAnyProviderId { get; set; }
+ public Dictionary<string, string[]>? HasAnyProviderIds { get; set; }
+
public Guid[] AlbumArtistIds { get; set; }
public Guid[] BoxSetLibraryFolders { get; set; }
@@ -385,6 +396,10 @@ namespace MediaBrowser.Controller.Entities
public bool IncludeExtras { get; set; }
+ public IReadOnlyList<string> AudioLanguages { get; set; }
+
+ public IReadOnlyList<string> SubtitleLanguages { get; set; }
+
public void SetUser(User user)
{
var maxRating = user.MaxParentalRatingScore;
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index dbe6f94dfd..42e4f79942 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -153,6 +153,12 @@ namespace MediaBrowser.Controller.Entities.TV
return 16.0 / 9;
}
+ /// <inheritdoc />
+ public override string GetInheritedOriginalLanguage()
+ {
+ return OriginalLanguage ?? Series?.GetInheritedOriginalLanguage();
+ }
+
public override List<string> GetUserDataKeys()
{
var list = base.GetUserDataKeys();
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
index f70f7dfb4c..e96ed05a5e 100644
--- a/MediaBrowser.Controller/Entities/TV/Season.cs
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -128,6 +128,12 @@ namespace MediaBrowser.Controller.Entities.TV
return result;
}
+ /// <inheritdoc />
+ public override string GetInheritedOriginalLanguage()
+ {
+ return OriginalLanguage ?? Series?.GetInheritedOriginalLanguage();
+ }
+
public override string CreatePresentationUniqueKey()
{
if (IndexNumber.HasValue)
diff --git a/MediaBrowser.Controller/Entities/TagExtensions.cs b/MediaBrowser.Controller/Entities/TagExtensions.cs
index c1e4d1db2f..07c2298fce 100644
--- a/MediaBrowser.Controller/Entities/TagExtensions.cs
+++ b/MediaBrowser.Controller/Entities/TagExtensions.cs
@@ -15,6 +15,7 @@ namespace MediaBrowser.Controller.Entities
throw new ArgumentNullException(nameof(name));
}
+ name = name.Trim();
var current = item.Tags;
if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
@@ -25,7 +26,7 @@ namespace MediaBrowser.Controller.Entities
}
else
{
- item.Tags = [..current, name];
+ item.Tags = [.. current, name];
}
}
}
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index 80bcd62dcd..e7a5672ebd 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -10,6 +10,7 @@ using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
@@ -278,6 +279,17 @@ namespace MediaBrowser.Controller.Entities
return linkedVersionCount + localVersionCount + 1;
}
+ /// <inheritdoc />
+ public override string GetInheritedOriginalLanguage()
+ {
+ if (ExtraType.GetValueOrDefault() == Model.Entities.ExtraType.Trailer)
+ {
+ return GetOwner()?.GetInheritedOriginalLanguage();
+ }
+
+ return OriginalLanguage ?? GetOwner()?.GetInheritedOriginalLanguage();
+ }
+
public override List<string> GetUserDataKeys()
{
var list = base.GetUserDataKeys();
@@ -379,13 +391,13 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Gets the additional parts.
/// </summary>
+ /// <param name="user">The user to apply parental restrictions for, or <c>null</c> to skip restriction checks.</param>
/// <returns>IEnumerable{Video}.</returns>
- public IOrderedEnumerable<Video> GetAdditionalParts()
+ public IOrderedEnumerable<Video> GetAdditionalParts(User user = null)
{
return GetAdditionalPartIds()
- .Select(i => LibraryManager.GetItemById(i))
+ .Select(i => LibraryManager.GetItemById<Video>(i, user))
.Where(i => i is not null)
- .OfType<Video>()
.OrderBy(i => i.SortName);
}
diff --git a/MediaBrowser.Controller/IO/IPathManager.cs b/MediaBrowser.Controller/IO/IPathManager.cs
index eb67437545..30961c7610 100644
--- a/MediaBrowser.Controller/IO/IPathManager.cs
+++ b/MediaBrowser.Controller/IO/IPathManager.cs
@@ -22,30 +22,30 @@ public interface IPathManager
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="streamIndex">The stream index.</param>
/// <param name="extension">The subtitle file extension.</param>
- /// <returns>The absolute path.</returns>
- public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension);
+ /// <returns>The absolute path, or <c>null</c> if <paramref name="mediaSourceId"/> is not a valid GUID.</returns>
+ public string? GetSubtitlePath(string mediaSourceId, int streamIndex, string extension);
/// <summary>
/// Gets the path to the subtitle file.
/// </summary>
/// <param name="mediaSourceId">The media source id.</param>
- /// <returns>The absolute path.</returns>
- public string GetSubtitleFolderPath(string mediaSourceId);
+ /// <returns>The absolute path, or <c>null</c> if <paramref name="mediaSourceId"/> is not a valid GUID.</returns>
+ public string? GetSubtitleFolderPath(string mediaSourceId);
/// <summary>
/// Gets the path to the attachment file.
/// </summary>
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="fileName">The attachmentFileName index.</param>
- /// <returns>The absolute path.</returns>
- public string GetAttachmentPath(string mediaSourceId, string fileName);
+ /// <returns>The absolute path, or <c>null</c> if <paramref name="mediaSourceId"/> is not a valid GUID.</returns>
+ public string? GetAttachmentPath(string mediaSourceId, string fileName);
/// <summary>
/// Gets the path to the attachment folder.
/// </summary>
/// <param name="mediaSourceId">The media source id.</param>
- /// <returns>The absolute path.</returns>
- public string GetAttachmentFolderPath(string mediaSourceId);
+ /// <returns>The absolute path, or <c>null</c> if <paramref name="mediaSourceId"/> is not a valid GUID.</returns>
+ public string? GetAttachmentFolderPath(string mediaSourceId);
/// <summary>
/// Gets the chapter images data path.
diff --git a/MediaBrowser.Controller/Library/IBatchLocalSimilarItemsProvider.cs b/MediaBrowser.Controller/Library/IBatchLocalSimilarItemsProvider.cs
new file mode 100644
index 0000000000..af49711606
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IBatchLocalSimilarItemsProvider.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// A local similar items provider that supports batch queries across multiple source items.
+/// Implementations share access filtering and entity loading across all sources for better performance.
+/// </summary>
+public interface IBatchLocalSimilarItemsProvider : ISimilarItemsProvider
+{
+ /// <summary>
+ /// Gets similar items for multiple source items in a single batch.
+ /// </summary>
+ /// <param name="sourceItems">The source items to find similar items for.</param>
+ /// <param name="query">The query options.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Per-source-item results keyed by source item ID.</returns>
+ Task<Dictionary<Guid, IReadOnlyList<BaseItem>>> GetBatchSimilarItemsAsync(
+ IReadOnlyList<BaseItem> sourceItems,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken);
+}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index f5e3d7034e..0b64da291c 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -598,6 +598,14 @@ namespace MediaBrowser.Controller.Library
IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query);
/// <summary>
+ /// Gets the distinct people names per item for multiple items.
+ /// </summary>
+ /// <param name="itemIds">The item IDs.</param>
+ /// <param name="personTypes">The person types to include.</param>
+ /// <returns>A dictionary mapping each item ID to its distinct people names. Items with no matching people are omitted.</returns>
+ IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IReadOnlyList<string> personTypes);
+
+ /// <summary>
/// Queries the items.
/// </summary>
/// <param name="query">The query.</param>
@@ -784,5 +792,12 @@ namespace MediaBrowser.Controller.Library
/// <param name="query">The query filter.</param>
/// <returns>Aggregated filter values.</returns>
QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery query);
+
+ /// <summary>
+ /// Gets a list of all language codes of the provided stream type.
+ /// </summary>
+ /// <param name="mediaStreamType">The stream type.</param>
+ /// <returns>List of language codes.</returns>
+ IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType);
}
}
diff --git a/MediaBrowser.Controller/Library/ILocalSimilarItemsProvider.cs b/MediaBrowser.Controller/Library/ILocalSimilarItemsProvider.cs
new file mode 100644
index 0000000000..b8e41ec810
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ILocalSimilarItemsProvider.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// Provides similar items from the local library.
+/// Returns fully resolved BaseItems directly - no additional resolution needed.
+/// </summary>
+public interface ILocalSimilarItemsProvider : ISimilarItemsProvider
+{
+ /// <summary>
+ /// Determines whether the provider can handle items of the specified type.
+ /// </summary>
+ /// <param name="itemType">The item type.</param>
+ /// <returns><c>true</c> if the provider handles this item type; otherwise <c>false</c>.</returns>
+ bool Supports(Type itemType);
+
+ /// <summary>
+ /// Gets similar items from the local library.
+ /// </summary>
+ /// <param name="item">The source item to find similar items for.</param>
+ /// <param name="query">The query options (user, limit, exclusions, etc.).</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>The list of similar items from the library.</returns>
+ Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(
+ BaseItem item,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken);
+}
+
+/// <summary>
+/// Provides similar items from the local library for a specific item type.
+/// Returns fully resolved BaseItems directly - no additional resolution needed.
+/// </summary>
+/// <typeparam name="TItemType">The type of item this provider handles.</typeparam>
+public interface ILocalSimilarItemsProvider<TItemType> : ILocalSimilarItemsProvider
+ where TItemType : BaseItem
+{
+ /// <summary>
+ /// Gets similar items from the local library.
+ /// </summary>
+ /// <param name="item">The source item to find similar items for.</param>
+ /// <param name="query">The query options (user, limit, exclusions, etc.).</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>The list of similar items from the library.</returns>
+ Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(
+ TItemType item,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken);
+
+ bool ILocalSimilarItemsProvider.Supports(Type itemType)
+ => typeof(TItemType).IsAssignableFrom(itemType);
+
+ Task<IReadOnlyList<BaseItem>> ILocalSimilarItemsProvider.GetSimilarItemsAsync(
+ BaseItem item,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken)
+ => GetSimilarItemsAsync((TItemType)item, query, cancellationToken);
+}
diff --git a/MediaBrowser.Controller/Library/IRemoteSimilarItemsProvider.cs b/MediaBrowser.Controller/Library/IRemoteSimilarItemsProvider.cs
new file mode 100644
index 0000000000..3803e51769
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IRemoteSimilarItemsProvider.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// Provides similar item references from remote/external sources.
+/// Returns lightweight references with ProviderIds that the manager resolves to library items.
+/// </summary>
+public interface IRemoteSimilarItemsProvider : ISimilarItemsProvider
+{
+ /// <summary>
+ /// Determines whether the provider can handle items of the specified type.
+ /// </summary>
+ /// <param name="itemType">The item type.</param>
+ /// <returns><c>true</c> if the provider handles this item type; otherwise <c>false</c>.</returns>
+ bool Supports(Type itemType);
+
+ /// <summary>
+ /// Gets similar item references from an external source as an async stream.
+ /// </summary>
+ /// <param name="item">The source item to find similar items for.</param>
+ /// <param name="query">The query options (user, limit, exclusions).</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>An async enumerable of similar item references.</returns>
+ IAsyncEnumerable<SimilarItemReference> GetSimilarItemsAsync(
+ BaseItem item,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken);
+}
+
+/// <summary>
+/// Provides similar item references from remote/external sources for a specific item type.
+/// Returns lightweight references with ProviderIds that the manager resolves to library items.
+/// </summary>
+/// <typeparam name="TItemType">The type of item this provider handles.</typeparam>
+public interface IRemoteSimilarItemsProvider<TItemType> : IRemoteSimilarItemsProvider
+ where TItemType : BaseItem
+{
+ /// <summary>
+ /// Gets similar item references from an external source as an async stream.
+ /// </summary>
+ /// <param name="item">The source item to find similar items for.</param>
+ /// <param name="query">The query options (user, limit, exclusions).</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>An async enumerable of similar item references.</returns>
+ IAsyncEnumerable<SimilarItemReference> GetSimilarItemsAsync(
+ TItemType item,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken);
+
+ bool IRemoteSimilarItemsProvider.Supports(Type itemType)
+ => typeof(TItemType).IsAssignableFrom(itemType);
+
+ IAsyncEnumerable<SimilarItemReference> IRemoteSimilarItemsProvider.GetSimilarItemsAsync(
+ BaseItem item,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken)
+ => GetSimilarItemsAsync((TItemType)item, query, cancellationToken);
+}
diff --git a/MediaBrowser.Controller/Library/ISimilarItemsManager.cs b/MediaBrowser.Controller/Library/ISimilarItemsManager.cs
new file mode 100644
index 0000000000..36fa547eeb
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ISimilarItemsManager.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Database.Implementations.Entities;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// Interface for managing similar items providers and operations.
+/// </summary>
+public interface ISimilarItemsManager
+{
+ /// <summary>
+ /// Registers similar items providers discovered through dependency injection.
+ /// </summary>
+ /// <param name="providers">The similar items providers to register.</param>
+ void AddParts(IEnumerable<ISimilarItemsProvider> providers);
+
+ /// <summary>
+ /// Gets the similar items providers for a specific item type.
+ /// </summary>
+ /// <typeparam name="T">The item type.</typeparam>
+ /// <returns>The list of similar items providers for that type.</returns>
+ IReadOnlyList<ISimilarItemsProvider> GetSimilarItemsProviders<T>()
+ where T : BaseItem;
+
+ /// <summary>
+ /// Gets similar items for the specified item.
+ /// </summary>
+ /// <param name="item">The source item to find similar items for.</param>
+ /// <param name="excludeArtistIds">Artist IDs to exclude from results.</param>
+ /// <param name="user">The user context.</param>
+ /// <param name="dtoOptions">The DTO options.</param>
+ /// <param name="limit">Maximum number of results.</param>
+ /// <param name="libraryOptions">The library options for provider configuration.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The list of similar items.</returns>
+ Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(
+ BaseItem item,
+ IReadOnlyList<Guid> excludeArtistIds,
+ User? user,
+ DtoOptions dtoOptions,
+ int? limit,
+ LibraryOptions? libraryOptions,
+ CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Builds movie recommendations for a user: a mix of similar-items and person-based categories,
+ /// scheduled round-robin and capped to <paramref name="categoryLimit"/>.
+ /// </summary>
+ /// <param name="user">The user the recommendations are for. May be <see langword="null"/> for anonymous access.</param>
+ /// <param name="parentId">The library/folder to localize the search to. Pass <see cref="Guid.Empty"/> to use the root.</param>
+ /// <param name="categoryLimit">Maximum number of recommendation categories to return.</param>
+ /// <param name="itemLimit">Maximum number of items per category.</param>
+ /// <param name="dtoOptions">DTO options used when querying the library.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The list of recommendation categories, ordered by <see cref="RecommendationType"/>.</returns>
+ Task<IReadOnlyList<SimilarItemsRecommendation>> GetMovieRecommendationsAsync(
+ User? user,
+ Guid parentId,
+ int categoryLimit,
+ int itemLimit,
+ DtoOptions dtoOptions,
+ CancellationToken cancellationToken);
+}
diff --git a/MediaBrowser.Controller/Library/ISimilarItemsProvider.cs b/MediaBrowser.Controller/Library/ISimilarItemsProvider.cs
new file mode 100644
index 0000000000..0d089369a8
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ISimilarItemsProvider.cs
@@ -0,0 +1,26 @@
+using System;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// Base marker interface for similar items providers.
+/// </summary>
+public interface ISimilarItemsProvider
+{
+ /// <summary>
+ /// Gets the name of the provider.
+ /// </summary>
+ string Name { get; }
+
+ /// <summary>
+ /// Gets the type of the provider.
+ /// </summary>
+ MetadataPluginType Type { get; }
+
+ /// <summary>
+ /// Gets the cache duration for results from this provider.
+ /// If null, results will not be cached.
+ /// </summary>
+ TimeSpan? CacheDuration => null;
+}
diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
index b558ef73d5..c5e7ae4913 100644
--- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -117,7 +117,7 @@ namespace MediaBrowser.Controller.Library
get
{
var paths = string.IsNullOrEmpty(Path) ? Array.Empty<string>() : [Path];
- return AdditionalLocations is null ? paths : [..paths, ..AdditionalLocations];
+ return AdditionalLocations is null ? paths : [.. paths, .. AdditionalLocations];
}
}
diff --git a/MediaBrowser.Controller/Library/SimilarItemReference.cs b/MediaBrowser.Controller/Library/SimilarItemReference.cs
new file mode 100644
index 0000000000..2a40c93bdd
--- /dev/null
+++ b/MediaBrowser.Controller/Library/SimilarItemReference.cs
@@ -0,0 +1,22 @@
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// A reference to a similar item by provider ID with a similarity score.
+/// </summary>
+public class SimilarItemReference
+{
+ /// <summary>
+ /// Gets or sets the provider name (e.g., "Tmdb", "MusicBrainzArtist").
+ /// </summary>
+ public required string ProviderName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the provider ID value.
+ /// </summary>
+ public required string ProviderId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the similarity score (0.0 to 1.0).
+ /// </summary>
+ public float? Score { get; set; }
+}
diff --git a/MediaBrowser.Controller/Library/SimilarItemsQuery.cs b/MediaBrowser.Controller/Library/SimilarItemsQuery.cs
new file mode 100644
index 0000000000..1ed3ceec16
--- /dev/null
+++ b/MediaBrowser.Controller/Library/SimilarItemsQuery.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using Jellyfin.Database.Implementations.Entities;
+using MediaBrowser.Controller.Dto;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// Query options for similar items requests.
+/// </summary>
+public class SimilarItemsQuery
+{
+ /// <summary>
+ /// Gets or sets the user context.
+ /// </summary>
+ public User? User { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum number of results.
+ /// </summary>
+ public int? Limit { get; set; }
+
+ /// <summary>
+ /// Gets or sets the DTO options.
+ /// </summary>
+ public DtoOptions? DtoOptions { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item IDs to exclude from results.
+ /// </summary>
+ public IReadOnlyList<Guid> ExcludeItemIds { get; set; } = [];
+
+ /// <summary>
+ /// Gets or sets the artist IDs to exclude from results.
+ /// </summary>
+ public IReadOnlyList<Guid> ExcludeArtistIds { get; set; } = [];
+}
diff --git a/MediaBrowser.Controller/Library/SimilarItemsRecommendation.cs b/MediaBrowser.Controller/Library/SimilarItemsRecommendation.cs
new file mode 100644
index 0000000000..71346fcadf
--- /dev/null
+++ b/MediaBrowser.Controller/Library/SimilarItemsRecommendation.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Controller.Library;
+
+/// <summary>
+/// A recommendation category derived from a baseline item, holding similar items prior to DTO conversion.
+/// </summary>
+public sealed class SimilarItemsRecommendation
+{
+ /// <summary>
+ /// Gets the display name of the baseline item the recommendation is based on.
+ /// </summary>
+ public required string BaselineItemName { get; init; }
+
+ /// <summary>
+ /// Gets an identifier for the recommendation category.
+ /// </summary>
+ public required Guid CategoryId { get; init; }
+
+ /// <summary>
+ /// Gets the recommendation type.
+ /// </summary>
+ public required RecommendationType RecommendationType { get; init; }
+
+ /// <summary>
+ /// Gets the similar items for the baseline, ordered by relevance.
+ /// </summary>
+ public required IReadOnlyList<BaseItem> Items { get; init; }
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
index 10f2f04af6..34826982af 100644
--- a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
+++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs
@@ -92,6 +92,12 @@ namespace MediaBrowser.Controller.MediaEncoding
public string CodecTag { get; set; }
/// <summary>
+ /// Gets or sets the rotation.
+ /// </summary>
+ /// <value>The video rotation angle, usually 0 or +-90/180.</value>
+ public string Rotation { get; set; }
+
+ /// <summary>
/// Gets or sets the framerate.
/// </summary>
/// <value>The framerate.</value>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index a0e04eae63..ff8d84d45e 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -86,6 +86,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1);
private readonly Version _minFFmpegReadrateCatchupOption = new Version(8, 0);
+ private readonly Version _minFFmpegNoiseBsfDrop = new Version(5, 0);
private static readonly string[] _videoProfilesH264 =
[
@@ -1267,16 +1268,13 @@ namespace MediaBrowser.Controller.MediaEncoding
.Append(_mediaEncoder.GetInputPathArgument(state));
}
- // sub2video for external graphical subtitles
- if (state.SubtitleStream is not null
- && ShouldEncodeSubtitle(state)
- && !state.SubtitleStream.IsTextSubtitleStream
- && state.SubtitleStream.IsExternal)
+ if (NeedsExternalSubtitleMuxing(state))
{
var subtitlePath = state.SubtitleStream.Path;
- var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
+ var isGraphicalBurnIn = ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream;
// dvdsub/vobsub graphical subtitles use .sub+.idx pairs
+ var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase))
{
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
@@ -1307,7 +1305,7 @@ namespace MediaBrowser.Controller.MediaEncoding
arg.Append(' ').Append(seekSubParam);
}
- if (!string.IsNullOrEmpty(canvasArgs))
+ if (isGraphicalBurnIn && !string.IsNullOrEmpty(canvasArgs))
{
arg.Append(canvasArgs);
}
@@ -1550,20 +1548,61 @@ namespace MediaBrowser.Controller.MediaEncoding
public string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer)
{
- var bitStreamArgs = string.Empty;
+ var filters = new List<string>();
+
+ var noiseFilter = GetCopiedAudioTrimBsf(state);
+ if (!string.IsNullOrEmpty(noiseFilter))
+ {
+ filters.Add(noiseFilter);
+ }
+
var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.');
// Apply aac_adtstoasc bitstream filter when media source is in mpegts.
if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)
&& (string.Equals(mediaSourceContainer, "ts", StringComparison.OrdinalIgnoreCase)
|| string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase)
- || string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
+ || string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase))
+ && IsAAC(state.AudioStream))
{
- bitStreamArgs = GetBitStreamArgs(state, MediaStreamType.Audio);
- bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs;
+ filters.Add("aac_adtstoasc");
}
- return bitStreamArgs;
+ return filters.Count == 0
+ ? string.Empty
+ : " -bsf:a " + string.Join(',', filters);
+ }
+
+ // When video is transcoded, accurate_seek (the default) trims video to the
+ // exact seek point via decoder-side frame discard. But stream-copied audio
+ // bypasses the decoder, so it starts from the nearest keyframe — potentially
+ // seconds before the target. Use the noise bsf to drop copied audio packets
+ // before the seek target, achieving the same trim precision without
+ // re-encoding. The noise bsf's drop= parameter requires ffmpeg >= 5.0.
+ // Important: make sure not to use it with wtv because it breaks seeking
+ private string GetCopiedAudioTrimBsf(EncodingJobInfo state)
+ {
+ if (state.TranscodingType is not TranscodingJobType.Hls
+ || !state.IsVideoRequest
+ || IsCopyCodec(state.OutputVideoCodec)
+ || !IsCopyCodec(state.OutputAudioCodec)
+ || string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase)
+ || _mediaEncoder.EncoderVersion < _minFFmpegNoiseBsfDrop)
+ {
+ return null;
+ }
+
+ var startTicks = state.BaseRequest.StartTimeTicks ?? 0;
+ if (startTicks <= 0)
+ {
+ return null;
+ }
+
+ var seekSeconds = startTicks / (double)TimeSpan.TicksPerSecond;
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ "noise=drop='lt(pts*tb\\,{0:F3})'",
+ seekSeconds);
}
public static string GetSegmentFileExtension(string segmentContainer)
@@ -1645,10 +1684,9 @@ namespace MediaBrowser.Controller.MediaEncoding
}
if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "av1_amf", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase))
{
- // Override the too high default qmin 18 in transcoding preset
+ // Override the too high default qmin 18 in transcoding preset in legacy h26x_amf
return FormattableString.Invariant($" -rc cbr -qmin 0 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
}
@@ -1767,13 +1805,13 @@ namespace MediaBrowser.Controller.MediaEncoding
{
param += encoderPreset switch
{
- EncoderPreset.veryslow => " -preset p7",
- EncoderPreset.slower => " -preset p6",
- EncoderPreset.slow => " -preset p5",
- EncoderPreset.medium => " -preset p4",
- EncoderPreset.fast => " -preset p3",
- EncoderPreset.faster => " -preset p2",
- _ => " -preset p1"
+ EncoderPreset.veryslow => " -preset p7",
+ EncoderPreset.slower => " -preset p6",
+ EncoderPreset.slow => " -preset p5",
+ EncoderPreset.medium => " -preset p4",
+ EncoderPreset.fast => " -preset p3",
+ EncoderPreset.faster => " -preset p2",
+ _ => " -preset p1"
};
}
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf)
@@ -1783,11 +1821,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
param += encoderPreset switch
{
- EncoderPreset.veryslow => " -quality quality",
- EncoderPreset.slower => " -quality quality",
- EncoderPreset.slow => " -quality quality",
- EncoderPreset.medium => " -quality balanced",
- _ => " -quality speed"
+ EncoderPreset.veryslow => " -quality quality",
+ EncoderPreset.slower => " -quality quality",
+ EncoderPreset.slow => " -quality quality",
+ EncoderPreset.medium => " -quality balanced",
+ _ => " -quality speed"
};
if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
@@ -1807,11 +1845,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
param += encoderPreset switch
{
- EncoderPreset.veryslow => " -prio_speed 0",
- EncoderPreset.slower => " -prio_speed 0",
- EncoderPreset.slow => " -prio_speed 0",
- EncoderPreset.medium => " -prio_speed 0",
- _ => " -prio_speed 1"
+ EncoderPreset.veryslow => " -prio_speed 0",
+ EncoderPreset.slower => " -prio_speed 0",
+ EncoderPreset.slow => " -prio_speed 0",
+ EncoderPreset.medium => " -prio_speed 0",
+ _ => " -prio_speed 1"
};
}
@@ -1880,10 +1918,12 @@ namespace MediaBrowser.Controller.MediaEncoding
var sub2videoParam = enableSub2video ? ":sub2video=1" : string.Empty;
var fontPath = _pathManager.GetAttachmentFolderPath(state.MediaSource.Id);
- var fontParam = string.Format(
- CultureInfo.InvariantCulture,
- ":fontsdir='{0}'",
- _mediaEncoder.EscapeSubtitleFilterPath(fontPath));
+ var fontParam = fontPath is null
+ ? string.Empty
+ : string.Format(
+ CultureInfo.InvariantCulture,
+ ":fontsdir='{0}'",
+ _mediaEncoder.EscapeSubtitleFilterPath(fontPath));
if (state.SubtitleStream.IsExternal)
{
@@ -2016,11 +2056,15 @@ namespace MediaBrowser.Controller.MediaEncoding
args += keyFrameArg + gopArg;
}
- // global_header produced by AMD HEVC VA-API encoder causes non-playable fMP4 on iOS
+ // The in-band Parameter Sets generated by the AMD HEVC VA-API encoder is inconsistent
+ // with the extradata generated by ffmpeg, causing decoding failures when using hvc1.
if (string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
&& _mediaEncoder.IsVaapiDeviceAmd)
{
- args += " -flags:v -global_header";
+ // Extracting the extradata from the in-band PS to bypass the issue.
+ // This can be removed once the issue is resolved in libva or Mesa.
+ // Transcoding is unavoidable here, so using BSF will not conflict with BSF in remuxing.
+ args += " -flags:v -global_header -bsf:v extract_extradata=remove=0";
}
return args;
@@ -2466,6 +2510,17 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
+ var requestedRotations = state.GetRequestedRotations(videoStream.Codec);
+ if (requestedRotations.Length > 0)
+ {
+ var rotation = state.VideoStream?.Rotation ?? 0;
+ if (rotation != 0
+ && !requestedRotations.Contains(rotation.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal))
+ {
+ return false;
+ }
+ }
+
// Video width must fall within requested value
if (request.MaxWidth.HasValue
&& (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value))
@@ -2750,25 +2805,29 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
{
+#pragma warning disable SA1008
return (inputChannels, outputChannels) switch
{
- (>= 6, >= 6 or 0) => Math.Min(640000, bitrate),
- (> 0, > 0) => Math.Min(outputChannels * 128000, bitrate),
- (> 0, _) => Math.Min(inputChannels * 128000, bitrate),
+ ( >= 6, >= 6 or 0) => Math.Min(640000, bitrate),
+ ( > 0, > 0) => Math.Min(outputChannels * 128000, bitrate),
+ ( > 0, _) => Math.Min(inputChannels * 128000, bitrate),
(_, _) => Math.Min(384000, bitrate)
};
+#pragma warning restore SA1008
}
if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "dca", StringComparison.OrdinalIgnoreCase))
{
+#pragma warning disable SA1008
return (inputChannels, outputChannels) switch
{
- (>= 6, >= 6 or 0) => Math.Min(768000, bitrate),
- (> 0, > 0) => Math.Min(outputChannels * 136000, bitrate),
- (> 0, _) => Math.Min(inputChannels * 136000, bitrate),
+ ( >= 6, >= 6 or 0) => Math.Min(768000, bitrate),
+ ( > 0, > 0) => Math.Min(outputChannels * 136000, bitrate),
+ ( > 0, _) => Math.Min(inputChannels * 136000, bitrate),
(_, _) => Math.Min(672000, bitrate)
};
+#pragma warning restore SA1008
}
// Empty bitrate area is not allow on iOS
@@ -2989,23 +3048,6 @@ namespace MediaBrowser.Controller.MediaEncoding
}
seekParam += string.Format(CultureInfo.InvariantCulture, "-ss {0}", _mediaEncoder.GetTimeParameter(seekTick));
-
- if (state.IsVideoRequest)
- {
- // If we are remuxing, then the copied stream cannot be seeked accurately (it will seek to the nearest
- // keyframe). If we are using fMP4, then force all other streams to use the same inaccurate seeking to
- // avoid A/V sync issues which cause playback issues on some devices.
- // When remuxing video, the segment start times correspond to key frames in the source stream, so this
- // option shouldn't change the seeked point that much.
- // Important: make sure not to use it with wtv because it breaks seeking
- if (state.TranscodingType is TranscodingJobType.Hls
- && string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase)
- && (IsCopyCodec(state.OutputVideoCodec) || IsCopyCodec(state.OutputAudioCodec))
- && !string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase))
- {
- seekParam += " -noaccurate_seek";
- }
- }
}
return seekParam;
@@ -3060,11 +3102,8 @@ namespace MediaBrowser.Controller.MediaEncoding
int audioStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.AudioStream);
if (state.AudioStream.IsExternal)
{
- bool hasExternalGraphicsSubs = state.SubtitleStream is not null
- && ShouldEncodeSubtitle(state)
- && state.SubtitleStream.IsExternal
- && !state.SubtitleStream.IsTextSubtitleStream;
- int externalAudioMapIndex = hasExternalGraphicsSubs ? 2 : 1;
+ bool hasExternalSubAsInput = NeedsExternalSubtitleMuxing(state);
+ int externalAudioMapIndex = hasExternalSubAsInput ? 2 : 1;
args += string.Format(
CultureInfo.InvariantCulture,
@@ -3092,12 +3131,31 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (subtitleMethod == SubtitleDeliveryMethod.Embed)
{
- int subtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream);
+ if (state.SubtitleStream.IsExternal)
+ {
+ // External subtitle file is added as second FFmpeg input.
+ // For single-stream files (SRT/ASS/VTT) the in-file index is always 0.
+ // For multi-stream containers (MKS) we count how many streams from
+ // the same file appear before the selected one.
+ var inFileIndex = state.MediaSource.MediaStreams
+ .Where(s => string.Equals(s.Path, state.SubtitleStream.Path, StringComparison.Ordinal))
+ .TakeWhile(s => s.Index != state.SubtitleStream.Index)
+ .Count();
- args += string.Format(
- CultureInfo.InvariantCulture,
- " -map 0:{0}",
- subtitleStreamIndex);
+ args += string.Format(
+ CultureInfo.InvariantCulture,
+ " -map 1:{0}",
+ inFileIndex);
+ }
+ else
+ {
+ int subtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream);
+
+ args += string.Format(
+ CultureInfo.InvariantCulture,
+ " -map 0:{0}",
+ subtitleStreamIndex);
+ }
}
else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
{
@@ -7874,6 +7932,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|| (state.BaseRequest.AlwaysBurnInSubtitleWhenTranscoding && !IsCopyCodec(state.OutputVideoCodec));
}
+ private static bool NeedsExternalSubtitleMuxing(EncodingJobInfo state)
+ {
+ return state.SubtitleStream is not null
+ && state.SubtitleStream.IsExternal
+ && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed
+ || (ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream));
+ }
+
public static string GetVideoSyncOption(string videoSync, Version encoderVersion)
{
if (string.IsNullOrEmpty(videoSync))
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index 7d0384ef27..3a1897a244 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -571,62 +571,50 @@ namespace MediaBrowser.Controller.MediaEncoding
public string[] GetRequestedProfiles(string codec)
{
- if (!string.IsNullOrEmpty(BaseRequest.Profile))
- {
- return BaseRequest.Profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
- }
+ var profile = BaseRequest.Profile;
- if (!string.IsNullOrEmpty(codec))
+ if (string.IsNullOrEmpty(profile) && !string.IsNullOrEmpty(codec))
{
- var profile = BaseRequest.GetOption(codec, "profile");
-
- if (!string.IsNullOrEmpty(profile))
- {
- return profile.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
- }
+ profile = BaseRequest.GetOption(codec, "profile");
}
- return Array.Empty<string>();
+ return (profile ?? string.Empty).Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
public string[] GetRequestedRangeTypes(string codec)
{
- if (!string.IsNullOrEmpty(BaseRequest.VideoRangeType))
- {
- return BaseRequest.VideoRangeType.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
- }
+ var rangetype = BaseRequest.VideoRangeType;
- if (!string.IsNullOrEmpty(codec))
+ if (string.IsNullOrEmpty(rangetype) && !string.IsNullOrEmpty(codec))
{
- var rangetype = BaseRequest.GetOption(codec, "rangetype");
-
- if (!string.IsNullOrEmpty(rangetype))
- {
- return rangetype.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
- }
+ rangetype = BaseRequest.GetOption(codec, "rangetype");
}
- return Array.Empty<string>();
+ return (rangetype ?? string.Empty).Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
public string[] GetRequestedCodecTags(string codec)
{
- if (!string.IsNullOrEmpty(BaseRequest.CodecTag))
+ var codectag = BaseRequest.CodecTag;
+
+ if (string.IsNullOrEmpty(codectag) && !string.IsNullOrEmpty(codec))
{
- return BaseRequest.CodecTag.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
+ codectag = BaseRequest.GetOption(codec, "codectag");
}
- if (!string.IsNullOrEmpty(codec))
- {
- var codectag = BaseRequest.GetOption(codec, "codectag");
+ return (codectag ?? string.Empty).Split(_separators, StringSplitOptions.RemoveEmptyEntries);
+ }
- if (!string.IsNullOrEmpty(codectag))
- {
- return codectag.Split(_separators, StringSplitOptions.RemoveEmptyEntries);
- }
+ public string[] GetRequestedRotations(string codec)
+ {
+ var rotation = BaseRequest.Rotation;
+
+ if (string.IsNullOrEmpty(rotation) && !string.IsNullOrEmpty(codec))
+ {
+ rotation = BaseRequest.GetOption(codec, "rotation");
}
- return Array.Empty<string>();
+ return (rotation ?? string.Empty).Split(_separators, StringSplitOptions.RemoveEmptyEntries);
}
public string GetRequestedLevel(string codec)
diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs
index 54da218530..9bee653e2e 100644
--- a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs
+++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index 1e0d77fe51..2bcce168cf 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -40,11 +40,6 @@ namespace MediaBrowser.Controller.Net
/// </summary>
private readonly List<(IWebSocketConnection Connection, CancellationTokenSource CancellationTokenSource, TStateType State)> _activeConnections = new();
- /// <summary>
- /// The logger.
- /// </summary>
- protected readonly ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger;
-
private readonly Task _messageConsumerTask;
protected BasePeriodicWebSocketListener(ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> logger)
@@ -57,6 +52,11 @@ namespace MediaBrowser.Controller.Net
}
/// <summary>
+ /// Gets the Logger.
+ /// </summary>
+ protected ILogger<BasePeriodicWebSocketListener<TReturnDataType, TStateType>> Logger { get; }
+
+ /// <summary>
/// Gets the type used for the messages sent to the client.
/// </summary>
/// <value>The type.</value>
@@ -209,6 +209,11 @@ namespace MediaBrowser.Controller.Net
var (connection, cts, state) = tuple;
var cancellationToken = cts.Token;
+ // Restore the culture context captured when the connection was established
+ // so that GetDataToSendForConnection produces a localized payload matching
+ // the client's Accept-Language preference rather than the server default.
+ connection.ApplyRequestCulture();
+
var data = await GetDataToSendForConnection(connection).ConfigureAwait(false);
if (data is null)
{
diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
index bdc0f9a10f..48431e75c3 100644
--- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs
+++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
@@ -77,5 +77,14 @@ namespace MediaBrowser.Controller.Net
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task ReceiveAsync(CancellationToken cancellationToken = default);
+
+ /// <summary>
+ /// Applies the culture context captured when the connection was established
+ /// (from the upgrade request's <c>Accept-Language</c> header) to the current
+ /// async flow. Server-initiated message senders should call this before
+ /// localising any payload so that the response uses the client's preferred
+ /// language rather than the server default.
+ /// </summary>
+ void ApplyRequestCulture();
}
}
diff --git a/MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs b/MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs
index d0cddf54a6..a4614fc125 100644
--- a/MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs
+++ b/MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.Audio;
using LinkedChildType = MediaBrowser.Controller.Entities.LinkedChildType;
@@ -29,8 +30,9 @@ public interface ILinkedChildrenService
/// Gets parent IDs that reference the specified child with LinkedChildType.Manual.
/// </summary>
/// <param name="childId">The child item ID.</param>
+ /// <param name="parentType">Optional parent item type filter.</param>
/// <returns>List of parent IDs that reference the child.</returns>
- IReadOnlyList<Guid> GetManualLinkedParentIds(Guid childId);
+ IReadOnlyList<Guid> GetManualLinkedParentIds(Guid childId, BaseItemKind? parentType = null);
/// <summary>
/// Updates LinkedChildren references from one child to another.
diff --git a/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs b/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs
index 665129eafd..de04ff021d 100644
--- a/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs
@@ -22,6 +22,13 @@ public interface IMediaStreamRepository
IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery filter);
/// <summary>
+ /// Gets all language codes of the provided stream type.
+ /// </summary>
+ /// <param name="mediaStreamType">The type of the media stream.</param>
+ /// <returns>IEnumerable{string}.</returns>
+ IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType);
+
+ /// <summary>
/// Saves the media streams.
/// </summary>
/// <param name="id">The identifier.</param>
diff --git a/MediaBrowser.Controller/Persistence/IPeopleRepository.cs b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs
index a89f3ef9ee..e2833dc722 100644
--- a/MediaBrowser.Controller/Persistence/IPeopleRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs
@@ -32,4 +32,12 @@ public interface IPeopleRepository
/// <param name="filter">The query.</param>
/// <returns>The list of people names matching the filter.</returns>
IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery filter);
+
+ /// <summary>
+ /// Gets the distinct people names per item for multiple items efficiently by querying from the mapping table.
+ /// </summary>
+ /// <param name="itemIds">The item IDs to get people for.</param>
+ /// <param name="personTypes">The person types to include (e.g. "Actor", "Director").</param>
+ /// <returns>A dictionary mapping each item ID to its distinct people names, ordered by cast list order. Items with no matching people are omitted.</returns>
+ IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IReadOnlyList<string> personTypes);
}
diff --git a/MediaBrowser.Controller/Plugins/IHasEmbeddedImage.cs b/MediaBrowser.Controller/Plugins/IHasEmbeddedImage.cs
new file mode 100644
index 0000000000..4196cd9f24
--- /dev/null
+++ b/MediaBrowser.Controller/Plugins/IHasEmbeddedImage.cs
@@ -0,0 +1,17 @@
+namespace MediaBrowser.Controller.Plugins;
+
+/// <summary>
+/// Marker interface for integrated/bundled plugins that ship their plugin image as an embedded
+/// resource inside the plugin assembly rather than as a file on disk.
+/// </summary>
+/// <remarks>
+/// This interface is intended for plugins compiled into the server. External plugins should
+/// continue to declare their image via the <c>imagePath</c> field in <c>meta.json</c>.
+/// </remarks>
+public interface IHasEmbeddedImage
+{
+ /// <summary>
+ /// Gets the name of the embedded resource in this plugin's assembly to serve as the plugin image.
+ /// </summary>
+ string ImageResourceName { get; }
+}
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
index 0d3a334dfb..c87f09a117 100644
--- a/MediaBrowser.Controller/Providers/IProviderManager.cs
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -144,6 +144,17 @@ namespace MediaBrowser.Controller.Providers
where T : BaseItem;
/// <summary>
+ /// Gets the metadata providers for the provided item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="libraryOptions">The library options.</param>
+ /// <param name="includeDisabled">Whether to include disabled providers.</param>
+ /// <typeparam name="T">The type of metadata provider.</typeparam>
+ /// <returns>The metadata providers.</returns>
+ IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions, bool includeDisabled)
+ where T : BaseItem;
+
+ /// <summary>
/// Gets the metadata savers for the provided item.
/// </summary>
/// <param name="item">The item.</param>
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 96783f6073..fb68bfb770 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -45,7 +45,6 @@ namespace MediaBrowser.Controller.Session
PlayState = new PlayerStateInfo();
SessionControllers = [];
NowPlayingQueue = [];
- NowPlayingQueueFullItems = [];
}
/// <summary>
@@ -272,15 +271,9 @@ namespace MediaBrowser.Controller.Session
public IReadOnlyList<QueueItem> NowPlayingQueue { get; set; }
/// <summary>
- /// Gets or sets the now playing queue full items.
- /// </summary>
- /// <value>The now playing queue full items.</value>
- public IReadOnlyList<BaseItemDto> NowPlayingQueueFullItems { get; set; }
-
- /// <summary>
/// Gets or sets a value indicating whether the session has a custom device name.
/// </summary>
- /// <value><c>true</c> if this session has a custom device name; otherwise, <c>false</c>.</value>
+ /// <value><c>true</c> if the session has a custom device name; otherwise, <c>false</c>.</value>
public bool HasCustomDeviceName { get; set; }
/// <summary>
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
index 132765b719..eb38eeb503 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
@@ -141,7 +141,8 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
_logger.LogError("Unable to set playing queue in group {GroupId}.", context.GroupId.ToString());
// Ignore request and return to previous state.
- IGroupState newState = prevState switch {
+ IGroupState newState = prevState switch
+ {
GroupStateType.Playing => new PlayingGroupState(LoggerFactory),
GroupStateType.Paused => new PausedGroupState(LoggerFactory),
_ => new IdleGroupState(LoggerFactory)
diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
index cf1423d02d..340d9843ff 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
@@ -143,16 +143,16 @@ namespace MediaBrowser.LocalMetadata.Parsers
item.Name = reader.ReadNormalizedString();
break;
case "CriticRating":
- {
- var text = reader.ReadElementContentAsString();
-
- if (float.TryParse(text, CultureInfo.InvariantCulture, out var value))
{
- item.CriticRating = value;
- }
+ var text = reader.ReadElementContentAsString();
- break;
- }
+ if (float.TryParse(text, CultureInfo.InvariantCulture, out var value))
+ {
+ item.CriticRating = value;
+ }
+
+ break;
+ }
case "SortTitle":
item.ForcedSortName = reader.ReadNormalizedString();
@@ -176,55 +176,55 @@ namespace MediaBrowser.LocalMetadata.Parsers
break;
case "LockedFields":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
{
- item.LockedFields = val.Split('|').Select(i =>
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
{
- if (Enum.TryParse(i, true, out MetadataField field))
+ item.LockedFields = val.Split('|').Select(i =>
{
- return (MetadataField?)field;
- }
+ if (Enum.TryParse(i, true, out MetadataField field))
+ {
+ return (MetadataField?)field;
+ }
- return null;
- }).Where(i => i.HasValue).Select(i => i!.Value).ToArray();
- }
+ return null;
+ }).Where(i => i.HasValue).Select(i => i!.Value).ToArray();
+ }
- break;
- }
+ break;
+ }
case "TagLines":
- {
- if (!reader.IsEmptyElement)
{
- using (var subtree = reader.ReadSubtree())
+ if (!reader.IsEmptyElement)
{
- FetchFromTaglinesNode(subtree, item);
+ using (var subtree = reader.ReadSubtree())
+ {
+ FetchFromTaglinesNode(subtree, item);
+ }
+ }
+ else
+ {
+ reader.Read();
}
- }
- else
- {
- reader.Read();
- }
- break;
- }
+ break;
+ }
case "Countries":
- {
- if (!reader.IsEmptyElement)
- {
- reader.Skip();
- }
- else
{
- reader.Read();
- }
+ if (!reader.IsEmptyElement)
+ {
+ reader.Skip();
+ }
+ else
+ {
+ reader.Read();
+ }
- break;
- }
+ break;
+ }
case "ContentRating":
case "MPAARating":
@@ -307,19 +307,19 @@ namespace MediaBrowser.LocalMetadata.Parsers
break;
case "Trailers":
- {
- if (!reader.IsEmptyElement)
- {
- using var subtree = reader.ReadSubtree();
- FetchDataFromTrailersNode(subtree, item);
- }
- else
{
- reader.Read();
- }
+ if (!reader.IsEmptyElement)
+ {
+ using var subtree = reader.ReadSubtree();
+ FetchDataFromTrailersNode(subtree, item);
+ }
+ else
+ {
+ reader.Read();
+ }
- break;
- }
+ break;
+ }
case "ProductionYear":
if (reader.TryReadInt(out var productionYear) && productionYear > 1850)
@@ -330,20 +330,20 @@ namespace MediaBrowser.LocalMetadata.Parsers
break;
case "Rating":
case "IMDBrating":
- {
- var rating = reader.ReadNormalizedString();
-
- if (!string.IsNullOrEmpty(rating))
{
- // All external meta is saving this as '.' for decimal I believe...but just to be sure
- if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val))
+ var rating = reader.ReadNormalizedString();
+
+ if (!string.IsNullOrEmpty(rating))
{
- item.CommunityRating = val;
+ // All external meta is saving this as '.' for decimal I believe...but just to be sure
+ if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val))
+ {
+ item.CommunityRating = val;
+ }
}
- }
- break;
- }
+ break;
+ }
case "BirthDate":
case "PremiereDate":
@@ -370,144 +370,144 @@ namespace MediaBrowser.LocalMetadata.Parsers
break;
case "Genres":
- {
- if (!reader.IsEmptyElement)
- {
- using var subtree = reader.ReadSubtree();
- FetchFromGenresNode(subtree, item);
- }
- else
{
- reader.Read();
- }
+ if (!reader.IsEmptyElement)
+ {
+ using var subtree = reader.ReadSubtree();
+ FetchFromGenresNode(subtree, item);
+ }
+ else
+ {
+ reader.Read();
+ }
- break;
- }
+ break;
+ }
case "Tags":
- {
- if (!reader.IsEmptyElement)
- {
- using var subtree = reader.ReadSubtree();
- FetchFromTagsNode(subtree, item);
- }
- else
{
- reader.Read();
- }
+ if (!reader.IsEmptyElement)
+ {
+ using var subtree = reader.ReadSubtree();
+ FetchFromTagsNode(subtree, item);
+ }
+ else
+ {
+ reader.Read();
+ }
- break;
- }
+ break;
+ }
case "Persons":
- {
- if (!reader.IsEmptyElement)
- {
- using var subtree = reader.ReadSubtree();
- FetchDataFromPersonsNode(subtree, itemResult);
- }
- else
{
- reader.Read();
- }
+ if (!reader.IsEmptyElement)
+ {
+ using var subtree = reader.ReadSubtree();
+ FetchDataFromPersonsNode(subtree, itemResult);
+ }
+ else
+ {
+ reader.Read();
+ }
- break;
- }
+ break;
+ }
case "Studios":
- {
- if (!reader.IsEmptyElement)
- {
- using var subtree = reader.ReadSubtree();
- FetchFromStudiosNode(subtree, item);
- }
- else
{
- reader.Read();
- }
+ if (!reader.IsEmptyElement)
+ {
+ using var subtree = reader.ReadSubtree();
+ FetchFromStudiosNode(subtree, item);
+ }
+ else
+ {
+ reader.Read();
+ }
- break;
- }
+ break;
+ }
case "Shares":
- {
- if (!reader.IsEmptyElement)
{
- using var subtree = reader.ReadSubtree();
- if (item is IHasShares hasShares)
+ if (!reader.IsEmptyElement)
{
- FetchFromSharesNode(subtree, hasShares);
+ using var subtree = reader.ReadSubtree();
+ if (item is IHasShares hasShares)
+ {
+ FetchFromSharesNode(subtree, hasShares);
+ }
+ }
+ else
+ {
+ reader.Read();
}
- }
- else
- {
- reader.Read();
- }
- break;
- }
+ break;
+ }
case "OwnerUserId":
- {
- var val = reader.ReadNormalizedString();
-
- if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty))
{
- if (item is Playlist playlist)
+ var val = reader.ReadNormalizedString();
+
+ if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty))
{
- playlist.OwnerUserId = guid;
+ if (item is Playlist playlist)
+ {
+ playlist.OwnerUserId = guid;
+ }
}
- }
- break;
- }
+ break;
+ }
case "Format3D":
- {
- var val = reader.ReadNormalizedString();
-
- if (item is Video video)
{
- if (string.Equals("HSBS", val, StringComparison.OrdinalIgnoreCase))
- {
- video.Video3DFormat = Video3DFormat.HalfSideBySide;
- }
- else if (string.Equals("HTAB", val, StringComparison.OrdinalIgnoreCase))
- {
- video.Video3DFormat = Video3DFormat.HalfTopAndBottom;
- }
- else if (string.Equals("FTAB", val, StringComparison.OrdinalIgnoreCase))
+ var val = reader.ReadNormalizedString();
+
+ if (item is Video video)
{
- video.Video3DFormat = Video3DFormat.FullTopAndBottom;
+ if (string.Equals("HSBS", val, StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.HalfSideBySide;
+ }
+ else if (string.Equals("HTAB", val, StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.HalfTopAndBottom;
+ }
+ else if (string.Equals("FTAB", val, StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.FullTopAndBottom;
+ }
+ else if (string.Equals("FSBS", val, StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.FullSideBySide;
+ }
+ else if (string.Equals("MVC", val, StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.MVC;
+ }
}
- else if (string.Equals("FSBS", val, StringComparison.OrdinalIgnoreCase))
+
+ break;
+ }
+
+ default:
+ {
+ string readerName = reader.Name;
+ if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue))
{
- video.Video3DFormat = Video3DFormat.FullSideBySide;
+ var id = reader.ReadNormalizedString();
+ item.TrySetProviderId(providerIdValue, id);
}
- else if (string.Equals("MVC", val, StringComparison.OrdinalIgnoreCase))
+ else
{
- video.Video3DFormat = Video3DFormat.MVC;
+ reader.Skip();
}
- }
- break;
- }
-
- default:
- {
- string readerName = reader.Name;
- if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue))
- {
- var id = reader.ReadNormalizedString();
- item.TrySetProviderId(providerIdValue, id);
+ break;
}
- else
- {
- reader.Skip();
- }
-
- break;
- }
}
}
@@ -526,31 +526,31 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "Share":
- {
- if (reader.IsEmptyElement)
{
- reader.Read();
- continue;
- }
-
- using (var subReader = reader.ReadSubtree())
- {
- var child = GetShare(subReader);
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ continue;
+ }
- if (child is not null)
+ using (var subReader = reader.ReadSubtree())
{
- list.Add(child);
+ var child = GetShare(subReader);
+
+ if (child is not null)
+ {
+ list.Add(child);
+ }
}
- }
- break;
- }
+ break;
+ }
default:
- {
- reader.Skip();
- break;
- }
+ {
+ reader.Skip();
+ break;
+ }
}
}
else
diff --git a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs
index 00634de5b5..324505d17c 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs
@@ -64,32 +64,32 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "CollectionItem":
- {
- if (!reader.IsEmptyElement)
{
- using (var subReader = reader.ReadSubtree())
+ if (!reader.IsEmptyElement)
{
- var child = GetLinkedChild(subReader);
-
- if (child is not null)
+ using (var subReader = reader.ReadSubtree())
{
- list.Add(child);
+ var child = GetLinkedChild(subReader);
+
+ if (child is not null)
+ {
+ list.Add(child);
+ }
}
}
- }
- else
- {
- reader.Read();
- }
+ else
+ {
+ reader.Read();
+ }
- break;
- }
+ break;
+ }
default:
- {
- reader.Skip();
- break;
- }
+ {
+ reader.Skip();
+ break;
+ }
}
}
else
diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
index e0277870d1..0bda9e300a 100644
--- a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
@@ -76,25 +76,25 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "PlaylistItem":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
-
- using (var subReader = reader.ReadSubtree())
{
- var child = GetLinkedChild(subReader);
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ continue;
+ }
- if (child is not null)
+ using (var subReader = reader.ReadSubtree())
{
- list.Add(child);
+ var child = GetLinkedChild(subReader);
+
+ if (child is not null)
+ {
+ list.Add(child);
+ }
}
- }
- break;
- }
+ break;
+ }
default:
reader.Skip();
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index f7a1581a76..9dd3dcecba 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -1,8 +1,10 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
@@ -102,13 +104,10 @@ namespace MediaBrowser.MediaEncoding.Attachments
&& (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase)));
if (shouldExtractOneByOne && !inputFile.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
{
- foreach (var attachment in mediaSource.MediaAttachments)
- {
- if (!string.Equals(attachment.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase))
- {
- await ExtractAttachment(inputFile, mediaSource, attachment, cancellationToken).ConfigureAwait(false);
- }
- }
+ await ExtractAllAttachmentsIndividuallyInternal(
+ inputFile,
+ mediaSource,
+ cancellationToken).ConfigureAwait(false);
}
else
{
@@ -119,6 +118,140 @@ namespace MediaBrowser.MediaEncoding.Attachments
}
}
+ private async Task ExtractAllAttachmentsIndividuallyInternal(
+ string inputFile,
+ MediaSourceInfo mediaSource,
+ CancellationToken cancellationToken)
+ {
+ var inputPath = _mediaEncoder.GetInputArgument(inputFile, mediaSource);
+
+ ArgumentException.ThrowIfNullOrEmpty(inputPath);
+
+ var outputFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
+ if (outputFolder is null)
+ {
+ _logger.LogDebug("Skipping attachment extraction for input {InputFile}: MediaSource Id is not a GUID.", inputFile);
+ return;
+ }
+
+ using (await _semaphoreLocks.LockAsync(outputFolder, cancellationToken).ConfigureAwait(false))
+ {
+ Directory.CreateDirectory(outputFolder);
+
+ var dumpArgs = new StringBuilder();
+ var missingPaths = new List<string>();
+ foreach (var attachment in mediaSource.MediaAttachments)
+ {
+ if (string.Equals(attachment.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ var indexName = attachment.Index.ToString(CultureInfo.InvariantCulture);
+ var attachmentPath = _pathManager.GetAttachmentPath(mediaSource.Id, attachment.FileName ?? indexName)
+ ?? _pathManager.GetAttachmentPath(mediaSource.Id, indexName)!;
+ if (File.Exists(attachmentPath))
+ {
+ continue;
+ }
+
+ dumpArgs.AppendFormat(
+ CultureInfo.InvariantCulture,
+ "-dump_attachment:{0} \"{1}\" ",
+ attachment.Index,
+ EncodingUtils.NormalizePath(attachmentPath));
+ missingPaths.Add(attachmentPath);
+ }
+
+ if (missingPaths.Count == 0)
+ {
+ // Skip extraction if all files already exist
+ return;
+ }
+
+ var hasVideoOrAudioStream = mediaSource.MediaStreams
+ .Any(s => s.Type == MediaStreamType.Video || s.Type == MediaStreamType.Audio);
+ var processArgs = string.Format(
+ CultureInfo.InvariantCulture,
+ "{0}{1} -i {2} {3}",
+ dumpArgs,
+ inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty,
+ inputPath,
+ hasVideoOrAudioStream ? "-t 0 -f null null" : string.Empty);
+
+ int exitCode;
+
+ using (var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ Arguments = processArgs,
+ FileName = _mediaEncoder.EncoderPath,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ },
+ EnableRaisingEvents = true
+ })
+ {
+ _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
+
+ process.Start();
+
+ try
+ {
+ await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
+ exitCode = process.ExitCode;
+ }
+ catch (OperationCanceledException)
+ {
+ process.Kill(true);
+ exitCode = -1;
+ }
+ }
+
+ var failed = false;
+
+ if (exitCode != 0 && (hasVideoOrAudioStream || exitCode != 1))
+ {
+ failed = true;
+
+ foreach (var path in missingPaths)
+ {
+ if (!File.Exists(path))
+ {
+ continue;
+ }
+
+ try
+ {
+ _fileSystem.DeleteFile(path);
+ }
+ catch (IOException ex)
+ {
+ _logger.LogError(ex, "Error deleting extracted attachment {Path}", path);
+ }
+ }
+ }
+
+ if (!failed && missingPaths.Exists(p => !File.Exists(p)))
+ {
+ failed = true;
+ }
+
+ if (failed)
+ {
+ _logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputFolder);
+
+ throw new InvalidOperationException(
+ string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputFolder));
+ }
+
+ _logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputFolder);
+ }
+ }
+
private async Task ExtractAllAttachmentsInternal(
string inputFile,
MediaSourceInfo mediaSource,
@@ -129,6 +262,12 @@ namespace MediaBrowser.MediaEncoding.Attachments
ArgumentException.ThrowIfNullOrEmpty(inputPath);
var outputFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
+ if (outputFolder is null)
+ {
+ _logger.LogDebug("Skipping attachment extraction for input {InputFile}: MediaSource Id is not a GUID.", inputFile);
+ return;
+ }
+
using (await _semaphoreLocks.LockAsync(outputFolder, cancellationToken).ConfigureAwait(false))
{
var directory = Directory.CreateDirectory(outputFolder);
@@ -157,19 +296,19 @@ namespace MediaBrowser.MediaEncoding.Attachments
int exitCode;
using (var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
{
- StartInfo = new ProcessStartInfo
- {
- Arguments = processArgs,
- FileName = _mediaEncoder.EncoderPath,
- UseShellExecute = false,
- CreateNoWindow = true,
- WindowStyle = ProcessWindowStyle.Hidden,
- WorkingDirectory = outputFolder,
- ErrorDialog = false
- },
- EnableRaisingEvents = true
- })
+ Arguments = processArgs,
+ FileName = _mediaEncoder.EncoderPath,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ WorkingDirectory = outputFolder,
+ ErrorDialog = false
+ },
+ EnableRaisingEvents = true
+ })
{
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
@@ -241,9 +380,14 @@ namespace MediaBrowser.MediaEncoding.Attachments
CancellationToken cancellationToken)
{
var attachmentFolderPath = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
+ if (attachmentFolderPath is null)
+ {
+ throw new ResourceNotFoundException($"MediaSource {mediaSource.Id} has no attachment cache (non-GUID Id, e.g. Live TV stream).");
+ }
+
using (await _semaphoreLocks.LockAsync(attachmentFolderPath, cancellationToken).ConfigureAwait(false))
{
- var attachmentPath = _pathManager.GetAttachmentPath(mediaSource.Id, mediaAttachment.FileName ?? mediaAttachment.Index.ToString(CultureInfo.InvariantCulture));
+ var attachmentPath = _pathManager.GetAttachmentPath(mediaSource.Id, mediaAttachment.FileName ?? mediaAttachment.Index.ToString(CultureInfo.InvariantCulture))!;
if (!File.Exists(attachmentPath))
{
await ExtractAttachmentInternal(
@@ -284,18 +428,18 @@ namespace MediaBrowser.MediaEncoding.Attachments
int exitCode;
using (var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
{
- StartInfo = new ProcessStartInfo
- {
- Arguments = processArgs,
- FileName = _mediaEncoder.EncoderPath,
- UseShellExecute = false,
- CreateNoWindow = true,
- WindowStyle = ProcessWindowStyle.Hidden,
- ErrorDialog = false
- },
- EnableRaisingEvents = true
- })
+ Arguments = processArgs,
+ FileName = _mediaEncoder.EncoderPath,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ },
+ EnableRaisingEvents = true
+ })
{
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index f34e911a05..66bf6ebd24 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -85,6 +85,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
private bool _isVaapiDeviceSupportVulkanDrmModifier = false;
private bool _isVaapiDeviceSupportVulkanDrmInterop = false;
+ private bool _canSetProcessPriority = true;
+
private bool _isVideoToolboxAv1DecodeAvailable = false;
private static string[] _vulkanImageDrmFmtModifierExts =
@@ -1123,13 +1125,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
process.Process.Start();
- try
- {
- process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
- }
- catch (Exception ex)
+ if (_canSetProcessPriority)
{
- _logger.LogWarning(ex, "Unable to set process priority to BelowNormal for {ProcessFileName}", process.Process.StartInfo.FileName);
+ try
+ {
+ process.Process.PriorityClass = ProcessPriorityClass.BelowNormal;
+ }
+ catch (Exception ex)
+ {
+ _canSetProcessPriority = false;
+ _logger.LogWarning(ex, "Unable to set process priority to BelowNormal for {ProcessFileName}. Further attempts will be skipped.", process.Process.StartInfo.FileName);
+ }
}
lock (_runningProcessesLock)
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 791a7f9053..06060988e2 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -1708,6 +1708,13 @@ namespace MediaBrowser.MediaEncoding.Probing
return;
}
+ // Skip timestamp extration for remote resource (http, rtsp, etc.)
+ // as they cannot be opened with FileStream
+ if (video.Protocol != MediaProtocol.File)
+ {
+ return;
+ }
+
if (!string.Equals(video.Container, "mpeg2ts", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(video.Container, "m2ts", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(video.Container, "ts", StringComparison.OrdinalIgnoreCase))
diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/AssWriter.cs
deleted file mode 100644
index 7d7b80e99d..0000000000
--- a/MediaBrowser.MediaEncoding/Subtitles/AssWriter.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-using System;
-using System.Globalization;
-using System.IO;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using MediaBrowser.Model.MediaInfo;
-
-namespace MediaBrowser.MediaEncoding.Subtitles
-{
- /// <summary>
- /// ASS subtitle writer.
- /// </summary>
- public partial class AssWriter : ISubtitleWriter
- {
- [GeneratedRegex(@"\n", RegexOptions.IgnoreCase)]
- private static partial Regex NewLineRegex();
-
- /// <inheritdoc />
- public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
- {
- using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
- {
- var trackEvents = info.TrackEvents;
- var timeFormat = @"hh\:mm\:ss\.ff";
-
- // Write ASS header
- writer.WriteLine("[Script Info]");
- writer.WriteLine("Title: Jellyfin transcoded ASS subtitle");
- writer.WriteLine("ScriptType: v4.00+");
- writer.WriteLine();
- writer.WriteLine("[V4+ Styles]");
- writer.WriteLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding");
- writer.WriteLine("Style: Default,Arial,20,&H00FFFFFF,&H00FFFFFF,&H19333333,&H910E0807,0,0,0,0,100,100,0,0,0,1,0,2,10,10,10,1");
- writer.WriteLine();
- writer.WriteLine("[Events]");
- writer.WriteLine("Format: Layer, Start, End, Style, Text");
-
- for (int i = 0; i < trackEvents.Count; i++)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- var trackEvent = trackEvents[i];
- var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
- var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
- var text = NewLineRegex().Replace(trackEvent.Text, "\\n");
-
- writer.WriteLine(
- "Dialogue: 0,{0},{1},Default,{2}",
- startTime,
- endTime,
- text);
- }
- }
- }
- }
-}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs
deleted file mode 100644
index dec714121d..0000000000
--- a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleWriter.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System.IO;
-using System.Threading;
-using MediaBrowser.Model.MediaInfo;
-
-namespace MediaBrowser.MediaEncoding.Subtitles
-{
- /// <summary>
- /// Interface ISubtitleWriter.
- /// </summary>
- public interface ISubtitleWriter
- {
- /// <summary>
- /// Writes the specified information.
- /// </summary>
- /// <param name="info">The information.</param>
- /// <param name="stream">The stream.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken);
- }
-}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs
index 1b452b0cec..0e40181016 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/JsonWriter.cs
@@ -1,44 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
using System.IO;
+using System.Text;
using System.Text.Json;
-using System.Threading;
-using MediaBrowser.Model.MediaInfo;
+using Nikse.SubtitleEdit.Core.Common;
+using Nikse.SubtitleEdit.Core.SubtitleFormats;
-namespace MediaBrowser.MediaEncoding.Subtitles
+namespace MediaBrowser.MediaEncoding.Subtitles;
+
+/// <summary>
+/// JSON subtitle writer.
+/// </summary>
+public class JsonWriter : SubtitleFormat
{
- /// <summary>
- /// JSON subtitle writer.
- /// </summary>
- public class JsonWriter : ISubtitleWriter
+ /// <inheritdoc />
+ public override string Extension => ".json";
+
+ /// <inheritdoc />
+ public override string Name => "JSON Jellyfin";
+
+ /// <inheritdoc />
+ public override string ToText(Subtitle subtitle, string title)
{
- /// <inheritdoc />
- public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
+ using var ms = new MemoryStream();
+ using (var writer = new Utf8JsonWriter(ms))
{
- using (var writer = new Utf8JsonWriter(stream))
+ var trackevents = subtitle.Paragraphs;
+ writer.WriteStartObject();
+ writer.WriteStartArray("TrackEvents");
+
+ for (int i = 0; i < trackevents.Count; i++)
{
- var trackevents = info.TrackEvents;
+ var current = trackevents[i];
writer.WriteStartObject();
- writer.WriteStartArray("TrackEvents");
-
- for (int i = 0; i < trackevents.Count; i++)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- var current = trackevents[i];
- writer.WriteStartObject();
- writer.WriteString("Id", current.Id);
- writer.WriteString("Text", current.Text);
- writer.WriteNumber("StartPositionTicks", current.StartPositionTicks);
- writer.WriteNumber("EndPositionTicks", current.EndPositionTicks);
+ writer.WriteString("Id", current.Number.ToString(CultureInfo.InvariantCulture));
+ writer.WriteString("Text", current.Text);
+ writer.WriteNumber("StartPositionTicks", current.StartTime.TimeSpan.Ticks);
+ writer.WriteNumber("EndPositionTicks", current.EndTime.TimeSpan.Ticks);
- writer.WriteEndObject();
- }
-
- writer.WriteEndArray();
writer.WriteEndObject();
-
- writer.Flush();
}
+
+ writer.WriteEndArray();
+ writer.WriteEndObject();
+
+ writer.Flush();
}
+
+ return Encoding.UTF8.GetString(ms.GetBuffer(), 0, (int)ms.Length);
}
+
+ /// <inheritdoc />
+ public override void LoadSubtitle(Subtitle subtitle, List<string> lines, string fileName)
+ => throw new NotImplementedException();
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs
deleted file mode 100644
index 86f77aa067..0000000000
--- a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using System;
-using System.Globalization;
-using System.IO;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using MediaBrowser.Model.MediaInfo;
-
-namespace MediaBrowser.MediaEncoding.Subtitles
-{
- /// <summary>
- /// SRT subtitle writer.
- /// </summary>
- public partial class SrtWriter : ISubtitleWriter
- {
- [GeneratedRegex(@"\\n", RegexOptions.IgnoreCase)]
- private static partial Regex NewLineEscapedRegex();
-
- /// <inheritdoc />
- public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
- {
- using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
- {
- var trackEvents = info.TrackEvents;
-
- for (int i = 0; i < trackEvents.Count; i++)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- var trackEvent = trackEvents[i];
-
- writer.WriteLine((i + 1).ToString(CultureInfo.InvariantCulture));
- writer.WriteLine(
- @"{0:hh\:mm\:ss\,fff} --> {1:hh\:mm\:ss\,fff}",
- TimeSpan.FromTicks(trackEvent.StartPositionTicks),
- TimeSpan.FromTicks(trackEvent.EndPositionTicks));
-
- var text = trackEvent.Text;
-
- // TODO: Not sure how to handle these
- text = NewLineEscapedRegex().Replace(text, " ");
-
- writer.WriteLine(text);
- writer.WriteLine();
- }
- }
- }
- }
-}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaWriter.cs
deleted file mode 100644
index b5fd1ed935..0000000000
--- a/MediaBrowser.MediaEncoding/Subtitles/SsaWriter.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-using System;
-using System.Globalization;
-using System.IO;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using MediaBrowser.Model.MediaInfo;
-
-namespace MediaBrowser.MediaEncoding.Subtitles
-{
- /// <summary>
- /// SSA subtitle writer.
- /// </summary>
- public partial class SsaWriter : ISubtitleWriter
- {
- [GeneratedRegex(@"\n", RegexOptions.IgnoreCase)]
- private static partial Regex NewLineRegex();
-
- /// <inheritdoc />
- public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
- {
- using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
- {
- var trackEvents = info.TrackEvents;
- var timeFormat = @"hh\:mm\:ss\.ff";
-
- // Write SSA header
- writer.WriteLine("[Script Info]");
- writer.WriteLine("Title: Jellyfin transcoded SSA subtitle");
- writer.WriteLine("ScriptType: v4.00");
- writer.WriteLine();
- writer.WriteLine("[V4 Styles]");
- writer.WriteLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding");
- writer.WriteLine("Style: Default,Arial,20,&H00FFFFFF,&H00FFFFFF,&H19333333,&H19333333,0,0,0,1,0,2,10,10,10,0,1");
- writer.WriteLine();
- writer.WriteLine("[Events]");
- writer.WriteLine("Format: Layer, Start, End, Style, Text");
-
- for (int i = 0; i < trackEvents.Count; i++)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- var trackEvent = trackEvents[i];
- var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
- var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
- var text = NewLineRegex().Replace(trackEvent.Text, "\\n");
-
- writer.WriteLine(
- "Dialogue: 0,{0},{1},Default,{2}",
- startTime,
- endTime,
- text);
- }
- }
- }
- }
-}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 894d0a3574..77aadee704 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -26,7 +26,10 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
+using Nikse.SubtitleEdit.Core.Common;
+using Nikse.SubtitleEdit.Core.SubtitleFormats;
using UtfUnknown;
+using SubtitleFormat = MediaBrowser.Model.MediaInfo.SubtitleFormat;
namespace MediaBrowser.MediaEncoding.Subtitles
{
@@ -72,55 +75,42 @@ namespace MediaBrowser.MediaEncoding.Subtitles
private MemoryStream ConvertSubtitles(
Stream stream,
- string inputFormat,
+ SubtitleInfo inputInfo,
string outputFormat,
long startTimeTicks,
long endTimeTicks,
- bool preserveOriginalTimestamps,
- CancellationToken cancellationToken)
+ bool preserveOriginalTimestamps)
{
- var ms = new MemoryStream();
+ var subtitle = Subtitle.Parse(stream, Path.GetExtension(inputInfo.Path));
- try
- {
- var trackInfo = _subtitleParser.Parse(stream, inputFormat);
+ FilterEvents(subtitle, startTimeTicks, endTimeTicks, preserveOriginalTimestamps);
- FilterEvents(trackInfo, startTimeTicks, endTimeTicks, preserveOriginalTimestamps);
+ var formatter = GetWriter(outputFormat);
- var writer = GetWriter(outputFormat);
+ var text = formatter.ToText(subtitle, "untitled");
+ var bytes = Encoding.UTF8.GetBytes(text);
- writer.Write(trackInfo, ms, cancellationToken);
- ms.Position = 0;
- }
- catch
- {
- ms.Dispose();
- throw;
- }
-
- return ms;
+ return new MemoryStream(bytes, 0, bytes.Length, false, true);
}
- internal void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps)
+ internal void FilterEvents(Subtitle track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps)
{
// Drop subs that have fully elapsed before the requested start position
- track.TrackEvents = track.TrackEvents
- .SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 && (i.EndPositionTicks - startPositionTicks) < 0)
- .ToArray();
+ track.Paragraphs
+ .RemoveAll(i => (i.StartTime.TimeSpan.Ticks - startPositionTicks) < 0 && (i.EndTime.TimeSpan.Ticks - startPositionTicks) < 0);
if (endTimeTicks > 0)
{
- track.TrackEvents = track.TrackEvents
- .TakeWhile(i => i.StartPositionTicks <= endTimeTicks)
- .ToArray();
+ track.Paragraphs
+ .RemoveAll(i => i.StartTime.TimeSpan.Ticks > endTimeTicks);
}
if (!preserveTimestamps)
{
- foreach (var trackEvent in track.TrackEvents)
+ foreach (var trackEvent in track.Paragraphs)
{
- trackEvent.EndPositionTicks = Math.Max(0, trackEvent.EndPositionTicks - startPositionTicks);
- trackEvent.StartPositionTicks = Math.Max(0, trackEvent.StartPositionTicks - startPositionTicks);
+ trackEvent.StartTime = new TimeCode(TimeSpan.FromTicks(Math.Max(0, trackEvent.StartTime.TimeSpan.Ticks - startPositionTicks)));
+ trackEvent.EndTime = new TimeCode(TimeSpan.FromTicks(Math.Max(0, trackEvent.EndTime.TimeSpan.Ticks - startPositionTicks)));
}
}
}
@@ -142,14 +132,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var subtitleStream = mediaSource.MediaStreams
.First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex);
- var (stream, inputFormat) = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken)
+ var (stream, info) = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken)
.ConfigureAwait(false);
// Return the original if the same format is being requested
// Character encoding was already handled in GetSubtitleStream
// ASS is a superset of SSA, skipping the conversion and preserving the styles
- if (string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase)
- || (string.Equals(inputFormat, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase)
+ if (string.Equals(info.Format, outputFormat, StringComparison.OrdinalIgnoreCase)
+ || (string.Equals(info.Format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase)
&& string.Equals(outputFormat, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase)))
{
return stream;
@@ -157,11 +147,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
using (stream)
{
- return ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimestamps, cancellationToken);
+ return ConvertSubtitles(stream, info, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimestamps);
}
}
- private async Task<(Stream Stream, string Format)> GetSubtitleStream(
+ private async Task<(Stream Stream, SubtitleInfo Info)> GetSubtitleStream(
MediaSourceInfo mediaSource,
MediaStream subtitleStream,
CancellationToken cancellationToken)
@@ -170,7 +160,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var stream = await GetSubtitleStream(fileInfo, cancellationToken).ConfigureAwait(false);
- return (stream, fileInfo.Format);
+ return (stream, fileInfo);
}
private async Task<Stream> GetSubtitleStream(SubtitleInfo fileInfo, CancellationToken cancellationToken)
@@ -190,10 +180,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
await using (stream.ConfigureAwait(false))
{
- using var reader = new StreamReader(stream, detected.Encoding);
- var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
+ using var reader = new StreamReader(stream, detected.Encoding);
+ var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
- return new MemoryStream(Encoding.UTF8.GetBytes(text));
+ return new MemoryStream(Encoding.UTF8.GetBytes(text));
}
}
}
@@ -212,19 +202,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var outputFileExtension = GetExtractableSubtitleFileExtension(subtitleStream);
var outputFormat = GetExtractableSubtitleFormat(subtitleStream);
- var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFileExtension);
+ var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFileExtension)
+ ?? throw new ResourceNotFoundException($"MediaSource {mediaSource.Id} has no subtitle cache (non-GUID Id, e.g. Live TV stream).");
return new SubtitleInfo()
{
Path = outputPath,
Protocol = MediaProtocol.File,
Format = outputFormat,
- IsExternal = false
+ IsExternal = MediaStream.IsVobSubFormat(outputFormat)
};
}
- var currentFormat = subtitleStream.Codec ?? Path.GetExtension(subtitleStream.Path)
- .TrimStart('.');
+ var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec).TrimStart('.');
// Handle PGS subtitles as raw streams for the client to render
if (MediaStream.IsPgsFormat(currentFormat))
@@ -242,7 +232,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (!_subtitleParser.SupportsFileExtension(currentFormat))
{
// Convert
- var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt");
+ var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt")
+ ?? throw new ResourceNotFoundException($"MediaSource {mediaSource.Id} has no subtitle cache (non-GUID Id, e.g. Live TV stream).");
await ConvertTextSubtitleToSrt(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
@@ -265,13 +256,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
};
}
- private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value)
+ private bool TryGetWriter(string format, [NotNullWhen(true)] out Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat? value)
{
ArgumentException.ThrowIfNullOrEmpty(format);
if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
{
- value = new AssWriter();
+ value = new AdvancedSubStationAlpha();
return true;
}
@@ -281,27 +272,29 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return true;
}
- if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase) || string.Equals(format, SubtitleFormat.SUBRIP, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(format, SubtitleFormat.SUBRIP, StringComparison.OrdinalIgnoreCase))
{
- value = new SrtWriter();
+ value = new SubRip();
return true;
}
if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
{
- value = new SsaWriter();
+ value = new SubStationAlpha();
return true;
}
- if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase) || string.Equals(format, SubtitleFormat.WEBVTT, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(format, SubtitleFormat.WEBVTT, StringComparison.OrdinalIgnoreCase))
{
- value = new VttWriter();
+ value = new WebVTT();
return true;
}
if (string.Equals(format, SubtitleFormat.TTML, StringComparison.OrdinalIgnoreCase))
{
- value = new TtmlWriter();
+ value = new TimedText10();
return true;
}
@@ -309,7 +302,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return false;
}
- private ISubtitleWriter GetWriter(string format)
+ private Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat GetWriter(string format)
{
if (TryGetWriter(format, out var writer))
{
@@ -473,6 +466,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
return subtitleStream.Codec;
}
+ else if (MediaStream.IsVobSubFormat(subtitleStream.Codec))
+ {
+ return "mks";
+ }
else
{
return "srt";
@@ -486,6 +483,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
return "sup";
}
+ else if (MediaStream.IsVobSubFormat(subtitleStream.Codec))
+ {
+ // FFmpeg cannot mux VobSub subtitle streams back into the .idx/.sub pair, so we use .mks container instead.
+ return "mks";
+ }
else
{
return GetExtractableSubtitleFormat(subtitleStream);
@@ -498,7 +500,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|| string.Equals(codec, "ssa", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "srt", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "subrip", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "pgssub", StringComparison.OrdinalIgnoreCase);
+ || string.Equals(codec, "pgssub", StringComparison.OrdinalIgnoreCase)
+ || MediaStream.IsVobSubFormat(codec);
}
/// <inheritdoc />
@@ -514,12 +517,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles
foreach (var subtitleStream in subtitleStreams)
{
- if (subtitleStream.IsExternal && !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
+ if (subtitleStream.IsExternal
+ && !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream));
+ if (outputPath is null)
+ {
+ continue;
+ }
var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false);
@@ -591,7 +599,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream));
+ if (outputPath is null)
+ {
+ continue;
+ }
+
var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
+ // FFmpeg does not provide an .idx/.sub muxer, so VobSub streams must be written as MKS files.
+ var outputFormatOption = MediaStream.IsVobSubFormat(subtitleStream.Codec) ? " -f matroska" : string.Empty;
var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
if (streamIndex == -1)
@@ -605,9 +620,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
outputPaths.Add(outputPath);
args += string.Format(
CultureInfo.InvariantCulture,
- " -map 0:{0} -an -vn -c:s {1} -flush_packets 1 \"{2}\"",
+ " -map 0:{0} -an -vn -c:s {1}{2} -flush_packets 1 \"{3}\"",
streamIndex,
outputCodec,
+ outputFormatOption,
outputPath);
}
@@ -636,7 +652,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream));
+ if (outputPath is null)
+ {
+ continue;
+ }
+
var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
+ // FFmpeg does not provide an .idx/.sub muxer, so VobSub streams must be written as MKS files.
+ var outputFormatOption = MediaStream.IsVobSubFormat(subtitleStream.Codec) ? " -f matroska" : string.Empty;
var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
if (streamIndex == -1)
@@ -650,18 +673,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles
outputPaths.Add(outputPath);
args += string.Format(
CultureInfo.InvariantCulture,
- " -map 0:{0} -an -vn -c:s {1} -flush_packets 1 \"{2}\"",
+ " -map 0:{0} -an -vn -c:s {1}{2} -flush_packets 1 \"{3}\"",
streamIndex,
outputCodec,
+ outputFormatOption,
outputPath);
}
- if (outputPaths.Count == 0)
+ if (outputPaths.Count > 0)
{
- return;
+ await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false);
}
-
- await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false);
}
private async Task ExtractSubtitlesForFile(
@@ -968,7 +990,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
- private string GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension)
+ private string? GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension)
{
return _pathManager.GetSubtitlePath(mediaSource.Id, subtitleStreamIndex, outputSubtitleExtension);
}
@@ -981,9 +1003,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
{
- path = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + subtitleCodec);
- await ExtractTextSubtitle(mediaSource, subtitleStream, subtitleCodec, path, cancellationToken)
- .ConfigureAwait(false);
+ var cachePath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + subtitleCodec);
+ if (cachePath is not null)
+ {
+ path = cachePath;
+ await ExtractTextSubtitle(mediaSource, subtitleStream, subtitleCodec, path, cancellationToken)
+ .ConfigureAwait(false);
+ }
}
var result = await DetectCharset(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false);
@@ -1007,20 +1033,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles
switch (protocol)
{
case MediaProtocol.Http:
- {
- using var stream = await _httpClientFactory
- .CreateClient(NamedClient.Default)
- .GetStreamAsync(new Uri(path), cancellationToken)
- .ConfigureAwait(false);
+ {
+ using var stream = await _httpClientFactory
+ .CreateClient(NamedClient.Default)
+ .GetStreamAsync(new Uri(path), cancellationToken)
+ .ConfigureAwait(false);
- return await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false);
- }
+ return await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false);
+ }
case MediaProtocol.File:
- {
- return await CharsetDetector.DetectFromFileAsync(path, cancellationToken)
- .ConfigureAwait(false);
- }
+ {
+ return await CharsetDetector.DetectFromFileAsync(path, cancellationToken)
+ .ConfigureAwait(false);
+ }
default:
throw new ArgumentOutOfRangeException(nameof(protocol), protocol, "Unsupported protocol");
diff --git a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs
deleted file mode 100644
index ea45f2070a..0000000000
--- a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using System.IO;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using MediaBrowser.Model.MediaInfo;
-
-namespace MediaBrowser.MediaEncoding.Subtitles
-{
- /// <summary>
- /// TTML subtitle writer.
- /// </summary>
- public partial class TtmlWriter : ISubtitleWriter
- {
- [GeneratedRegex(@"\\n", RegexOptions.IgnoreCase)]
- private static partial Regex NewLineEscapeRegex();
-
- /// <inheritdoc />
- public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
- {
- // Example: https://github.com/zmalltalker/ttml2vtt/blob/master/data/sample.xml
- // Parser example: https://github.com/mozilla/popcorn-js/blob/master/parsers/parserTTML/popcorn.parserTTML.js
-
- using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
- {
- writer.WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
- writer.WriteLine("<tt xmlns=\"http://www.w3.org/ns/ttml\" xmlns:tts=\"http://www.w3.org/2006/04/ttaf1#styling\" lang=\"no\">");
-
- writer.WriteLine("<head>");
- writer.WriteLine("<styling>");
- writer.WriteLine("<style id=\"italic\" tts:fontStyle=\"italic\" />");
- writer.WriteLine("<style id=\"left\" tts:textAlign=\"left\" />");
- writer.WriteLine("<style id=\"center\" tts:textAlign=\"center\" />");
- writer.WriteLine("<style id=\"right\" tts:textAlign=\"right\" />");
- writer.WriteLine("</styling>");
- writer.WriteLine("</head>");
-
- writer.WriteLine("<body>");
- writer.WriteLine("<div>");
-
- foreach (var trackEvent in info.TrackEvents)
- {
- var text = trackEvent.Text;
-
- text = NewLineEscapeRegex().Replace(text, "<br/>");
-
- writer.WriteLine(
- "<p begin=\"{0}\" dur=\"{1}\">{2}</p>",
- trackEvent.StartPositionTicks,
- trackEvent.EndPositionTicks - trackEvent.StartPositionTicks,
- text);
- }
-
- writer.WriteLine("</div>");
- writer.WriteLine("</body>");
-
- writer.WriteLine("</tt>");
- }
- }
- }
-}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
deleted file mode 100644
index 3e0f47b5ae..0000000000
--- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System;
-using System.IO;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using MediaBrowser.Model.MediaInfo;
-
-namespace MediaBrowser.MediaEncoding.Subtitles
-{
- /// <summary>
- /// Subtitle writer for the WebVTT format.
- /// </summary>
- public partial class VttWriter : ISubtitleWriter
- {
- [GeneratedRegex(@"\\n", RegexOptions.IgnoreCase)]
- private static partial Regex NewlineEscapeRegex();
-
- /// <inheritdoc />
- public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
- {
- using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
- {
- writer.WriteLine("WEBVTT");
- writer.WriteLine();
- writer.WriteLine("Region: id:subtitle width:80% lines:3 regionanchor:50%,100% viewportanchor:50%,90%");
- writer.WriteLine();
- foreach (var trackEvent in info.TrackEvents)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks);
- var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks);
-
- // make sure the start and end times are different and sequential
- if (endTime.TotalMilliseconds <= startTime.TotalMilliseconds)
- {
- endTime = startTime.Add(TimeSpan.FromMilliseconds(1));
- }
-
- writer.WriteLine(@"{0:hh\:mm\:ss\.fff} --> {1:hh\:mm\:ss\.fff} region:subtitle line:90%", startTime, endTime);
-
- var text = trackEvent.Text;
-
- // TODO: Not sure how to handle these
- text = NewlineEscapeRegex().Replace(text, " ");
-
- writer.WriteLine(text);
- writer.WriteLine();
- }
- }
- }
- }
-}
diff --git a/MediaBrowser.Model/Channels/ChannelFeatures.cs b/MediaBrowser.Model/Channels/ChannelFeatures.cs
index 1ca8e80a6f..57803c9765 100644
--- a/MediaBrowser.Model/Channels/ChannelFeatures.cs
+++ b/MediaBrowser.Model/Channels/ChannelFeatures.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Generic;
namespace MediaBrowser.Model.Channels
{
@@ -8,9 +9,9 @@ namespace MediaBrowser.Model.Channels
{
public ChannelFeatures(string name, Guid id)
{
- MediaTypes = Array.Empty<ChannelMediaType>();
- ContentTypes = Array.Empty<ChannelMediaContentType>();
- DefaultSortFields = Array.Empty<ChannelItemSortField>();
+ MediaTypes = [];
+ ContentTypes = [];
+ DefaultSortFields = [];
Name = name;
Id = id;
@@ -38,13 +39,13 @@ namespace MediaBrowser.Model.Channels
/// Gets or sets the media types.
/// </summary>
/// <value>The media types.</value>
- public ChannelMediaType[] MediaTypes { get; set; }
+ public IReadOnlyList<ChannelMediaType> MediaTypes { get; set; }
/// <summary>
/// Gets or sets the content types.
/// </summary>
/// <value>The content types.</value>
- public ChannelMediaContentType[] ContentTypes { get; set; }
+ public IReadOnlyList<ChannelMediaContentType> ContentTypes { get; set; }
/// <summary>
/// Gets or sets the maximum number of records the channel allows retrieving at a time.
@@ -61,7 +62,7 @@ namespace MediaBrowser.Model.Channels
/// Gets or sets the default sort orders.
/// </summary>
/// <value>The default sort orders.</value>
- public ChannelItemSortField[] DefaultSortFields { get; set; }
+ public IReadOnlyList<ChannelItemSortField> DefaultSortFields { get; set; }
/// <summary>
/// Gets or sets a value indicating whether a sort ascending/descending toggle is supported.
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index 98fc2e632f..4d052d8012 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -43,6 +43,7 @@ public class EncodingOptions
VppTonemappingContrast = 1;
H264Crf = 23;
H265Crf = 28;
+ EncoderPreset = EncoderPreset.auto;
DeinterlaceDoubleRate = false;
DeinterlaceMethod = DeinterlaceMethod.yadif;
EnableDecodingColorDepth10Hevc = true;
@@ -61,7 +62,7 @@ public class EncodingOptions
SubtitleExtractionTimeoutMinutes = 30;
AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = ["mkv"];
HardwareDecodingCodecs = ["h264", "vc1"];
- HlsAudioSeekStrategy = HlsAudioSeekStrategy.DisableAccurateSeek;
+ HlsAudioSeekStrategy = HlsAudioSeekStrategy.TrimCopiedAudio;
}
/// <summary>
@@ -217,7 +218,7 @@ public class EncodingOptions
/// <summary>
/// Gets or sets the encoder preset.
/// </summary>
- public EncoderPreset? EncoderPreset { get; set; }
+ public EncoderPreset EncoderPreset { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the framerate is doubled when deinterlacing.
@@ -307,6 +308,6 @@ public class EncodingOptions
/// <summary>
/// Gets or sets the method used for audio seeking in HLS.
/// </summary>
- [DefaultValue(HlsAudioSeekStrategy.DisableAccurateSeek)]
+ [DefaultValue(HlsAudioSeekStrategy.TrimCopiedAudio)]
public HlsAudioSeekStrategy HlsAudioSeekStrategy { get; set; }
}
diff --git a/MediaBrowser.Model/Configuration/HlsAudioSeekStrategy.cs b/MediaBrowser.Model/Configuration/HlsAudioSeekStrategy.cs
index 49feeb435f..c9155faeb1 100644
--- a/MediaBrowser.Model/Configuration/HlsAudioSeekStrategy.cs
+++ b/MediaBrowser.Model/Configuration/HlsAudioSeekStrategy.cs
@@ -7,11 +7,12 @@ namespace MediaBrowser.Model.Configuration
public enum HlsAudioSeekStrategy
{
/// <summary>
- /// If the video stream is transcoded and the audio stream is copied,
- /// seek the video stream to the same keyframe as the audio stream. The
- /// resulting timestamps in the output streams may be inaccurate.
+ /// When video is transcoded and audio is copied, use a bitstream filter
+ /// to drop copied audio packets before the seek point, aligning them
+ /// with the accurately-seeked video. Timestamps are accurate and audio
+ /// remains stream-copied (no re-encoding overhead).
/// </summary>
- DisableAccurateSeek = 0,
+ TrimCopiedAudio = 0,
/// <summary>
/// Prevent audio streams from being copied if the video stream is transcoded.
diff --git a/MediaBrowser.Model/Configuration/MetadataPluginType.cs b/MediaBrowser.Model/Configuration/MetadataPluginType.cs
index 670d6e3837..476060ceef 100644
--- a/MediaBrowser.Model/Configuration/MetadataPluginType.cs
+++ b/MediaBrowser.Model/Configuration/MetadataPluginType.cs
@@ -15,6 +15,8 @@ namespace MediaBrowser.Model.Configuration
MetadataSaver,
SubtitleFetcher,
LyricFetcher,
- MediaSegmentProvider
+ MediaSegmentProvider,
+ LocalSimilarityProvider,
+ SimilarityProvider
}
}
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index a58c01c960..ac5c12304e 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -287,5 +287,5 @@ public class ServerConfiguration : BaseApplicationConfiguration
/// <summary>
/// Gets or sets a value indicating whether old authorization methods are allowed.
/// </summary>
- public bool EnableLegacyAuthorization { get; set; } = true;
+ public bool EnableLegacyAuthorization { get; set; }
}
diff --git a/MediaBrowser.Model/Configuration/TypeOptions.cs b/MediaBrowser.Model/Configuration/TypeOptions.cs
index d0179e5aab..3aa85034e5 100644
--- a/MediaBrowser.Model/Configuration/TypeOptions.cs
+++ b/MediaBrowser.Model/Configuration/TypeOptions.cs
@@ -304,11 +304,13 @@ namespace MediaBrowser.Model.Configuration
public TypeOptions()
{
- MetadataFetchers = Array.Empty<string>();
- MetadataFetcherOrder = Array.Empty<string>();
- ImageFetchers = Array.Empty<string>();
- ImageFetcherOrder = Array.Empty<string>();
- ImageOptions = Array.Empty<ImageOption>();
+ MetadataFetchers = [];
+ MetadataFetcherOrder = [];
+ ImageFetchers = [];
+ ImageFetcherOrder = [];
+ ImageOptions = [];
+ SimilarItemProviders = [];
+ SimilarItemProviderOrder = [];
}
public string Type { get; set; }
@@ -323,6 +325,10 @@ namespace MediaBrowser.Model.Configuration
public ImageOption[] ImageOptions { get; set; }
+ public string[] SimilarItemProviders { get; set; }
+
+ public string[] SimilarItemProviderOrder { get; set; }
+
public ImageOption GetImageOptions(ImageType type)
{
foreach (var i in ImageOptions)
diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
index 79ee683a2d..a6018f369d 100644
--- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs
+++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
@@ -33,6 +33,7 @@ namespace MediaBrowser.Model.Dlna
/// <param name="numAudioStreams">The number of audio streams.</param>
/// <param name="videoCodecTag">The video codec tag.</param>
/// <param name="isAvc">A value indicating whether the video is AVC.</param>
+ /// <param name="videoRotation">The video rotation angle, usually 0 or +-90/180.</param>
/// <returns><b>True</b> if the condition is satisfied.</returns>
public static bool IsVideoConditionSatisfied(
ProfileCondition condition,
@@ -53,7 +54,8 @@ namespace MediaBrowser.Model.Dlna
int? numVideoStreams,
int? numAudioStreams,
string? videoCodecTag,
- bool? isAvc)
+ bool? isAvc,
+ int? videoRotation)
{
switch (condition.Property)
{
@@ -93,6 +95,8 @@ namespace MediaBrowser.Model.Dlna
return IsConditionSatisfied(condition, numVideoStreams);
case ProfileConditionValue.VideoTimestamp:
return IsConditionSatisfied(condition, timestamp);
+ case ProfileConditionValue.VideoRotation:
+ return IsConditionSatisfied(condition, videoRotation);
default:
return true;
}
diff --git a/MediaBrowser.Model/Dlna/ProfileConditionValue.cs b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs
index b66a15840b..c6171c7ab2 100644
--- a/MediaBrowser.Model/Dlna/ProfileConditionValue.cs
+++ b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs
@@ -28,6 +28,7 @@ namespace MediaBrowser.Model.Dlna
AudioSampleRate = 22,
AudioBitDepth = 23,
VideoRangeType = 24,
- NumStreams = 25
+ NumStreams = 25,
+ VideoRotation = 26
}
}
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index c9697c685c..d875bbe8ed 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -22,7 +22,7 @@ namespace MediaBrowser.Model.Dlna
internal const TranscodeReason ContainerReasons = TranscodeReason.ContainerNotSupported | TranscodeReason.ContainerBitrateExceedsLimit;
internal const TranscodeReason AudioCodecReasons = TranscodeReason.AudioBitrateNotSupported | TranscodeReason.AudioChannelsNotSupported | TranscodeReason.AudioProfileNotSupported | TranscodeReason.AudioSampleRateNotSupported | TranscodeReason.SecondaryAudioNotSupported | TranscodeReason.AudioBitDepthNotSupported | TranscodeReason.AudioIsExternal;
internal const TranscodeReason AudioReasons = TranscodeReason.AudioCodecNotSupported | AudioCodecReasons;
- internal const TranscodeReason VideoCodecReasons = TranscodeReason.VideoResolutionNotSupported | TranscodeReason.AnamorphicVideoNotSupported | TranscodeReason.InterlacedVideoNotSupported | TranscodeReason.VideoBitDepthNotSupported | TranscodeReason.VideoBitrateNotSupported | TranscodeReason.VideoFramerateNotSupported | TranscodeReason.VideoLevelNotSupported | TranscodeReason.RefFramesNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.VideoProfileNotSupported;
+ internal const TranscodeReason VideoCodecReasons = TranscodeReason.VideoResolutionNotSupported | TranscodeReason.AnamorphicVideoNotSupported | TranscodeReason.InterlacedVideoNotSupported | TranscodeReason.VideoBitDepthNotSupported | TranscodeReason.VideoBitrateNotSupported | TranscodeReason.VideoFramerateNotSupported | TranscodeReason.VideoLevelNotSupported | TranscodeReason.RefFramesNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.VideoProfileNotSupported | TranscodeReason.VideoRotationNotSupported;
internal const TranscodeReason VideoReasons = TranscodeReason.VideoCodecNotSupported | VideoCodecReasons;
internal const TranscodeReason DirectStreamReasons = AudioReasons | TranscodeReason.ContainerNotSupported | TranscodeReason.VideoCodecTagNotSupported;
@@ -380,6 +380,9 @@ namespace MediaBrowser.Model.Dlna
case ProfileConditionValue.VideoRangeType:
return TranscodeReason.VideoRangeTypeNotSupported;
+ case ProfileConditionValue.VideoRotation:
+ return TranscodeReason.VideoRotationNotSupported;
+
case ProfileConditionValue.VideoTimestamp:
// TODO
return 0;
@@ -572,7 +575,12 @@ namespace MediaBrowser.Model.Dlna
{
foreach (var profile in subtitleProfiles)
{
- if (profile.Method == SubtitleDeliveryMethod.External && string.Equals(profile.Format, stream.Codec, StringComparison.OrdinalIgnoreCase))
+ if (profile.Method == SubtitleDeliveryMethod.External
+ && (string.Equals(profile.Format, stream.Codec, StringComparison.OrdinalIgnoreCase)
+ // FFmpeg cannot mux VobSub back into an .idx/.sub pair, so extracted VobSub streams are exposed as .mks.
+ || (string.Equals(profile.Format, "mks", StringComparison.OrdinalIgnoreCase)
+ && stream.IsVobSubSubtitleStream
+ && (!stream.IsExternal || stream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)))))
{
return stream.Index;
}
@@ -1040,6 +1048,7 @@ namespace MediaBrowser.Model.Dlna
bool? isInterlaced = videoStream?.IsInterlaced;
string? videoCodecTag = videoStream?.CodecTag;
bool? isAvc = videoStream?.IsAVC;
+ int? videoRotation = videoStream?.Rotation;
TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp;
int? packetLength = videoStream?.PacketLength;
@@ -1054,7 +1063,7 @@ namespace MediaBrowser.Model.Dlna
var appliedVideoConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.Video &&
i.ContainsAnyCodec(playlistItem.VideoCodecs, container, useSubContainer) &&
- i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numStreams, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)))
+ i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numStreams, numVideoStreams, numAudioStreams, videoCodecTag, isAvc, videoRotation)))
// Reverse codec profiles for backward compatibility - first codec profile has higher priority
.Reverse();
foreach (var condition in appliedVideoConditions)
@@ -1447,7 +1456,7 @@ namespace MediaBrowser.Model.Dlna
string? outputContainer,
MediaStreamProtocol? transcodingSubProtocol)
{
- if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || transcodingSubProtocol != MediaStreamProtocol.hls))
+ if (CanConsiderEmbedSubtitle(subtitleStream, playMethod, transcodingSubProtocol, outputContainer))
{
// Look for supported embedded subs of the same format
foreach (var profile in subtitleProfiles)
@@ -1536,6 +1545,19 @@ namespace MediaBrowser.Model.Dlna
return false;
}
+ private static bool CanConsiderEmbedSubtitle(MediaStream subtitleStream, PlayMethod playMethod, MediaStreamProtocol? transcodingSubProtocol, string? outputContainer)
+ {
+ if (subtitleStream.IsExternal)
+ {
+ return playMethod == PlayMethod.Transcode
+ && transcodingSubProtocol != MediaStreamProtocol.hls
+ && IsSubtitleEmbedSupported(outputContainer);
+ }
+
+ return playMethod != PlayMethod.Transcode
+ || transcodingSubProtocol != MediaStreamProtocol.hls;
+ }
+
private static SubtitleProfile? GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, bool allowConversion)
{
foreach (var profile in subtitleProfiles)
@@ -1560,10 +1582,17 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if ((profile.Method == SubtitleDeliveryMethod.External && subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format)) ||
+ // FFmpeg cannot mux VobSub back into an .idx/.sub pair, so extracted VobSub streams are matched against external .mks delivery profiles.
+ bool isVobSubMksProfile = string.Equals(profile.Format, "mks", StringComparison.OrdinalIgnoreCase)
+ && subtitleStream.IsVobSubSubtitleStream
+ && (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase));
+
+ if ((profile.Method == SubtitleDeliveryMethod.External
+ && (isVobSubMksProfile || subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format))) ||
(profile.Method == SubtitleDeliveryMethod.Hls && subtitleStream.IsTextSubtitleStream))
{
- bool requiresConversion = !string.Equals(subtitleStream.Codec, profile.Format, StringComparison.OrdinalIgnoreCase);
+ bool requiresConversion = !isVobSubMksProfile
+ && !string.Equals(subtitleStream.Codec, profile.Format, StringComparison.OrdinalIgnoreCase);
if (!requiresConversion)
{
@@ -2059,6 +2088,38 @@ namespace MediaBrowser.Model.Dlna
break;
}
+ case ProfileConditionValue.VideoRotation:
+ {
+ if (string.IsNullOrEmpty(qualifier))
+ {
+ continue;
+ }
+
+ // change from split by | to comma
+ // strip spaces to avoid having to encode
+ var values = value
+ .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ if (condition.Condition == ProfileConditionType.Equals)
+ {
+ item.SetOption(qualifier, "rotation", string.Join(',', values));
+ }
+ else if (condition.Condition == ProfileConditionType.EqualsAny)
+ {
+ var currentValue = item.GetOption(qualifier, "rotation");
+ if (!string.IsNullOrEmpty(currentValue) && values.Any(v => string.Equals(v, currentValue, StringComparison.OrdinalIgnoreCase)))
+ {
+ item.SetOption(qualifier, "rotation", currentValue);
+ }
+ else
+ {
+ item.SetOption(qualifier, "rotation", string.Join(',', values));
+ }
+ }
+
+ break;
+ }
+
case ProfileConditionValue.Height:
{
if (!enableNonQualifiedConditions)
@@ -2281,6 +2342,7 @@ namespace MediaBrowser.Model.Dlna
bool? isInterlaced = videoStream?.IsInterlaced;
string? videoCodecTag = videoStream?.CodecTag;
bool? isAvc = videoStream?.IsAVC;
+ int? videoRotation = videoStream?.Rotation;
TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : mediaSource.Timestamp;
int? packetLength = videoStream?.PacketLength;
@@ -2290,7 +2352,7 @@ namespace MediaBrowser.Model.Dlna
int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio);
int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video);
- return conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numStreams, numVideoStreams, numAudioStreams, videoCodecTag, isAvc));
+ return conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numStreams, numVideoStreams, numAudioStreams, videoCodecTag, isAvc, videoRotation));
}
/// <summary>
diff --git a/MediaBrowser.Model/Drawing/ImageDimensions.cs b/MediaBrowser.Model/Drawing/ImageDimensions.cs
index f84fe68305..49528ef8ae 100644
--- a/MediaBrowser.Model/Drawing/ImageDimensions.cs
+++ b/MediaBrowser.Model/Drawing/ImageDimensions.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#pragma warning disable CA1815
using System.Globalization;
diff --git a/MediaBrowser.Model/Dto/SessionInfoDto.cs b/MediaBrowser.Model/Dto/SessionInfoDto.cs
index d727cd8741..16b201de9d 100644
--- a/MediaBrowser.Model/Dto/SessionInfoDto.cs
+++ b/MediaBrowser.Model/Dto/SessionInfoDto.cs
@@ -149,13 +149,7 @@ public class SessionInfoDto
public IReadOnlyList<QueueItem>? NowPlayingQueue { get; set; }
/// <summary>
- /// Gets or sets the now playing queue full items.
- /// </summary>
- /// <value>The now playing queue full items.</value>
- public IReadOnlyList<BaseItemDto>? NowPlayingQueueFullItems { get; set; }
-
- /// <summary>
- /// Gets or sets a value indicating whether the session has a custom device name.
+ /// Gets or sets a value indicating whether this session has a custom device name.
/// </summary>
/// <value><c>true</c> if this session has a custom device name; otherwise, <c>false</c>.</value>
public bool HasCustomDeviceName { get; set; }
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index dad4a6e149..f057714bea 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -644,13 +644,32 @@ namespace MediaBrowser.Model.Entities
}
}
+ [JsonIgnore]
+ public bool IsVobSubSubtitleStream
+ {
+ get
+ {
+ if (Type != MediaStreamType.Subtitle)
+ {
+ return false;
+ }
+
+ if (string.IsNullOrEmpty(Codec) && !IsExternal)
+ {
+ return false;
+ }
+
+ return IsVobSubFormat(Codec);
+ }
+ }
+
/// <summary>
/// Gets a value indicating whether this is a subtitle steam that is extractable by ffmpeg.
/// All text-based and pgs subtitles can be extracted.
/// </summary>
/// <value><c>true</c> if this is a extractable subtitle steam otherwise, <c>false</c>.</value>
[JsonIgnore]
- public bool IsExtractableSubtitleStream => IsTextSubtitleStream || IsPgsSubtitleStream;
+ public bool IsExtractableSubtitleStream => IsTextSubtitleStream || IsPgsSubtitleStream || IsVobSubSubtitleStream;
/// <summary>
/// Gets or sets a value indicating whether [supports external stream].
@@ -728,6 +747,7 @@ namespace MediaBrowser.Model.Entities
return codec.Contains("microdvd", StringComparison.OrdinalIgnoreCase)
|| (!codec.Contains("pgs", StringComparison.OrdinalIgnoreCase)
&& !codec.Contains("dvdsub", StringComparison.OrdinalIgnoreCase)
+ && !codec.Contains("vobsub", StringComparison.OrdinalIgnoreCase)
&& !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase));
@@ -741,6 +761,14 @@ namespace MediaBrowser.Model.Entities
|| string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase);
}
+ public static bool IsVobSubFormat(string format)
+ {
+ string codec = format ?? string.Empty;
+
+ return codec.Contains("dvdsub", StringComparison.OrdinalIgnoreCase)
+ || codec.Contains("vobsub", StringComparison.OrdinalIgnoreCase);
+ }
+
public bool SupportsSubtitleConversionTo(string toCodec)
{
if (!IsTextSubtitleStream)
diff --git a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
index 7c9ee18ca4..28c3c66af7 100644
--- a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
+++ b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
@@ -50,7 +50,7 @@ namespace MediaBrowser.Model.Extensions
return 0;
})
- .ThenByDescending(i => Math.Round(i.CommunityRating ?? 0, 1) )
+ .ThenByDescending(i => Math.Round(i.CommunityRating ?? 0, 1))
.ThenByDescending(i => i.VoteCount ?? 0);
}
}
diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
index f6e65028e4..7ad240abfb 100644
--- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs
+++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
@@ -51,6 +51,15 @@ public interface ILocalizationManager
string GetLocalizedString(string phrase);
/// <summary>
+ /// Gets the localized string using the server's configured UICulture,
+ /// ignoring the current request's culture. Use this for data that is
+ /// persisted (e.g. activity log entries) rather than returned per-request.
+ /// </summary>
+ /// <param name="phrase">The phrase.</param>
+ /// <returns>System.String.</returns>
+ string GetServerLocalizedString(string phrase);
+
+ /// <summary>
/// Gets the localization options.
/// </summary>
/// <returns><see cref="IEnumerable{LocalizationOption}" />.</returns>
diff --git a/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs b/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs
index 53d0173750..9a21461d82 100644
--- a/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs
+++ b/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Model.MediaSegments;
diff --git a/MediaBrowser.Model/Querying/QueryFilters.cs b/MediaBrowser.Model/Querying/QueryFilters.cs
index 73b27a7b06..095f460923 100644
--- a/MediaBrowser.Model/Querying/QueryFilters.cs
+++ b/MediaBrowser.Model/Querying/QueryFilters.cs
@@ -2,6 +2,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Generic;
using MediaBrowser.Model.Dto;
namespace MediaBrowser.Model.Querying
@@ -12,10 +13,16 @@ namespace MediaBrowser.Model.Querying
{
Tags = Array.Empty<string>();
Genres = Array.Empty<NameGuidPair>();
+ AudioLanguages = Array.Empty<NameValuePair>();
+ SubtitleLanguages = Array.Empty<NameValuePair>();
}
- public NameGuidPair[] Genres { get; set; }
+ public IReadOnlyList<NameGuidPair> Genres { get; set; }
- public string[] Tags { get; set; }
+ public IReadOnlyList<string> Tags { get; set; }
+
+ public IReadOnlyList<NameValuePair> AudioLanguages { get; set; }
+
+ public IReadOnlyList<NameValuePair> SubtitleLanguages { get; set; }
}
}
diff --git a/MediaBrowser.Model/Session/TranscodeReason.cs b/MediaBrowser.Model/Session/TranscodeReason.cs
index 902bab9a6e..4ea60f115a 100644
--- a/MediaBrowser.Model/Session/TranscodeReason.cs
+++ b/MediaBrowser.Model/Session/TranscodeReason.cs
@@ -24,6 +24,7 @@ namespace MediaBrowser.Model.Session
VideoResolutionNotSupported = 1 << 8,
VideoBitDepthNotSupported = 1 << 9,
VideoFramerateNotSupported = 1 << 10,
+ VideoRotationNotSupported = 1 << 27,
RefFramesNotSupported = 1 << 11,
AnamorphicVideoNotSupported = 1 << 12,
InterlacedVideoNotSupported = 1 << 13,
diff --git a/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs
index 4429623dd9..ded66652ce 100644
--- a/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs
+++ b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs
@@ -50,7 +50,7 @@ namespace MediaBrowser.Model.SyncPlay
/// </summary>
Seek = 8,
- /// <summary>
+ /// <summary>
/// A user is signaling that playback is buffering.
/// </summary>
Buffer = 9,
diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayGroupDoesNotExistUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayGroupDoesNotExistUpdate.cs
index 7e2d10c8b8..ccf5fdb07e 100644
--- a/MediaBrowser.Model/SyncPlay/SyncPlayGroupDoesNotExistUpdate.cs
+++ b/MediaBrowser.Model/SyncPlay/SyncPlayGroupDoesNotExistUpdate.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel;
namespace MediaBrowser.Model.SyncPlay;
diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayGroupJoinedUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayGroupJoinedUpdate.cs
index bfb49152a3..dcb039ee93 100644
--- a/MediaBrowser.Model/SyncPlay/SyncPlayGroupJoinedUpdate.cs
+++ b/MediaBrowser.Model/SyncPlay/SyncPlayGroupJoinedUpdate.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel;
namespace MediaBrowser.Model.SyncPlay;
diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayGroupLeftUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayGroupLeftUpdate.cs
index 5ff60c5c27..f20e143e02 100644
--- a/MediaBrowser.Model/SyncPlay/SyncPlayGroupLeftUpdate.cs
+++ b/MediaBrowser.Model/SyncPlay/SyncPlayGroupLeftUpdate.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel;
namespace MediaBrowser.Model.SyncPlay;
diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayLibraryAccessDeniedUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayLibraryAccessDeniedUpdate.cs
index 0d9a722f78..89e5706d86 100644
--- a/MediaBrowser.Model/SyncPlay/SyncPlayLibraryAccessDeniedUpdate.cs
+++ b/MediaBrowser.Model/SyncPlay/SyncPlayLibraryAccessDeniedUpdate.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel;
namespace MediaBrowser.Model.SyncPlay;
diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayNotInGroupUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayNotInGroupUpdate.cs
index a3b610f619..4ba893be5b 100644
--- a/MediaBrowser.Model/SyncPlay/SyncPlayNotInGroupUpdate.cs
+++ b/MediaBrowser.Model/SyncPlay/SyncPlayNotInGroupUpdate.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel;
namespace MediaBrowser.Model.SyncPlay;
diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayPlayQueueUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayPlayQueueUpdate.cs
index 83d9bd40bc..a39f20735b 100644
--- a/MediaBrowser.Model/SyncPlay/SyncPlayPlayQueueUpdate.cs
+++ b/MediaBrowser.Model/SyncPlay/SyncPlayPlayQueueUpdate.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel;
namespace MediaBrowser.Model.SyncPlay;
diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayStateUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayStateUpdate.cs
index 744ca46a0b..61cb8adbaa 100644
--- a/MediaBrowser.Model/SyncPlay/SyncPlayStateUpdate.cs
+++ b/MediaBrowser.Model/SyncPlay/SyncPlayStateUpdate.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel;
namespace MediaBrowser.Model.SyncPlay;
diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayUserJoinedUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayUserJoinedUpdate.cs
index e8c6b4df41..247e6a57b2 100644
--- a/MediaBrowser.Model/SyncPlay/SyncPlayUserJoinedUpdate.cs
+++ b/MediaBrowser.Model/SyncPlay/SyncPlayUserJoinedUpdate.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel;
namespace MediaBrowser.Model.SyncPlay;
diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayUserLeftUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayUserLeftUpdate.cs
index 97be8e63a8..ba053747cc 100644
--- a/MediaBrowser.Model/SyncPlay/SyncPlayUserLeftUpdate.cs
+++ b/MediaBrowser.Model/SyncPlay/SyncPlayUserLeftUpdate.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel;
namespace MediaBrowser.Model.SyncPlay;
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index 2c393ca862..95e4d46c59 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -187,7 +187,7 @@ namespace MediaBrowser.Model.Users
[Required(AllowEmptyStrings = false)]
public string AuthenticationProviderId { get; set; }
- [Required(AllowEmptyStrings= false)]
+ [Required(AllowEmptyStrings = false)]
public string PasswordResetProviderId { get; set; }
/// <summary>
diff --git a/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalId.cs b/MediaBrowser.Providers/Books/ComicVine/ComicVineExternalId.cs
index 8cbd1f89a7..e2e785eaca 100644
--- a/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalId.cs
+++ b/MediaBrowser.Providers/Books/ComicVine/ComicVineExternalId.cs
@@ -3,7 +3,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
-namespace MediaBrowser.Providers.Plugins.ComicVine
+namespace MediaBrowser.Providers.Books.ComicVine
{
/// <inheritdoc />
public class ComicVineExternalId : IExternalId
diff --git a/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalUrlProvider.cs b/MediaBrowser.Providers/Books/ComicVine/ComicVineExternalUrlProvider.cs
index 9122399179..a8450ec599 100644
--- a/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalUrlProvider.cs
+++ b/MediaBrowser.Providers/Books/ComicVine/ComicVineExternalUrlProvider.cs
@@ -3,7 +3,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-namespace MediaBrowser.Providers.Plugins.ComicVine;
+namespace MediaBrowser.Providers.Books.ComicVine;
/// <inheritdoc/>
public class ComicVineExternalUrlProvider : IExternalUrlProvider
diff --git a/MediaBrowser.Providers/Plugins/ComicVine/ComicVinePersonExternalId.cs b/MediaBrowser.Providers/Books/ComicVine/ComicVinePersonExternalId.cs
index 26b8e11380..f625fb9649 100644
--- a/MediaBrowser.Providers/Plugins/ComicVine/ComicVinePersonExternalId.cs
+++ b/MediaBrowser.Providers/Books/ComicVine/ComicVinePersonExternalId.cs
@@ -3,7 +3,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
-namespace MediaBrowser.Providers.Plugins.ComicVine
+namespace MediaBrowser.Providers.Books.ComicVine
{
/// <inheritdoc />
public class ComicVinePersonExternalId : IExternalId
diff --git a/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalId.cs b/MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalId.cs
index 02d3b36974..aac8cdff65 100644
--- a/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalId.cs
+++ b/MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalId.cs
@@ -3,7 +3,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
-namespace MediaBrowser.Providers.Plugins.GoogleBooks
+namespace MediaBrowser.Providers.Books.GoogleBooks
{
/// <inheritdoc />
public class GoogleBooksExternalId : IExternalId
diff --git a/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalUrlProvider.cs b/MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalUrlProvider.cs
index 95047ee83e..0559db2e2b 100644
--- a/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalUrlProvider.cs
+++ b/MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalUrlProvider.cs
@@ -3,7 +3,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-namespace MediaBrowser.Providers.Plugins.GoogleBooks;
+namespace MediaBrowser.Providers.Books.GoogleBooks;
/// <inheritdoc/>
public class GoogleBooksExternalUrlProvider : IExternalUrlProvider
diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs
index 6e678802c1..d2331c6864 100644
--- a/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs
+++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs
@@ -1,4 +1,4 @@
-using System.IO;
+using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 65edcb2a92..73df6d03d2 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -1,17 +1,20 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Extensions;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.BaseItemManager;
@@ -64,6 +67,7 @@ namespace MediaBrowser.Providers.Manager
private readonly PriorityQueue<(Guid ItemId, MetadataRefreshOptions RefreshOptions), RefreshPriority> _refreshQueue = new();
private readonly IMemoryCache _memoryCache;
private readonly IMediaSegmentManager _mediaSegmentManager;
+ private readonly ISimilarItemsManager _similarItemsManager;
private readonly AsyncKeyedLocker<string> _imageSaveLock = new(o =>
{
o.PoolSize = 20;
@@ -101,6 +105,7 @@ namespace MediaBrowser.Providers.Manager
/// <param name="lyricManager">The lyric manager.</param>
/// <param name="memoryCache">The memory cache.</param>
/// <param name="mediaSegmentManager">The media segment manager.</param>
+ /// <param name="similarItemsManager">The similar items manager.</param>
public ProviderManager(
IHttpClientFactory httpClientFactory,
ISubtitleManager subtitleManager,
@@ -113,7 +118,8 @@ namespace MediaBrowser.Providers.Manager
IBaseItemManager baseItemManager,
ILyricManager lyricManager,
IMemoryCache memoryCache,
- IMediaSegmentManager mediaSegmentManager)
+ IMediaSegmentManager mediaSegmentManager,
+ ISimilarItemsManager similarItemsManager)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
@@ -127,6 +133,7 @@ namespace MediaBrowser.Providers.Manager
_lyricManager = lyricManager;
_memoryCache = memoryCache;
_mediaSegmentManager = mediaSegmentManager;
+ _similarItemsManager = similarItemsManager;
CollectionFolder.LibraryOptionsUpdated += OnLibraryOptionsUpdated;
}
@@ -687,6 +694,14 @@ namespace MediaBrowser.Providers.Manager
Type = MetadataPluginType.MediaSegmentProvider
}));
+ // Similar items providers
+ var similarItemsProviders = _similarItemsManager.GetSimilarItemsProviders<T>();
+ pluginList.AddRange(similarItemsProviders.Select(i => new MetadataPlugin
+ {
+ Name = i.Name,
+ Type = i.Type
+ }));
+
summary.Plugins = pluginList.ToArray();
var supportedImageTypes = imageProviders.OfType<IRemoteImageProvider>()
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index ed0c63b97f..1032582900 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -47,16 +47,53 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
+ <!-- AudioDb -->
<ItemGroup>
<None Remove="Plugins\AudioDb\Configuration\config.html" />
<EmbeddedResource Include="Plugins\AudioDb\Configuration\config.html" />
- <None Remove="Plugins\Omdb\Configuration\config.html" />
- <EmbeddedResource Include="Plugins\Omdb\Configuration\config.html" />
+ <None Remove="Plugins\AudioDb\jellyfin-plugin-tadb.svg" />
+ <EmbeddedResource Include="Plugins\AudioDb\jellyfin-plugin-tadb.svg" />
+ </ItemGroup>
+
+ <!-- ListenBrainz -->
+ <ItemGroup>
+ <None Remove="Plugins\ListenBrainz\Configuration\config.html" />
+ <EmbeddedResource Include="Plugins\ListenBrainz\Configuration\config.html" />
+ <None Remove="Plugins\ListenBrainz\Configuration\ListenBrainz_logo.svg" />
+ <EmbeddedResource Include="Plugins\ListenBrainz\Configuration\ListenBrainz_logo.svg" />
+ <None Remove="Plugins\ListenBrainz\Configuration\NOTICE.md" />
+ <EmbeddedResource Include="Plugins\ListenBrainz\Configuration\NOTICE.md" />
+ </ItemGroup>
+
+ <!-- MusicBrainz -->
+ <ItemGroup>
<None Remove="Plugins\MusicBrainz\Configuration\config.html" />
<EmbeddedResource Include="Plugins\MusicBrainz\Configuration\config.html" />
+ <None Remove="Plugins\MusicBrainz\jellyfin-plugin-musicbrainz.svg" />
+ <EmbeddedResource Include="Plugins\MusicBrainz\jellyfin-plugin-musicbrainz.svg" />
+ </ItemGroup>
+
+ <!-- Omdb -->
+ <ItemGroup>
+ <None Remove="Plugins\Omdb\Configuration\config.html" />
+ <EmbeddedResource Include="Plugins\Omdb\Configuration\config.html" />
+ <None Remove="Plugins\Omdb\jellyfin-plugin-omdb.png" />
+ <EmbeddedResource Include="Plugins\Omdb\jellyfin-plugin-omdb.png" />
+ </ItemGroup>
+
+ <!-- StudioImages -->
+ <ItemGroup>
<None Remove="Plugins\StudioImages\Configuration\config.html" />
<EmbeddedResource Include="Plugins\StudioImages\Configuration\config.html" />
+ <None Remove="Plugins\StudioImages\jellyfin-plugin-studioimages.svg" />
+ <EmbeddedResource Include="Plugins\StudioImages\jellyfin-plugin-studioimages.svg" />
+ </ItemGroup>
+
+ <!-- Tmdb -->
+ <ItemGroup>
<None Remove="Plugins\Tmdb\Configuration\config.html" />
<EmbeddedResource Include="Plugins\Tmdb\Configuration\config.html" />
+ <None Remove="Plugins\Tmdb\jellyfin-plugin-tmdb.svg" />
+ <EmbeddedResource Include="Plugins\Tmdb\jellyfin-plugin-tmdb.svg" />
</ItemGroup>
</Project>
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
index 6c2ad0573e..bb6b67286d 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
@@ -5,12 +5,13 @@ using System;
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
+using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Providers.Plugins.AudioDb
{
- public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
+ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages, IHasEmbeddedImage
{
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
@@ -29,6 +30,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
// TODO remove when plugin removed from server.
public override string ConfigurationFileName => "Jellyfin.Plugin.AudioDb.xml";
+ public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-tadb.svg";
+
public IEnumerable<PluginPageInfo> GetPages()
{
yield return new PluginPageInfo
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/jellyfin-plugin-tadb.svg b/MediaBrowser.Providers/Plugins/AudioDb/jellyfin-plugin-tadb.svg
new file mode 100644
index 0000000000..94fa55cc9c
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/AudioDb/jellyfin-plugin-tadb.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 1920 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect x="-0" y="-0" width="1920" height="1080" style="fill:#15191f;"/><path d="M537.166,347.444c8.07,-10.159 20.665,-17.047 33.734,-17.407c7.234,-0.375 14.064,3.372 19.136,8.286c7.925,7.81 8.69,21.499 1.816,30.217c-1.671,0.994 -2.666,2.406 -2.983,4.265c-6.023,7.061 -15.808,9.51 -24.741,7.954c0.129,-0.807 0.404,-2.406 0.547,-3.213c-0.288,0.692 -0.865,2.075 -1.153,2.767c-5.908,-2.19 -11.629,-5.576 -14.438,-11.47c-0.534,-0.663 -1.586,-1.989 -2.118,-2.651c1.513,-2.262 3.156,-4.438 4.813,-6.6c1.08,5.908 3.674,11.917 9.12,15.015c5.261,3.703 11.888,2.968 17.911,2.306c7.033,-4.856 13.603,-11.701 13.906,-20.765c1.672,-12.825 -11.052,-23.632 -23.329,-22.436c-12.551,0.418 -22.855,8.487 -31.384,16.931c-0.216,-0.793 -0.635,-2.406 -0.837,-3.199Z" style="fill-rule:nonzero;"/><path d="M594.388,339.719c13.79,10.995 11.341,35.506 -4.222,43.662c-0.259,-0.144 -0.777,-0.447 -1.023,-0.591c0.347,-2.277 0.101,-4.467 -0.734,-6.6c0.23,-0.231 0.677,-0.677 0.893,-0.893c2.581,-0.965 5.246,-2.147 5.188,-5.389c1.413,-5.029 3.891,-9.871 3.761,-15.217c0.288,-5.288 -2.089,-10.145 -3.862,-14.972Z" style="fill-rule:nonzero;"/><path d="M533.896,352.054c0.533,0.13 1.585,0.403 2.119,0.548c0.432,1.614 -0.692,2.997 -1.903,3.905c-0.62,-0.605 -1.844,-1.816 -2.45,-2.406c0.562,-0.519 1.671,-1.542 2.234,-2.046Z" style="fill-rule:nonzero;"/><path d="M527.399,362.127c0.867,0.145 2.602,0.448 3.469,0.593c-0.347,0.694 -1.055,2.082 -1.402,2.775c-0.636,-0.593 -1.908,-1.778 -2.559,-2.371l0.492,-0.997Z" style="fill-rule:nonzero;"/><path d="M541.691,374.548c1.325,-1.902 2.652,-3.804 3.919,-5.75c0.591,0.418 1.758,1.254 2.349,1.672c-1.096,2.161 -2.104,4.366 -3.113,6.556c-0.792,-0.62 -2.377,-1.859 -3.156,-2.478Z" style="fill-rule:nonzero;"/><path d="M548.693,415.501c-6.671,-13.113 -8.098,-29.9 0.375,-42.552c1.887,1.816 3.818,3.617 5.75,5.404c-7.493,11.038 -7.306,26.183 -0.345,37.437c-1.845,0.548 -5.232,3.285 -5.779,-0.288Z" style="fill-rule:nonzero;"/><path d="M555.382,379.275c8.617,3.343 18.603,5.706 27.191,0.908c1.283,1.888 2.911,3.458 5.346,3.43c-0.259,0.533 -0.778,1.614 -1.037,2.147c-10.591,4.237 -23.819,2.406 -31.5,-6.484Z" style="fill-rule:nonzero;"/><path d="M545.324,432.764c0.765,0.737 0.765,0.737 0,0Z" style="fill-rule:nonzero;"/><path d="M598.524,436.814c0.246,-0.159 0.737,-0.477 0.997,-0.636l-0.449,0.304l-0.549,0.332Z" style="fill-rule:nonzero;"/><path d="M603.119,441.538c0.781,0.853 0.781,0.853 0,0Z" style="fill-rule:nonzero;"/><path d="M647.1,441.308c1.557,1.038 2.291,3.185 1.584,4.957c-1.441,1.254 -2.939,2.464 -4.351,3.761c0.619,-2.997 0.966,-6.153 2.767,-8.718Z" style="fill-rule:nonzero;"/><path d="M612.63,449.046l0.693,0.029l-0.015,0.723l-0.723,-0.029l0.044,-0.723Z" style="fill-rule:nonzero;"/><path d="M640.572,472.908c2.897,0.606 -0.331,6.758 -2.708,5.173c-0.894,-2.003 0.951,-4.338 2.708,-5.173Z" style="fill-rule:nonzero;"/><path d="M448.849,474.551c0.708,0.765 0.708,0.765 0,0Z" style="fill-rule:nonzero;"/><path d="M451.63,477.55c0.766,0.723 0.766,0.723 0,0Z" style="fill-rule:nonzero;"/><path d="M443.057,503.803c0.766,0.809 0.766,0.809 0,0Z" style="fill-rule:nonzero;"/><path d="M457.653,506.586l0.246,0.043l0.101,0.101c-0.087,-0.043 -0.26,-0.116 -0.347,-0.143Z" style="fill-rule:nonzero;"/><path d="M454.569,509.249c0.781,0.708 0.781,0.708 0,0Z" style="fill-rule:nonzero;"/><path d="M460.377,509.077c0.781,0.723 0.781,0.723 0,0Z" style="fill-rule:nonzero;"/><path d="M457.481,511.945c0.737,0.766 0.737,0.766 0,0Z" style="fill-rule:nonzero;"/><path d="M443.201,520.72l0.015,-0.015l0.247,0.043c-0.073,-0.015 -0.189,-0.028 -0.262,-0.028Z" style="fill-rule:nonzero;"/><path d="M420.001,526.948l0.188,-0.145c-0.029,0.086 -0.087,0.246 -0.13,0.317l-0.058,-0.173Z" style="fill-rule:nonzero;"/><path d="M425.477,526.644c0.231,0.058 0.708,0.174 0.94,0.216l-0.679,0.333l-0.26,-0.55Z" style="fill-rule:nonzero;"/><path d="M417.061,529.48c0.781,0.796 0.781,0.796 0,0Z" style="fill-rule:nonzero;"/><path d="M636.898,539.382c0.59,-0.563 1.786,-1.687 2.392,-2.249c-0.938,2.752 0.316,5.217 1.642,7.565c-1.44,-0.663 -2.752,-1.527 -3.717,-2.767c-0.086,-0.634 -0.244,-1.917 -0.316,-2.55Z" style="fill-rule:nonzero;"/><path d="M634.347,540.173c0.274,-0.043 0.821,-0.143 1.096,-0.187c0.244,0.275 0.734,0.808 0.979,1.081c-0.114,0.707 -0.331,2.118 -0.446,2.84c-0.707,0.993 -2.118,3.011 -2.824,4.006c-0.145,3.17 4.048,1.7 5.965,2.594c-0.49,0.576 -1.484,1.729 -1.974,2.306c-1.225,-1.744 -2.882,-2.537 -4.957,-2.379c-0.145,-0.648 -0.433,-1.959 -0.576,-2.607l1.109,-0.332c0.057,-0.634 0.173,-1.902 0.216,-2.522c0.446,-1.614 0.922,-3.213 1.412,-4.799Z" style="fill-rule:nonzero;"/><path d="M440.204,604.558c4.51,-2.024 -0.217,4.9 0,0Z" style="fill-rule:nonzero;"/><path d="M461.529,619.04l0.419,-0.506l0.477,0.796l-0.636,0.143l-0.26,-0.433Z" style="fill-rule:nonzero;"/><path d="M464.571,621.776l0.347,0.304l0.289,0.376l-0.347,-0.304l-0.289,-0.376Z" style="fill-rule:nonzero;"/><path d="M461.487,624.702c0.361,0.418 0.361,0.418 0,0Z" style="fill-rule:nonzero;"/><path d="M467.741,624.976l0.26,0.246l-0.058,0.044c-0.058,-0.073 -0.159,-0.218 -0.202,-0.29Z" style="fill-rule:nonzero;"/><path d="M444.18,630.076c0.853,0.926 0.853,0.926 0,0Z" style="fill-rule:nonzero;"/><path d="M467.236,630.911l0.043,0.057l0.029,0.088c-0.014,-0.044 -0.058,-0.101 -0.072,-0.145Z" style="fill-rule:nonzero;"/><path d="M435.49,633.967c0.072,-0.028 0.231,-0.086 0.303,-0.116l-0.289,0.116l-0.014,0Z" style="fill-rule:nonzero;"/><path d="M441.486,633.087c0.867,0.911 0.867,0.911 0,0Z" style="fill-rule:nonzero;"/><path d="M447.249,633.116c0.925,0.867 0.925,0.867 0,0Z" style="fill-rule:nonzero;"/><path d="M453.259,633.015c0.867,0.926 0.867,0.926 0,0Z" style="fill-rule:nonzero;"/><path d="M432.465,636.317c0.925,0.867 0.925,0.867 0,0Z" style="fill-rule:nonzero;"/><path d="M461.919,635.739c0.462,0.39 0.462,0.39 0,0Z" style="fill-rule:nonzero;"/><path d="M478.981,636.026c0.824,0.766 0.824,0.766 0,0Z" style="fill-rule:nonzero;"/><path d="M459.195,638.997c0.911,0.838 0.911,0.838 0,0Z" style="fill-rule:nonzero;"/><path d="M476.098,639.054c0.824,0.766 0.824,0.766 0,0Z" style="fill-rule:nonzero;"/><path d="M481.878,638.925c0.781,0.809 0.781,0.809 0,0Z" style="fill-rule:nonzero;"/><path d="M499.153,638.997c0.824,0.765 0.824,0.765 0,0Z" style="fill-rule:nonzero;"/><path d="M484.73,641.878c0.809,0.78 0.809,0.78 0,0Z" style="fill-rule:nonzero;"/><path d="M490.508,641.762c0.824,0.766 0.824,0.766 0,0Z" style="fill-rule:nonzero;"/><path d="M496.359,641.733c0.795,0.81 0.795,0.81 0,0Z" style="fill-rule:nonzero;"/><path d="M487.77,644.846c0.81,0.781 0.81,0.781 0,0Z" style="fill-rule:nonzero;"/><path d="M493.519,644.73c0.795,0.81 0.795,0.81 0,0Z" style="fill-rule:nonzero;"/><path d="M499.182,644.716c0.766,0.825 0.766,0.825 0,0Z" style="fill-rule:nonzero;"/><path d="M496.315,647.64c0.838,0.752 0.838,0.752 0,0Z" style="fill-rule:nonzero;"/><path d="M459.052,650.638c0.376,0.376 0.376,0.376 0,0Z" style="fill-rule:nonzero;"/><path d="M522.368,667.872c0.752,0.781 0.752,0.781 0,0Z" style="fill-rule:nonzero;"/><path d="M541.964,668.94c0.983,0.809 0.983,0.809 0,0Z" style="fill-rule:nonzero;"/><path d="M524.73,674.673c3.602,-3.213 8.66,-3.126 13.156,-3.861c0.244,0.461 0.764,1.397 1.008,1.873c3.229,4.279 2.941,9.654 2.017,14.655c0.721,0.461 2.162,1.412 2.867,1.873c0.246,2.292 0.505,4.583 0.749,6.874c-7.334,1.57 -15.778,2.118 -21.484,-3.69c0.259,1.197 0.778,3.589 1.038,4.784c-4.337,-2.983 -6.47,-7.839 -8.487,-12.493c0.879,-3.242 2.089,-6.398 3.919,-9.237c-0.043,2.638 -0.101,5.333 -0.231,7.983c1.225,-3.227 2.45,-6.73 5.447,-8.762Z" style="fill-rule:nonzero;"/><path d="M592.328,684.388c1.397,-4.294 1.829,-9.006 4.711,-12.681c5.562,11.312 8.43,26.04 0.418,36.918c-0.332,0 -0.98,-0.015 -1.311,-0.029c-4.194,-3.444 -8.56,-6.829 -13.95,-8.142c4.626,-4.423 8.215,-9.957 10.131,-16.067Z" style="fill-rule:nonzero;"/><path d="M609.042,675.121l2.867,-0.721c3.978,7.205 7.565,14.857 8.215,23.185c2.767,22.321 -14.9,45.723 -37.638,47.538c-9.669,1.109 -20.16,-6.902 -19.123,-17.133c0.576,-11.168 14.05,-20.318 24.166,-14.078c6.311,3.415 7.58,11.095 5.634,17.478c8.488,-4.74 14.8,-12.782 18.372,-21.715c4.18,-11.255 1.975,-23.747 -2.493,-34.554Z" style="fill-rule:nonzero;"/><path d="M482.669,327.658c0.418,4.092 0.461,8.199 0.663,12.306c5.101,1.801 10.245,3.53 15.159,5.822c-5.144,1.657 -10.346,3.141 -15.519,4.712c0.216,3.055 0.461,6.124 0.735,9.179c0.288,3.17 0.562,6.34 0.98,9.51c-0.821,-2.651 -1.556,-5.303 -2.291,-7.968c-0.533,1.974 -1.081,3.948 -1.614,5.922c-2.277,-3.574 -4.885,-6.902 -7.392,-10.303c-0.476,-0.749 -1.398,-2.262 -1.873,-3.012l-0.331,-0.62c-5.476,1.383 -10.995,2.565 -16.586,3.343c1.873,-2.234 3.847,-4.366 5.85,-6.499c1.801,-1.96 3.574,-3.963 5.346,-5.951c-1.787,-2.896 -3.66,-5.735 -5.303,-8.704c-0.23,-0.62 -0.706,-1.859 -0.937,-2.478c4.51,0.994 8.862,2.579 13.401,3.401c3.646,-2.392 6.556,-5.692 9.712,-8.66Zm-21.6,25.462c3.848,-0.303 7.652,-0.908 11.47,-1.484c2.147,3.819 4.568,7.479 7.464,10.779c0.922,-4.337 1.398,-8.747 1.715,-13.171c4.15,-1.167 8.415,-2.046 12.248,-4.121c-11.369,-5.519 -24.122,0.403 -32.898,7.997Z" style="fill:#b2c7e5;fill-rule:nonzero;"/><path d="M649.435,426.192c3.473,-1.47 5.144,5.072 1.369,5.461c-3.214,0.879 -4.799,-4.64 -1.369,-5.461Z" style="fill:#b2c7e5;fill-rule:nonzero;"/><path d="M532.441,431.719c0.361,-0.679 1.099,-2.024 1.46,-2.689l1.041,0.347c0.303,2.009 -0.535,2.775 -2.501,2.342Z" style="fill:#b2c7e5;fill-rule:nonzero;"/><path d="M483.433,448.974c1.859,-0.98 3.689,-2.032 5.605,-2.896c0.663,1.283 1.282,2.579 1.873,3.919l-1.095,1.081c-1.989,2.536 -4.539,-0.994 -6.384,-2.104Z" style="fill:#b2c7e5;fill-rule:nonzero;"/><path d="M485.307,527.608c1.138,0.202 3.415,0.591 4.554,0.778c-0.821,1.586 -1.326,3.848 -3.631,3.113c-0.231,-0.966 -0.692,-2.911 -0.922,-3.891Z" style="fill:#b2c7e5;fill-rule:nonzero;"/><path d="M654.883,535.089c1.403,-2.312 4.799,0.376 3.31,2.415c-1.445,2.298 -4.929,-0.332 -3.31,-2.415Z" style="fill:#b2c7e5;fill-rule:nonzero;"/><path d="M678.181,535.432c1.599,-0.533 3.602,1.399 2.954,2.941c-0.158,1.93 -3.04,2.651 -4.136,1.311c-1.296,-1.268 -0.894,-4.035 1.182,-4.251Z" style="fill:#b2c7e5;fill-rule:nonzero;"/><path d="M671.409,546.283c0.705,-3.804 6.311,-0.936 3.846,1.874c-1.368,1.368 -4.149,0.072 -3.846,-1.874Z" style="fill:#b2c7e5;fill-rule:nonzero;"/><path d="M668.009,552.177c1.167,0.922 2.32,1.845 3.502,2.796c-1.182,1.037 -2.364,2.06 -3.545,3.084c-0.029,-1.96 -0.015,-3.921 0.043,-5.88Z" style="fill:#b2c7e5;fill-rule:nonzero;"/><path d="M633.381,569.585c2.651,0.402 5.303,0.835 7.969,1.296c-0.98,3.314 -3.66,4.727 -6.974,4.409l-0.303,-1.773c-0.231,-1.311 -0.462,-2.636 -0.692,-3.933Z" style="fill:#b2c7e5;fill-rule:nonzero;"/><path d="M653.374,578.52c2.63,-0.044 1.619,3.86 -0.491,3.801c-2.616,0.058 -1.692,-3.961 0.491,-3.801Z" style="fill:#b2c7e5;fill-rule:nonzero;"/><path d="M637.143,582.61c1.628,-0.433 3.271,-0.822 4.942,-1.167c0.995,2.608 0.475,4.87 -2.017,6.268c-0.173,-2.146 -1.153,-3.846 -2.925,-5.1Z" style="fill:#b2c7e5;fill-rule:nonzero;"/><path d="M635.037,589.296c1.716,0.894 3.805,2.176 2.998,4.481c-0.057,2.796 -3.358,2.061 -5.188,2.695c0.591,-2.436 1.369,-4.813 2.19,-7.176Z" style="fill:#b2c7e5;fill-rule:nonzero;"/><path d="M562.47,626.576c4.064,6.974 4.251,15.577 2.695,23.3c-1.643,2.926 -3.486,5.75 -5.634,8.33c-4.986,2.262 -5.878,7.91 -8.371,12.219c1.094,-3.214 2.493,-6.297 3.919,-9.366c-4.741,0.547 -9.324,-0.822 -12.724,-4.208c3.314,0.375 6.541,1.311 9.812,1.93c2.998,0.189 5.346,-2.089 7.622,-3.688c0.98,-1.542 1.931,-3.084 2.796,-4.668c0.505,-1.513 0.951,-3.041 1.355,-4.569c0.303,-1.902 0.534,-3.818 0.707,-5.72c-0.216,-4.582 -1.067,-9.107 -2.177,-13.559Z" style="fill:#b2c7e5;fill-rule:nonzero;"/><path d="M547.643,335.597c9.654,-6.268 22.191,-9.611 33.359,-5.562c9.568,3.127 18.934,9.597 22.378,19.453c4.828,12.335 0.13,27.523 -10.879,34.901c-11.154,8.487 -27.509,6.47 -37.71,-2.579c-3.978,7.551 -4.885,16.571 -2.594,24.799c1.21,4.755 3.631,9.078 5.173,13.733l0.215,0.591c-1.065,-0.677 -3.818,-4.726 -4.336,-1.398c-1.47,2.104 1.743,4.121 1.052,6.355c-1.153,0.375 -2.349,0.562 -3.574,0.548c0.72,-0.836 2.133,-2.478 2.853,-3.3c-1.181,0 -3.516,0.014 -4.698,0.014c0.837,-0.807 2.493,-2.435 3.328,-3.257c-0.993,0.173 -2.968,0.504 -3.947,0.677c0.202,-0.519 0.591,-1.556 0.792,-2.075c-7.218,-11.571 -9.884,-26.067 -5.418,-39.166c0.303,-0.576 0.908,-1.729 1.21,-2.306c1.009,-2.19 2.017,-4.395 3.113,-6.556c0.274,-0.403 0.835,-1.21 1.124,-1.628c2.81,5.894 8.531,9.28 14.438,11.47c0.288,-0.692 0.865,-2.075 1.153,-2.767c-0.143,0.807 -0.417,2.406 -0.547,3.213c8.934,1.556 18.719,-0.893 24.742,-7.954l0.432,2.493c-0.216,0.216 -0.663,0.663 -0.893,0.893c-1.903,1.383 -3.833,2.738 -5.837,3.992c-8.588,4.798 -18.574,2.435 -27.191,-0.908c7.681,8.891 20.909,10.721 31.5,6.484c0.26,-0.533 0.778,-1.614 1.039,-2.147c0.301,-0.202 0.907,-0.62 1.225,-0.821c0.244,0.144 0.764,0.447 1.023,0.591c15.563,-8.156 18.012,-32.667 4.222,-43.662c1.773,4.827 4.15,9.683 3.862,14.972c0.129,5.346 -2.349,10.188 -3.761,15.217c-0.966,0.504 -2.897,1.528 -3.862,2.046c0.303,-0.865 0.923,-2.565 1.226,-3.415c6.873,-8.718 6.109,-22.407 -1.816,-30.217c-5.072,-4.914 -11.903,-8.66 -19.137,-8.286c-13.069,0.36 -25.664,7.248 -33.732,17.407c-1.441,1.225 -3.2,2.536 -3.272,4.611c-0.562,0.504 -1.671,1.528 -2.234,2.046c-1.772,2.478 -3.17,5.187 -4.265,8.026l-0.49,0.994c-1.484,2.565 -2.651,5.332 -3.012,8.314c-0.864,6.528 -2.795,12.954 -1.801,19.612c-4.035,-1.744 -1.441,-7.709 -1.729,-11.211c0.793,-9.366 4.986,-17.883 9.41,-26.01c1.11,-1.917 2.133,-3.905 3.113,-5.908c5.116,-3.876 9.237,-8.963 14.755,-12.32Zm1.052,79.902c0.547,3.574 3.934,0.836 5.779,0.288c-6.959,-11.254 -7.148,-26.399 0.345,-37.437c-1.931,-1.787 -3.861,-3.588 -5.75,-5.404c-8.472,12.652 -7.046,29.439 -0.375,42.552Z" style="fill:#a9a9aa;fill-rule:nonzero;"/><path d="M538.003,350.642c8.531,-8.444 18.834,-16.514 31.385,-16.932c12.276,-1.196 25.001,9.611 23.329,22.436c-0.303,9.064 -6.873,15.908 -13.906,20.765c-6.023,0.663 -12.652,1.398 -17.911,-2.306c-5.447,-3.098 -8.041,-9.107 -9.12,-15.015c-1.658,2.161 -3.3,4.337 -4.813,6.6c-0.332,0.648 -1.009,1.96 -1.355,2.608c-1.268,1.945 -2.594,3.847 -3.919,5.749c-1.081,1.138 -1.499,2.479 -1.268,4.02c-2.363,6.715 -3.099,13.848 -2.421,20.923c-2.983,-1.182 -1.685,-4.856 -1.974,-7.277c0.619,-14.367 7.839,-28.258 18.761,-37.523c-3.286,10.404 5.447,21.226 16.067,21.24c2.739,0.475 4.914,-1.196 6.686,-3.069c1.052,0.144 2.118,0.288 3.185,0.447c1.47,-1.297 2.941,-2.579 4.338,-3.963c0.158,-0.173 0.49,-0.519 0.648,-0.692c4.957,-5.548 6.946,-13.905 4.15,-20.909c-1.917,-5.245 -6.673,-8.948 -11.686,-11.023c-6.715,-2.608 -14.223,-0.965 -20.65,1.7c-12.522,5.591 -22.132,16.831 -26.902,29.555c-0.634,1.96 -1.225,3.919 -1.888,5.85l-2.032,1.527c0.331,-1.124 0.98,-3.372 1.297,-4.496c0.547,-1.772 1.009,-3.574 1.455,-5.375c0.346,-0.692 1.052,-2.075 1.398,-2.767c1.153,-2.032 2.248,-4.092 3.257,-6.211c1.21,-0.908 2.335,-2.291 1.902,-3.905c0.505,-0.49 1.485,-1.47 1.988,-1.96Z" style="fill:#a9a9aa;fill-rule:nonzero;"/><path d="M414.469,547.522c0.72,-1.628 2.176,-4.482 4.352,-3.978c1.254,2.291 -2.406,4.338 -4.352,3.978Z" style="fill:#a9a9aa;fill-rule:nonzero;"/><path d="M422.565,543.431c2.277,-1.325 3.703,2.017 5.115,3.285c-0.346,0.274 -1.037,0.837 -1.383,1.11c-1.816,-0.418 -4.842,-2.162 -3.732,-4.395Z" style="fill:#a9a9aa;fill-rule:nonzero;"/><path d="M407.55,574.426c0.951,0.013 2.853,0.028 3.804,0.028c6.845,1.312 11.297,7.163 15.663,12.105c1.412,1.195 0.029,2.608 -0.504,3.848c0.432,1.715 0.879,3.444 1.081,5.23c-0.216,0.404 -0.663,1.211 -0.893,1.599c0.879,1.067 1.744,2.162 2.594,3.271c-3.285,3.559 -4.64,-5.317 -6.859,-0.619c0.893,0.303 2.695,0.922 3.602,1.239c-0.288,0.72 -0.865,2.161 -1.153,2.882c-2.248,-1.283 -4.078,-3.156 -5.98,-4.857c-2.882,-0.865 -3.242,-4.437 -5.822,-5.735c-3.069,-1.555 -4.366,-5.188 -7.205,-6.844c-0.476,0.764 -1.427,2.276 -1.902,3.026c-2.651,-1.456 0.115,-3.126 1.859,-3.617c-0.951,-1.485 -2.147,-2.767 -3.343,-4.035c1.138,-0.533 2.277,-1.052 3.415,-1.557l-1.081,-2.506c0.778,-0.015 2.32,-0.058 3.098,-0.073c-0.086,-0.85 -0.274,-2.535 -0.375,-3.385Zm3.761,6.829c4.236,-2.363 0.389,4.771 0,0Zm-3.026,3.185c4.035,-2.608 0.562,4.784 0,0Zm15.62,8.459c4.784,-0.116 -1.917,4.437 0,0Zm-3.689,2.695c-0.692,1.988 -0.014,2.752 2.017,2.306c0.677,-2.004 0,-2.768 -2.017,-2.306Z" style="fill:#a9a9aa;fill-rule:nonzero;"/><path d="M446.934,618.418c4.814,0.348 -2.399,4.193 0,0Z" style="fill:#a9a9aa;fill-rule:nonzero;"/><path d="M464.268,647.47c4.25,-2.27 0.246,4.828 0,0Z" style="fill:#a9a9aa;fill-rule:nonzero;"/><path d="M450.149,661.619c4.799,0.376 -2.385,4.206 0,0Z" style="fill:#a9a9aa;fill-rule:nonzero;"/><path d="M460.492,335.395c1.643,2.968 3.516,5.807 5.303,8.704c-1.772,1.989 -3.545,3.991 -5.346,5.951c-0.072,-4.885 -0.937,-9.813 0.043,-14.655Z" style="fill:#51667b;fill-rule:nonzero;"/><path d="M461.069,353.121c8.776,-7.594 21.528,-13.516 32.898,-7.997c-3.833,2.075 -8.098,2.954 -12.248,4.121c-0.317,4.424 -0.793,8.833 -1.715,13.171c-2.896,-3.3 -5.317,-6.96 -7.464,-10.779c-3.819,0.576 -7.623,1.182 -11.47,1.484Z" style="fill:#4c71b3;fill-rule:nonzero;"/><path d="M525.207,343.523c2.342,-0.781 3.151,0 2.414,2.371c-2.356,0.781 -3.166,-0.014 -2.414,-2.371Z" style="fill:#2d7eab;fill-rule:nonzero;"/><path d="M482.971,350.496c5.173,-1.571 10.375,-3.055 15.519,-4.712c-0.62,2.911 -1.182,5.822 -1.686,8.747c-4.164,2.176 -8.559,3.876 -13.098,5.144c-0.274,-3.055 -0.519,-6.124 -0.735,-9.179Z" style="fill:#5d83ab;fill-rule:nonzero;"/><path d="M533.895,352.054c0.072,-2.075 1.83,-3.386 3.271,-4.611c0.202,0.793 0.619,2.406 0.835,3.199c-0.505,0.49 -1.485,1.47 -1.988,1.96c-0.534,-0.144 -1.586,-0.418 -2.119,-0.547Z" style="fill:#2a95cd;fill-rule:nonzero;"/><path d="M517.051,353.278c-0.548,-4.798 7.407,-6.038 8.459,-1.513c0.029,0.994 0.101,3.012 0.13,4.006c1.038,-0.475 3.098,-1.455 4.136,-1.945c-4.424,8.127 -8.617,16.643 -9.41,26.01c-1.542,-0.922 -4.15,-1.052 -4.28,-3.357c-0.922,-3.847 4.294,-4.698 4.481,-8.142c-0.014,-4.337 3.833,-7.046 5.303,-10.851c-3.401,0.288 -9.078,0.807 -8.819,-4.208Z" style="fill:#2095d2;fill-rule:nonzero;"/><path d="M513.867,358.395c3.127,-2.594 8.286,1.196 6.643,4.928c-0.735,2.781 -4.51,3.833 -6.7,2.089c-2.378,-1.556 -2.306,-5.49 0.058,-7.018Z" style="fill:#2095d2;fill-rule:nonzero;"/><path d="M534.992,360.901c1.758,-3.732 8.501,-1.902 8.084,2.306c0.604,4.063 -3.921,4.943 -6.961,4.914c-0.865,-2.334 -3.53,-4.928 -1.124,-7.219Z" style="fill:#2095d2;fill-rule:nonzero;"/><path d="M530.625,367.976c4.136,-2.133 9.713,2.219 6.787,6.585c-2.132,3.473 -6.47,1.643 -8.674,-0.735c0.663,-1.931 1.254,-3.891 1.888,-5.85Z" style="fill:#2095d2;fill-rule:nonzero;"/><path d="M614.289,349.287c2.356,-0.636 3.137,0.217 2.327,2.53c-2.385,0.651 -3.165,-0.188 -2.327,-2.53Z" style="fill:#266f96;fill-rule:nonzero;"/><path d="M498.839,350.815c4.785,0.246 -2.226,4.322 0,0Z" style="fill:#27495e;fill-rule:nonzero;"/><path d="M542.772,352.817c2.594,-1.499 4.611,3.473 1.599,4.064c-2.407,1.297 -4.38,-3.257 -1.599,-4.064Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M588.868,372.804c0.317,-1.859 1.312,-3.271 2.983,-4.265c-0.301,0.85 -0.922,2.551 -1.225,3.415c0.966,-0.519 2.897,-1.542 3.862,-2.046c0.057,3.242 -2.608,4.424 -5.188,5.389l-0.433,-2.493Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M525.842,383.626c-3.026,-0.375 -2.839,-6.874 0.461,-5.663c0.403,1.888 0.144,3.833 -0.461,5.663Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M582.572,380.183c2.003,-1.254 3.934,-2.608 5.836,-3.991c0.837,2.133 1.081,4.323 0.736,6.6c-0.317,0.202 -0.923,0.62 -1.226,0.821c-2.434,0.029 -4.063,-1.542 -5.346,-3.43Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M623.452,420.745l0.85,-1.412c0.317,2.046 0.663,4.107 0.923,6.167c0.562,6.167 0.446,12.364 0.475,18.545c-0.029,6.917 -0.822,13.79 -2.017,20.592c-1.268,7.896 -3.473,15.591 -6.383,23.026l-1.053,-0.806c1.889,-10.174 4.209,-20.274 5.188,-30.606c0.418,-1.182 0.808,-2.378 1.182,-3.574c2.003,-8.487 1.672,-17.522 -0.044,-26.024c-0.028,-0.245 -0.072,-0.735 -0.1,-0.98c0.23,-1.657 0.547,-3.314 0.979,-4.928Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M494.371,479.306l2.075,-1.037c-1.801,4.395 -2.032,9.179 -1.354,13.833c0.504,-3.299 0.864,-6.614 1.398,-9.913c2.19,6.368 4.784,12.623 8.329,18.372c-0.865,0.116 -2.594,0.375 -3.458,0.505c-4.496,-1.628 -7.176,-5.922 -8.675,-10.245c-0.014,-1.917 0.029,-3.833 0.101,-5.75c0.375,-1.946 0.562,-4.02 1.585,-5.764Zm2.017,14.179c0.735,0.749 0.735,0.749 0,0Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M471.617,521.154c2.219,-0.865 4.467,-1.744 6.657,-2.709c-0.922,1.786 -1.845,3.587 -2.795,5.375c-1.225,0.663 -2.45,1.325 -3.675,1.974c-5.159,2.306 -9.971,5.333 -14.093,9.193l-0.548,0.086c-2.176,0.202 -4.366,0.361 -6.542,0.49c1.239,-1.542 2.478,-3.084 3.804,-4.553c2.637,-2.003 5.447,-3.747 8.314,-5.404c2.882,-1.628 5.822,-3.156 8.876,-4.452Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M685.427,541.269c2.19,-2.825 5.908,1.34 3.574,3.299c-1.715,2.638 -6.111,-1.21 -3.574,-3.299Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M669.868,566.011c1.656,0.116 3.285,0.231 4.942,0.375c0,1.182 -0.029,3.53 -0.043,4.712c-1.542,0.044 -3.084,0.259 -4.597,-0.015c-1.428,-1.34 -0.317,-3.429 -0.303,-5.072Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M696.451,582.508c1.484,-2.017 4.927,0.389 3.343,2.421c-1.441,2.234 -5.188,-0.331 -3.343,-2.421Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M642.143,596.877c2.356,-0.752 3.15,0.058 2.371,2.429c-2.343,0.693 -3.137,-0.116 -2.371,-2.429Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M637.226,601.53c3.805,0.361 6.788,3.271 5.692,7.35c-2.508,-1.917 -4.309,-4.554 -5.692,-7.35Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M624.681,608.618c1.936,0.926 1.936,1.879 0.015,2.891c-1.894,-0.939 -1.909,-1.894 -0.015,-2.891Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M646.004,609.63c2.089,-1.889 4.927,1.584 3.299,3.573c-1.843,2.133 -4.784,-1.786 -3.299,-3.573Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M454.598,356.549c5.591,-0.778 11.11,-1.96 16.586,-3.343l0.331,0.62c-0.115,3.487 -0.36,6.974 -0.605,10.447c-4.078,4.871 -7.752,10.159 -12.926,13.963c-1.599,-7.147 -2.536,-14.424 -3.386,-21.687Z" style="fill:#6497ca;fill-rule:nonzero;"/><path d="M527.397,362.126c1.095,-2.839 2.493,-5.548 4.265,-8.026c0.605,0.591 1.83,1.801 2.45,2.406c-1.009,2.118 -2.104,4.179 -3.257,6.211c-0.865,-0.144 -2.594,-0.447 -3.458,-0.591Z" style="fill:#6497ca;fill-rule:nonzero;"/><path d="M523.895,371.435c0.36,-2.983 1.527,-5.75 3.012,-8.314c0.649,0.591 1.917,1.772 2.551,2.363c-0.447,1.801 -0.908,3.602 -1.455,5.375c-1.023,0.144 -3.084,0.432 -4.107,0.576Z" style="fill:#6497ca;fill-rule:nonzero;"/><path d="M541.691,374.547c0.778,0.62 2.364,1.859 3.156,2.478c-0.303,0.576 -0.907,1.729 -1.21,2.306c-0.808,-0.187 -2.421,-0.576 -3.214,-0.764c-0.23,-1.542 0.189,-2.882 1.268,-4.02Z" style="fill:#6497ca;fill-rule:nonzero;"/><path d="M694.768,546.673c2.284,-0.794 3.093,-0.015 2.429,2.356c-2.328,0.823 -3.152,0.044 -2.429,-2.356Z" style="fill:#6497ca;fill-rule:nonzero;"/><path d="M540.999,654.011c6.282,-0.317 12.536,0.562 18.791,1.081c-2.276,1.599 -4.626,3.876 -7.624,3.688c-3.27,-0.619 -6.499,-1.557 -9.812,-1.931l-0.274,-0.215c-0.274,-0.649 -0.808,-1.96 -1.081,-2.623Z" style="fill:#6497ca;fill-rule:nonzero;"/><path d="M471.516,353.827c0.476,0.749 1.398,2.262 1.873,3.012c-0.634,6.6 -0.072,13.243 1.7,19.64c-3.919,5.548 -8.329,10.822 -13.617,15.102c-1.297,-4.409 -2.507,-8.848 -3.487,-13.343c5.173,-3.804 8.848,-9.093 12.926,-13.963c0.245,-3.473 0.49,-6.96 0.605,-10.447Z" style="fill:#608bb6;fill-rule:nonzero;"/><path d="M483.707,359.675c4.539,-1.268 8.934,-2.968 13.098,-5.144c-0.331,4.899 -0.418,9.799 -0.389,14.712c-3.386,1.398 -6.801,2.738 -10.231,4.006c-0.245,-0.62 -0.749,-1.873 -0.994,-2.507c-0.13,-0.389 -0.375,-1.167 -0.504,-1.556c-0.418,-3.17 -0.692,-6.34 -0.98,-9.51Z" style="fill:#597a9c;fill-rule:nonzero;"/><path d="M650.124,437.722c3.041,0.922 6.313,1.628 8.877,3.646c1.067,2.536 -0.231,5.461 -0.288,8.156c-0.649,2.334 -1.355,4.654 -2.061,6.989c-2.032,-2.435 -4.927,-3.588 -7.91,-4.337c1.124,-4.741 1.239,-9.611 1.383,-14.453Z" style="fill:#597a9c;fill-rule:nonzero;"/><path d="M473.388,356.838c2.507,3.401 5.115,6.729 7.392,10.303c1.398,5.058 2.147,10.26 3.113,15.418c-2.262,0.908 -4.366,2.147 -6.369,3.516c-0.908,-3.17 -1.7,-6.384 -2.435,-9.597c-1.772,-6.398 -2.334,-13.041 -1.7,-19.64Z" style="fill:#53708c;fill-rule:nonzero;"/><path d="M443.272,421.768c2.795,-1.989 5.605,-3.977 8.415,-5.951c2.723,4.842 5.591,9.64 8.949,14.078c-1.902,2.334 -3.804,4.669 -5.706,7.003c-4.481,-4.568 -7.666,-10.159 -11.657,-15.13Z" style="fill:#53708c;fill-rule:nonzero;"/><path d="M482.396,361.219c0.735,2.666 1.47,5.317 2.291,7.969c0.13,0.389 0.375,1.167 0.504,1.556c-1.196,1.556 -2.133,8.026 1.254,5.778c0.014,0.461 0.058,1.369 0.072,1.816c-0.288,7.219 2.363,14.021 4.467,20.779c0.879,2.623 1.744,5.389 3.775,7.392c0.403,0.475 1.239,1.426 1.657,1.902c-0.317,0.994 -0.98,2.997 -1.297,3.991c-0.202,0.432 -0.62,1.297 -0.821,1.729c-4.64,-10.087 -8.242,-20.678 -10.404,-31.572c-0.966,-5.159 -1.715,-10.361 -3.113,-15.418c0.533,-1.974 1.081,-3.948 1.614,-5.922Z" style="fill:#7199c2;fill-rule:nonzero;"/><path d="M581.461,364c0.908,-0.115 2.709,-0.346 3.617,-0.461c0.159,1.283 0.475,3.833 0.635,5.115c-0.159,0.173 -0.49,0.519 -0.648,0.692c-3.545,1.124 -6.399,-2.695 -3.603,-5.346Z" style="fill:#227cad;fill-rule:nonzero;"/><path d="M545.611,368.798c0.345,-0.649 1.023,-1.96 1.355,-2.608c0.533,0.663 1.584,1.989 2.117,2.651c-0.288,0.418 -0.85,1.225 -1.124,1.628c-0.591,-0.418 -1.757,-1.254 -2.348,-1.671Z" style="fill:#226f99;fill-rule:nonzero;"/><path d="M486.184,373.252c3.43,-1.268 6.845,-2.608 10.231,-4.006c0.36,5.361 1.066,10.678 1.859,15.981c-2.925,1.888 -5.865,3.746 -8.819,5.62c-1.167,-4.121 -2.089,-8.314 -2.94,-12.508c-0.014,-0.447 -0.058,-1.355 -0.072,-1.816c-0.072,-0.821 -0.202,-2.45 -0.259,-3.271Z" style="fill:#526d88;fill-rule:nonzero;"/><path d="M509.485,369.432c1.383,-2.104 5.332,0.072 3.502,2.262c-1.427,1.917 -5.173,-0.144 -3.502,-2.262Z" style="fill:#23678c;fill-rule:nonzero;"/><path d="M486.443,376.522c-3.386,2.248 -2.45,-4.222 -1.254,-5.778c0.245,0.634 0.749,1.888 0.994,2.507c0.058,0.821 0.187,2.45 0.259,3.271Z" style="fill:#eef1f2;fill-rule:nonzero;"/><path d="M622.574,426.653c1.715,8.502 2.045,17.537 0.043,26.024c0.404,-5.461 0.448,-10.923 0.418,-16.37c-0.086,-3.228 -0.231,-6.456 -0.461,-9.655Z" style="fill:#eef1f2;fill-rule:nonzero;"/><path d="M503.852,471.063c9.266,-4.942 19.799,-7.42 30.318,-6.469c6.052,0.734 12.047,1.931 17.94,3.516c-4.567,-0.663 -9.122,-1.484 -13.689,-2.146c-3.905,-0.591 -7.853,-0.75 -11.787,-0.505c-3.141,0.303 -6.268,0.764 -9.352,1.412c-5.548,1.557 -10.663,4.395 -15.188,7.925c-2.248,2.148 -4.121,4.67 -5.605,7.392c-0.533,3.3 -0.893,6.614 -1.398,9.915c-0.677,-4.655 -0.447,-9.439 1.354,-13.834c2.608,-2.247 4.971,-4.769 7.407,-7.205Z" style="fill:#eef1f2;fill-rule:nonzero;"/><path d="M550.395,482.894c2.695,-0.475 5.39,-0.907 8.112,-1.21c7.104,0.259 14.295,0.634 21.155,2.607c4.799,1.586 9.856,3.17 14.842,1.312c5.576,3.242 10.923,6.902 15.562,11.398c-0.375,0.806 -1.138,2.434 -1.513,3.242c4.237,5.951 8.876,11.743 11.672,18.56c2.017,4.929 3.286,10.116 4.885,15.189c0.274,0.993 0.835,2.968 1.11,3.962c-3.559,0.721 -7.191,0.936 -10.779,0.332c-1.729,-11.932 -7.58,-23.402 -16.701,-31.342c1.658,-0.634 3.3,-1.254 4.986,-1.83c0.448,-0.966 0.879,-1.946 1.327,-2.911c-1.802,-1.08 -3.387,-3.602 -5.75,-2.392c-0.549,2.292 -0.677,4.655 -0.808,7.003c-13.948,-13.992 -36.01,-17.998 -54.542,-11.931c1.542,-1.946 2.205,-4.121 2.004,-6.57c-0.534,-0.275 -1.586,-0.837 -2.104,-1.11c-0.202,-0.72 -0.576,-2.133 -0.765,-2.838c2.421,-0.505 4.857,-1.024 7.306,-1.47Z" style="fill:#eef1f2;fill-rule:nonzero;"/><path d="M549.501,533.068c4.309,-3.112 9.338,-5.202 14.728,-5.202c5.922,1.24 11.167,4.771 14.308,9.972c1.34,3.862 1.96,7.954 1.268,12.018c-0.259,1.182 -0.533,2.377 -0.793,3.587l-0.086,-1.181c-0.158,-3.055 -0.244,-6.096 -0.49,-9.136c-1.873,-4.107 -4.351,-8.199 -8.386,-10.475c-4.136,-2.682 -9.223,-2.594 -13.935,-3.084l0.029,0.85c-1.658,0.648 -3.286,1.296 -4.914,2.003l-1.729,0.648Z" style="fill:#eef1f2;fill-rule:nonzero;"/><path d="M657.747,375.225c0.781,0.752 0.781,0.752 0,0Z" style="fill:#265570;fill-rule:nonzero;"/><path d="M475.09,376.478c0.735,3.213 1.528,6.427 2.435,9.597c1.066,3.588 2.205,7.147 3.357,10.706c-1.326,-0.231 -2.637,-0.49 -3.977,-0.605c-3.415,3.473 -6.182,7.536 -9.669,10.937c-2.104,-5.101 -4.107,-10.26 -5.764,-15.534c5.288,-4.28 9.698,-9.554 13.617,-15.101Z" style="fill:#5b7ea2;fill-rule:nonzero;"/><path d="M526.086,375.902l1.96,-1.11c-0.331,3.559 -0.692,7.133 -0.879,10.707c-0.115,2.32 -0.101,4.654 -0.187,6.989c-2.003,-2.493 -1.888,-5.937 -1.138,-8.862c0.605,-1.83 0.865,-3.775 0.461,-5.663l-0.216,-2.06Z" style="fill:#b1b1b2;fill-rule:nonzero;"/><path d="M514.112,530.232c7.176,-20.808 29.136,-35.463 51.154,-33.517c15.418,0.979 30.059,9.682 38.547,22.551c8.156,11.859 10.302,27.277 6.743,41.126c-4.61,14.741 -15.288,27.825 -29.928,33.387c-3.53,1.571 -7.752,1.7 -10.866,4.05c4.309,1.296 8.617,-0.879 12.595,-2.349c7.608,-2.982 13.847,-8.371 20.26,-13.3c3.128,2.089 6.471,3.833 9.957,5.259c-3.472,4.382 -7.767,8.013 -12.061,11.558c-3.04,-1.571 -5.937,-3.761 -9.351,-4.382c-3.978,1.024 -7.565,3.128 -11.398,4.51c-3.89,1.557 -8.041,2.379 -12.191,2.796c-1.599,-0.562 -3.905,-0.216 -4.942,-1.7c0.764,-1.801 2.464,-2.262 5.115,-1.426l-0.013,-0.246c-1.514,-1.397 -8.316,-0.013 -5.836,-3.558c10.433,-1.268 20.952,-4.281 29.222,-11.053c6.557,-5.346 11.629,-12.435 14.627,-20.333c3.184,-8.775 2.146,-18.227 1.772,-27.349c-3.286,-11.211 -9.943,-21.759 -19.957,-28.07c-16.413,-11.095 -39.584,-9.799 -55.176,2.219c-12.594,9.684 -19.208,26.139 -17.076,41.861l2.161,-0.073l1.182,-0.072c1.081,-0.072 3.242,-0.244 4.323,-0.331c3.948,0.835 7.825,2.045 11.657,3.314c3.012,2.897 6.311,5.706 8.373,9.41c0.936,2.075 -1.037,3.804 -1.788,5.576c-2.031,-1.656 -4.135,-3.213 -6.326,-4.61c1.226,-1.226 2.436,-2.45 3.66,-3.66c-7.766,-8.689 -20.534,-8.127 -31.168,-7.45c0.389,-0.145 1.167,-0.433 1.556,-0.591c0.562,-0.216 1.671,-0.677 2.219,-0.908c-0.187,-7.637 0.115,-15.446 2.954,-22.637Z" style="fill:#b1b1b2;fill-rule:nonzero;"/><path d="M438.635,661.678c4.799,0.333 -2.269,4.265 0,0Z" style="fill:#b1b1b2;fill-rule:nonzero;"/><path d="M528.046,374.792c2.464,1.686 6.081,3.602 4.813,7.248c-0.375,2.795 -3.689,2.695 -5.692,3.458c0.187,-3.574 0.548,-7.147 0.879,-10.707Z" style="fill:#2896d1;fill-rule:nonzero;"/><path d="M490.984,399.117c-2.104,-6.758 -4.755,-13.56 -4.467,-20.779c0.85,4.193 1.772,8.387 2.94,12.508c1.571,5.274 3.199,10.562 5.303,15.663c-2.032,-2.003 -2.896,-4.77 -3.775,-7.392Z" style="fill:#eff1f3;fill-rule:nonzero;"/><path d="M497.627,558.576c2.334,-1.283 4.755,-2.392 7.248,-3.33c0.62,-0.23 1.873,-0.663 2.507,-0.878c10.634,-0.677 23.401,-1.24 31.168,7.45c-1.225,1.21 -2.434,2.434 -3.66,3.66c-17.968,-10.794 -43.964,1.037 -47.509,21.715c-1.816,0.417 -3.602,1.469 -5.505,1.081c-0.231,-10.88 7.234,-19.352 13.012,-27.769c0.893,-0.677 1.801,-1.325 2.738,-1.93Z" style="fill:#eff1f3;fill-rule:nonzero;"/><path d="M647.098,566.731c0.49,-0.547 1.47,-1.628 1.96,-2.176c0.619,0.057 1.858,0.173 2.478,0.231c0.375,0.316 1.109,0.951 1.484,1.282c0.347,0.389 1.039,1.197 1.384,1.599l-1.369,-0.878c-0.893,1.166 -1.801,2.334 -2.695,3.501c1.167,-2.133 -1.138,-4.237 -3.242,-3.559Z" style="fill:#eff1f3;fill-rule:nonzero;"/><path d="M477.525,386.075c2.003,-1.369 4.107,-2.608 6.369,-3.516c2.162,10.894 5.764,21.485 10.404,31.572c0.202,-0.432 0.62,-1.297 0.821,-1.729l1.556,1.499l0.836,1.355c-4.193,-1.254 -3.458,3.473 -3.141,5.706c1.96,9.136 6.182,17.695 11.153,25.577c-1.729,-0.231 -3.444,-0.447 -5.159,-0.663c-1.83,2.248 -3.646,4.51 -5.418,6.801c-0.461,-0.576 -1.412,-1.744 -1.873,-2.32c-0.101,-0.504 -0.288,-1.499 -0.375,-2.003c-0.447,0.403 -1.34,1.225 -1.787,1.643c-0.591,-1.34 -1.21,-2.637 -1.873,-3.919c-1.254,-2.479 -2.723,-4.842 -4.251,-7.147c2.94,-2.262 5.764,-4.654 8.487,-7.176c0.721,0.115 2.147,0.36 2.867,0.49c-3.386,-6.888 -6.744,-13.79 -10.015,-20.736c-2.075,-4.784 -3.66,-9.756 -5.245,-14.727c-1.153,-3.559 -2.291,-7.118 -3.357,-10.706Z" style="fill:#435668;fill-rule:nonzero;"/><path d="M630.326,382.501c0.823,0.636 0.823,0.636 0,0Z" style="fill:#243846;fill-rule:nonzero;"/><path d="M498.275,385.225l0.216,-0.13c0.807,4.726 2.637,9.251 3.069,14.035c-1.484,3.213 -3.545,6.124 -5.144,9.28c-0.418,-0.476 -1.254,-1.427 -1.657,-1.902c-2.104,-5.101 -3.732,-10.389 -5.303,-15.663c2.954,-1.873 5.894,-3.732 8.819,-5.62Z" style="fill:#496074;fill-rule:nonzero;"/><path d="M614.289,388.409c3.314,1.657 6.614,3.386 9.885,5.159c3.371,-2.032 6.728,-4.092 10.201,-5.951c0.044,3.602 -0.634,7.162 -1.067,10.721c3.963,1.542 6.456,5.418 10.245,7.392c-4.813,0.576 -9.625,1.009 -14.438,1.383c-1.239,4.208 -2.349,8.531 -4.813,12.22l-0.85,1.412c-0.432,1.614 -0.749,3.271 -0.98,4.928c-0.215,-2.017 -0.417,-4.02 -0.604,-6.023c-0.347,-4.409 -1.312,-8.718 -2.493,-12.954c-4.352,-0.418 -8.733,-0.36 -13.056,0.331l-0.793,0.101c3.214,-3.43 7.277,-5.85 11.125,-8.473c-0.778,-3.429 -1.584,-6.83 -2.363,-10.245Zm-4.136,16.528c3.271,0.591 7.161,-0.418 10.115,1.282c1.557,3.127 2.349,6.657 4.51,9.496c1.197,-3.53 2.392,-7.075 3.717,-10.562c3.128,-0.101 6.269,-0.245 9.395,-0.49c-8.228,-4.409 -20.432,-7.075 -27.738,0.274Z" style="fill:#b0c6e5;fill-rule:nonzero;"/><path d="M503.089,400.788c4.971,-6.499 13.416,-9.366 21.413,-8.228l-0.086,0.173c-6.067,13.79 -13.055,27.263 -17.854,41.572c-5.418,-8.257 -8.415,-19.511 -3.113,-28.56c2.867,-5.663 8.776,-8.415 14.323,-10.793c-7.868,0.49 -13.747,6.398 -17.393,12.897c-1.383,-2.795 1.34,-4.986 2.709,-7.061Z" style="fill:#f8cb25;fill-rule:nonzero;"/><path d="M499.572,406.061l0.807,1.787c-0.864,4.381 -1.974,8.862 -1.153,13.343c0.793,6.182 4.337,11.441 7.176,16.845c-3.718,-1.585 -5.115,-5.692 -6.816,-9.021c-2.089,-4.28 -2.723,-9.064 -2.075,-13.761l-0.836,-1.355c0.908,-2.637 2.075,-5.173 2.896,-7.839Z" style="fill:#f8cb25;fill-rule:nonzero;"/><path d="M506.56,434.305c4.798,-14.309 11.787,-27.782 17.854,-41.572c11.917,2.652 19.741,17.349 13.474,28.315c-3.055,6.355 -10.534,8.199 -17.018,7.522c0.937,1.153 1.873,2.32 2.81,3.502c-2.219,4.294 -4.366,8.617 -6.47,12.983c-4.035,-3.069 -7.623,-6.686 -10.649,-10.75Z" style="fill:#f8ba25;fill-rule:nonzero;"/><path d="M506.401,438.038c5.029,4.453 10.274,8.646 15.692,12.652c-3.775,0.072 -6.614,-2.723 -9.453,-4.784c-2.507,-2.205 -5.793,-4.251 -6.239,-7.868Z" style="fill:#f8ba25;fill-rule:nonzero;"/><path d="M476.905,396.177c1.34,0.115 2.651,0.375 3.977,0.605c1.585,4.971 3.17,9.943 5.245,14.727c-4.553,3.444 -7.003,8.617 -10.13,13.228c-3.156,-5.749 -6.211,-11.571 -8.761,-17.623c3.487,-3.401 6.254,-7.464 9.669,-10.937Z" style="fill:#54728f;fill-rule:nonzero;"/><path d="M494.368,479.306c1.83,-4.02 5.361,-6.816 9.482,-8.242c-2.435,2.434 -4.798,4.957 -7.407,7.205l-2.075,1.037Z" style="fill:#54728f;fill-rule:nonzero;"/><path d="M559.532,658.206c2.146,-2.579 3.991,-5.404 5.634,-8.329c-1.195,3.703 -2.752,7.292 -4.077,10.951c-0.505,-0.878 -1.024,-1.757 -1.557,-2.622Z" style="fill:#54728f;fill-rule:nonzero;"/><path d="M500.379,407.848c3.646,-6.499 9.525,-12.407 17.393,-12.897c-5.548,2.378 -11.456,5.13 -14.323,10.793c-5.303,9.049 -2.306,20.303 3.113,28.56c3.026,4.063 6.614,7.68 10.649,10.75c5.259,3.862 11.023,6.989 17.018,9.539c5.634,2.392 11.441,4.352 17.249,6.254c5.994,1.974 11.844,4.395 17.522,7.176c5.605,2.781 11.052,5.937 16.153,9.583c2.464,1.988 4.683,4.25 7.018,6.412c-0.361,-0.246 -1.081,-0.721 -1.426,-0.966c-9.597,-8.041 -21.155,-13.185 -32.711,-17.724c-2.003,-0.721 -3.976,-1.485 -5.966,-2.219c-3.011,-1.023 -6.038,-2.133 -9.221,-2.421l0.518,-0.244c-6.44,-4.366 -14.309,-6.254 -21.268,-9.756c-5.418,-4.006 -10.663,-8.199 -15.692,-12.652c-2.839,-5.404 -6.383,-10.663 -7.176,-16.845c-0.821,-4.481 0.288,-8.963 1.153,-13.343Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M571.274,419.938c0.636,0.824 0.636,0.824 0,0Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M615.528,458.305c-1.894,-2.833 3.15,-3.18 3.57,-0.578l-0.882,0.882c-0.679,-0.072 -2.024,-0.231 -2.688,-0.303Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M618.955,488.401c0.576,-0.015 1.744,-0.044 2.32,-0.058c0.475,0.446 1.399,1.369 1.859,1.816c-0.677,0.547 -2.032,1.628 -2.709,2.162c-0.086,1.599 -0.244,3.198 -0.461,4.799c-5.072,-0.966 -0.433,-5.549 -1.009,-8.718Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M413.906,541.11l1.388,-0.231l0.81,1.012c-0.506,0.274 -1.547,0.823 -2.067,1.098c-0.029,-0.462 -0.101,-1.403 -0.13,-1.879Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M627.158,578.158c0.028,0.202 0.072,0.592 0.101,0.794l-0.101,-0.794Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M403.17,594.858c1.614,-2.133 2.176,-5.995 5.778,-5.072c-1.369,2.262 -3.473,3.862 -5.778,5.072Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M406.197,597.713c1.153,-2.061 2.234,-4.15 3.213,-6.283c2.046,2.537 4.049,5.303 6.859,7.075c0.058,0.591 0.187,1.773 0.245,2.364c-0.447,0.028 -1.326,0.101 -1.772,0.129c-0.331,-0.893 -1.009,-2.68 -1.355,-3.558c-1.297,1.138 -2.608,2.276 -3.934,3.4c0.692,-1.931 1.47,-3.862 1.859,-5.893c-1.772,0.792 -3.444,1.801 -5.115,2.767Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M582.731,642.672c4.755,10.864 8.472,22.58 7.031,34.598c-2.708,7.319 -5.403,15.274 -11.844,20.23c-7.479,6.341 -19.828,5.173 -25.937,-2.535c4.035,2.363 8.213,5.115 13.127,4.841c7.551,0.332 13.876,-4.913 17.724,-10.98c1.584,-2.781 2.925,-5.72 4.006,-8.733c0.894,-3.257 1.412,-6.614 1.772,-9.972c0.159,-4.437 -0.288,-8.876 -1.498,-13.142c-1.441,-4.784 -2.939,-9.539 -4.38,-14.308Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M554.011,395.457c0.752,0.766 0.752,0.766 0,0Z" style="fill:#495663;fill-rule:nonzero;"/><path d="M428.978,396.091c2.032,2.478 3.617,5.288 5.043,8.156c4.064,-0.36 8.142,-0.793 12.205,-1.239c-2.075,3.415 -5.015,6.167 -8.084,8.689c1.124,3.17 2.565,6.211 4.366,9.064c-0.13,1.023 -0.375,3.084 -0.504,4.121c-3.084,-1.816 -6.268,-3.43 -9.467,-5.015c-0.389,-0.202 -1.196,-0.591 -1.585,-0.793c-3.502,2.262 -6.816,4.827 -10.274,7.147c-0.029,-0.62 -0.086,-1.844 -0.115,-2.464c0.231,-1.585 0.533,-3.141 0.908,-4.683c0.36,0.994 1.081,2.969 1.441,3.963c2.45,-1.96 4.885,-3.934 7.32,-5.922c3.343,1.672 6.715,3.3 10.101,4.914c-1.787,-4.237 -3.271,-8.588 -4.265,-13.07c-5.259,1.931 -10.072,4.928 -13.905,9.035c0.303,-1.066 0.893,-3.199 1.196,-4.265c-2.594,-1.556 -5.159,-3.141 -7.68,-4.813l-0.937,-0.62c3.386,-1.023 6.888,-1.528 10.317,-2.421c2.133,-2.839 2.623,-6.542 3.919,-9.784Z" style="fill:#b1c6e5;fill-rule:nonzero;"/><path d="M582.962,399.966c3.055,-2.349 6.671,1.427 4.582,4.496c-3.099,2.435 -7.335,-1.34 -4.582,-4.496Z" style="fill:#83b7e3;fill-rule:nonzero;"/><path d="M610.153,404.937c7.306,-7.349 19.51,-4.683 27.738,-0.274c-3.126,0.245 -6.268,0.389 -9.395,0.49c-1.325,3.487 -2.521,7.032 -3.717,10.562c-2.161,-2.839 -2.954,-6.369 -4.51,-9.496c-2.954,-1.7 -6.844,-0.692 -10.115,-1.282Z" style="fill:#4c70b2;fill-rule:nonzero;"/><path d="M422.061,478.284c1.989,-2.882 4.337,-5.533 7.536,-7.09c1.729,2.796 3.718,5.418 5.706,8.041c-2.666,-0.072 -5.332,-0.145 -7.997,-0.216c-1.182,2.306 -2.637,4.453 -4.496,6.282c-0.259,-2.348 -0.504,-4.683 -0.749,-7.017Z" style="fill:#4c70b2;fill-rule:nonzero;"/><path d="M446.716,402.285c1.412,4.597 3.055,9.121 4.971,13.531c-2.81,1.974 -5.62,3.963 -8.415,5.951l-0.764,-1.009c-1.801,-2.853 -3.242,-5.894 -4.366,-9.064c3.069,-2.522 6.009,-5.274 8.084,-8.689l0.49,-0.721Z" style="fill:#5a7c9f;fill-rule:nonzero;"/><path d="M643.554,405.729l0.174,-0.014c0.49,5.505 2.132,11.038 1.355,16.571c-0.49,0.375 -1.47,1.11 -1.96,1.47c-3.833,-2.306 -7.782,-4.38 -11.859,-6.211c-0.519,-3.516 -1.384,-6.974 -2.148,-10.433c4.813,-0.375 9.626,-0.807 14.438,-1.383Z" style="fill:#6496c9;fill-rule:nonzero;"/><path d="M442.51,420.758l0.764,1.009c3.991,4.971 7.176,10.562 11.657,15.13c4.251,4.424 8.531,8.862 13.286,12.753c3.747,3.041 7.565,6.009 11.571,8.704c6.297,4.179 12.508,8.53 19.208,12.047c-3.098,1.599 -5.98,-0.764 -8.603,-2.177c-8.56,-5.245 -17.162,-10.576 -24.626,-17.32c-8.949,-7.709 -16.326,-16.917 -23.762,-26.024c0.13,-1.038 0.375,-3.098 0.504,-4.121Z" style="fill:#6496c9;fill-rule:nonzero;"/><path d="M656.653,456.51c0.707,-2.334 1.412,-4.654 2.06,-6.989c0.116,1.297 0.361,3.876 0.49,5.173c-2.104,9.222 -6.095,17.868 -10.361,26.254c-6.412,11.384 -14.193,22.205 -23.689,31.211c5.101,-9.395 12.911,-16.974 18.2,-26.297c2.91,-4.626 5.403,-9.482 7.738,-14.396c2.205,-4.856 3.905,-9.899 5.562,-14.957Z" style="fill:#6496c9;fill-rule:nonzero;"/><path d="M606.319,407.027c4.323,-0.692 8.703,-0.749 13.056,-0.332c1.181,4.237 2.146,8.545 2.493,12.954c-3.559,0 -7.104,-0.014 -10.649,-0.029c-1.441,-4.265 -3.141,-8.444 -4.9,-12.594Z" style="fill:#5c80a6;fill-rule:nonzero;"/><path d="M562.7,408.467c0.708,0.766 0.708,0.766 0,0Z" style="fill:#333335;fill-rule:nonzero;"/><path d="M579.834,413.887c4.293,-2.313 0.303,4.77 0,0Z" style="fill:#333335;fill-rule:nonzero;"/><path d="M624.302,419.333c2.464,-3.689 3.574,-8.012 4.813,-12.219c0.764,3.458 1.628,6.917 2.146,10.433c0.086,2.608 0.073,5.245 0.073,7.882c-2.047,0.014 -4.079,0.043 -6.111,0.072c-0.259,-2.061 -0.604,-4.121 -0.922,-6.167Z" style="fill:#557391;fill-rule:nonzero;"/><path d="M441.199,488.919c4.035,2.19 8.17,4.221 12.436,5.937c0.115,4.12 -2.493,7.349 -6.845,7.262c-3.199,-0.216 -2.306,3.631 0.115,3.934c-3.833,2.276 -6.729,-1.096 -9.453,-3.358c0.821,-2.493 1.052,-5.072 -2.118,-5.85l0.13,-0.173c2.046,-2.48 3.891,-5.116 5.735,-7.752Zm-1.052,11.988c0.793,0.764 0.793,0.764 0,0Zm2.911,2.897c0.764,0.808 0.764,0.808 0,0Z" style="fill:#557391;fill-rule:nonzero;"/><path d="M415.678,408.915c2.522,1.671 5.087,3.257 7.68,4.813c-0.303,1.066 -0.893,3.199 -1.196,4.265l-0.721,0.994c-2.94,-2.695 -4.712,-6.283 -5.764,-10.072Z" style="fill:#3f4a56;fill-rule:nonzero;"/><path d="M422.161,417.992c3.833,-4.107 8.646,-7.104 13.905,-9.035c0.994,4.481 2.479,8.833 4.265,13.07c-3.386,-1.614 -6.758,-3.242 -10.101,-4.914c-2.435,1.989 -4.871,3.963 -7.32,5.922c-0.36,-0.994 -1.081,-2.968 -1.441,-3.963l-0.029,-0.086l0.72,-0.994Z" style="fill:#4a6fb1;fill-rule:nonzero;"/><path d="M559.689,411.264c0.766,0.708 0.766,0.708 0,0Z" style="fill:#323335;fill-rule:nonzero;"/><path d="M578.436,411.279c0.766,0.708 0.766,0.708 0,0Z" style="fill:#3a3b3d;fill-rule:nonzero;"/><path d="M475.998,424.737c3.127,-4.611 5.577,-9.784 10.13,-13.228c3.271,6.946 6.628,13.848 10.015,20.736c-0.721,-0.13 -2.147,-0.375 -2.868,-0.49c-2.723,2.522 -5.548,4.914 -8.487,7.176c-3.141,-4.597 -6.182,-9.28 -8.79,-14.194Z" style="fill:#4b6379;fill-rule:nonzero;"/><path d="M556.794,412.892c0.781,0.679 0.781,0.679 0,0Z" style="fill:#37383a;fill-rule:nonzero;"/><path d="M569.892,412.676c0.68,0.824 0.68,0.824 0,0Z" style="fill:#4d4f52;fill-rule:nonzero;"/><path d="M575.438,412.992c1.96,-0.259 3.502,3.084 2.508,4.683c-2.19,0.49 -3.473,-3.055 -2.508,-4.683Z" style="fill:#202122;fill-rule:nonzero;"/><path d="M588.508,412.733c0.781,0.723 0.781,0.723 0,0Z" style="fill:#494a4d;fill-rule:nonzero;"/><path d="M541.618,413.296c1.513,1.528 2.68,3.66 5.043,3.992c-0.475,0.346 -1.412,1.037 -1.873,1.369c2.161,3.631 4.366,7.234 6.469,10.908c-0.764,0.346 -2.276,1.066 -3.04,1.427l-0.244,-0.634c-0.995,-6.225 -7.71,-10.289 -6.355,-17.061Z" style="fill:#4d4e50;fill-rule:nonzero;"/><path d="M585.426,414.289c4.769,0.145 -2.154,4.365 0,0Z" style="fill:#303133;fill-rule:nonzero;"/><path d="M494.37,420.961c-0.317,-2.233 -1.052,-6.96 3.141,-5.706c-0.648,4.698 -0.014,9.482 2.075,13.761c1.7,3.329 3.098,7.435 6.816,9.021c0.447,3.617 3.732,5.663 6.239,7.868c2.839,2.061 5.677,4.856 9.453,4.784c0.793,2.162 2.968,3.026 4.885,3.948c5.274,2.406 10.692,4.554 16.384,5.807l-0.519,0.246c-2.867,1.355 -5.777,2.636 -8.674,3.905c-10.519,-0.951 -21.053,1.527 -30.318,6.469c-4.121,1.428 -7.652,4.222 -9.482,8.242c-1.023,1.744 -1.21,3.818 -1.585,5.764c-1.167,-1.052 -2.291,-2.133 -3.358,-3.257c0.36,-2.449 2.234,-4.351 3.43,-6.455c-10.361,-6.254 -19.799,-13.819 -29.54,-20.952c-0.591,-0.403 -1.772,-1.196 -2.363,-1.585c1.599,-0.649 3.199,-1.283 4.813,-1.917c7.464,6.744 16.067,12.076 24.626,17.32c2.623,1.412 5.505,3.776 8.603,2.176c5,-3.429 10.548,-5.85 15.995,-8.43c-2.954,-5.259 -6.009,-10.475 -9.467,-15.432c-4.971,-7.882 -9.193,-16.442 -11.153,-25.577Z" style="fill:#272e33;fill-rule:nonzero;"/><path d="M571.434,415.429c4.307,-2.298 0.275,4.77 0,0Z" style="fill:#38393c;fill-rule:nonzero;"/><path d="M599.936,422.578c4.192,-2.457 0.491,4.77 0,0Z" style="fill:#38393c;fill-rule:nonzero;"/><path d="M563.998,416.984c0.823,0.766 0.823,0.766 0,0Z" style="fill:#73757a;fill-rule:nonzero;"/><path d="M582.76,416.971c0.823,0.752 0.823,0.752 0,0Z" style="fill:#707276;fill-rule:nonzero;"/><path d="M595.916,416.984c0.741,0.726 0.741,0.726 0,0Z" style="fill:#313234;fill-rule:nonzero;"/><path d="M601.52,416.971c0.737,0.766 0.737,0.766 0,0Z" style="fill:#404144;fill-rule:nonzero;"/><path d="M561.39,417.848c0.98,2.565 3.213,5.591 0,7.579c1.441,1.571 2.897,3.17 4.338,4.77c1.397,-3.689 2.781,-3.819 4.149,-0.403c-0.821,0.144 -2.478,0.418 -3.299,0.548c0.778,2.176 1.426,4.453 -0.216,6.427c4.554,-2.824 5.908,3.674 8.848,5.865c10.561,10.13 21.398,20.419 28.329,33.488c0.216,0.778 0.648,2.349 0.85,3.142c-0.648,-0.477 -1.96,-1.441 -2.608,-1.917c-6.758,-12.176 -16.96,-21.918 -27.248,-31.111c-2.522,-2.147 -4.611,-4.741 -6.023,-7.738c-0.462,0.259 -1.355,0.793 -1.816,1.052c-2.003,-1.974 -4.035,-3.934 -6.111,-5.807c1.758,2.868 3.632,5.677 5.461,8.531c0.462,-0.245 1.399,-0.735 1.859,-0.98c8.661,8.66 17.739,16.975 25.447,26.528c2.68,3.286 4.655,7.306 8.43,9.525c-0.072,0.922 -0.202,2.767 -0.274,3.688c-8.127,-7.853 -17.118,-14.798 -26.989,-20.318c-4.567,-2.406 -8.142,-6.167 -11.903,-9.625c-5.274,-4.381 -10.26,-9.194 -13.776,-15.145l-0.158,-0.245c2.291,1.124 4.366,2.637 6.24,4.395c10.071,7.724 18.747,17.205 29.438,24.122c-3.516,-4.828 -8.199,-8.589 -12.002,-13.157c-5.232,-4.453 -11.183,-8.531 -14.209,-14.929c-0.044,-0.562 -0.158,-1.7 -0.216,-2.262c-0.619,-0.288 -1.873,-0.864 -2.508,-1.153c-0.432,-0.908 -1.296,-2.709 -1.729,-3.617c1.816,0.029 3.589,-0.85 4.453,-2.493c1.643,1.844 3.126,4.15 5.75,4.654c-1.845,-3.602 -5.015,-6.456 -6.311,-10.332l-0.216,-0.591c0.922,0.562 2.737,1.7 3.646,2.262c0.101,-1.196 0.288,-3.559 0.375,-4.755Zm2.911,14.957c0.792,0.807 0.792,0.807 0,0Z" style="fill:#7d8187;fill-rule:nonzero;"/><path d="M568.35,422.79c0.81,0.809 0.81,0.809 0,0Z" style="fill:#7d8187;fill-rule:nonzero;"/><path d="M594.488,427.042c0.752,0.867 0.752,0.867 0,0Z" style="fill:#7d8187;fill-rule:nonzero;"/><path d="M571.318,430.919c0.677,-0.893 2.032,-2.709 2.709,-3.617c0.231,0.98 0.692,2.94 0.922,3.919c1.816,-1.412 6.888,-5.346 5.649,0.778c-0.907,-0.13 -2.723,-0.403 -3.631,-0.533c-0.043,0.548 -0.158,1.657 -0.215,2.205c1.252,-0.303 3.775,-0.922 5.043,-1.239c-0.576,1.081 -1.7,3.213 -2.276,4.28l1.542,0.245l0.301,-2.738c0.591,0.331 1.788,1.009 2.392,1.34c0.534,1.974 -1.469,3.055 -2.506,4.366c2.622,-1.513 4.856,-4.899 8.228,-4.208l0.303,0.807c0.274,0.461 0.821,1.369 1.094,1.816c0.519,0.533 1.527,1.599 2.047,2.133c0.043,0.058 0.158,0.173 0.215,0.231l0.13,-0.144l1.672,-1.801c2.607,1.974 4.611,4.597 6.499,7.234c-0.606,-1.527 -1.197,-3.055 -1.788,-4.582c1.399,-1.355 2.652,-2.868 3.2,-4.77c-1.729,1.138 -3.4,2.349 -5.058,3.574l-0.389,-2.234l1.124,-1.167l0.547,-0.332l0.448,-0.303c1.21,-0.259 3.646,-0.778 4.87,-1.038l-0.461,2.968c2.003,-2.781 3.617,-2.723 4.856,0.187c-3.17,-0.303 -6.067,2.378 -1.052,3.098c-3.055,2.968 -5.259,7.118 -9.713,8.242c4.597,-0.519 7.523,-4.597 11.615,-6.34l0.936,2.81c0.015,0.058 0.044,0.144 0.044,0.202c0.057,0.562 0.143,1.715 0.202,2.277c-0.995,1.599 -2.003,3.199 -3.012,4.77l-1.757,-0.288c0.821,0.634 2.478,1.931 3.314,2.565c0.057,0.173 0.173,0.504 0.231,0.663c-0.995,3.386 -2.234,6.744 -2.537,10.289c4.54,2.666 2.335,-6.024 7.003,-4.18c-0.029,0.908 -0.072,2.695 -0.101,3.603l-0.793,0.028l0.015,0.764l0.778,-0.015c-1.34,5.375 -2.623,10.779 -4.294,16.083l-0.36,1.181c-5.246,-12.637 -13.79,-23.689 -23.545,-33.171c-5.707,-5.173 -12.149,-9.943 -15.607,-17.032c1.773,-0.504 3.257,-1.441 4.439,-2.81l-3.271,-0.115Zm2.84,3.43c0.806,0.735 0.806,0.735 0,0Zm2.579,2.68c4.769,0.043 -2.061,4.409 0,0Zm10.317,1.499c0.764,0.749 0.764,0.749 0,0Zm-2.767,2.911c0.778,0.793 0.778,0.793 0,0Zm4.553,-0.202c-0.446,1.844 1.6,4.755 3.488,4.87c1.311,-2.017 -1.067,-5.793 -3.488,-4.87Zm14.28,0.303c0.778,0.85 0.778,0.85 0,0Zm-21.802,1.542c0.72,0.749 0.72,0.749 0,0Zm12.94,-0.346c4.193,-2.421 0.461,4.784 0,0Zm-8.444,1.47c4.265,-2.306 0.331,4.755 0,0Zm11.354,1.628c0.85,0.692 0.85,0.692 0,0Zm-3.083,1.455c4.769,0.187 -2.177,4.337 0,0Zm13.315,0.158c0.792,0.778 0.792,0.778 0,0Zm-17.292,2.536c4.222,-2.392 0.418,4.755 0,0Zm14.381,0.101c0.778,0.764 0.778,0.764 0,0Zm-8.632,1.484c0.707,0.764 0.707,0.764 0,0Zm5.418,-0.086c4.727,0.245 -2.205,4.352 0,0Zm-1.023,5.966c0.707,0.749 0.707,0.749 0,0Zm2.739,2.81c0.72,0.734 0.72,0.734 0,0Zm-3.2,1.555c4.769,0.13 -2.148,4.352 0,0Zm5.779,7.969c2.781,2.695 2.895,-3.574 0.013,-0.951c0,0.246 0,0.707 -0.013,0.951Zm3.285,4.9c0.721,0.764 0.721,0.764 0,0Z" style="fill:#7d8187;fill-rule:nonzero;"/><path d="M569.819,431.091c0.825,0.795 0.825,0.795 0,0Z" style="fill:#7d8187;fill-rule:nonzero;"/><path d="M588.235,431.624c0.766,0.853 0.766,0.853 0,0Z" style="fill:#7d8187;fill-rule:nonzero;"/><path d="M585.253,432.936l1.272,-0.101c2.644,2.486 -2.805,3.122 -1.272,0.101Z" style="fill:#7d8187;fill-rule:nonzero;"/><path d="M539.501,434.362c0.708,0.766 0.708,0.766 0,0Z" style="fill:#7d8187;fill-rule:nonzero;"/><path d="M536.692,437.216c0.723,0.752 0.723,0.752 0,0Z" style="fill:#7d8187;fill-rule:nonzero;"/><path d="M608.582,439.695c0.796,0.824 0.796,0.824 0,0Z" style="fill:#7d8187;fill-rule:nonzero;"/><path d="M543.665,472.879c7.018,-0.072 14.064,0.879 20.548,3.704c-3.126,0.187 -6.253,0.475 -9.351,0.806c-2.377,0.275 -4.727,0.663 -7.046,1.197c-1.384,-1.902 -2.781,-3.804 -4.15,-5.707Z" style="fill:#7d8187;fill-rule:nonzero;"/><path d="M574.186,418.54c0.823,0.781 0.823,0.781 0,0Z" style="fill:#6f7176;fill-rule:nonzero;"/><path d="M579.606,418.613c4.784,0.217 -2.226,4.337 0,0Z" style="fill:#3d3f42;fill-rule:nonzero;"/><path d="M592.875,418.484c0.781,0.737 0.781,0.737 0,0Z" style="fill:#505256;fill-rule:nonzero;"/><path d="M631.262,417.547c4.079,1.83 8.027,3.905 11.86,6.211c0.489,-0.36 1.469,-1.095 1.959,-1.47l0.648,-0.475c0.073,3.775 0.231,7.536 0.404,11.297c0.044,2.623 0.173,5.231 0.576,7.839c-0.461,-0.447 -1.369,-1.311 -1.83,-1.758c-0.375,-0.836 -1.124,-2.507 -1.484,-3.343c-3.906,-2.392 -7.954,-4.525 -12.033,-6.571c0,-0.965 -0.013,-2.896 -0.028,-3.847c0,-2.637 0.015,-5.274 -0.073,-7.882Z" style="fill:#5f89b4;fill-rule:nonzero;"/><path d="M420.678,426.221c3.458,-2.32 6.773,-4.885 10.274,-7.147c0.389,0.202 1.196,0.591 1.585,0.793c2.695,5.62 5.692,11.211 10.159,15.663c-3.415,2.839 -6.758,5.75 -10.015,8.761c-4.582,-5.764 -9.554,-11.297 -13.07,-17.796c0.245,-0.692 0.721,-2.061 0.951,-2.738c0.029,0.62 0.086,1.845 0.115,2.464Z" style="fill:#5b7da1;fill-rule:nonzero;"/><path d="M565.526,419.766c4.336,-2.226 0.231,4.77 0,0Z" style="fill:#4d4e51;fill-rule:nonzero;"/><path d="M584.201,419.81c4.235,-2.414 0.42,4.785 0,0Z" style="fill:#414245;fill-rule:nonzero;"/><path d="M598.612,419.837c0.809,0.665 0.809,0.665 0,0Z" style="fill:#3e4043;fill-rule:nonzero;"/><path d="M602.904,419.795c4.206,-2.443 0.448,4.77 0,0Z" style="fill:#232426;fill-rule:nonzero;"/><path d="M611.22,419.622c3.545,0.014 7.09,0.029 10.649,0.029c0.187,2.003 0.389,4.006 0.606,6.023c0.028,0.245 0.072,0.735 0.101,0.98c0.23,3.199 0.375,6.427 0.461,9.655c-2.897,-0.49 -6.01,-1.081 -8.602,0.677c-0.938,-5.807 -1.99,-11.6 -3.214,-17.364Z" style="fill:#55728f;fill-rule:nonzero;"/><path d="M634.001,444.088c3.775,1.585 8.415,2.983 9.496,7.522c-0.015,0.908 -0.044,2.738 -0.073,3.646c-0.389,1.772 -0.734,3.545 -1.067,5.318c-4.87,-1.96 -11.037,-4.107 -11.512,-10.275c1.067,-2.075 2.104,-4.15 3.156,-6.211Z" style="fill:#55728f;fill-rule:nonzero;"/><path d="M654.852,419.808c0.823,0.867 0.823,0.867 0,0Z" style="fill:#6c9abf;fill-rule:nonzero;"/><path d="M432.538,419.867c3.199,1.585 6.383,3.199 9.467,5.015c7.435,9.107 14.813,18.315 23.762,26.024c-1.614,0.634 -3.213,1.268 -4.813,1.917c-7.32,-4.352 -12.335,-11.369 -18.257,-17.292c-4.467,-4.453 -7.464,-10.044 -10.159,-15.663Z" style="fill:#415364;fill-rule:nonzero;"/><path d="M472.02,442.448c4.337,3.602 8.631,7.248 12.911,10.937c-1.729,1.643 -3.43,3.314 -5.144,4.971c-4.006,-2.695 -7.825,-5.663 -11.571,-8.703c1.268,-2.406 2.536,-4.798 3.804,-7.205Z" style="fill:#415364;fill-rule:nonzero;"/><path d="M539.01,420.63c3.156,3.069 6.038,6.427 8.964,9.727l0.244,0.634c0.764,-0.36 2.276,-1.081 3.04,-1.427l0.677,-0.317c0.663,1.686 1.83,2.839 3.488,3.473c0.634,0.288 1.887,0.864 2.508,1.153c0.057,0.562 0.173,1.7 0.215,2.262c-1.052,1.34 -2.132,2.651 -3.227,3.963c-1.873,-1.758 -3.949,-3.271 -6.24,-4.395l-0.015,0c0.044,0.058 0.13,0.187 0.174,0.245c3.516,5.951 8.501,10.764 13.776,15.145c0.114,0.922 0.36,2.752 0.475,3.675c-1.485,-0.677 -2.941,-1.369 -4.382,-2.075l-0.432,-0.72c-2.075,-4.222 -5.807,-7.147 -9.381,-10.029c-0.446,-0.014 -1.34,-0.014 -1.786,-0.029c-1.816,-2.262 -3.949,-4.28 -6.5,-5.692c1.009,1.801 2.104,3.559 3.229,5.303c-2.278,-0.864 -4.611,-1.772 -7.09,-1.225c-2.868,0.259 -4.842,-2.435 -7.133,-3.703c2.608,-2.219 5.318,-4.309 8.099,-6.326c-0.692,-0.216 -2.076,-0.663 -2.781,-0.893l-1.038,-0.346c1.614,-1.441 3.344,-2.767 5.13,-3.991c0.202,0.879 0.62,2.651 0.821,3.545c1.83,1.081 3.66,2.162 5.577,3.113c-1.946,-3.819 -6.585,-6.369 -6.412,-11.067Zm6.311,12.133c0.764,0.735 0.764,0.735 0,0Zm-8.472,1.686c0.749,0.721 0.749,0.721 0,0Zm2.651,-0.086c0.707,0.764 0.707,0.764 0,0Zm-2.81,2.853c0.721,0.749 0.721,0.749 0,0Z" style="fill:#4a4b4d;fill-rule:nonzero;"/><path d="M576.85,421.35c0.796,0.781 0.796,0.781 0,0Z" style="fill:#6e7175;fill-rule:nonzero;"/><path d="M590.065,421.237c0.737,0.824 0.737,0.824 0,0Z" style="fill:#616468;fill-rule:nonzero;"/><path d="M608.724,421.379c0.766,0.766 0.766,0.766 0,0Z" style="fill:#525356;fill-rule:nonzero;"/><path d="M577.486,426.395c0.259,-2.666 3.126,-2.925 4.87,-4.136c1.47,1.513 2.853,3.098 4.077,4.813l-2.809,0.014c0.143,-1.888 -0.62,-3.17 -2.32,-3.819c-0.951,0.778 -2.853,2.334 -3.818,3.127Z" style="fill:#797678;fill-rule:nonzero;"/><path d="M573.998,423.843c1.772,-0.058 4.063,2.464 3.429,4.323c-2.205,1.239 -3.905,-2.551 -3.429,-4.323Z" style="fill:#797678;fill-rule:nonzero;"/><path d="M445.968,559.006c1.772,1.915 3.617,3.76 5.562,5.504c4.309,3.66 7.81,8.112 10.937,12.795c-0.533,0.246 -1.585,0.707 -2.118,0.938c1.816,1.614 3.545,3.343 5.072,5.245c-0.548,1.584 -1.081,3.17 -1.614,4.769c1.657,-0.446 3.329,-0.894 5,-1.325c1.066,2.247 1.772,4.654 1.715,7.191c1.758,1.355 3.53,2.695 5.332,4.035c1.095,2.622 2.205,5.23 3.329,7.839c1.787,3.949 3.3,8.07 3.631,12.435c-0.836,0.116 -2.522,0.345 -3.372,0.475c0.086,1.816 0.159,3.646 0.245,5.491c0.519,1.944 1.023,3.89 1.542,5.864c-1.167,0.058 -3.516,0.159 -4.698,0.202c-0.187,0.995 -0.591,2.968 -0.793,3.963c1.484,-0.894 2.968,-1.786 4.467,-2.68c1.974,4.035 4.899,8.617 10,8.602c5.908,0.808 8.747,-5.028 12.637,-8.213c-0.158,1.671 -0.331,3.358 -0.504,5.028c3.228,3.719 6.672,7.422 11.254,9.454c-0.548,0 -1.657,-0.015 -2.205,-0.015c-2.695,-0.044 -5.418,-0.158 -8.069,0.404c-3.026,1.296 -5.072,4.035 -7.45,6.21c0.303,1.254 0.605,2.508 0.908,3.775c-1.009,-0.562 -2.032,-1.124 -3.04,-1.671l-1.715,-0.923c1.239,-1.671 2.421,-3.385 3.545,-5.144c-2.046,0.923 -3.631,2.551 -5.317,3.992c-0.576,-2.522 0.793,-4.598 2.061,-6.601c-1.614,1.067 -2.911,2.81 -4.87,3.3c-0.389,-0.835 -1.196,-2.522 -1.599,-3.358c-1.859,1.845 -4.64,5.087 -5.577,0.62c0.807,-0.389 2.406,-1.167 3.199,-1.571c-1.369,-1.441 -2.81,-2.81 -4.236,-4.193c-1.83,1.917 -3.934,3.53 -6.009,5.159c-0.216,-0.692 -0.648,-2.061 -0.85,-2.752c-0.663,-0.246 -1.989,-0.707 -2.652,-0.938l-0.403,-2.434c0.836,0.547 2.493,1.614 3.329,2.146c-0.259,-1.166 -0.519,-2.319 -0.764,-3.457c0.922,-1.009 1.844,-2.004 2.767,-3.012c-1.888,-1.009 -3.804,-4.856 -5.879,-1.786c-0.519,-1.081 -0.994,-2.234 -1.455,-3.315l-0.029,-0.086l-0.043,-0.057c-0.504,-0.562 -1.513,-1.701 -2.003,-2.278l1.11,-1.353c0.389,-0.505 1.196,-1.514 1.599,-2.017l0.058,-0.044l-0.259,-0.244c-0.634,-0.62 -1.902,-1.889 -2.536,-2.522l-0.288,-0.375l-0.346,-0.303c-0.533,-0.619 -1.599,-1.845 -2.147,-2.449l-0.475,-0.793c0.648,-0.562 1.974,-1.685 2.637,-2.249c0.086,0.073 0.245,0.187 0.332,0.26c0.591,0.591 1.801,1.772 2.406,2.377l0.519,0.432c1.859,0.116 3.732,0.145 5.605,0.202c0.994,-1.325 2.017,-2.607 3.055,-3.905c-2.19,-1.498 -4.424,-2.882 -6.859,-3.905l1.038,-1.153c-0.014,-0.029 -0.058,-0.086 -0.072,-0.116c-0.562,-0.734 -1.7,-2.205 -2.277,-2.939l-0.115,-0.231c-2.306,-6.153 -7.435,-10.418 -11.067,-15.692c-0.692,-0.231 -2.075,-0.663 -2.781,-0.879c0.807,-0.85 1.599,-1.7 2.406,-2.535c-3.199,-4.352 -6.902,-8.316 -10.995,-11.846c0.389,-1.267 0.778,-2.521 1.167,-3.775c-2.334,-2.68 -4.755,-5.303 -7.032,-8.026c1.542,-0.778 3.069,-1.571 4.582,-2.392c0.375,-0.793 1.11,-2.364 1.47,-3.156Zm9.928,16.412c4.294,-2.276 0.317,4.769 0,0Zm11.614,26.01c0.692,0.777 0.692,0.777 0,0Zm5.836,0.086c0.721,0.764 0.721,0.764 0,0Zm-2.911,2.853c0.721,0.764 0.721,0.764 0,0Zm5.692,0.072c0.778,0.707 0.778,0.707 0,0Zm-2.795,2.767c0.764,0.692 0.764,0.692 0,0Zm2.839,2.897c0.692,0.778 0.692,0.778 0,0Zm2.925,2.882c0.735,0.764 0.735,0.764 0,0Zm-8.732,8.674c0.735,0.793 0.735,0.793 0,0Zm5.807,0.058c0.749,0.705 0.749,0.705 0,0Zm-2.839,2.781c0.749,0.749 0.749,0.749 0,0Zm-2.968,2.867c0.821,0.734 0.821,0.734 0,0Zm5.735,0.072c0.793,0.692 0.793,0.692 0,0Zm-2.911,2.897c0.821,0.749 0.821,0.749 0,0Zm5.793,5.691c0.821,0.764 0.821,0.764 0,0Zm-2.882,3.027c0.821,0.764 0.821,0.764 0,0Zm5.778,-0.13c0.778,0.808 0.778,0.808 0,0Zm17.277,0.072c0.821,0.764 0.821,0.764 0,0Zm-14.424,2.882c0.807,0.778 0.807,0.778 0,0Zm5.778,-0.116c0.821,0.764 0.821,0.764 0,0Zm5.85,-0.028c0.793,0.806 0.793,0.806 0,0Zm5.706,0.072c0.821,0.749 0.821,0.749 0,0Zm-14.295,3.04c0.807,0.778 0.807,0.778 0,0Zm5.75,-0.116c0.793,0.808 0.793,0.808 0,0Zm5.663,-0.013c0.764,0.821 0.764,0.821 0,0Zm-2.868,2.925c0.836,0.749 0.836,0.749 0,0Z" style="fill:#797678;fill-rule:nonzero;"/><path d="M597.04,671.706c7.752,12.119 9.539,29.713 -1.081,40.779c1.6,1.672 3.2,3.343 4.813,5.03c3.89,-6.01 7.594,-12.58 7.435,-19.973c0.75,-8.545 -2.478,-16.571 -5.432,-24.381c4.107,8.011 7.22,16.874 6.412,26.009c-0.216,8.142 -4.496,15.391 -9.452,21.572c-3.128,-8.156 -11.6,-13.761 -20.333,-13.214c-11.615,0.461 -21.485,11.643 -20.505,23.215c0.505,7.42 5.908,13.732 12.393,16.946c7.622,3.227 16.485,2.031 23.949,-1.11c20.72,-8.573 33.791,-32.84 28.546,-54.814c-1.067,-7.205 -5.318,-13.287 -7.508,-20.103c4.611,8.589 9.063,17.551 9.654,27.481c2.421,24.208 -16.498,48.661 -40.678,51.889c-11.139,1.744 -22.306,-4.15 -28.79,-13.012c-6.513,-8.934 -5.16,-22.335 2.493,-30.16c1.023,-0.677 2.06,-1.327 3.126,-1.931c-2.867,3.703 -6.225,7.305 -7.363,11.989c-1.801,6.297 -0.677,13.227 2.867,18.732c0,-3.877 -0.446,-7.752 0.158,-11.585c2.104,-10.534 12.176,-19.41 23.185,-18.646c5.289,-0.202 9.871,2.693 14.554,4.711c-2.752,-5.59 -8.718,-7.94 -14.236,-9.856c0.23,-0.215 0.705,-0.619 0.951,-0.821c5.39,1.311 9.755,4.698 13.948,8.142c0.332,0.015 0.98,0.029 1.312,0.029c8.011,-10.88 5.144,-25.607 -0.418,-36.918Z" style="fill:#797678;fill-rule:nonzero;"/><path d="M611.276,673.175c5.966,8.358 10.203,18.359 9.944,28.777c1.584,22.306 -17.711,44.526 -40.276,44.757c-10.073,0.705 -20.303,-8.228 -19.136,-18.662c0.764,-11.253 12.955,-20.533 23.949,-16.456c6.715,2.278 10.662,9.252 10.231,16.168c9.583,-7.565 17.018,-18.949 16.658,-31.528c0.606,-7.479 -2.249,-14.497 -3.804,-21.643l0.202,0.533c4.467,10.807 6.671,23.301 2.493,34.554c-3.574,8.934 -9.885,16.975 -18.374,21.717c1.946,-6.384 0.677,-14.065 -5.634,-17.48c-10.115,-6.24 -23.588,2.911 -24.164,14.078c-1.039,10.231 9.452,18.243 19.121,17.133c22.739,-1.816 40.405,-25.218 37.638,-47.537c-0.648,-8.329 -4.237,-15.982 -8.213,-23.185l-0.635,-1.226Z" style="fill:#797678;fill-rule:nonzero;"/><path d="M587.067,422.95c0.781,0.824 0.781,0.824 0,0Z" style="fill:#717479;fill-rule:nonzero;"/><path d="M540.612,436.222c2.551,1.412 4.683,3.43 6.499,5.692c0.448,0.014 1.34,0.014 1.788,0.029c3.573,2.882 7.305,5.807 9.381,10.029c-2.276,-0.807 -4.54,-1.643 -6.801,-2.435c0.116,-0.519 0.331,-1.542 0.446,-2.061c-3.574,1.917 -7.839,-1.686 -8.142,-5.36l-0.116,3.127c-2.521,-1.34 -4.885,-2.94 -6.916,-4.943c2.478,-0.548 4.812,0.36 7.09,1.225c-1.125,-1.744 -2.219,-3.502 -3.229,-5.303Z" style="fill:#717479;fill-rule:nonzero;"/><path d="M605.873,422.95c0.766,0.737 0.766,0.737 0,0Z" style="fill:#47484a;fill-rule:nonzero;"/><path d="M663.583,422.778c1.959,1.182 2.407,3.141 1.225,5.115c1.21,1.47 2.434,2.954 3.675,4.438c3.257,-0.62 6.541,-1.095 9.856,-1.47c-1.197,2.306 -2.407,4.611 -3.545,6.946l-1.773,-0.692c-4.035,-1.542 -7.969,-3.329 -11.83,-5.231c-2.017,1.369 -4.15,2.651 -6.557,3.199c0.764,-3.314 3.113,-5.505 6.686,-4.928c0.173,-2.623 0.418,-5.332 2.263,-7.378Z" style="fill:#9cb7dd;fill-rule:nonzero;"/><path d="M565.222,424.463c4.815,0.231 -2.283,4.322 0,0Z" style="fill:#676a6f;fill-rule:nonzero;"/><path d="M588.482,426.869c-0.231,-2.205 3.141,-3.473 4.942,-2.882c-0.015,1.931 -3.185,3.905 -4.942,2.882Z" style="fill:#46474a;fill-rule:nonzero;"/><path d="M553.694,429.103c-1.412,-1.974 4.077,-5.26 4.452,-2.493c-0.863,1.643 -2.636,2.522 -4.452,2.493Z" style="fill:#414345;fill-rule:nonzero;"/><path d="M488.62,514.379c3.3,0.029 6.686,-0.475 9.957,0.332c1.009,2.003 -2.19,2.449 -3.156,3.675c0.418,0.475 0.836,0.966 1.254,1.441c-0.49,2.19 -0.98,4.366 -1.412,6.57c-0.85,3.213 -1.758,6.412 -2.608,9.612c-1.917,0.562 -3.833,1.124 -5.735,1.7c-0.043,2.146 -0.086,4.294 -0.115,6.455c-1.614,-0.028 -3.242,-0.028 -4.856,-0.043c-1.888,-3.126 -4.222,-5.937 -6.859,-8.444c0.231,-1.153 0.461,-2.291 0.721,-3.429c2.795,-1.426 5.231,-3.617 5.159,-7.018c-3.055,0.202 -6.124,0.36 -9.165,0.562c1.225,-0.648 2.45,-1.311 3.674,-1.974c3.574,-1.355 7.219,-2.537 10.851,-3.732c0.749,-1.441 1.499,-2.882 2.19,-4.352l0.101,-1.355Zm-3.314,13.228c0.231,0.98 0.692,2.926 0.922,3.891c2.306,0.734 2.81,-1.527 3.631,-3.113c-1.138,-0.187 -3.415,-0.576 -4.554,-0.778Z" style="fill:#414345;fill-rule:nonzero;"/><path d="M569.934,425.676c4.278,-2.371 0.332,4.813 0,0Z" style="fill:#6c6f73;fill-rule:nonzero;"/><path d="M602.948,425.659c0.823,0.766 0.823,0.766 0,0Z" style="fill:#76787d;fill-rule:nonzero;"/><path d="M607.371,425.603c4.366,-2.125 0.073,4.813 0,0Z" style="fill:#29292b;fill-rule:nonzero;"/><path d="M625.226,425.5c2.032,-0.029 4.063,-0.058 6.109,-0.072c0.015,0.951 0.029,2.882 0.029,3.848c0.072,4.856 0.259,9.727 0.086,14.597c-1.931,0.058 -3.848,0.115 -5.75,0.173c-0.029,-6.182 0.086,-12.378 -0.475,-18.545Z" style="fill:#4e667d;fill-rule:nonzero;"/><path d="M598.394,430.113c0.793,-1.311 1.614,-2.594 2.464,-3.876c0.057,0.735 0.158,2.219 0.216,2.954c2.06,0.476 3.011,1.744 2.853,3.79c0.116,0.533 0.345,1.614 0.461,2.161c-1.225,0.259 -3.66,0.778 -4.87,1.038c-0.259,0.158 -0.749,0.475 -0.995,0.634l-1.124,1.167c-0.331,-0.346 -1.008,-1.023 -1.355,-1.355c-0.274,0.533 -0.821,1.599 -1.109,2.133l-1.672,1.801l-0.345,-0.086c-0.519,-0.533 -1.527,-1.599 -2.047,-2.133c-0.274,-0.447 -0.821,-1.355 -1.094,-1.816l-0.303,-0.807l-0.116,-1.801c0.591,-0.461 1.786,-1.369 2.392,-1.816c-0.375,-1.182 -1.124,-3.53 -1.513,-4.712c1.988,0.98 3.227,3.141 5.447,3.703c0.677,-0.245 2.032,-0.735 2.709,-0.98Zm-5.375,4.265c0.778,0.706 0.778,0.706 0,0Z" style="fill:#525458;fill-rule:nonzero;"/><path d="M585.699,428.255c4.336,-2.197 0.159,4.842 0,0Z" style="fill:#777b80;fill-rule:nonzero;"/><path d="M604.402,428.356c4.395,-2.125 0.145,4.77 0,0Z" style="fill:#4c4e51;fill-rule:nonzero;"/><path d="M609.909,428.728c4.769,0.159 -2.14,4.38 0,0Z" style="fill:#45464a;fill-rule:nonzero;"/><path d="M585.815,444.205c4.278,-2.313 0.332,4.77 0,0Z" style="fill:#45464a;fill-rule:nonzero;"/><path d="M460.637,429.896c3.631,4.337 7.407,8.545 11.384,12.551c-1.268,2.406 -2.536,4.798 -3.804,7.205c-4.755,-3.891 -9.035,-8.329 -13.286,-12.753c1.902,-2.334 3.804,-4.669 5.706,-7.003Z" style="fill:#4a6176;fill-rule:nonzero;"/><path d="M595.687,431.097c0.433,-0.434 1.271,-1.286 1.691,-1.72l1.027,0.737c-0.68,0.246 -2.039,0.737 -2.717,0.983Z" style="fill:#686b70;fill-rule:nonzero;"/><path d="M631.364,429.276c4.079,2.046 8.127,4.179 12.033,6.571c0.36,0.836 1.109,2.507 1.484,3.343c0.461,0.447 1.369,1.311 1.83,1.758l0.389,0.36c-1.801,2.565 -2.148,5.721 -2.767,8.718l-0.519,1.009l-0.316,0.576c-1.081,-4.539 -5.722,-5.937 -9.496,-7.522c-1.052,2.061 -2.089,4.136 -3.156,6.211c0.244,-2.147 0.446,-4.28 0.604,-6.427c0.174,-4.87 -0.013,-9.741 -0.086,-14.597Z" style="fill:#5b7da0;fill-rule:nonzero;"/><path d="M607.17,431.438c0.838,0.781 0.838,0.781 0,0Z" style="fill:#7b7e84;fill-rule:nonzero;"/><path d="M661.193,431.883c3.861,1.902 7.795,3.689 11.83,5.231c0.576,1.268 1.182,2.55 1.801,3.833c1.672,2.551 3.372,5.087 5.015,7.666c-4.178,-1.282 -8.343,-2.911 -12.767,-2.954c-2.723,2.925 -5.375,5.922 -7.868,9.035c-0.129,-1.297 -0.375,-3.876 -0.489,-5.173c0.057,-2.695 1.353,-5.62 0.288,-8.156c-2.566,-2.017 -5.837,-2.723 -8.877,-3.646c-0.143,4.842 -0.259,9.712 -1.383,14.453c-0.347,0.692 -1.009,2.075 -1.34,2.767c-0.995,-1.499 -2.19,-2.796 -3.589,-3.905l0.519,-1.009c1.412,-1.297 2.91,-2.507 4.351,-3.761c0.707,-1.772 -0.028,-3.919 -1.584,-4.957l-0.389,-0.36c-0.404,-2.608 -0.534,-5.216 -0.576,-7.839c0.966,0.706 2.897,2.104 3.862,2.81c1.153,-0.202 3.486,-0.634 4.639,-0.836c2.407,-0.548 4.54,-1.83 6.557,-3.199Zm-2.551,6.311c4.294,3.069 1.384,8.545 1.658,12.81c2.522,-2.349 4.74,-5 7.262,-7.349c2.926,0.576 5.735,1.585 8.632,2.363c-1.513,-8.084 -11.283,-7.522 -17.551,-7.825Z" style="fill:#b5c9e7;fill-rule:nonzero;"/><path d="M523.679,432.072c3.847,4.928 8.804,8.804 14.136,12.018c-1.254,3.487 -2.449,6.974 -3.588,10.505c-5.994,-2.551 -11.758,-5.677 -17.018,-9.539c2.104,-4.366 4.251,-8.689 6.47,-12.983Z" style="fill:#f6a926;fill-rule:nonzero;"/><path d="M522.094,450.689c6.96,3.502 14.828,5.389 21.268,9.755c-5.691,-1.253 -11.11,-3.4 -16.383,-5.807c-1.917,-0.922 -4.092,-1.787 -4.885,-3.948Z" style="fill:#f6a926;fill-rule:nonzero;"/><path d="M529.43,432.85c0.809,0.795 0.809,0.795 0,0Z" style="fill:#797c82;fill-rule:nonzero;"/><path d="M564.3,432.806c0.796,0.809 0.796,0.809 0,0Z" style="fill:#030303;fill-rule:nonzero;"/><path d="M603.927,432.979c0.521,0.231 1.547,0.708 2.053,0.94l-0.462,1.185l-1.128,0.043c-0.116,-0.549 -0.348,-1.633 -0.464,-2.168Z" style="fill:#6e7277;fill-rule:nonzero;"/><path d="M536.849,434.448c0.752,0.723 0.752,0.723 0,0Z" style="fill:#0b0b0c;fill-rule:nonzero;"/><path d="M560.583,433.741c2.075,1.873 4.107,3.833 6.109,5.807c0.461,-0.259 1.355,-0.793 1.816,-1.052c1.412,2.997 3.502,5.591 6.023,7.738c10.289,9.193 20.491,18.934 27.25,31.111c-3.776,-2.219 -5.75,-6.24 -8.43,-9.525c-7.71,-9.553 -16.788,-17.867 -25.449,-26.528c-0.461,0.245 -1.397,0.735 -1.858,0.98c-1.83,-2.853 -3.703,-5.663 -5.461,-8.531Z" style="fill:#050708;fill-rule:nonzero;"/><path d="M534.169,464.594c2.896,-1.268 5.807,-2.551 8.675,-3.905c3.185,0.288 6.21,1.397 9.223,2.42c1.988,0.736 3.962,1.499 5.965,2.219c11.558,4.54 23.114,9.684 32.711,17.724c-7.321,-2.161 -14.583,-4.626 -22.205,-5.476c-1.456,-0.316 -2.897,-0.648 -4.323,-0.993c-6.484,-2.825 -13.531,-3.776 -20.549,-3.704c1.369,1.902 2.768,3.805 4.15,5.707c-1.383,0.345 -2.781,0.692 -4.164,1.037c-5.663,2.767 -7.163,-5.101 -11.658,-6.759c1.455,1.946 2.997,3.833 4.495,5.764c-3.443,2.162 -6.268,-1.109 -9.207,-2.464c-2.608,-1.628 -5.822,-1.325 -8.761,-1.47c2.824,-0.663 5.649,-1.353 8.502,-1.959c3.516,-0.734 7.061,-1.268 10.62,-1.83c0.187,-1.239 0.591,-3.703 0.778,-4.942c4.567,0.663 9.122,1.484 13.689,2.146c-5.893,-1.584 -11.888,-2.781 -17.94,-3.516Z" style="fill:#050708;fill-rule:nonzero;"/><path d="M654.088,500.863c2.335,-3.501 4.597,-7.176 8.185,-9.568c-0.822,1.325 -1.628,2.666 -2.421,4.006c-2.017,3.387 -4.323,6.6 -7.06,9.439c-1.887,2.003 -3.69,4.092 -5.346,6.297c-0.216,-3.949 2.607,-6.629 5.388,-8.962c0.317,-0.303 0.938,-0.908 1.254,-1.211Z" style="fill:#050708;fill-rule:nonzero;"/><path d="M532.382,510.402c15.591,-12.017 38.762,-13.315 55.175,-2.218c10.014,6.311 16.672,16.859 19.957,28.07c0.376,9.12 1.413,18.574 -1.772,27.349c-1.859,-5.908 -1.672,-12.595 -5.418,-17.781c-10.058,-15.65 -26.025,-27.927 -44.368,-31.861c-7.681,-2.464 -15.98,-0.778 -23.575,-3.559Z" style="fill:#050708;fill-rule:nonzero;"/><path d="M495.263,526.397c1.124,2.695 2.104,5.448 2.421,8.373c-0.663,4.208 -1.196,8.444 -1.225,12.709c0.115,3.719 0.62,7.407 1.167,11.097c-0.937,0.604 -1.845,1.252 -2.738,1.93l-1.225,0.894c-0.951,-3.429 -2.104,-6.787 -3.631,-10.001c-3.199,1.514 -6.701,1.946 -10.159,1.211c-0.562,0.865 -1.11,1.729 -1.643,2.608c-1.888,2.117 -3.732,4.25 -5.562,6.426c-2.882,-2.708 -5.721,-5.461 -8.804,-7.94c0.375,-3.833 0.36,-8.689 4.51,-10.49c4.366,-2.32 8.689,0.793 11.427,4.077c0.548,-0.792 1.614,-2.377 2.147,-3.17c1.614,0.015 3.242,0.015 4.856,0.044c0.029,-2.161 0.072,-4.309 0.115,-6.456c1.902,-0.576 3.819,-1.138 5.735,-1.7c0.85,-3.198 1.758,-6.398 2.608,-9.612Z" style="fill:#050708;fill-rule:nonzero;"/><path d="M543.017,552.293c-1.009,-5.232 1.081,-10.073 4.712,-13.748c0.518,3.502 0.634,7.133 1.858,10.506c2.421,5.302 8.128,9.265 14.122,8.516c5.548,0.331 8.992,-4.683 13.776,-6.513c-1.584,4.856 -3.011,10.332 -7.651,13.214c-0.922,0.331 -2.781,0.979 -3.703,1.311c-1.744,0.303 -3.488,0.547 -5.246,0.72c-7.204,-0.417 -13.616,-4.841 -17.047,-11.11c-0.215,-0.72 -0.619,-2.176 -0.821,-2.895Z" style="fill:#050708;fill-rule:nonzero;"/><path d="M437.466,555.219c0.115,0.749 0.346,2.262 0.461,3.011c1.931,-0.086 3.876,-0.158 5.822,-0.23c0.187,1.037 0.562,3.126 0.749,4.164c-1.513,0.822 -3.04,1.615 -4.582,2.392c-1.11,-1.944 -1.859,-4.063 -2.536,-6.181c-0.807,0.13 -2.435,0.389 -3.257,0.519c0.533,-0.692 1.599,-2.061 2.133,-2.753c0.303,-0.23 0.908,-0.691 1.21,-0.922Z" style="fill:#050708;fill-rule:nonzero;"/><path d="M445.967,559.006c2.233,-0.619 4.467,-1.498 6.83,-1.456c2.089,2.335 -0.072,4.857 -1.268,6.961c-1.945,-1.744 -3.79,-3.589 -5.562,-5.505Z" style="fill:#050708;fill-rule:nonzero;"/><path d="M487.38,587.194c3.545,-20.678 29.54,-32.509 47.51,-21.715c2.19,1.397 4.294,2.954 6.326,4.61c1.225,1.369 2.348,2.81 3.501,4.295c-7.435,0.36 -14.9,-1.399 -22.277,0.072c-11.802,2.377 -23.012,8.79 -30.174,18.589c-2.248,3.905 -2.478,8.573 -3.559,12.867c-1.47,-5.36 -2.291,-10.936 -1.801,-16.498c0.115,-0.562 0.36,-1.672 0.475,-2.219Z" style="fill:#050708;fill-rule:nonzero;"/><path d="M625.167,584.901c-3.198,-0.922 -0.086,-6.499 1.99,-6.743l0.101,0.792c-0.692,1.874 0.158,5.03 -2.091,5.951Z" style="fill:#050708;fill-rule:nonzero;"/><path d="M408.287,584.442c4.047,-2.616 0.564,4.799 0,0Z" style="fill:#050708;fill-rule:nonzero;"/><path d="M528.434,584.6c0.519,0.562 1.556,1.671 2.075,2.218c1.83,3.618 2.68,7.638 3.213,11.644c-0.504,1.195 -1.066,2.363 -1.686,3.516c-1.052,1.369 -2.147,2.739 -3.3,4.035c-2.219,1.296 -4.582,2.348 -7.032,3.084c-1.902,-0.029 -3.79,-0.116 -5.677,-0.274c-2.637,-1.096 -5.144,-2.493 -7.551,-4.02l-0.259,-0.591c0.158,-0.778 0.461,-2.349 0.62,-3.141c5,1.729 11.153,4.048 15.62,-0.216c5.26,-3.732 4.856,-10.605 3.977,-16.254Z" style="fill:#050708;fill-rule:nonzero;"/><path d="M560.223,595.319c0.259,2.276 0.505,4.553 0.606,6.859c-0.663,3.3 -1.527,6.557 -2.335,9.814c9.179,-0.015 18.473,-1.024 27.033,-4.54c4.336,-1.83 8.472,-4.063 12.507,-6.455c2.493,-0.159 5,-0.216 7.508,-0.376c-0.894,1.514 -1.773,3.027 -2.695,4.525c2.796,2.853 6.254,4.9 9.524,7.148c-0.173,-1.125 -0.331,-2.249 -0.475,-3.358c0.938,-1.081 1.889,-2.148 2.854,-3.214c2.607,1.153 7.277,1.441 6.627,5.419c0.707,4.452 -4.38,4.222 -7.305,5.014c-0.288,6.038 7.593,3.877 10.418,7.666c-4.279,4.943 -7.104,12.667 -1.628,17.897c-1.023,1.211 -2.047,2.421 -3.055,3.646c-4.799,-1.786 -10.203,-2.565 -13.805,-6.528c-0.49,1.946 -0.519,4.079 -1.542,5.85c-1.685,1.889 -4.309,0.418 -6.383,0.361c0.619,4.235 4.208,6.916 6.715,10.071c1.527,0.389 3.055,0.778 4.582,1.182c-2.003,6.629 6.469,9.237 6.902,15.361c2.19,6.816 6.44,12.896 7.507,20.101c5.246,21.974 -7.824,46.241 -28.546,54.814c-7.464,3.142 -16.326,4.338 -23.949,1.11c-6.484,-3.214 -11.887,-9.525 -12.392,-16.946c-0.98,-11.571 8.891,-22.754 20.505,-23.215c8.731,-0.547 17.205,5.058 20.331,13.214c4.958,-6.181 9.237,-13.43 9.454,-21.572c0.806,-9.135 -2.306,-17.998 -6.412,-26.009c2.954,7.811 6.181,15.837 5.432,24.381c0.158,7.392 -3.545,13.964 -7.435,19.973c-1.615,-1.685 -3.214,-3.358 -4.813,-5.03c10.62,-11.066 8.834,-28.66 1.081,-40.779c-2.882,3.675 -3.315,8.387 -4.712,12.681c-1.917,6.109 -5.505,11.643 -10.131,16.067c-0.244,0.202 -0.72,0.606 -0.951,0.822c5.52,1.915 11.484,4.265 14.238,9.856c-4.683,-2.017 -9.266,-4.914 -14.554,-4.712c-11.009,-0.764 -21.082,8.112 -23.185,18.646c-0.606,3.833 -0.159,7.71 -0.159,11.585c-3.545,-5.504 -4.668,-12.435 -2.867,-18.732c1.138,-4.683 4.496,-8.286 7.365,-11.989c-1.067,0.606 -2.105,1.254 -3.128,1.931c-3.876,-2.882 -7.982,-5.476 -11.455,-8.834c-0.303,-0.259 -0.908,-0.764 -1.211,-1.023c-7.349,1.643 -15.187,2.579 -22.205,-0.822c-0.259,-1.195 -0.778,-3.587 -1.038,-4.784c5.706,5.808 14.15,5.259 21.485,3.69c-0.246,-2.291 -0.505,-4.582 -0.749,-6.874c-0.707,-0.461 -2.148,-1.412 -2.867,-1.873c0.922,-5 1.21,-10.375 -2.017,-14.655c1.397,-0.244 2.794,-0.475 4.193,-0.692c0.764,1.211 1.527,2.436 2.306,3.66c1.109,-0.995 2.233,-1.988 3.372,-2.968c-3.747,-3.516 -7.277,-7.306 -9.9,-11.729c2.017,0.878 4.077,1.728 6.139,2.593c-0.98,-2.306 -1.96,-4.61 -2.926,-6.916l0.274,0.215c3.401,3.387 7.983,4.756 12.725,4.208c-1.426,3.069 -2.825,6.153 -3.919,9.367c2.493,-4.309 3.385,-9.957 8.371,-12.22c0.534,0.865 1.052,1.744 1.557,2.623c1.325,-3.66 2.882,-7.248 4.077,-10.952c1.557,-7.723 1.369,-16.326 -2.695,-23.301c-3.703,-2.334 -4.309,3.891 -5.23,6.269c-1.685,-4.178 -3.141,-8.444 -4.251,-12.81c-3.356,3.458 -6.469,7.248 -10.374,10.115c-3.3,1.975 -7.047,2.998 -10.606,4.38c-0.072,1.889 -0.13,3.791 -0.173,5.692c-3.098,1.426 -5.735,3.617 -8.689,5.289c-3.084,0.98 -6.355,1.067 -9.554,1.109c-4.582,-2.032 -8.026,-5.735 -11.254,-9.452c0.173,-1.672 0.346,-3.358 0.504,-5.03c-3.891,3.185 -6.729,9.021 -12.637,8.215c-5.101,0.013 -8.026,-4.569 -10,-8.604c-1.499,0.894 -2.983,1.788 -4.467,2.68c0.202,-0.993 0.605,-2.968 0.793,-3.962c1.182,-0.044 3.53,-0.145 4.698,-0.202c-0.519,-1.974 -1.023,-3.919 -1.542,-5.865c-0.086,-1.845 -0.158,-3.674 -0.245,-5.489c0.85,-0.13 2.536,-0.361 3.372,-0.477c-0.332,-4.366 -1.845,-8.487 -3.631,-12.435c2.075,2.508 3.948,5.159 5.75,7.868c1.585,2.377 3.343,4.641 5.216,6.816c2.003,2.133 4.222,4.064 6.571,5.836c2.147,1.399 4.381,2.68 6.7,3.804c3.862,1.744 8.012,2.68 12.205,3.214c17.551,1.959 35.563,-9.482 41.024,-26.312c0.663,-2.407 1.225,-4.857 1.801,-7.277c0.549,-1.557 1.14,-3.099 1.773,-4.626Zm35.377,13.963c-0.793,0.116 -2.349,0.345 -3.142,0.461c1.283,0.62 2.579,1.211 3.906,1.773c-2.882,3.76 -2.032,8.544 1.239,11.672c-4.121,1.57 -8.516,2.176 -12.868,1.254l-1.426,-0.749c-0.058,-1.283 -0.116,-2.551 -0.173,-3.82c-0.086,0.923 -0.274,2.739 -0.361,3.66c-5.058,-1.643 -8.703,-5.577 -13.199,-8.184c3.978,4.437 7.624,10.273 12.999,12.766c6.844,2.652 14.596,3.229 21.355,-0.057l-0.549,-0.116c5.274,-2.291 8.516,-9.006 5.491,-14.266c-1.917,-4.02 -6.902,-4.826 -10.793,-3.631c-0.576,-1.412 -1.268,-2.897 -2.032,-4.178c-0.101,0.85 -0.332,2.565 -0.446,3.415Zm-13.301,4.409c-0.835,2.262 -2.737,4.251 -3.659,0.734c-0.462,0.347 -1.384,1.067 -1.845,1.413c1.167,1.353 2.335,2.693 3.516,4.063c2.739,-2.723 5.908,-5.072 8.099,-8.285c-2.061,0.634 -4.093,1.325 -6.111,2.075Zm-25.908,3.069c5.028,4.611 9.972,9.351 13.876,15c2.205,3.747 4.15,7.651 5.922,11.629c1.6,4.856 2.594,9.856 3.17,14.928c-0.173,4.05 -0.606,8.084 -1.426,12.061c-0.908,2.017 -1.816,4.05 -2.724,6.082c-2.838,-3.646 -6.6,-7.148 -11.571,-6.975c-8.847,-0.648 -16.759,6.513 -17.55,15.231c0.446,2.781 0.951,5.577 1.614,8.329c5.201,10.721 20.346,13.863 29.8,7.018c6.6,-4.841 11.224,-12.479 12.766,-20.447c4.914,-16.889 -2.045,-34.655 -10.951,-48.965l-0.505,0.057c-3.646,-5.36 -8.213,-10.245 -13.675,-13.746c-2.897,-0.461 -5.85,-0.231 -8.747,-0.202Zm13.574,40.996c-1.023,2.853 -1.124,6.412 -0.072,9.28c1.974,-1.628 1.873,-7.536 0.072,-9.28Zm41.313,15.418l0.634,1.225l-2.867,0.721l-0.202,-0.534c1.557,7.148 4.409,14.165 3.805,21.643c0.36,12.58 -7.076,23.964 -16.659,31.529c0.433,-6.917 -3.516,-13.891 -10.231,-16.168c-10.994,-4.079 -23.185,5.201 -23.949,16.456c-1.166,10.433 9.065,19.367 19.137,18.66c22.565,-0.23 41.861,-22.45 40.275,-44.757c0.259,-10.418 -3.976,-20.418 -9.943,-28.776Z" style="fill:#050708;fill-rule:nonzero;"/><path d="M467.828,613.417c0.461,-0.417 1.355,-1.252 1.816,-1.671c2.435,1.023 4.669,2.407 6.859,3.905c-1.037,1.298 -2.06,2.579 -3.055,3.905c-1.873,-0.057 -3.746,-0.086 -5.605,-0.201l-0.519,-0.433c0.13,-1.845 0.288,-3.688 0.504,-5.505Z" style="fill:#050708;fill-rule:nonzero;"/><path d="M446.212,641.213c-2.666,3.458 -6.34,-2.132 -2.162,-3.343c0.533,0.837 1.614,2.508 2.162,3.343Z" style="fill:#050708;fill-rule:nonzero;"/><path d="M495.868,653.219c2.378,-2.177 4.424,-4.914 7.45,-6.21c2.651,-0.563 5.375,-0.448 8.069,-0.404c3.285,2.666 6.845,5.979 6.384,10.662c0.706,4.409 -2.723,7.58 -5.26,10.664c1.009,0.634 2.032,1.268 3.055,1.931c-0.418,0.663 -1.225,1.988 -1.628,2.651l-2.378,-1.643c-0.086,-0.057 -0.274,-0.173 -0.375,-0.23c-2.623,-1.816 -5.836,-1.614 -8.848,-1.441c-1.11,2.234 -2.219,4.481 -3.703,6.513c-3.156,-1.499 -5.865,-4.18 -7.104,-7.464l-0.144,-0.692c0.014,-1.197 0.029,-3.589 0.029,-4.784l0.101,-0.734c0.519,-0.519 1.571,-1.571 2.089,-2.091l0.504,-0.288l-0.058,-0.489l-0.576,-0.073c-0.893,-0.173 -2.666,-0.503 -3.559,-0.677l1.167,-1.195l0.591,-0.49c0.519,-0.345 1.556,-1.052 2.061,-1.412c1.009,0.547 2.032,1.109 3.041,1.671c-0.303,-1.267 -0.605,-2.521 -0.908,-3.775Z" style="fill:#050708;fill-rule:nonzero;"/><path d="M574.157,434.348c0.809,0.737 0.809,0.737 0,0Z" style="fill:#212122;fill-rule:nonzero;"/><path d="M597.169,445.833c0.854,0.694 0.854,0.694 0,0Z" style="fill:#212122;fill-rule:nonzero;"/><path d="M593.019,434.377c0.781,0.708 0.781,0.708 0,0Z" style="fill:#101010;fill-rule:nonzero;"/><path d="M610.57,446.105l0.044,0.103l0,0.103c0,-0.059 -0.029,-0.148 -0.044,-0.207Z" style="fill:#101010;fill-rule:nonzero;"/><path d="M609.36,455.632l0.361,0.275l-0.13,0.39c-0.057,-0.159 -0.174,-0.491 -0.231,-0.665Z" style="fill:#101010;fill-rule:nonzero;"/><path d="M608.724,434.32c4.323,-2.298 0.275,4.814 0,0Z" style="fill:#696c71;fill-rule:nonzero;"/><path d="M442.697,435.53c5.922,5.922 10.937,12.94 18.257,17.292c0.591,0.389 1.772,1.182 2.363,1.585l-0.605,1.052c-1.787,-0.173 -3.559,-0.476 -5.346,-0.519c-2.608,1.398 -3.934,4.28 -5.577,6.585c-7.003,-4.971 -13.387,-10.836 -19.107,-17.234c3.257,-3.012 6.6,-5.922 10.015,-8.761Z" style="fill:#506a83;fill-rule:nonzero;"/><path d="M548.667,435.703l0.015,0l0.159,0.247c-0.044,-0.058 -0.13,-0.189 -0.174,-0.247Z" style="fill:#111112;fill-rule:nonzero;"/><path d="M592.919,440.471l0.347,0.087l-0.13,0.145c-0.057,-0.058 -0.174,-0.173 -0.216,-0.231Z" style="fill:#111112;fill-rule:nonzero;"/><path d="M614.431,436.984c2.594,-1.758 5.706,-1.167 8.602,-0.677c0.029,5.447 -0.015,10.908 -0.418,16.37c-0.375,1.196 -0.764,2.392 -1.181,3.574c-0.591,0.375 -1.758,1.11 -2.349,1.47c-0.418,-2.594 -5.447,-2.248 -3.559,0.576c0.663,0.072 2.003,0.231 2.68,0.303c-0.993,0.951 -2.003,1.887 -2.996,2.825l-0.534,-0.519c-2.464,-3.084 -0.907,-7.45 -1.369,-11.11l0.015,-0.721l-0.692,-0.029l-0.043,0.721c-0.072,1.095 -0.202,3.285 -0.274,4.38c-1.125,-0.202 -3.387,-0.591 -4.51,-0.793c1.008,-1.571 2.017,-3.17 3.011,-4.77c-0.057,-0.562 -0.143,-1.715 -0.201,-2.277l0,-0.101l-0.044,-0.101l-0.936,-2.81l-0.62,-1.816c0.85,0.158 2.537,0.447 3.387,0.591l0.663,2.219l1.426,-0.072c-2.104,-2.205 -1.744,-4.914 -0.057,-7.234Z" style="fill:#496075;fill-rule:nonzero;"/><path d="M554.92,440.097c1.096,-1.311 2.176,-2.623 3.227,-3.963c3.027,6.398 8.978,10.476 14.209,14.929c3.804,4.568 8.487,8.329 12.004,13.156c-10.693,-6.917 -19.367,-16.398 -29.44,-24.122Z" style="fill:#1d1e1e;fill-rule:nonzero;"/><path d="M643.495,451.612l0.317,-0.576c1.397,1.11 2.594,2.406 3.587,3.905c-0.316,0.331 -0.951,1.009 -1.267,1.34c-0.677,-0.259 -2.032,-0.764 -2.709,-1.023c0.029,-0.908 0.057,-2.738 0.072,-3.646Z" style="fill:#1d1e1e;fill-rule:nonzero;"/><path d="M492.077,489.178l0.605,1.643c1.499,4.323 4.179,8.617 8.675,10.245c1.787,0.49 3.602,0.907 5.433,1.267c-0.115,1.226 -0.259,2.436 -0.375,3.675c-3.127,-1.311 -6.542,-1.47 -9.871,-1.456c-6.095,-0.375 -12.047,-1.757 -18.012,-2.895c-6.067,-1.11 -11.917,-3.041 -17.825,-4.655c1.7,-0.606 3.43,-1.167 5.159,-1.7c7.911,3.026 16.312,4.222 24.612,5.678c2.493,0.677 5.043,0.979 7.623,1.166c-2.19,-2.579 -4.467,-5.1 -6.701,-7.637c0.173,-1.34 0.519,-3.991 0.677,-5.331Z" style="fill:#1d1e1e;fill-rule:nonzero;"/><path d="M619.851,507.765c0.576,-0.259 1.729,-0.764 2.291,-1.008c1.974,1.628 2.291,4.611 -0.707,5.274c-0.432,-1.311 -2.954,-3.012 -1.584,-4.266Z" style="fill:#1d1e1e;fill-rule:nonzero;"/><path d="M515.307,552.263c-2.133,-15.721 4.481,-32.177 17.076,-41.861c7.594,2.781 15.894,1.096 23.575,3.559c18.343,3.934 34.31,16.211 44.367,31.86c3.747,5.188 3.559,11.874 5.419,17.783c-2.998,7.896 -8.07,14.985 -14.626,20.331c-8.272,6.774 -18.791,9.784 -29.223,11.053c-2.478,3.545 4.323,2.162 5.836,3.559l0.015,0.244c-2.652,-0.835 -4.352,-0.375 -5.116,1.426c1.037,1.485 3.343,1.14 4.942,1.701c-2.247,0.259 -4.509,0.23 -6.743,0.259c-0.101,-2.306 -0.347,-4.582 -0.606,-6.859c-0.634,1.527 -1.225,3.069 -1.772,4.625c0.015,-0.907 0.072,-2.723 0.101,-3.631c0.028,-2.247 0,-4.496 -0.116,-6.743c-0.36,-2.263 -0.764,-4.496 -1.34,-6.7c-0.36,-0.923 -1.052,-2.768 -1.399,-3.675c-1.037,-2.464 -1.368,-5.116 -1.34,-7.752c3.761,5.865 6.01,12.522 7.365,19.323c1.166,-0.129 3.516,-0.404 4.683,-0.533c1.181,-0.187 3.545,-0.549 4.726,-0.736c1.96,-0.446 3.906,-0.979 5.85,-1.599c5.52,-2.205 10.39,-5.75 14.669,-9.842c3.473,-3.646 8.314,-8.387 6.139,-13.891c-2.508,-7.796 -6.974,-14.827 -12.061,-21.183c-1.83,-2.407 -4.554,-3.804 -7.191,-5.144c-3.141,-5.202 -8.386,-8.733 -14.309,-9.972c-9.755,-6.772 -22.435,-6.499 -33.776,-5.779c-7.118,0.375 -9.323,8.602 -11.067,14.223c-1.124,5.216 -1.542,10.561 -1.917,15.879l-2.161,0.072Z" style="fill:#1d1e1e;fill-rule:nonzero;"/><path d="M641.925,549.727l0.23,1.298c-0.057,1.946 -0.619,3.775 -1.656,5.491c1.656,0.936 3.429,1.555 5.259,2.132l0.446,1.47c-2.594,-0.505 -5.173,-0.951 -7.767,-1.384c1.167,-2.996 2.377,-5.979 3.488,-9.006Z" style="fill:#1d1e1e;fill-rule:nonzero;"/><path d="M548.479,603.432l0.806,0.505l0.317,1.096c-2.104,6.728 -7.292,11.657 -12.09,16.527c-0.49,-0.519 -0.966,-1.023 -1.441,-1.542c1.628,-1.254 3.214,-2.535 4.784,-3.862c1.283,-1.383 2.493,-2.867 3.646,-4.366c0.461,-0.808 1.399,-2.436 1.859,-3.257c0.764,-1.672 1.412,-3.401 2.118,-5.101Z" style="fill:#1d1e1e;fill-rule:nonzero;"/><path d="M555.25,633.578c1.296,1.988 2.55,4.035 3.775,6.095c-0.72,2.003 -1.412,4.035 -2.089,6.067c-5.331,-4.338 -15.332,-3.905 -16.845,4.035c0.244,1.412 0.547,2.824 0.907,4.237c0.275,0.663 0.808,1.974 1.081,2.622c0.966,2.306 1.946,4.611 2.926,6.917c-2.061,-0.865 -4.121,-1.715 -6.139,-2.594c-0.648,-0.49 -1.917,-1.456 -2.551,-1.931c0.879,-0.591 1.773,-1.167 2.667,-1.729c-0.073,-4.424 0.057,-8.832 0.331,-13.228c3.141,-1.555 6.398,-2.867 9.741,-3.934c1.917,0.231 3.833,0.433 5.75,0.62c0.404,0.576 1.211,1.729 1.615,2.306c-0.477,-3.156 -0.894,-6.311 -1.167,-9.482Z" style="fill:#1d1e1e;fill-rule:nonzero;"/><path d="M576.737,437.028c4.784,0.043 -2.068,4.423 0,0Z" style="fill:#363739;fill-rule:nonzero;"/><path d="M470.364,627.366c0.824,0.738 0.824,0.738 0,0Z" style="fill:#080808;fill-rule:nonzero;"/><path d="M596.046,436.626c0.345,0.332 1.023,1.009 1.355,1.355l0.389,2.234c1.656,-1.225 3.328,-2.435 5.058,-3.574c-0.549,1.902 -1.801,3.415 -3.2,4.77c0.591,1.527 1.182,3.055 1.788,4.582c-1.889,-2.637 -3.891,-5.259 -6.5,-7.234c0.288,-0.533 0.837,-1.599 1.11,-2.133Z" style="fill:#222;fill-rule:nonzero;"/><path d="M522.438,574.455c7.378,-1.47 14.842,0.288 22.277,-0.072l0.519,0.692c1.225,1.772 2.377,3.602 3.444,5.489c2.651,5.52 3.3,11.787 2.464,17.825c-0.23,1.254 -0.677,3.761 -0.907,5.03c-0.375,-2.19 -0.736,-4.382 -0.879,-6.586c-0.173,-4.423 0.202,-9.524 -3.099,-12.983c-6.08,-3.617 -13.473,-2.104 -20.173,-2.636c-7.652,-3.243 -17.306,-0.088 -20.087,8.112c-0.418,0.505 -1.239,1.527 -1.657,2.032c-4.107,4.336 -8.819,8.573 -10.779,14.409c-0.994,2.767 1.081,5.289 2.363,7.565c2.06,2.407 4.352,4.626 6.845,6.572c2.723,1.513 5.533,2.867 8.444,4.006c1.124,0.173 3.343,0.547 4.467,0.72c1.066,0.058 3.213,0.187 4.294,0.246c1.182,-0.086 3.53,-0.246 4.712,-0.332c3.804,-0.764 7.392,-2.291 10.923,-3.905c-0.303,0.634 -0.908,1.902 -1.211,2.55c-3.4,1.052 -6.772,2.162 -10.26,2.854c-8.026,0.475 -16.283,-0.677 -23.113,-5.173c-1.917,-1.312 -3.775,-2.724 -5.533,-4.222c-2.795,-2.867 -4.928,-6.298 -6.528,-9.944l-0.259,-0.792c1.081,-4.294 1.311,-8.964 3.559,-12.868c7.162,-9.799 18.372,-16.211 30.174,-18.589Z" style="fill:#222;fill-rule:nonzero;"/><path d="M576.894,601.617c0.98,-0.015 2.926,-0.044 3.891,-0.058c-0.029,1.889 0.331,4.121 -1.946,4.799c-1.383,-1.225 -1.571,-3.069 -1.946,-4.74Z" style="fill:#222;fill-rule:nonzero;"/><path d="M611.507,437.561c0.825,0.781 0.825,0.781 0,0Z" style="fill:#7d8086;fill-rule:nonzero;"/><path d="M587.053,438.526c0.766,0.752 0.766,0.752 0,0Z" style="fill:#c9d8e5;fill-rule:nonzero;"/><path d="M658.643,438.197c6.268,0.303 16.037,-0.259 17.55,7.825c-2.895,-0.778 -5.706,-1.787 -8.632,-2.363c-2.521,2.349 -4.74,5 -7.262,7.349c-0.274,-4.265 2.638,-9.741 -1.656,-12.81Z" style="fill:#4b6fb2;fill-rule:nonzero;"/><path d="M584.288,441.438c0.78,0.795 0.78,0.795 0,0Z" style="fill:#0f0f0f;fill-rule:nonzero;"/><path d="M588.841,441.235c2.421,-0.922 4.799,2.853 3.486,4.87c-1.887,-0.115 -3.933,-3.026 -3.486,-4.87Z" style="fill:#5b5d61;fill-rule:nonzero;"/><path d="M581.319,443.08c0.723,0.752 0.723,0.752 0,0Z" style="fill:#424346;fill-rule:nonzero;"/><path d="M594.258,442.737c4.206,-2.428 0.462,4.799 0,0Z" style="fill:#bcc7d0;fill-rule:nonzero;"/><path d="M537.816,444.088c5.735,3.17 11.585,6.168 17.579,8.833c-1.311,2.637 -2.608,5.274 -3.919,7.926c-5.807,-1.902 -11.615,-3.862 -17.249,-6.254c1.138,-3.53 2.335,-7.018 3.589,-10.505Z" style="fill:#f49426;fill-rule:nonzero;"/><path d="M625.702,444.046c1.902,-0.058 3.818,-0.115 5.75,-0.173c-0.159,2.147 -0.36,4.28 -0.606,6.427c-0.606,4.669 -1.225,9.352 -2.089,13.991c-1.7,0.101 -3.387,0.231 -5.072,0.347c1.195,-6.802 1.988,-13.675 2.017,-20.592Z" style="fill:#435566;fill-rule:nonzero;"/><path d="M659.204,454.694c2.493,-3.113 5.144,-6.11 7.867,-9.035c-0.663,4.654 -1.311,9.352 -2.939,13.79c-1.786,4.625 -3.775,9.179 -5.893,13.675c-1.83,3.848 -3.704,7.709 -6.139,11.211c-1.096,-1.138 -2.177,-2.263 -3.257,-3.387c4.265,-8.386 8.257,-17.032 10.361,-26.254Z" style="fill:#435566;fill-rule:nonzero;"/><path d="M640.715,500.907c3.732,-4.712 6.643,-10.001 10.159,-14.857c4.496,-0.231 8.516,1.83 12.494,3.631c-0.274,0.404 -0.822,1.21 -1.096,1.614c-3.587,2.392 -5.85,6.067 -8.184,9.568c-0.317,0.303 -0.938,0.908 -1.254,1.211c-1.384,-1.153 -2.84,-2.205 -4.38,-3.113c-2.638,0.418 -5.173,1.254 -7.739,1.946Z" style="fill:#435566;fill-rule:nonzero;"/><path d="M500.365,445.875c1.715,0.216 3.43,0.432 5.159,0.663c3.458,4.957 6.513,10.173 9.467,15.433c-5.447,2.579 -10.995,5 -15.995,8.43c-6.7,-3.516 -12.911,-7.868 -19.208,-12.047c1.715,-1.657 3.415,-3.329 5.144,-4.971l0.259,-0.259c2.118,1.455 4.28,2.868 6.513,4.164c0.101,-2.075 0.216,-4.136 0.332,-6.196l-2.219,-0.014l1.095,-1.081c0.447,-0.418 1.34,-1.239 1.787,-1.643c0.086,0.504 0.274,1.499 0.375,2.003c-0.014,2.565 -0.072,5.159 0.346,7.695c0.159,0.403 0.476,1.21 0.634,1.614c2.551,1.614 5.389,2.724 8.257,3.689c-0.965,-1.658 -1.902,-3.3 -2.925,-4.886c-1.398,-2.003 -2.896,-3.919 -4.438,-5.793c1.772,-2.291 3.588,-4.553 5.418,-6.801Z" style="fill:#394653;fill-rule:nonzero;"/><path d="M667.071,445.659c4.424,0.043 8.589,1.671 12.767,2.954c-1.384,5.692 -3.227,11.24 -4.971,16.816c-3.429,-2.262 -7.018,-4.25 -10.735,-5.98c1.628,-4.438 2.276,-9.136 2.939,-13.79Z" style="fill:#5b7fa3;fill-rule:nonzero;"/><path d="M594.088,447.287c4.784,0.188 -2.182,4.351 0,0Z" style="fill:#585a5e;fill-rule:nonzero;"/><path d="M607.4,447.446c0.796,0.781 0.796,0.781 0,0Z" style="fill:#0d0d0d;fill-rule:nonzero;"/><path d="M440.146,500.908c0.795,0.765 0.795,0.765 0,0Z" style="fill:#0d0d0d;fill-rule:nonzero;"/><path d="M536.82,664.92c2.176,-0.576 5.23,1.729 5.144,4.02c-2.104,0.345 -5.432,-1.744 -5.144,-4.02Z" style="fill:#0d0d0d;fill-rule:nonzero;"/><path d="M590.108,449.984c4.235,-2.4 0.42,4.77 0,0Z" style="fill:#a1a7ad;fill-rule:nonzero;"/><path d="M604.49,450.084c0.781,0.766 0.781,0.766 0,0Z" style="fill:#1e1e1f;fill-rule:nonzero;"/><path d="M547.902,634.874c1.98,-0.651 2.732,0.044 2.27,2.096c-1.995,0.651 -2.747,-0.058 -2.27,-2.096Z" style="fill:#1e1e1f;fill-rule:nonzero;"/><path d="M612.588,449.766l0.721,0.029c0.461,3.66 -1.096,8.026 1.369,11.109l0.533,0.519c0.303,0.303 0.908,0.908 1.197,1.21c-0.576,-0.057 -1.758,-0.173 -2.349,-0.23c-4.668,-1.845 -2.464,6.844 -7.003,4.178c0.303,-3.545 1.542,-6.902 2.537,-10.288l0.129,-0.389l-0.36,-0.274c-0.835,-0.634 -2.493,-1.931 -3.314,-2.565l1.757,0.288c1.125,0.202 3.387,0.591 4.51,0.793c0.073,-1.095 0.202,-3.285 0.274,-4.381Z" style="fill:#fffcfd;fill-rule:nonzero;"/><path d="M628.754,464.291c0.865,-4.64 1.485,-9.323 2.091,-13.991c0.475,6.167 6.642,8.314 11.512,10.274c-0.028,0.936 -0.072,2.796 -0.086,3.732c-0.114,1.672 -0.259,3.343 -0.446,5.015c-0.475,-4.05 -3.776,0.158 -0.475,1.094l0.519,0.677c-4.943,0.058 -8.791,-4.063 -13.834,-3.486c0.173,-0.837 0.533,-2.493 0.72,-3.315Z" style="fill:#4e6880;fill-rule:nonzero;"/><path d="M493.073,450.356c0.461,0.576 1.412,1.744 1.873,2.32c1.542,1.873 3.041,3.79 4.438,5.793c-1.499,-0.101 -4.481,-0.317 -5.966,-0.418c-0.418,-2.536 -0.36,-5.13 -0.346,-7.695Z" style="fill:#83a6c6;fill-rule:nonzero;"/><path d="M595.857,451.567c0.708,0.766 0.708,0.766 0,0Z" style="fill:#444649;fill-rule:nonzero;"/><path d="M601.279,451.481c4.741,0.246 -2.211,4.365 0,0Z" style="fill:#46484b;fill-rule:nonzero;"/><path d="M555.395,452.923c5.491,2.507 10.851,5.274 16.182,8.113c-0.865,2.319 -1.729,4.654 -2.579,6.989c-5.676,-2.781 -11.527,-5.202 -17.522,-7.176c1.311,-2.652 2.608,-5.289 3.919,-7.926Z" style="fill:#f27e25;fill-rule:nonzero;"/><path d="M648.743,452.174c2.983,0.749 5.88,1.902 7.912,4.337c-1.658,5.057 -3.358,10.102 -5.562,14.957c-2.19,-1.138 -4.367,-2.247 -6.542,-3.372c-0.057,-0.922 -0.202,-2.737 -0.274,-3.66c2.594,-1.786 3.574,-5.446 1.859,-8.155c0.316,-0.331 0.951,-1.009 1.267,-1.34c0.332,-0.692 0.995,-2.075 1.34,-2.767Z" style="fill:#516c87;fill-rule:nonzero;"/><path d="M457.365,454.939c1.787,0.043 3.559,0.346 5.346,0.519l0.605,-1.052c9.741,7.133 19.179,14.698 29.54,20.952c-1.196,2.104 -3.069,4.006 -3.43,6.455c1.066,1.124 2.19,2.205 3.358,3.257c-0.072,1.917 -0.115,3.833 -0.101,5.75l-0.605,-1.643c-0.072,-0.806 -0.231,-2.407 -0.303,-3.213c-0.62,0.173 -1.844,0.49 -2.464,0.648c-1.599,-2.32 -3.257,-4.597 -4.928,-6.844c-0.447,0.576 -1.34,1.729 -1.787,2.306c-1.945,-1.889 -3.934,-3.761 -6.066,-5.433c-2.45,-1.094 -5.029,-1.873 -7.695,-2.19c-4.971,-5.245 -11.946,-7.91 -17.047,-12.926c1.643,-2.306 2.968,-5.187 5.577,-6.585Z" style="fill:#3f4f5e;fill-rule:nonzero;"/><path d="M643.425,455.257c0.677,0.259 2.032,0.764 2.709,1.023c1.715,2.709 0.734,6.369 -1.859,8.155c-0.503,-0.028 -1.498,-0.101 -2.003,-0.129c0.015,-0.936 0.058,-2.796 0.086,-3.732c0.332,-1.773 0.677,-3.545 1.067,-5.318Z" style="fill:#b7d2eb;fill-rule:nonzero;"/><path d="M600.253,457.448c0.709,0.752 0.709,0.752 0,0Z" style="fill:#afb4ba;fill-rule:nonzero;"/><path d="M619.088,457.721c0.591,-0.36 1.757,-1.095 2.348,-1.47c-0.979,10.332 -3.299,20.433 -5.188,30.606c-2.291,0.202 -2.146,4.395 0.317,2.911c0.173,-0.519 0.547,-1.586 0.734,-2.104c2.911,-7.436 5.116,-15.13 6.384,-23.027c1.685,-0.116 3.372,-0.244 5.072,-0.345c-0.187,0.821 -0.547,2.478 -0.721,3.314c-0.951,4.121 -1.858,8.242 -2.809,12.378c-0.793,2.983 -1.527,6.196 -3.949,8.357c-0.576,0.015 -1.744,0.044 -2.32,0.058c0.576,3.17 -4.063,7.752 1.009,8.718c-0.404,0.936 -1.197,2.796 -1.6,3.732c-1.786,-1.687 -3.53,-3.415 -5.245,-5.159l1.153,-0.778c2.81,-0.749 1.21,-4.309 -1.268,-3.328c-2.118,-2.379 -2.781,-5.678 -3.343,-8.718c1.672,-5.303 2.954,-10.708 4.294,-16.081l0,-0.778c0.029,-0.908 0.073,-2.695 0.101,-3.603c0.591,0.058 1.773,0.173 2.349,0.231c-0.288,-0.303 -0.894,-0.908 -1.197,-1.21c0.995,-0.938 2.003,-1.874 2.998,-2.825l0.879,-0.879Z" style="fill:#36424d;fill-rule:nonzero;"/><path d="M494.052,459.666c-0.159,-0.403 -0.476,-1.21 -0.634,-1.614c1.484,0.101 4.467,0.317 5.966,0.418c1.023,1.585 1.96,3.228 2.925,4.884c-2.868,-0.966 -5.706,-2.075 -8.257,-3.688Z" style="fill:#151719;fill-rule:nonzero;"/><path d="M484.613,492.519c1.254,-2.148 7.248,-1.369 4.179,1.296c-1.052,-0.331 -3.141,-0.979 -4.179,-1.296Z" style="fill:#151719;fill-rule:nonzero;"/><path d="M602.992,460.257c0.726,0.741 0.726,0.741 0,0Z" style="fill:#48494c;fill-rule:nonzero;"/><path d="M658.236,473.125c2.118,-4.496 4.108,-9.049 5.894,-13.675c3.717,1.729 7.306,3.717 10.735,5.981c-1.167,2.622 -2.133,5.403 -3.89,7.709c-1.096,0.865 -2.234,1.685 -3.344,2.522c-0.259,0.764 -0.806,2.306 -1.065,3.084c-0.475,-0.793 -1.413,-2.349 -1.874,-3.128c-2.161,-0.835 -4.309,-1.671 -6.456,-2.493Z" style="fill:#53718d;fill-rule:nonzero;"/><path d="M571.576,461.036c5.491,3.084 10.794,6.484 15.866,10.231c-0.778,2.104 -1.542,4.222 -2.291,6.341c-5.101,-3.646 -10.548,-6.801 -16.154,-9.583c0.85,-2.335 1.715,-4.668 2.579,-6.989Z" style="fill:#f16624;fill-rule:nonzero;"/><path d="M599.794,461.813c4.785,0.13 -2.154,4.364 0,0Z" style="fill:#585b5f;fill-rule:nonzero;"/><path d="M421.167,463.514c1.917,1.6 3.819,3.229 5.706,4.885c2.954,-1.498 5.908,-2.954 8.92,-4.336c-1.398,2.895 -2.925,5.735 -3.991,8.775c1.816,2.666 4.179,4.9 6.398,7.233c-0.692,0.044 -2.075,0.13 -2.767,0.189c-2.19,-0.26 -4.366,-0.477 -6.542,-0.664c-2.306,2.623 -4.309,5.491 -6.34,8.329c-0.303,-1.959 -0.548,-3.933 -0.778,-5.893c-0.115,-0.908 -0.331,-2.752 -0.447,-3.66c-1.859,-0.835 -3.761,-1.57 -5.62,-2.377l0.418,-1.34c1.571,-1.096 3.415,-1.83 4.813,-3.156c0.245,-2.666 0.187,-5.331 0.231,-7.983Zm0.893,14.77c0.245,2.335 0.49,4.668 0.749,7.018c1.859,-1.83 3.314,-3.978 4.496,-6.283c2.666,0.073 5.332,0.145 7.997,0.216c-1.989,-2.622 -3.977,-5.245 -5.706,-8.041c-3.199,1.557 -5.548,4.208 -7.536,7.09Z" style="fill:#b9cce7;fill-rule:nonzero;"/><path d="M435.793,464.061c1.196,1.498 2.392,2.983 3.646,4.452c2.464,2.926 5.188,5.678 8.214,8.041c-1.427,1.153 -2.853,2.335 -4.265,3.502c0.476,1.715 0.951,3.444 1.412,5.173c-3.055,-1.788 -6.153,-3.488 -9.366,-4.971c0.692,-0.058 2.075,-0.145 2.767,-0.187c-2.219,-2.335 -4.582,-4.569 -6.398,-7.234c1.066,-3.04 2.594,-5.88 3.991,-8.775Z" style="fill:#5d81a7;fill-rule:nonzero;"/><path d="M540.091,649.775c7.493,0 15.001,0.187 22.495,0.648c-0.865,1.584 -1.816,3.126 -2.796,4.668c-6.254,-0.518 -12.508,-1.397 -18.791,-1.08c-0.36,-1.413 -0.663,-2.825 -0.908,-4.237Z" style="fill:#5d81a7;fill-rule:nonzero;"/><path d="M642.272,464.306c0.505,0.029 1.498,0.101 2.003,0.13c0.072,0.922 0.216,2.737 0.274,3.659c-0.505,0.764 -1.542,2.291 -2.06,3.069l-0.62,-0.072l-0.518,-0.677l0.475,-1.094c0.187,-1.672 0.331,-3.343 0.446,-5.015Z" style="fill:#131516;fill-rule:nonzero;"/><path d="M443.044,465.935c0.694,0.781 0.694,0.781 0,0Z" style="fill:#3a3a3b;fill-rule:nonzero;"/><path d="M448.806,465.992c0.694,0.794 0.694,0.794 0,0Z" style="fill:#424142;fill-rule:nonzero;"/><path d="M517.282,466.87c3.084,-0.648 6.211,-1.109 9.352,-1.412c0.144,2.421 0.274,4.841 0.389,7.277c-2.853,0.606 -5.677,1.296 -8.502,1.959c-0.403,-2.622 -0.821,-5.23 -1.239,-7.824Z" style="fill:#34404b;fill-rule:nonzero;"/><path d="M526.633,465.458c3.934,-0.244 7.882,-0.086 11.787,0.505c-0.187,1.239 -0.591,3.703 -0.778,4.942c-3.559,0.563 -7.103,1.096 -10.619,1.83c-0.115,-2.434 -0.245,-4.856 -0.389,-7.277Z" style="fill:#22282d;fill-rule:nonzero;"/><path d="M613.164,466.035l0.794,-0.028l0,0.78l-0.78,0.015l-0.015,-0.766Z" style="fill:#fef4f8;fill-rule:nonzero;"/><path d="M502.094,474.795c4.525,-3.53 9.64,-6.368 15.188,-7.925c0.418,2.594 0.836,5.202 1.239,7.824c-3.141,0.764 -5.533,2.983 -7.853,5.087c2.392,1.614 4.798,3.229 7.205,4.828c0.403,2.32 0.778,4.639 1.182,6.974c-0.965,0.778 -1.902,1.614 -2.81,2.464c-1.657,1.586 -3.256,3.214 -4.942,4.756c-2.291,-8.286 -5.908,-16.096 -9.208,-24.008Z" style="fill:#516b86;fill-rule:nonzero;"/><path d="M628.035,467.606c5.044,-0.575 8.892,3.545 13.834,3.488l0.619,0.072c0.519,-0.778 1.557,-2.306 2.061,-3.069c2.176,1.125 4.352,2.234 6.542,3.372c-2.335,4.914 -4.828,9.77 -7.739,14.396c-2.565,-1.96 -5.129,-3.89 -7.651,-5.894c0.677,2.695 1.931,5.692 -0.49,7.926c-1.599,-4.54 -5.489,-6.989 -9.985,-7.912c0.951,-4.136 1.858,-8.257 2.809,-12.378Zm12.538,5.303c-1.758,0.835 -3.603,3.17 -2.709,5.173c2.377,1.586 5.605,-4.567 2.709,-5.173Z" style="fill:#475c70;fill-rule:nonzero;"/><path d="M453.633,494.853c2.32,0.822 4.698,1.485 7.075,2.148c5.908,1.614 11.758,3.545 17.825,4.654c-2.248,2.004 -4.222,4.295 -5.922,6.788l-0.922,-0.101c-4.597,-0.534 -9.179,-1.226 -13.79,-1.715l-0.245,-0.044c-2.623,-1.037 -5.187,-2.161 -7.781,-3.227c-0.764,-0.303 -2.306,-0.923 -3.084,-1.239c4.352,0.086 6.96,-3.142 6.845,-7.264Z" style="fill:#475c70;fill-rule:nonzero;"/><path d="M641.354,470.424c-3.31,-0.939 0,-5.16 0.477,-1.098l-0.477,1.098Z" style="fill:#bacedf;fill-rule:nonzero;"/><path d="M439.441,468.513c2.551,1.052 4.871,2.579 7.075,4.237c0.965,-0.952 1.931,-1.889 2.911,-2.84c0.576,0.808 1.7,2.421 2.277,3.214c1.052,-0.865 2.118,-1.744 3.184,-2.608c1.974,1.786 3.934,3.602 5.836,5.461c-0.821,2.521 -1.585,5.807 2.205,5.979c-0.187,0.606 -0.562,1.83 -0.749,2.436c-0.418,-0.015 -1.254,-0.057 -1.671,-0.072c-4.568,-2.104 -8.833,-4.784 -12.854,-7.767c-3.026,-2.364 -5.75,-5.116 -8.214,-8.041Zm9.41,6.038c0.706,0.764 0.706,0.764 0,0Zm2.781,2.996c0.764,0.721 0.764,0.721 0,0Z" style="fill:#39383a;fill-rule:nonzero;"/><path d="M445.823,468.902c0.766,0.737 0.766,0.737 0,0Z" style="fill:#414143;fill-rule:nonzero;"/><path d="M463.274,474.551c0.723,0.752 0.723,0.752 0,0Z" style="fill:#414143;fill-rule:nonzero;"/><path d="M476.53,476.64c2.133,1.672 4.121,3.545 6.067,5.432c0.447,-0.576 1.34,-1.729 1.787,-2.306c1.672,2.249 3.329,4.525 4.928,6.845c0.62,-0.158 1.844,-0.475 2.464,-0.648c0.072,0.806 0.231,2.407 0.303,3.213c-0.159,1.34 -0.504,3.992 -0.677,5.333c-0.648,-0.174 -1.96,-0.519 -2.608,-0.692c3.069,-2.666 -2.925,-3.444 -4.179,-1.298c-2.363,-0.778 -4.77,-1.426 -7.162,-2.06c-3.069,-0.808 -6.124,-1.687 -9.049,-2.897c-1.066,-2.06 -2.104,-4.121 -3.127,-6.196c1.239,1.008 2.478,2.032 3.718,3.055c2.493,-2.608 5.015,-5.202 7.536,-7.782Z" style="fill:#414143;fill-rule:nonzero;"/><path d="M615.73,540.189c0.215,1.571 0.402,3.156 0.562,4.755c0,2.508 -0.13,5.03 -0.361,7.537c-0.244,2.031 -0.59,4.035 -0.878,6.08c0.764,-0.505 2.262,-1.513 3.026,-2.017c0.778,0.418 2.32,1.268 3.084,1.7c0.317,1.167 0.648,2.349 0.98,3.545c-0.995,0.389 -1.99,0.793 -2.968,1.197c-1.615,-1.397 -3.214,-2.781 -4.828,-4.165c-0.317,0.995 -0.966,2.983 -1.296,3.992c-1.931,3.646 -3.66,7.392 -4.842,11.354c-1.484,2.926 -3.242,5.72 -5.591,8.013c-6.412,4.927 -12.651,10.317 -20.26,13.3c-3.976,1.469 -8.285,3.646 -12.595,2.348c3.113,-2.348 7.335,-2.478 10.866,-4.05c14.64,-5.561 25.318,-18.646 29.93,-33.387c0.417,0.418 1.252,1.268 1.671,1.685c2.061,-7.176 0.317,-14.985 3.502,-21.888Z" style="fill:#414143;fill-rule:nonzero;"/><path d="M582.298,613.693c2.017,-0.749 4.05,-1.441 6.109,-2.075c-2.19,3.213 -5.36,5.561 -8.098,8.285c-1.182,-1.369 -2.349,-2.709 -3.516,-4.064c0.461,-0.345 1.383,-1.065 1.843,-1.412c0.923,3.517 2.825,1.527 3.66,-0.734Z" style="fill:#414143;fill-rule:nonzero;"/><path d="M451.602,468.845c0.752,0.723 0.752,0.723 0,0Z" style="fill:#2e2f30;fill-rule:nonzero;"/><path d="M457.41,468.917c0.824,0.649 0.824,0.649 0,0Z" style="fill:#434243;fill-rule:nonzero;"/><path d="M605.571,469.787c0.015,-0.246 0.015,-0.709 0.015,-0.955l-0.015,0.955Z" style="fill:#404245;fill-rule:nonzero;"/><path d="M605.586,468.833c2.891,-2.63 2.776,3.658 -0.015,0.955l0.015,-0.955Z" style="fill:#b3afb5;fill-rule:nonzero;"/><path d="M678.759,468.383c1.138,2.047 2.233,4.107 3.314,6.196c1.816,0.677 3.617,1.327 5.447,2.003c-1.023,1.413 -2.032,2.825 -3.026,4.237c-2.162,-1.21 -4.468,-2.132 -6.816,-2.867l-1.211,0.433c-0.576,0.187 -1.728,0.576 -2.304,0.778c-2.219,-1.096 -4.367,-2.335 -6.528,-3.502c1.109,-0.835 2.247,-1.658 3.343,-2.522l-0.216,1.687c1.384,-0.044 4.178,-0.116 5.577,-0.145c0.793,-2.104 1.599,-4.208 2.421,-6.297Z" style="fill:#c5d5ec;fill-rule:nonzero;"/><path d="M460.391,471.77c0.781,0.708 0.781,0.708 0,0Z" style="fill:#434244;fill-rule:nonzero;"/><path d="M587.442,471.265c10.937,8.604 21.529,18.575 27.956,31.068c-1.744,-1.816 -3.502,-3.617 -5.333,-5.331c-4.639,-4.496 -9.985,-8.156 -15.562,-11.398c-0.576,-0.389 -1.758,-1.197 -2.335,-1.586c-2.334,-2.161 -4.553,-4.423 -7.018,-6.412c0.75,-2.118 1.514,-4.235 2.291,-6.341Z" style="fill:#ef4823;fill-rule:nonzero;"/><path d="M531.995,472.866c4.496,1.658 5.995,9.525 11.657,6.759c-0.143,1.181 -0.432,3.559 -0.562,4.74c-2.68,0.923 -5.346,1.859 -7.94,2.998c-0.173,-1.124 -0.518,-3.387 -0.692,-4.51c0.98,-0.404 2.955,-1.21 3.934,-1.614c-0.634,-0.879 -1.267,-1.744 -1.902,-2.608c-1.498,-1.931 -3.04,-3.818 -4.496,-5.764Z" style="fill:#8d8d8e;fill-rule:nonzero;"/><path d="M652.099,484.334c2.436,-3.501 4.309,-7.363 6.139,-11.211c2.148,0.822 4.295,1.658 6.456,2.493c0.461,0.778 1.397,2.335 1.873,3.128c-0.316,1.109 -0.936,3.358 -1.252,4.481c-0.576,2.176 -1.197,4.338 -1.946,6.456c-3.978,-1.802 -7.998,-3.862 -12.494,-3.632l1.225,-1.715Z" style="fill:#4b6278;fill-rule:nonzero;"/><path d="M413.833,476.539c0.749,-0.649 1.513,-1.268 2.291,-1.889l-0.418,1.34c1.859,0.808 3.761,1.542 5.62,2.377c0.115,0.908 0.331,2.753 0.447,3.66c-2.867,-1.498 -5.447,-3.444 -7.94,-5.489Z" style="fill:#4d5b6a;fill-rule:nonzero;"/><path d="M462.927,481.959c0.677,-3.328 2.161,-6.845 5.908,-7.508c2.666,0.317 5.245,1.096 7.695,2.19c-2.522,2.579 -5.043,5.173 -7.536,7.782c-1.239,-1.023 -2.478,-2.047 -3.718,-3.055c1.023,2.075 2.06,4.136 3.127,6.196c-2.089,-1.037 -4.121,-2.176 -6.225,-3.17c0.187,-0.604 0.562,-1.83 0.749,-2.434Z" style="fill:#212021;fill-rule:nonzero;"/><path d="M510.667,479.781c2.32,-2.104 4.712,-4.323 7.853,-5.087c2.94,0.145 6.153,-0.158 8.761,1.47c2.94,1.355 5.764,4.626 9.208,2.464c0.635,0.865 1.268,1.729 1.902,2.608c-0.979,0.404 -2.954,1.21 -3.934,1.614c-1.801,0.734 -3.602,1.441 -5.389,2.176c-4.553,-2.594 -9.208,-5.159 -14.338,-6.383c0.893,1.197 1.787,2.407 2.709,3.602l0.432,2.364c-2.406,-1.6 -4.813,-3.214 -7.205,-4.828Z" style="fill:#242a2f;fill-rule:nonzero;"/><path d="M608.856,474.68c0.723,0.766 0.723,0.766 0,0Z" style="fill:#c4c9ce;fill-rule:nonzero;"/><path d="M682.072,474.58c2.695,0 5.39,0.114 8.084,0.288c-1.037,2.737 -2.118,5.447 -3.214,8.155l-0.734,-0.663c-0.418,-0.375 -1.283,-1.153 -1.715,-1.542c0.995,-1.412 2.003,-2.824 3.026,-4.235c-1.83,-0.677 -3.631,-1.325 -5.447,-2.003Z" style="fill:#596472;fill-rule:nonzero;"/><path d="M496.487,482.189c1.484,-2.724 3.358,-5.245 5.605,-7.392c3.3,7.912 6.917,15.721 9.208,24.008c-1.47,1.225 -2.954,2.42 -4.51,3.53c-1.83,-0.361 -3.646,-0.778 -5.433,-1.268c0.865,-0.13 2.594,-0.389 3.458,-0.505c-3.545,-5.75 -6.139,-12.004 -8.329,-18.372Z" style="fill:#5e87af;fill-rule:nonzero;"/><path d="M667.631,475.66c2.162,1.166 4.309,2.405 6.528,3.501c0.576,-0.202 1.729,-0.591 2.306,-0.778c-0.389,0.62 -1.195,1.845 -1.599,2.464c-1.268,2.003 -2.508,4.02 -3.646,6.111c-2.104,-1.052 -4.481,-1.758 -5.908,-3.732c0.316,-1.125 0.936,-3.372 1.254,-4.482c0.259,-0.777 0.806,-2.319 1.065,-3.083Z" style="fill:#5d85ad;fill-rule:nonzero;"/><path d="M447.654,476.554c4.02,2.983 8.286,5.663 12.854,7.767c-1.571,1.325 -3.084,2.724 -4.582,4.121c4.784,2.364 9.669,4.626 14.885,5.88c6.528,2.205 14.251,1.917 19.669,6.657c-8.3,-1.454 -16.701,-2.651 -24.612,-5.676c-7.162,-3.055 -14.424,-5.951 -21.067,-10.073c-0.461,-1.729 -0.937,-3.458 -1.412,-5.173c1.412,-1.167 2.839,-2.349 4.265,-3.502Z" style="fill:#526e89;fill-rule:nonzero;"/><path d="M547.815,478.587c2.32,-0.534 4.668,-0.923 7.047,-1.197c-1.499,1.83 -2.983,3.66 -4.468,5.505c-2.449,0.446 -4.885,0.966 -7.306,1.469c0.13,-1.181 0.418,-3.558 0.563,-4.74c1.383,-0.345 2.781,-0.692 4.164,-1.037Z" style="fill:#a2a4a5;fill-rule:nonzero;"/><path d="M554.861,477.391c3.099,-0.332 6.225,-0.62 9.353,-0.808c1.426,0.347 2.867,0.677 4.323,0.995c-3.69,0.345 -8.114,0.274 -10.03,4.107c-2.723,0.303 -5.418,0.734 -8.112,1.211c1.484,-1.845 2.968,-3.675 4.467,-5.505Z" style="fill:#bdc0c2;fill-rule:nonzero;"/><path d="M514.732,478.644c5.13,1.225 9.784,3.789 14.338,6.383c1.787,-0.734 3.588,-1.441 5.389,-2.176c0.173,1.124 0.519,3.387 0.692,4.51c-2.551,1.21 -5.043,2.55 -7.507,3.934l-0.836,0.518c-0.173,-1.195 -0.533,-3.587 -0.706,-4.784l-0.187,-0.936c-2.781,-1.397 -5.663,-2.55 -8.473,-3.848c-0.922,-1.195 -1.816,-2.407 -2.709,-3.602Z" style="fill:#727373;fill-rule:nonzero;"/><path d="M558.507,481.684c1.917,-3.833 6.341,-3.761 10.03,-4.107c7.622,0.85 14.885,3.314 22.205,5.475c0.345,0.246 1.067,0.721 1.426,0.966c0.576,0.389 1.758,1.197 2.335,1.586c-4.986,1.858 -10.045,0.274 -14.843,-1.311c-6.859,-1.975 -14.049,-2.349 -21.153,-2.608Z" style="fill:#d6dadd;fill-rule:nonzero;"/><path d="M604.388,479.262l0.721,-0.993c1.671,3.299 3.257,6.671 3.919,10.346c-2.45,-2.104 -5.924,-6.168 -4.641,-9.353Z" style="fill:#686b6f;fill-rule:nonzero;"/><path d="M676.466,478.384l1.211,-0.432c2.348,0.734 4.654,1.656 6.816,2.867c0.432,0.389 1.296,1.167 1.715,1.542l0.865,2.55c-2.623,0.404 -4.914,-0.879 -7.119,-2.047c-1.296,0.951 -2.579,1.917 -3.89,2.825l-1.485,0.692c0.072,-1.384 0.216,-4.15 0.288,-5.534c0.404,-0.619 1.211,-1.843 1.599,-2.464Z" style="fill:#4e6dae;fill-rule:nonzero;"/><path d="M422.553,487.924c2.032,-2.838 4.035,-5.706 6.34,-8.329c3.833,3.445 7.767,6.831 12.306,9.324c-1.844,2.638 -3.689,5.274 -5.735,7.752c-5.548,0.433 -8.502,-6.095 -13.142,-8.43l0.231,-0.317Z" style="fill:#5f88b2;fill-rule:nonzero;"/><path d="M428.892,479.595c2.176,0.189 4.352,0.404 6.542,0.664c3.213,1.484 6.311,3.184 9.366,4.971c6.643,4.121 13.905,7.017 21.067,10.071c-1.729,0.534 -3.458,1.096 -5.159,1.701c-2.378,-0.663 -4.755,-1.325 -7.075,-2.148c-4.265,-1.715 -8.401,-3.747 -12.436,-5.937c-4.539,-2.493 -8.473,-5.878 -12.306,-9.324Z" style="fill:#3d4d5b;fill-rule:nonzero;"/><path d="M621.278,488.341c2.42,-2.162 3.156,-5.375 3.947,-8.358c4.496,0.922 8.387,3.372 9.986,7.91c2.421,-2.233 1.167,-5.23 0.49,-7.925c2.522,2.003 5.087,3.934 7.651,5.894c-5.289,9.322 -13.098,16.902 -18.2,26.297c-0.576,0.547 -1.729,1.628 -2.291,2.176c-0.36,-0.576 -1.081,-1.729 -1.426,-2.306c2.998,-0.663 2.68,-3.644 0.707,-5.273c-0.677,-1.355 -1.369,-2.695 -2.392,-3.805c-0.347,-0.533 -1.039,-1.57 -1.384,-2.104c0.404,-0.936 1.195,-2.796 1.599,-3.732c0.216,-1.599 0.375,-3.198 0.461,-4.799c0.677,-0.533 2.032,-1.614 2.709,-2.161c-0.461,-0.446 -1.383,-1.369 -1.858,-1.816Zm3.011,2.276c4.769,0.029 -2.047,4.395 0,0Zm-3.804,9.713c4.309,-2.249 0.303,4.74 0,0Zm4.035,6.297c0.734,0.764 0.734,0.764 0,0Z" style="fill:#3e4d5c;fill-rule:nonzero;"/><path d="M648.844,480.95c1.08,1.124 2.161,2.247 3.257,3.385l-1.226,1.715c-3.516,4.857 -6.426,10.146 -10.158,14.857c-1.903,2.566 -3.732,5.202 -5.289,8.013c3.861,1.325 10.201,-0.475 11.426,4.625c-0.907,-0.432 -2.737,-1.311 -3.644,-1.757c-1.298,1.282 -2.594,2.579 -3.862,3.89c0.951,-0.331 2.853,-0.993 3.804,-1.325c0.015,0.475 0.044,1.426 0.058,1.902c-3.271,0.044 -4.554,1.399 -3.833,4.035c0.979,0.187 2.939,0.547 3.933,0.734c-0.705,2.739 -0.562,6.197 -3.429,7.739c0.591,-1.83 1.138,-3.675 1.643,-5.534c0.749,-3.415 -4.294,-2.607 -4.309,0.015c-0.778,2.003 -1.426,4.05 -2.089,6.096c1.283,0.013 3.862,0.057 5.144,0.072c-0.518,2.42 -1.067,4.885 -0.591,7.378l-0.389,0.345c-0.604,0.562 -1.801,1.687 -2.392,2.249c0.073,0.634 0.231,1.917 0.317,2.55l-0.793,-0.865c-0.244,-0.274 -0.734,-0.808 -0.979,-1.081c-0.275,0.044 -0.822,0.145 -1.096,0.187c-0.835,0.145 -2.508,0.433 -3.343,0.591c-0.274,-3.126 -0.576,-6.253 -1.167,-9.322c4.828,2.781 3.227,-5.591 -0.259,-1.312l-1.037,0.894c0.389,-2.075 0.792,-4.136 1.225,-6.196c1.369,0.951 2.767,1.917 4.15,2.882c0.114,-0.519 0.36,-1.557 0.475,-2.076c-2.825,-1.282 -5.606,-2.666 -8.402,-3.976c0.389,-0.98 1.182,-2.954 1.571,-3.949c-1.628,-1.052 -3.328,-2.003 -4.698,-3.372c0.562,-0.547 1.715,-1.628 2.291,-2.176c9.496,-9.006 17.278,-19.828 23.69,-31.211Zm-18.532,38.387c0.736,0.793 0.736,0.793 0,0Zm4.525,11.427c-1.974,1.383 0.043,5.115 2.219,3.53c2.233,-1.355 -0.029,-5.274 -2.219,-3.53Z" style="fill:#323b45;fill-rule:nonzero;"/><path d="M671.22,486.957c1.138,-2.089 2.377,-4.107 3.646,-6.109c-0.072,1.383 -0.216,4.15 -0.288,5.533l1.484,-0.692c-1.037,1.254 -2.06,2.537 -3.04,3.833c-0.446,-0.634 -1.355,-1.931 -1.801,-2.565Z" style="fill:#849cc2;fill-rule:nonzero;"/><path d="M517.441,482.246c2.81,1.298 5.692,2.45 8.473,3.848l0.187,0.936c-2.378,1.456 -4.77,2.911 -7.046,4.554c-0.403,-2.335 -0.778,-4.655 -1.182,-6.974l-0.432,-2.364Z" style="fill:#101214;fill-rule:nonzero;"/><path d="M663.368,489.681c0.749,-2.118 1.368,-4.279 1.944,-6.455c1.426,1.974 3.805,2.68 5.909,3.732c0.446,0.634 1.353,1.93 1.801,2.565c-2.709,3.66 -5.058,7.565 -7.651,11.297c-0.116,-1.599 -0.216,-3.185 -0.303,-4.769c-1.744,-0.26 -3.488,-0.505 -5.217,-0.749c0.793,-1.341 1.6,-2.682 2.421,-4.007c0.274,-0.402 0.821,-1.21 1.096,-1.614Z" style="fill:#577797;fill-rule:nonzero;"/><path d="M676.061,485.689c1.311,-0.908 2.594,-1.874 3.89,-2.825c2.205,1.167 4.496,2.45 7.119,2.047c0.389,1.283 0.806,2.55 1.225,3.833c-2.306,4.698 -4.971,9.193 -7.796,13.603c-2.579,-1.456 -5.144,-2.925 -7.681,-4.453c2.608,-3.703 4.51,-7.824 6.153,-12.017c-0.72,-0.044 -2.176,-0.145 -2.91,-0.187Z" style="fill:#5f89b3;fill-rule:nonzero;"/><path d="M460.507,484.322c0.418,0.015 1.254,0.057 1.671,0.072c2.104,0.995 4.136,2.133 6.225,3.17c2.925,1.21 5.98,2.089 9.049,2.897c-2.219,1.267 -4.438,2.55 -6.643,3.861c-5.216,-1.254 -10.101,-3.516 -14.885,-5.878c1.499,-1.399 3.012,-2.796 4.582,-4.121Z" style="fill:#465b6f;fill-rule:nonzero;"/><path d="M535.149,487.362c2.594,-1.138 5.259,-2.075 7.94,-2.996c0.187,0.705 0.562,2.118 0.764,2.838c-2.306,0.461 -3.861,1.816 -3.962,4.208c1.325,1.225 2.636,2.464 3.949,3.717c-4.079,1.6 -8.142,3.271 -11.831,5.663c-1.455,-3.17 -2.911,-6.326 -4.366,-9.496c2.464,-1.383 4.957,-2.723 7.507,-3.934Z" style="fill:#d6d8db;fill-rule:nonzero;"/><path d="M676.063,485.69c0.734,0.044 2.19,0.145 2.911,0.187c-1.643,4.193 -3.545,8.314 -6.153,12.018c-2.68,3.89 -5.36,7.824 -8.589,11.283c-2.954,2.983 -5.965,5.908 -8.731,9.049c2.434,1.298 5.014,2.263 7.651,3.069c-6.673,4.784 -12.839,10.304 -19.771,14.728l-1.109,0.23c-0.072,-1.153 -0.13,-2.306 -0.173,-3.444c1.195,-1.859 2.276,-3.775 3.328,-5.72c1.484,0.705 2.983,1.369 4.51,1.988c0.259,-1.167 0.806,-3.516 1.067,-4.698c-1.34,0.101 -4.02,0.317 -5.36,0.418c0.402,-1.024 1.181,-3.084 1.584,-4.107l0.894,1.21c2.767,4.841 5.965,-3.055 0.547,-1.124c4.525,-7.422 12.277,-12.334 16.701,-19.958c2.594,-3.732 4.942,-7.637 7.651,-11.297c0.979,-1.296 2.003,-2.579 3.04,-3.833Zm-29.453,45.233c0.863,0.734 0.863,0.734 0,0Z" style="fill:#3f505f;fill-rule:nonzero;"/><path d="M519.054,491.583c2.277,-1.643 4.669,-3.099 7.046,-4.554c0.173,1.197 0.533,3.589 0.706,4.784c-3.113,1.931 -5.807,4.396 -8.444,6.902l-0.403,-0.792c-0.591,-1.298 -1.153,-2.579 -1.715,-3.877c0.908,-0.85 1.844,-1.685 2.81,-2.464Z" style="fill:#606061;fill-rule:nonzero;"/><path d="M539.89,491.41c0.101,-2.392 1.658,-3.747 3.963,-4.208c0.519,0.274 1.571,0.835 2.104,1.109c0.202,2.45 -0.461,4.626 -2.003,6.572c18.53,-6.067 40.592,-2.061 54.54,11.931c0.13,-2.348 0.26,-4.712 0.808,-7.003c2.363,-1.21 3.949,1.312 5.75,2.392c-0.446,0.966 -0.879,1.946 -1.325,2.911c-1.687,0.576 -3.328,1.197 -4.986,1.83c9.122,7.94 14.972,19.41 16.701,31.342c0.072,0.475 0.215,1.426 0.288,1.902c-3.185,6.902 -1.441,14.712 -3.502,21.888c-0.418,-0.417 -1.254,-1.268 -1.671,-1.685c3.558,-13.849 1.412,-29.267 -6.744,-41.126c-8.487,-12.867 -23.128,-21.572 -38.547,-22.551c-22.018,-1.946 -43.978,12.709 -51.154,33.517c-2.839,7.191 -3.141,15 -2.954,22.637c-0.548,0.231 -1.657,0.692 -2.219,0.908c-0.058,-0.707 -0.187,-2.148 -0.245,-2.867c-0.677,-13.906 3.747,-27.595 11.686,-38.936c1.268,-1.801 2.637,-3.516 4.121,-5.144c2.363,-2.161 4.856,-4.208 7.507,-6.038c3.689,-2.392 7.753,-4.063 11.83,-5.663c-1.311,-1.254 -2.622,-2.493 -3.949,-3.717Z" style="fill:#232020;fill-rule:nonzero;"/><path d="M616.57,489.777c-2.472,1.489 -2.616,-2.717 -0.317,-2.919l1.055,0.809c-0.187,0.521 -0.563,1.591 -0.737,2.11Z" style="fill:#b0a2b3;fill-rule:nonzero;"/><path d="M477.452,490.46c2.392,0.635 4.798,1.283 7.162,2.061c1.038,0.317 3.127,0.966 4.179,1.296c0.648,0.174 1.96,0.519 2.608,0.692c2.234,2.537 4.51,5.058 6.7,7.637c-2.579,-0.187 -5.13,-0.49 -7.623,-1.166c-5.418,-4.741 -13.142,-4.453 -19.669,-6.658c2.205,-1.311 4.424,-2.594 6.643,-3.862Z" style="fill:#3c4a58;fill-rule:nonzero;"/><path d="M624.289,490.618c4.785,0.029 -2.052,4.408 0,0Z" style="fill:#6f6e7e;fill-rule:nonzero;"/><path d="M422.869,491.987c0.737,0.752 0.737,0.752 0,0Z" style="fill:#383738;fill-rule:nonzero;"/><path d="M470.609,610.48c0.015,0.029 0.06,0.089 0.075,0.118l-0.075,-0.118Z" style="fill:#383738;fill-rule:nonzero;"/><path d="M526.807,491.814l0.836,-0.519c1.455,3.17 2.911,6.326 4.366,9.496c-2.652,1.83 -5.144,3.876 -7.508,6.038c-2.234,-2.551 -4.409,-5.188 -6.139,-8.112c2.637,-2.508 5.332,-4.973 8.444,-6.902Z" style="fill:#afafb0;fill-rule:nonzero;"/><path d="M613.005,491.584c2.486,-0.983 4.091,2.587 1.272,3.339l-1.258,-0.607c0,-0.679 -0.015,-2.052 -0.015,-2.732Z" style="fill:#efcadb;fill-rule:nonzero;"/><path d="M496.388,493.485c0.737,0.752 0.737,0.752 0,0Z" style="fill:#bed2ea;fill-rule:nonzero;"/><path d="M425.837,494.955c0.81,0.709 0.81,0.709 0,0Z" style="fill:#595759;fill-rule:nonzero;"/><path d="M511.301,498.803c1.686,-1.542 3.285,-3.17 4.943,-4.755c0.562,1.296 1.124,2.579 1.715,3.876c-1.787,2.032 -3.574,4.064 -5.375,6.082c-1.009,1.109 -2.003,2.234 -2.954,3.4c-0.807,-0.345 -2.406,-1.052 -3.213,-1.397c0.115,-1.239 0.259,-2.449 0.375,-3.675c1.556,-1.109 3.04,-2.306 4.51,-3.53Z" style="fill:#464645;fill-rule:nonzero;"/><path d="M652.791,504.74c2.737,-2.838 5.043,-6.052 7.06,-9.439c1.729,0.246 3.473,0.49 5.217,0.75c0.086,1.584 0.187,3.17 0.301,4.769c-4.423,7.622 -12.175,12.536 -16.7,19.957l-0.547,1.125l-0.894,-1.211c0.547,-1.555 1.182,-3.069 1.83,-4.567c3.185,-2.767 6.21,-5.764 8.371,-9.425c-1.555,-0.663 -3.097,-1.311 -4.639,-1.959Z" style="fill:#4e677f;fill-rule:nonzero;"/><path d="M428.747,497.852c0.781,0.752 0.781,0.752 0,0Z" style="fill:#5d5b5d;fill-rule:nonzero;"/><path d="M610.066,497.002c1.83,1.715 3.589,3.516 5.333,5.331c1.225,1.586 2.579,2.853 4.351,0.62c1.023,1.109 1.715,2.449 2.392,3.804c-0.562,0.246 -1.715,0.749 -2.291,1.009c-1.369,1.252 1.153,2.954 1.586,4.265c0.345,0.576 1.065,1.729 1.426,2.306c1.368,1.369 3.069,2.32 4.698,3.372c-0.389,0.993 -1.182,2.968 -1.571,3.947c2.796,1.312 5.577,2.695 8.401,3.978c-0.116,0.519 -0.36,1.557 -0.475,2.075c-1.383,-0.966 -2.781,-1.931 -4.149,-2.882c-0.433,2.061 -0.837,4.121 -1.226,6.197c-1.138,0.993 -2.291,1.974 -3.429,2.968c-1.599,-5.072 -2.867,-10.26 -4.885,-15.189c-2.796,-6.816 -7.435,-12.608 -11.672,-18.56c0.375,-0.806 1.138,-2.434 1.513,-3.242Z" style="fill:#d1d2d6;fill-rule:nonzero;"/><path d="M664.233,509.178c3.227,-3.458 5.908,-7.392 8.588,-11.283c2.537,1.527 5.101,2.996 7.681,4.452c-2.853,3.789 -5.562,7.782 -9.166,10.923c-2.42,-1.283 -4.769,-2.68 -7.103,-4.092Z" style="fill:#54728e;fill-rule:nonzero;"/><path d="M512.583,504.005c1.801,-2.017 3.588,-4.05 5.375,-6.08l0.403,0.792c1.729,2.926 3.905,5.562 6.139,8.114c-1.484,1.628 -2.853,3.343 -4.121,5.144c-0.922,-0.231 -2.781,-0.692 -3.703,-0.923c-1.369,-2.348 -2.738,-4.711 -4.092,-7.046Z" style="fill:#939293;fill-rule:nonzero;"/><path d="M485.35,596.229c0.519,-2.278 1.052,-4.54 1.556,-6.816c-0.49,5.562 0.332,11.138 1.801,16.498l0.259,0.793l-1.744,1.11c-5.894,-0.418 -6.787,-6.471 -7.09,-11.196c1.729,-0.13 3.473,-0.26 5.216,-0.389Z" style="fill:#939293;fill-rule:nonzero;"/><path d="M470.349,647.555c0.853,0.823 0.853,0.823 0,0Z" style="fill:#939293;fill-rule:nonzero;"/><path d="M640.715,500.907c2.565,-0.691 5.101,-1.527 7.738,-1.944c1.542,0.907 2.998,1.959 4.382,3.112c-2.781,2.335 -5.606,5.015 -5.39,8.964c-0.202,0.835 -0.404,1.671 -0.591,2.506c-1.225,-5.1 -7.565,-3.299 -11.427,-4.625c1.557,-2.81 3.387,-5.447 5.289,-8.013Z" style="fill:#394754;fill-rule:nonzero;"/><path d="M431.371,500.216c2.038,-0.636 2.689,0.072 1.966,2.11c-2.038,0.636 -2.703,-0.073 -1.966,-2.11Z" style="fill:#454445;fill-rule:nonzero;"/><path d="M620.485,500.332c4.323,-2.254 0.304,4.756 0,0Z" style="fill:#ba9baf;fill-rule:nonzero;"/><path d="M478.533,501.657c5.966,1.138 11.917,2.521 18.012,2.895c-6.456,1.614 -12.162,7.047 -19.179,4.597c-1.182,-0.173 -3.559,-0.533 -4.755,-0.705c1.7,-2.493 3.674,-4.784 5.922,-6.787Z" style="fill:#384653;fill-rule:nonzero;"/><path d="M449.873,503.357c2.594,1.067 5.159,2.19 7.781,3.227c0.086,0.029 0.259,0.101 0.346,0.145l-0.101,-0.101c4.611,0.49 9.193,1.182 13.79,1.715c-0.764,0.116 -2.277,0.345 -3.04,0.446c-0.49,1.99 -0.908,4.007 -1.34,6.023c-3.026,1.11 -6.052,2.292 -8.977,3.675c-2.392,1.153 -4.741,2.392 -7.104,3.617c-2.565,-0.619 -5.159,-1.023 -7.767,-1.355l-0.245,-0.043l-0.014,0.015c-3.761,1.931 -7.263,4.336 -11.038,6.24c-0.144,-0.015 -0.432,-0.044 -0.562,-0.058c-0.634,-0.086 -1.888,-0.288 -2.507,-0.389c-0.893,0.749 -1.787,1.485 -2.666,2.234l-0.692,-1.557l0.677,-0.331c-0.231,-0.044 -0.706,-0.159 -0.937,-0.216c-0.504,-0.044 -1.499,-0.13 -1.989,-0.173c-1.11,0.158 -3.386,4.063 -3.43,0.648c0.043,-0.072 0.101,-0.231 0.13,-0.317c0.605,-0.475 1.816,-1.426 2.421,-1.902c2.075,-1.369 4.525,-3.458 6.398,-0.518c1.787,-0.404 3.559,-0.822 5.36,-1.211c-0.115,-0.907 -0.346,-2.709 -0.461,-3.617c0.965,0.591 2.882,1.758 3.833,2.348c2.046,-1.829 3.991,-3.775 5.807,-5.836c0.864,0.923 1.729,1.845 2.608,2.767l0.98,-0.129c-0.865,-1.485 -1.7,-2.968 -2.536,-4.453c2.262,-1.225 3.934,0.793 5.634,1.946c-0.836,-1.557 -1.599,-3.141 -2.291,-4.755c1.945,-0.116 3.732,0.547 5.332,1.974c-1.239,-2.19 -3.285,-4.683 -0.072,-6.269c-0.231,-0.417 -0.721,-1.239 -0.966,-1.656c-0.965,1.614 -2.32,2.824 -4.035,3.602c-0.49,-2.19 0.303,-4.077 1.671,-5.735Zm4.698,5.894c0.778,0.705 0.778,0.705 0,0Zm5.807,-0.174c0.778,0.721 0.778,0.721 0,0Zm-2.896,2.867c0.735,0.764 0.735,0.764 0,0Zm-2.968,2.638c4.352,-2.162 0.13,4.769 0,0Z" style="fill:#454546;fill-rule:nonzero;"/><path d="M496.545,504.552c3.329,-0.013 6.744,0.145 9.871,1.456c0.807,0.347 2.406,1.052 3.213,1.399c-2.176,2.882 -4.121,5.922 -5.908,9.049c-0.202,-2.709 -0.504,-5.418 -0.778,-8.112c-4.755,3.097 -10.678,2.882 -15.966,4.452c0.403,0.404 1.225,1.197 1.643,1.586l-0.101,1.355c-3.271,0.648 -6.542,1.282 -9.827,1.873c-0.158,-2.003 -0.375,-3.991 -0.692,-5.979c-0.187,-0.837 -0.403,-1.658 -0.634,-2.48c7.018,2.45 12.724,-2.982 19.179,-4.597Z" style="fill:#2f3841;fill-rule:nonzero;"/><path d="M509.63,507.407c0.951,-1.167 1.945,-2.291 2.954,-3.401c1.354,2.335 2.723,4.698 4.092,7.047c0.922,0.23 2.781,0.692 3.703,0.922c-7.94,11.341 -12.364,25.03 -11.686,38.936c-0.634,-5.347 -0.461,-10.836 -2.161,-16.009c-0.072,-0.274 -0.231,-0.835 -0.303,-1.11c2.435,0.101 2.421,-2.737 3.257,-4.279c1.499,-4.813 3.919,-9.251 6.484,-13.559c-4.179,0.129 -8.343,0.417 -12.479,1.08l0.231,-0.576c1.787,-3.126 3.732,-6.167 5.908,-9.049Z" style="fill:#7b7a7a;fill-rule:nonzero;"/><path d="M647.444,511.038c1.658,-2.205 3.458,-4.295 5.346,-6.298c1.542,0.649 3.084,1.298 4.641,1.96c-2.161,3.66 -5.188,6.657 -8.373,9.423c-0.518,-0.489 -1.555,-1.484 -2.075,-1.988c-0.606,2.133 -1.845,3.891 -3.646,5.159l-0.129,-3.04c-0.015,-0.475 -0.044,-1.426 -0.058,-1.902c-0.951,0.332 -2.853,0.995 -3.804,1.325c1.268,-1.311 2.565,-2.608 3.862,-3.89c0.907,0.446 2.737,1.325 3.644,1.757c0.187,-0.835 0.389,-1.671 0.591,-2.506Z" style="fill:#45596c;fill-rule:nonzero;"/><path d="M437.308,506.324c0.737,0.781 0.737,0.781 0,0Z" style="fill:#575556;fill-rule:nonzero;"/><path d="M624.52,506.628c0.737,0.766 0.737,0.766 0,0Z" style="fill:#af8ea2;fill-rule:nonzero;"/><path d="M486.979,512.795c5.288,-1.57 11.211,-1.355 15.966,-4.452c0.274,2.695 0.576,5.403 0.778,8.112l-0.231,0.576c-0.908,2.205 -1.816,4.409 -2.695,6.643c-1.038,2.794 -1.917,5.649 -2.824,8.501c0.504,0.187 1.484,0.562 1.989,0.749c0.677,-0.461 2.017,-1.397 2.695,-1.859c1.268,0.793 2.493,1.643 3.574,2.724c0.072,0.274 0.231,0.835 0.303,1.109c-1.009,1.125 -1.974,2.32 -2.954,3.473c-1.599,-1.744 -3.761,-2.68 -5.894,-3.602c-0.317,-2.926 -1.297,-5.678 -2.421,-8.373c0.432,-2.205 0.922,-4.38 1.412,-6.57c-0.418,-0.475 -0.836,-0.966 -1.254,-1.441c0.965,-1.225 4.164,-1.672 3.156,-3.675c-3.271,-0.806 -6.657,-0.303 -9.957,-0.331c-0.418,-0.389 -1.239,-1.182 -1.643,-1.586Z" style="fill:#232224;fill-rule:nonzero;"/><path d="M440.291,509.249c0.723,0.781 0.723,0.781 0,0Z" style="fill:#4f4e4f;fill-rule:nonzero;"/><path d="M445.881,509.234c0.838,0.68 0.838,0.68 0,0Z" style="fill:#535152;fill-rule:nonzero;"/><path d="M468.649,508.789c0.764,-0.101 2.277,-0.331 3.041,-0.446l0.922,0.101c1.196,0.173 3.574,0.533 4.755,0.705c0.231,0.822 0.447,1.643 0.634,2.478c-3.242,0.851 -6.47,1.744 -9.64,2.81l-1.052,0.375c0.432,-2.017 0.85,-4.035 1.34,-6.023Z" style="fill:#0d1012;fill-rule:nonzero;"/><path d="M655.5,518.228c2.767,-3.141 5.779,-6.067 8.733,-9.049c2.334,1.412 4.683,2.809 7.104,4.092c-2.537,2.867 -5.347,5.461 -8.185,8.026c-2.636,-0.806 -5.216,-1.772 -7.651,-3.069Z" style="fill:#485e73;fill-rule:nonzero;"/><path d="M443.044,512.262c0.737,0.766 0.737,0.766 0,0Z" style="fill:#4f4d4e;fill-rule:nonzero;"/><path d="M468.361,514.439c3.17,-1.067 6.398,-1.96 9.64,-2.81c0.317,1.988 0.533,3.978 0.692,5.981l-0.418,0.835c-2.19,0.966 -4.438,1.845 -6.657,2.709c-0.62,-0.347 -1.859,-1.039 -2.478,-1.384c-0.288,-1.786 -0.548,-3.559 -0.778,-5.331Z" style="fill:#3a4856;fill-rule:nonzero;"/><path d="M422.94,515c0.741,0.726 0.741,0.726 0,0Z" style="fill:#313031;fill-rule:nonzero;"/><path d="M440.203,529.407c0.723,0.752 0.723,0.752 0,0Z" style="fill:#313031;fill-rule:nonzero;"/><path d="M440.262,515.057c0.795,0.723 0.795,0.723 0,0Z" style="fill:#525153;fill-rule:nonzero;"/><path d="M454.511,514.584c4.365,-2.169 0.13,4.784 0,0Z" style="fill:#1a191a;fill-rule:nonzero;"/><path d="M467.308,514.811l1.052,-0.375c0.231,1.773 0.49,3.545 0.778,5.333c0.62,0.345 1.859,1.037 2.478,1.383c-3.055,1.298 -5.994,2.825 -8.876,4.453c-1.47,-2.377 -2.94,-4.756 -4.409,-7.119c2.925,-1.383 5.951,-2.565 8.977,-3.675Z" style="fill:#465a6e;fill-rule:nonzero;"/><path d="M646.983,514.135c0.519,0.505 1.557,1.499 2.076,1.99c-0.649,1.498 -1.283,3.011 -1.83,4.567c-0.404,1.023 -1.182,3.084 -1.586,4.107c1.34,-0.101 4.02,-0.317 5.36,-0.418c-0.259,1.182 -0.806,3.53 -1.067,4.698c-1.527,-0.619 -3.026,-1.282 -4.509,-1.988c-1.052,1.946 -2.133,3.862 -3.33,5.72c0.044,1.138 0.101,2.291 0.174,3.444c-0.649,0.13 -1.946,0.389 -2.594,0.534c-0.475,-2.493 0.072,-4.957 0.59,-7.378c-1.282,-0.015 -3.861,-0.058 -5.144,-0.072c0.663,-2.047 1.311,-4.093 2.089,-6.096c0.015,-2.622 5.059,-3.429 4.309,-0.015c-0.503,1.859 -1.052,3.704 -1.643,5.534c2.869,-1.542 2.724,-5 3.431,-7.739c-0.995,-0.187 -2.954,-0.547 -3.934,-0.734c-0.721,-2.636 0.562,-3.991 3.833,-4.035l0.129,3.04c1.802,-1.267 3.041,-3.026 3.646,-5.159Z" style="fill:#cb8dac;fill-rule:nonzero;"/><path d="M478.692,517.607c3.285,-0.59 6.556,-1.225 9.827,-1.873c-0.692,1.47 -1.441,2.911 -2.19,4.351c-3.631,1.197 -7.277,2.379 -10.851,3.733c0.951,-1.788 1.873,-3.589 2.795,-5.375l0.418,-0.837Z" style="fill:#596d81;fill-rule:nonzero;"/><path d="M503.491,517.032c4.136,-0.663 8.3,-0.951 12.479,-1.08c-2.565,4.309 -4.986,8.746 -6.484,13.559c-3.113,-1.599 -6.067,-3.516 -8.689,-5.836c0.879,-2.234 1.787,-4.439 2.695,-6.643Z" style="fill:#f2f2f3;fill-rule:nonzero;"/><path d="M414.381,518.025c0.708,0.766 0.708,0.766 0,0Z" style="fill:#373637;fill-rule:nonzero;"/><path d="M437.322,532.291c0.726,0.741 0.726,0.741 0,0Z" style="fill:#373637;fill-rule:nonzero;"/><path d="M432.281,557.127c-1.344,-1.778 -0.853,-2.385 1.474,-1.822c-0.361,0.464 -1.113,1.373 -1.474,1.822Z" style="fill:#373637;fill-rule:nonzero;"/><path d="M420.103,517.997c0.766,0.766 0.766,0.766 0,0Z" style="fill:#575657;fill-rule:nonzero;"/><path d="M425.592,517.71c4.394,-2.11 0.072,4.799 0,0Z" style="fill:#3d3d3e;fill-rule:nonzero;"/><path d="M455.998,613.115c0.781,0.724 0.781,0.724 0,0Z" style="fill:#3d3d3e;fill-rule:nonzero;"/><path d="M437.322,517.882c0.81,0.724 0.81,0.724 0,0Z" style="fill:#5d5c5d;fill-rule:nonzero;"/><path d="M451.226,522.104c2.363,-1.225 4.712,-2.464 7.104,-3.617c1.47,2.363 2.94,4.74 4.409,7.118c-2.868,1.658 -5.677,3.401 -8.314,5.404c-0.922,-2.623 -1.787,-5.259 -2.767,-7.853l-0.432,-1.052Z" style="fill:#516b85;fill-rule:nonzero;"/><path d="M630.312,519.336c0.737,0.796 0.737,0.796 0,0Z" style="fill:#a77e96;fill-rule:nonzero;"/><path d="M411.256,520.662c4.799,0.216 -2.212,4.322 0,0Z" style="fill:#363637;fill-rule:nonzero;"/><path d="M417.162,520.893c0.838,0.723 0.838,0.723 0,0Z" style="fill:#717071;fill-rule:nonzero;"/><path d="M625.497,559.498c0.936,-4.338 0.692,-10.447 6.109,-11.672c0.145,0.648 0.433,1.959 0.576,2.607c0.058,4.611 -2.377,8.762 -2.723,13.329c-0.332,1.384 0.057,3.328 -1.744,3.717c-5.893,2.349 -7.666,9.107 -11.989,13.243c-1.513,-3.099 -3.126,-6.139 -4.726,-9.179c-0.692,0.648 -2.104,1.974 -2.796,2.622c1.181,-3.962 2.91,-7.709 4.841,-11.354c-0.143,2.219 -0.216,4.453 -0.288,6.7c3.084,-0.116 6.326,0.446 9.294,-0.663c2.032,-2.709 2.796,-6.095 3.444,-9.351Z" style="fill:#717071;fill-rule:nonzero;"/><path d="M422.609,520.794c4.308,-2.284 0.246,4.842 0,0Z" style="fill:#5f5d5f;fill-rule:nonzero;"/><path d="M428.862,520.735c0.679,0.853 0.679,0.853 0,0Z" style="fill:#666465;fill-rule:nonzero;"/><path d="M432.162,526.958c3.775,-1.902 7.277,-4.309 11.038,-6.238c0.072,0 0.187,0.013 0.259,0.028c2.608,0.332 5.202,0.736 7.767,1.355l0.432,1.052c-2.017,1.081 -4.035,2.176 -6.038,3.286c-0.605,-0.116 -1.816,-0.332 -2.421,-0.433c0.144,0.519 0.447,1.571 0.605,2.104l0.331,0.534c-0.187,2.666 -0.49,5.346 -0.793,8.011c-2.147,0.576 -4.28,1.153 -6.427,1.744c2.032,1.283 4.092,2.522 5.951,4.064c0.447,2.276 0.475,4.61 0.634,6.93c0.245,-0.288 0.749,-0.835 0.994,-1.109c0.648,-0.707 1.974,-2.089 2.623,-2.796c3.069,0.951 6.124,2.032 8.977,3.545l0.216,0.173c0.677,0.519 2.032,1.542 2.709,2.061c-0.49,0.402 -1.455,1.195 -1.945,1.599c2.032,0.072 4.078,0.143 6.11,0.36c-1.081,0.98 -2.161,1.975 -3.228,2.968c0.879,0.159 2.637,0.475 3.516,0.635c0.014,0.503 0.043,1.498 0.058,1.988c3.804,4.279 8.415,8.501 9.121,14.525c0.447,0.274 1.311,0.85 1.744,1.138c1.556,2.335 3.516,4.352 5.764,6.023c-0.778,5.347 -0.115,10.75 -0.029,16.11c-1.427,0.519 -2.853,1.023 -4.28,1.542c-1.801,-1.34 -3.574,-2.68 -5.332,-4.035c0.058,-2.535 -0.648,-4.942 -1.715,-7.191c-1.671,0.433 -3.343,0.879 -5,1.325c0.533,-1.599 1.066,-3.184 1.614,-4.769c-1.528,-1.902 -3.257,-3.631 -5.072,-5.245c0.533,-0.231 1.585,-0.692 2.118,-0.936c-3.127,-4.683 -6.628,-9.136 -10.937,-12.797c1.196,-2.104 3.358,-4.625 1.268,-6.959c-2.363,-0.044 -4.597,0.835 -6.83,1.456c-0.36,0.793 -1.095,2.363 -1.47,3.156c-0.187,-1.037 -0.562,-3.126 -0.749,-4.165c-1.945,0.073 -3.891,0.145 -5.822,0.231c-0.115,-0.749 -0.346,-2.262 -0.461,-3.012c-0.303,0.231 -0.908,0.692 -1.21,0.923c-0.62,-0.202 -1.873,-0.62 -2.507,-0.835c-2.32,-0.563 -2.81,0.043 -1.47,1.816c-1.182,0.101 -2.378,0.187 -3.545,0.244c-1.052,-1.225 -2.104,-2.436 -3.098,-3.675c0.519,-0.158 1.585,-0.475 2.118,-0.634l-0.014,-1.067c0.274,-1.917 -0.605,-3.112 -2.651,-3.602l1.21,-0.562c0.346,-0.275 1.037,-0.837 1.383,-1.11c0.749,-2.781 -2.752,-4.121 -4.193,-6.038c-0.749,1.513 -1.902,2.565 -3.444,3.128c-1.311,-2.695 -3.804,-4.439 -5.951,-6.399c-0.159,0.707 -0.476,2.118 -0.634,2.825l0.447,0.879c0.029,0.475 0.101,1.412 0.13,1.873c-0.389,0.707 -1.167,2.118 -1.556,2.825c-0.231,-0.908 -0.706,-2.695 -0.951,-3.589c-0.764,0.101 -2.32,0.303 -3.098,0.404c-0.014,-0.375 -0.058,-1.11 -0.086,-1.47c0.965,0.086 2.896,0.288 3.862,0.375c-1.527,-2.276 -3.069,-4.538 -4.553,-6.829c0.965,0.23 2.911,0.663 3.876,0.893c-0.317,-1.037 -0.965,-3.112 -1.282,-4.149c0.677,0 2.061,0.015 2.752,0.015c1.412,-3.891 4.813,-6.456 7.868,-9.006l1.528,0.777l0.216,1.672c-0.605,0.475 -1.816,1.426 -2.421,1.902l-0.187,0.145l0.058,0.173c0.043,3.415 2.32,-0.49 3.43,-0.648c0.49,0.043 1.484,0.129 1.989,0.173l0.259,0.547l0.692,1.557c0.879,-0.749 1.772,-1.485 2.666,-2.234c0.62,0.101 1.873,0.303 2.507,0.389l0.418,0.13l0.144,-0.073Zm13.79,-3.343c0.663,0.808 0.663,0.808 0,0Zm-8.545,2.911c0.749,0.749 0.749,0.749 0,0Zm-20.347,2.954c0.778,0.793 0.778,0.793 0,0Zm17.436,-0.057c0.778,0.72 0.778,0.72 0,0Zm5.706,-0.015c0.721,0.749 0.721,0.749 0,0Zm-20.087,2.91c0.735,0.764 0.735,0.764 0,0Zm5.706,0.015c0.749,0.75 0.749,0.75 0,0Zm5.591,-0.244c4.294,-2.263 0.303,4.755 0,0Zm5.908,0.202c0.721,0.734 0.721,0.734 0,0Zm-21.024,2.521c0.418,2.753 2.651,4.237 5.043,5.217c-0.13,-1.239 -0.389,-3.747 -0.519,-5c-1.138,-0.057 -3.386,-0.158 -4.525,-0.216Zm6.6,0.49c0.778,0.692 0.778,0.692 0,0Zm5.692,-0.316c4.366,-2.133 0.115,4.769 0,0Zm-2.868,2.968c4.366,-2.091 0.072,4.769 0,0Zm2.752,2.867c4.294,-2.263 0.259,4.769 0,0Zm3.084,8.819c0.764,0.749 0.764,0.749 0,0Z" style="fill:#696768;fill-rule:nonzero;"/><path d="M648.671,520.78c5.435,-1.936 2.226,5.985 -0.549,1.128l0.549,-1.128Z" style="fill:#eb9ec1;fill-rule:nonzero;"/><path d="M519.385,536.311c1.744,-5.619 3.948,-13.847 11.067,-14.222c11.341,-0.721 24.021,-0.995 33.776,5.777c-5.388,0 -10.418,2.089 -14.726,5.202l1.729,-0.648c-1.873,2.118 -3.905,4.077 -6.095,5.864c0.389,-1.628 0.749,-3.242 1.109,-4.856c-9.092,-1.34 -18.127,0.418 -26.86,2.882Z" style="fill:#383737;fill-rule:nonzero;"/><path d="M408.302,523.574c4.799,0.145 -2.183,4.351 0,0Z" style="fill:#2c2b2c;fill-rule:nonzero;"/><path d="M414.525,523.977c0.838,0.766 0.838,0.766 0,0Z" style="fill:#807e7f;fill-rule:nonzero;"/><path d="M445.954,523.617c0.665,0.809 0.665,0.809 0,0Z" style="fill:#2a292a;fill-rule:nonzero;"/><path d="M445.621,526.442c2.003,-1.109 4.02,-2.205 6.038,-3.286c0.98,2.594 1.844,5.232 2.767,7.853c-1.326,1.47 -2.565,3.012 -3.804,4.554c-3.184,-1.096 -4.582,-4.424 -6.484,-6.917l-0.331,-0.533c0.447,-0.418 1.354,-1.254 1.816,-1.672Z" style="fill:#597a9b;fill-rule:nonzero;"/><path d="M497.971,532.177c0.908,-2.853 1.787,-5.706 2.824,-8.501c2.623,2.32 5.577,4.237 8.689,5.836c-0.836,1.542 -0.821,4.38 -3.257,4.279c-1.081,-1.08 -2.306,-1.931 -3.574,-2.723c-0.677,0.461 -2.017,1.397 -2.695,1.858c-0.504,-0.187 -1.484,-0.562 -1.989,-0.749Z" style="fill:#d0ced0;fill-rule:nonzero;"/><path d="M471.804,525.794c3.04,-0.202 6.11,-0.36 9.165,-0.562c0.072,3.4 -2.363,5.59 -5.159,7.018c-2.32,1.34 -4.683,2.579 -6.917,4.02c-0.346,-1.845 -0.663,-3.69 -0.98,-5.52c-3.184,1.902 -6.456,3.791 -10.202,4.237c4.121,-3.862 8.934,-6.888 14.093,-9.193Z" style="fill:#516c86;fill-rule:nonzero;"/><path d="M411.213,526.312c1.894,-1.821 3.151,1.879 1.749,3.051c-1.72,1.575 -3.021,-1.951 -1.749,-3.051Z" style="fill:#898789;fill-rule:nonzero;"/><path d="M433.748,555.302c0.634,0.216 1.888,0.635 2.507,0.837c-0.533,0.691 -1.599,2.06 -2.133,2.752c0.821,-0.13 2.45,-0.389 3.257,-0.519c0.677,2.118 1.427,4.237 2.536,6.183c2.277,2.723 4.698,5.346 7.032,8.026c-0.389,1.254 -0.778,2.508 -1.167,3.776c4.092,3.53 7.796,7.492 10.995,11.844c-0.807,0.835 -1.599,1.685 -2.406,2.535c0.706,0.216 2.089,0.649 2.781,0.879c3.631,5.274 8.761,9.54 11.067,15.692l-0.13,-0.086c0.058,0.086 0.187,0.246 0.245,0.317c0.576,0.734 1.715,2.205 2.277,2.939l0.072,0.116l-1.038,1.153c-0.461,0.418 -1.354,1.254 -1.816,1.672c-0.216,1.816 -0.375,3.659 -0.504,5.504c-0.605,-0.606 -1.816,-1.786 -2.406,-2.377l-0.432,-0.49l0.101,0.231c-0.663,0.562 -1.989,1.685 -2.637,2.247l-0.418,0.505l0.259,0.432l0.634,-0.143c0.548,0.606 1.614,1.83 2.147,2.449l0.288,0.375l0.346,0.303c0.634,0.635 1.902,1.902 2.536,2.522c0.043,0.072 0.144,0.216 0.202,0.288c-0.403,0.505 -1.21,1.513 -1.599,2.017c-1.499,-0.446 -4.51,-1.311 -6.009,-1.758c0.058,0.648 0.159,1.96 0.216,2.623c-0.634,-0.721 -1.888,-2.133 -2.522,-2.84c0.893,-1.023 1.772,-2.045 2.623,-3.083c-1.931,-0.938 -3.43,-2.32 -4.453,-4.165c-0.793,1.153 -1.571,2.32 -2.349,3.488c-3.905,-1.283 -1.47,-3.804 0.663,-5.39c-4.049,1.067 -3.761,-4.611 -6.845,-5.937c2.824,-0.995 4.626,-3.257 6.11,-5.692c-2.406,-3.011 -5.778,-3.573 -9.395,-2.708c0.331,-3.242 -1.873,-4.193 -4.568,-4.382c-0.548,-0.59 -1.643,-1.801 -2.19,-2.392c0.461,1.34 1.383,4.035 1.845,5.39c-2.507,-1.167 -4.539,-3.069 -6.528,-4.957c0.706,-0.62 2.104,-1.859 2.796,-2.478l0.072,0.101c0.101,-0.13 0.317,-0.389 0.418,-0.519c0.649,-0.648 1.96,-1.931 2.608,-2.579l0.375,0.389l0,-0.749c-0.014,-0.606 -0.014,-1.801 -0.029,-2.407c-1.571,-1.873 -3.271,-3.631 -4.943,-5.39c0.634,-0.533 1.917,-1.614 2.551,-2.146c-1.845,-2.205 -3.732,-4.367 -5.649,-6.485c0.418,-1.065 0.85,-2.117 1.282,-3.169c-1.455,0.446 -2.911,0.907 -4.366,1.368c0.432,-1.369 0.922,-2.695 1.383,-4.035c-3.242,0.404 -6.484,-0.143 -9.539,-1.225c-2.493,-2.622 3.329,-4.51 4.755,-6.326c0.331,-2.176 1.7,-2.998 4.078,-2.464c1.167,-0.058 2.363,-0.145 3.545,-0.246c0.36,-0.446 1.11,-1.353 1.47,-1.816Zm-2.277,5.505c4.38,-2.118 0.086,4.769 0,0Zm-2.824,2.983c4.352,-2.148 0.173,4.769 0,0Zm5.865,-0.116c4.453,-1.931 -0.115,4.769 0,0Zm-2.94,3.069c4.309,-2.234 0.216,4.769 0,0Zm5.706,-0.13c4.265,-2.291 0.303,4.784 0,0Zm2.911,2.911c4.309,-2.219 0.187,4.769 0,0Zm2.867,2.882c4.309,-2.263 0.274,4.755 0,0Zm-2.752,2.998c4.208,-2.392 0.418,4.769 0,0Zm5.634,5.649c4.323,-2.191 0.202,4.784 0,0Zm2.925,2.881c4.208,-2.405 0.432,4.756 0,0Zm-7.392,3.113c0.749,0.734 0.749,0.734 0,0Zm4.496,-0.13c4.179,-2.464 0.519,4.741 0,0Zm5.879,-0.114c4.294,-2.234 0.245,4.769 0,0Zm-8.646,2.881c4.438,-2.003 -0.029,4.771 0,0Zm-1.772,3.229c0.764,0.734 0.764,0.734 0,0Zm2.868,2.796c0.749,0.734 0.749,0.734 0,0Zm5.937,0.043c0.735,0.749 0.735,0.749 0,0Zm2.954,2.767c0.735,0.749 0.735,0.749 0,0Zm5.678,0.086c0.735,0.721 0.735,0.721 0,0Zm-2.853,2.825c0.793,0.692 0.793,0.692 0,0Zm5.764,0.072c0.764,0.734 0.764,0.734 0,0Zm-2.882,2.825c0.663,0.806 0.663,0.806 0,0Zm-2.896,2.895c0.793,0.692 0.793,0.692 0,0Zm5.778,-0.028c0.692,0.792 0.692,0.792 0,0Zm-8.617,2.982c0.764,0.721 0.764,0.721 0,0Zm5.807,-0.057c0.735,0.734 0.735,0.734 0,0Zm5.764,-0.043c0.735,0.764 0.735,0.764 0,0Zm-8.732,3.04c0.778,0.72 0.778,0.72 0,0Zm5.764,-0.072c0.807,0.691 0.807,0.691 0,0Zm-2.954,2.982c0.778,0.793 0.778,0.793 0,0Zm2.68,8.675c0.36,0.417 0.36,0.417 0,0Z" style="fill:#898789;fill-rule:nonzero;"/><path d="M659.939,562.38c1.426,-2.638 5.808,0.503 6.816,2.132c-2.247,0.534 -5.922,0.448 -6.816,-2.132Z" style="fill:#898789;fill-rule:nonzero;"/><path d="M431.601,526.901c0.13,0.015 0.419,0.044 0.564,0.058l-0.145,0.072l-0.419,-0.13Z" style="fill:#0b0b0b;fill-rule:nonzero;"/><path d="M437.407,526.527c0.752,0.752 0.752,0.752 0,0Z" style="fill:#1b1a1b;fill-rule:nonzero;"/><path d="M443.201,526.009c0.607,0.101 1.821,0.317 2.429,0.433c-0.463,0.42 -1.373,1.258 -1.821,1.677c-0.159,-0.535 -0.462,-1.591 -0.607,-2.11Z" style="fill:#3b3c3f;fill-rule:nonzero;"/><path d="M660.344,526.557c2.312,-0.809 3.15,-0.029 2.529,2.327c-2.356,0.81 -3.209,0.044 -2.529,-2.327Z" style="fill:#788897;fill-rule:nonzero;"/><path d="M408.43,529.727c0.809,0.766 0.809,0.766 0,0Z" style="fill:#737274;fill-rule:nonzero;"/><path d="M434.496,529.425c0.781,0.723 0.781,0.723 0,0Z" style="fill:#1f1e1f;fill-rule:nonzero;"/><path d="M444.136,528.646c1.902,2.493 3.3,5.821 6.484,6.916c2.176,-0.129 4.366,-0.288 6.542,-0.489c-1.369,1.887 -2.752,3.775 -4.136,5.676c0.98,2.767 1.96,5.548 3.069,8.286c-2.853,-1.513 -5.908,-2.594 -8.977,-3.545l0.475,-0.448c2.334,0.576 4.64,1.283 6.96,1.96c-0.893,-1.672 -1.772,-3.328 -2.666,-4.986c0.259,-1.037 0.764,-3.126 1.009,-4.178c-2.882,1.254 -6.239,2.031 -8.3,4.597c-0.922,1.801 -0.086,3.905 -0.101,5.85c-0.245,0.274 -0.749,0.821 -0.994,1.109c-0.159,-2.32 -0.187,-4.654 -0.634,-6.932c-1.859,-1.542 -3.919,-2.781 -5.951,-4.063c2.147,-0.591 4.28,-1.167 6.427,-1.744c0.303,-2.666 0.605,-5.346 0.793,-8.011Z" style="fill:#c4d4ec;fill-rule:nonzero;"/><path d="M629.576,530.133c3.498,-4.293 5.103,4.105 0.26,1.315l-0.26,-1.315Z" style="fill:#e798bc;fill-rule:nonzero;"/><path d="M457.711,534.987c3.747,-0.446 7.018,-2.335 10.202,-4.237c0.317,1.83 0.634,3.675 0.98,5.518c-1.556,1.096 -3.041,2.278 -4.481,3.517c-3.271,2.608 -5.865,5.908 -8.098,9.423l-0.216,-0.173c-1.11,-2.737 -2.089,-5.518 -3.069,-8.285c1.383,-1.902 2.767,-3.791 4.136,-5.678l0.548,-0.086Z" style="fill:#5d82a8;fill-rule:nonzero;"/><path d="M556.115,529.568c4.712,0.49 9.799,0.404 13.934,3.084c4.035,2.276 6.513,6.368 8.387,10.476c-0.764,1.037 -1.527,2.075 -2.291,3.097c-1.009,-9.351 -12.191,-12.204 -19.987,-10.389c-2.348,4.193 -2.824,10.49 1.384,13.718c3.978,3.055 9.309,1.57 13.891,1.397c-5.23,5.908 -16.081,3.66 -18.877,-3.646c-2.853,-5.303 0.101,-11.154 4.208,-14.741c-0.143,-0.534 -0.461,-1.614 -0.619,-2.148l-0.029,-0.85Z" style="fill:#4a4243;fill-rule:nonzero;"/><path d="M628.539,531.022l1.037,-0.893l0.259,1.311c0.591,3.069 0.894,6.196 1.167,9.324c0.835,-0.159 2.508,-0.448 3.343,-0.591c-0.49,1.584 -0.966,3.184 -1.412,4.799c-0.332,0.705 -0.995,2.132 -1.325,2.853c-5.419,1.225 -5.173,7.334 -6.109,11.672c-1.817,-1.412 -3.632,-2.838 -5.505,-4.136c-0.475,0.288 -1.441,0.879 -1.917,1.182c-0.764,0.503 -2.262,1.513 -3.026,2.017c0.288,-2.047 0.634,-4.05 0.878,-6.082c3.099,-0.562 6.643,-0.202 9.353,-2.118c1.858,-3.861 1.557,-8.3 0.936,-12.406c-0.274,-0.995 -0.835,-2.968 -1.109,-3.963c1.138,-0.993 2.291,-1.974 3.429,-2.968Z" style="fill:#9f9fa0;fill-rule:nonzero;"/><path d="M634.837,530.765c2.19,-1.744 4.452,2.176 2.219,3.53c-2.176,1.586 -4.193,-2.146 -2.219,-3.53Z" style="fill:#d189ab;fill-rule:nonzero;"/><path d="M646.61,530.924c0.867,0.737 0.867,0.737 0,0Z" style="fill:#f398bf;fill-rule:nonzero;"/><path d="M405.549,532.161c4.799,0.376 -2.385,4.235 0,0Z" style="fill:#2f2f2f;fill-rule:nonzero;"/><path d="M615.615,582.985c1.786,0.202 3.558,0.402 5.36,0.634c-0.966,1.758 -1.903,3.545 -2.825,5.331c-3.963,4.121 -7.853,8.474 -12.608,11.673c-2.508,0.158 -5.015,0.215 -7.508,0.375c0.62,-0.505 1.859,-1.499 2.478,-2.003c4.294,-3.545 8.589,-7.177 12.061,-11.558c1.052,-1.456 2.061,-2.939 3.041,-4.452Z" style="fill:#2f2f2f;fill-rule:nonzero;"/><path d="M420.116,532.32c0.737,0.766 0.737,0.766 0,0Z" style="fill:#212121;fill-rule:nonzero;"/><path d="M425.822,532.335c0.752,0.752 0.752,0.752 0,0Z" style="fill:#202021;fill-rule:nonzero;"/><path d="M431.413,532.09c4.308,-2.27 0.304,4.771 0,0Z" style="fill:#3b3b3c;fill-rule:nonzero;"/><path d="M461.76,613.045c0.81,0.693 0.81,0.693 0,0Z" style="fill:#3b3b3c;fill-rule:nonzero;"/><path d="M468.892,536.269c2.234,-1.441 4.597,-2.68 6.917,-4.02c-0.259,1.138 -0.49,2.276 -0.72,3.429c2.637,2.508 4.971,5.318 6.859,8.444c-0.533,0.793 -1.599,2.377 -2.147,3.17c-2.738,-3.286 -7.061,-6.398 -11.427,-4.077c-4.15,1.801 -4.136,6.657 -4.51,10.49l-0.677,-0.475c-2.032,-0.216 -4.078,-0.288 -6.11,-0.36c0.49,-0.404 1.455,-1.197 1.945,-1.6c3.631,-2.666 4.237,-7.464 5.389,-11.484c1.441,-1.239 2.925,-2.42 4.481,-3.516Z" style="fill:#4c4b4c;fill-rule:nonzero;"/><path d="M551.233,532.422c1.628,-0.705 3.257,-1.355 4.913,-2.003c0.159,0.533 0.475,1.614 0.62,2.148c-4.107,3.587 -7.06,9.438 -4.208,14.741c2.796,7.305 13.645,9.553 18.877,3.646c1.7,-1.441 3.213,-3.069 4.712,-4.727c0.764,-1.023 1.527,-2.06 2.291,-3.099c0.244,3.041 0.331,6.082 0.49,9.136l-1.441,-1.21c-4.784,1.829 -8.228,6.844 -13.777,6.513c-5.994,0.749 -11.7,-3.214 -14.121,-8.516c-1.225,-3.372 -1.34,-7.003 -1.859,-10.506c-3.631,3.675 -5.72,8.516 -4.711,13.748c-0.404,-1.542 -0.765,-3.099 -1.081,-4.655c0.043,-1.902 0.173,-3.804 0.389,-5.691c0.893,-1.254 1.843,-2.465 2.809,-3.66c2.191,-1.786 4.222,-3.747 6.096,-5.865Z" style="fill:#2f2d2d;fill-rule:nonzero;"/><path d="M519.386,536.312c8.732,-2.464 17.767,-4.222 26.859,-2.882c-0.36,1.614 -0.72,3.229 -1.109,4.857c-0.966,1.195 -1.917,2.407 -2.81,3.659c-7.479,-0.288 -15.173,0.865 -21.845,4.382c-2.061,1.067 -1.254,3.934 -1.83,5.792l-1.182,0.072c0.375,-5.317 0.793,-10.662 1.917,-15.879Z" style="fill:#515151;fill-rule:nonzero;"/><path d="M475.853,598.159c1.427,-0.518 2.853,-1.023 4.28,-1.541c0.303,4.726 1.196,10.778 7.09,11.196c-1.081,1.873 -1.974,3.89 -2.291,6.051c-1.801,-2.708 -3.674,-5.36 -5.75,-7.867c-1.124,-2.608 -2.233,-5.217 -3.329,-7.84Z" style="fill:#515151;fill-rule:nonzero;"/><path d="M692.274,533.761c0.781,0.781 0.781,0.781 0,0Z" style="fill:#49677f;fill-rule:nonzero;"/><path d="M402.68,535.303c0.752,0.708 0.752,0.708 0,0Z" style="fill:#333;fill-rule:nonzero;"/><path d="M476.098,627.439c0.795,0.695 0.795,0.695 0,0Z" style="fill:#333;fill-rule:nonzero;"/><path d="M416.298,534.813c1.138,0.058 3.386,0.159 4.525,0.216c0.13,1.254 0.389,3.761 0.519,5c-2.392,-0.98 -4.626,-2.465 -5.043,-5.217Z" style="fill:#141516;fill-rule:nonzero;"/><path d="M422.898,535.303c0.781,0.693 0.781,0.693 0,0Z" style="fill:#2e2e2f;fill-rule:nonzero;"/><path d="M428.59,534.988c4.38,-2.14 0.116,4.784 0,0Z" style="fill:#434344;fill-rule:nonzero;"/><path d="M441.645,618.708c0.723,0.765 0.723,0.765 0,0Z" style="fill:#434344;fill-rule:nonzero;"/><path d="M497.685,534.769c2.133,0.923 4.294,1.859 5.894,3.603c0.98,-1.153 1.945,-2.349 2.954,-3.473c1.7,5.173 1.527,10.664 2.161,16.009c0.058,0.72 0.187,2.161 0.245,2.867c-0.389,0.158 -1.167,0.446 -1.556,0.591c-0.634,0.215 -1.888,0.648 -2.507,0.878c0.231,-0.821 0.706,-2.449 0.937,-3.257c-3.041,-1.642 -6.167,-3.156 -9.352,-4.509c0.029,-4.266 0.562,-8.503 1.225,-12.71Z" style="fill:#aeabac;fill-rule:nonzero;"/><path d="M556.159,535.836c7.795,-1.816 18.978,1.037 19.986,10.39c-1.498,1.656 -3.011,3.285 -4.712,4.726c-4.582,0.174 -9.913,1.658 -13.891,-1.397c-4.208,-3.227 -3.732,-9.525 -1.383,-13.718Zm1.7,6.082c1.426,6.023 9.151,9.64 14.497,6.167c-3.646,-4.006 -8.804,-7.536 -14.497,-6.167Z" style="fill:#7c7475;fill-rule:nonzero;"/><path d="M639.678,536.787c0.649,-0.145 1.946,-0.404 2.594,-0.533l1.11,-0.231c3.198,-0.332 4.222,3.617 3.271,5.951c-1.327,1.008 -2.638,2.045 -3.934,3.097c-0.13,0.995 -0.389,2.998 -0.519,4.007l-0.044,1.944l-0.23,-1.296c-0.072,-0.317 -0.202,-0.966 -0.274,-1.283c-1.672,-0.562 -3.33,-1.124 -4.971,-1.7c-0.174,-0.707 -0.534,-2.133 -0.707,-2.838c0.116,-0.721 0.332,-2.133 0.446,-2.84l0.793,0.865c0.966,1.239 2.276,2.104 3.717,2.767c-1.325,-2.348 -2.579,-4.812 -1.643,-7.565l0.389,-0.345Z" style="fill:#d6e8f8;fill-rule:nonzero;"/><path d="M405.433,537.955c4.799,0.216 -2.226,4.322 0,0Z" style="fill:#2c2c2c;fill-rule:nonzero;"/><path d="M419.971,552.41c4.25,-2.356 0.361,4.784 0,0Z" style="fill:#2c2c2c;fill-rule:nonzero;"/><path d="M425.722,537.955c4.38,-2.096 0.072,4.785 0,0Z" style="fill:#404041;fill-rule:nonzero;"/><path d="M461.774,607.193c0.694,0.796 0.694,0.796 0,0Z" style="fill:#404041;fill-rule:nonzero;"/><path d="M578.538,537.838c2.638,1.34 5.36,2.737 7.191,5.144c5.087,6.355 9.553,13.388 12.061,21.183c2.176,5.505 -2.666,10.245 -6.139,13.891c-1.643,-10.201 -5.403,-20.044 -11.844,-28.199c0.692,-4.064 0.072,-8.156 -1.268,-12.018Z" style="fill:#363635;fill-rule:nonzero;"/><path d="M615.442,538.286c3.587,0.606 7.218,0.389 10.778,-0.331c-0.417,2.247 -0.806,4.51 -1.195,6.772c-2.911,0.058 -5.836,0.13 -8.733,0.216c-0.158,-1.599 -0.345,-3.185 -0.562,-4.755c-0.072,-0.475 -0.216,-1.428 -0.288,-1.903Z" style="fill:#d2d4d6;fill-rule:nonzero;"/><path d="M626.22,537.955c0.62,4.107 0.922,8.545 -0.936,12.406c-2.709,1.917 -6.254,1.557 -9.353,2.118c0.231,-2.508 0.361,-5.028 0.361,-7.536c2.895,-0.086 5.821,-0.158 8.733,-0.216c0.388,-2.262 0.777,-4.525 1.195,-6.772Z" style="fill:#bababc;fill-rule:nonzero;"/><path d="M663.946,538.848c3.574,-1.802 5.72,4.481 1.873,5.317c-3.328,1.153 -5.014,-4.237 -1.873,-5.317Z" style="fill:#a5c9eb;fill-rule:nonzero;"/><path d="M444.598,542.435c2.061,-2.565 5.418,-3.343 8.3,-4.597c-0.245,1.052 -0.749,3.141 -1.009,4.178c0.893,1.658 1.772,3.315 2.666,4.986c-2.32,-0.677 -4.626,-1.383 -6.96,-1.959l-0.476,0.446c-0.648,0.707 -1.974,2.089 -2.623,2.796c0.014,-1.946 -0.821,-4.05 0.101,-5.85Z" style="fill:#5172b3;fill-rule:nonzero;"/><path d="M428.475,540.823c4.308,-2.268 0.26,4.785 0,0Z" style="fill:#414041;fill-rule:nonzero;"/><path d="M456.314,549.207c2.234,-3.516 4.827,-6.815 8.098,-9.423c-1.153,4.02 -1.758,8.819 -5.389,11.484c-0.677,-0.519 -2.032,-1.542 -2.709,-2.061Z" style="fill:#141719;fill-rule:nonzero;"/><path d="M647.143,545.345c-0.475,-3.04 3.011,-5.75 5.807,-4.452c2.276,0.634 2.55,3.141 3.126,5.043c-2.636,2.377 -5.85,4.222 -9.15,1.744c0.057,-0.576 0.173,-1.744 0.216,-2.335Z" style="fill:#bed9f2;fill-rule:nonzero;"/><path d="M520.482,546.325c6.672,-3.516 14.367,-4.668 21.846,-4.38c-0.216,1.887 -0.347,3.791 -0.389,5.692c-6.557,0.101 -12.912,1.685 -18.964,4.149c-1.081,0.088 -3.242,0.26 -4.323,0.332c0.576,-1.859 -0.23,-4.727 1.83,-5.793Z" style="fill:#656565;fill-rule:nonzero;"/><path d="M557.859,541.916c5.692,-1.368 10.851,2.162 14.497,6.168c-5.346,3.473 -13.07,-0.145 -14.497,-6.168Z" style="fill:#c1bdbe;fill-rule:nonzero;"/><path d="M631.609,547.834c0.332,-0.723 0.997,-2.154 1.329,-2.862c-0.044,0.622 -0.159,1.894 -0.216,2.53l-1.113,0.332Z" style="fill:#eb88b3;fill-rule:nonzero;"/><path d="M633.15,547.911c0.705,-0.995 2.118,-3.012 2.824,-4.006c0.173,0.705 0.534,2.133 0.707,2.838c1.643,0.576 3.299,1.138 4.971,1.7c0.072,0.317 0.202,0.966 0.274,1.283c-1.109,3.027 -2.32,6.009 -3.488,9.006c2.594,0.433 5.173,0.879 7.767,1.384l-0.446,-1.47c-1.83,-0.576 -3.602,-1.197 -5.259,-2.133c1.037,-1.715 1.599,-3.545 1.658,-5.489l0.043,-1.946c3.069,-0.246 6.24,0.303 8.689,2.291c1.542,1.7 -0.086,3.026 -1.267,4.279c-0.865,3.113 -1.441,6.326 -2.724,9.309c-1.355,-1.094 -2.651,-2.247 -3.89,-3.458c-0.995,3.084 -3.992,6.124 -1.658,9.381c-2.666,-0.461 -5.317,-0.893 -7.969,-1.296c0.231,1.296 0.461,2.622 0.692,3.934c-1.21,0.215 -3.646,0.619 -4.87,0.821c0.995,-2.824 1.917,-5.678 2.478,-8.617c1.974,0.375 3.818,1.239 5.591,2.219c0.057,-0.677 0.187,-2.047 0.244,-2.724c-2.535,1.153 -4.251,0.462 -5.144,-2.06c-0.721,0.158 -2.176,0.461 -2.911,0.606c0.347,-4.569 2.781,-8.718 2.724,-13.329c2.075,-0.159 3.732,0.634 4.957,2.377c0.49,-0.576 1.485,-1.729 1.974,-2.306c-1.917,-0.894 -6.109,0.576 -5.965,-2.594Zm13.113,4.496c-1.946,0.936 -2.061,1.946 -0.317,3.027c1.946,-0.952 2.047,-1.96 0.317,-3.027Zm-11.758,2.436c-0.778,2.075 -0.159,2.767 1.843,2.075c0.808,-2.075 0.187,-2.767 -1.843,-2.075Zm-2.219,4.467c-0.606,1.96 0.116,2.651 2.161,2.075c0.62,-2.003 -0.101,-2.693 -2.161,-2.075Zm4.467,1.557c-0.606,1.917 0.116,2.666 2.176,2.262c0.649,-1.959 -0.072,-2.709 -2.176,-2.262Z" style="fill:#d475a2;fill-rule:nonzero;"/><path d="M646.081,548.254c-2.717,-0.043 -0.954,-4.74 1.069,-2.905c-0.043,0.592 -0.158,1.764 -0.216,2.342l-0.853,0.563Z" style="fill:#b06889;fill-rule:nonzero;"/><path d="M408.603,546.817c0.766,0.709 0.766,0.709 0,0Z" style="fill:#2d2d2e;fill-rule:nonzero;"/><path d="M411.528,549.613c0.708,0.752 0.708,0.752 0,0Z" style="fill:#2d2d2e;fill-rule:nonzero;"/><path d="M453.13,638.852c0.81,0.853 0.81,0.853 0,0Z" style="fill:#2d2d2e;fill-rule:nonzero;"/><path d="M420.119,546.498c4.842,0.26 -2.27,4.307 0,0Z" style="fill:#696869;fill-rule:nonzero;"/><path d="M496.46,547.481c3.185,1.353 6.311,2.867 9.352,4.509c-0.231,0.808 -0.706,2.436 -0.937,3.257c-2.493,0.938 -4.914,2.047 -7.248,3.33c-0.548,-3.69 -1.052,-7.378 -1.167,-11.095Z" style="fill:#989697;fill-rule:nonzero;"/><path d="M522.972,551.788c6.052,-2.465 12.406,-4.05 18.963,-4.15c0.317,1.557 0.677,3.112 1.081,4.654c0.202,0.721 0.604,2.177 0.821,2.897c-3.069,0.015 -6.139,0.057 -9.208,-0.086c-3.833,-1.268 -7.709,-2.478 -11.657,-3.314Z" style="fill:#777879;fill-rule:nonzero;"/><path d="M416.932,549.512c4.813,0.058 -2.096,4.395 0,0Z" style="fill:#292929;fill-rule:nonzero;"/><path d="M431.559,549.639c0.766,0.752 0.766,0.752 0,0Z" style="fill:#1c1c1d;fill-rule:nonzero;"/><path d="M479.874,552.609c3.458,0.736 6.96,0.303 10.159,-1.21c1.527,3.213 2.68,6.57 3.631,10.001c-2.81,1.988 -5.231,4.423 -7.565,6.945c-0.663,-0.116 -1.989,-0.317 -2.666,-0.418c0.548,-0.835 1.095,-1.671 1.643,-2.493c-1.455,-3.905 -3.343,-7.738 -6.845,-10.216c0.533,-0.879 1.081,-1.744 1.643,-2.608Z" style="fill:#484849;fill-rule:nonzero;"/><path d="M455.998,607.222c0.795,0.695 0.795,0.695 0,0Z" style="fill:#484849;fill-rule:nonzero;"/><path d="M579.015,553.446c0.259,-1.211 0.533,-2.407 0.792,-3.589c6.442,8.156 10.203,17.998 11.846,28.201c-4.281,4.092 -9.151,7.637 -14.669,9.842c0.345,-8.084 0.143,-18.545 -7.148,-23.632c4.641,-2.882 6.067,-8.358 7.653,-13.214l1.44,1.21l0.088,1.182Z" style="fill:#525352;fill-rule:nonzero;"/><path d="M425.477,552.48c0.564,-0.116 1.706,-0.376 2.269,-0.491l0.014,1.07c-0.535,0.158 -1.604,0.477 -2.125,0.636l-0.159,-1.215Z" style="fill:#818082;fill-rule:nonzero;"/><path d="M646.268,552.407c1.735,1.07 1.634,2.081 -0.317,3.036c-1.749,-1.084 -1.634,-2.096 0.317,-3.036Z" style="fill:#c8cbe7;fill-rule:nonzero;"/><path d="M653.036,552.78c2.089,-1.541 4.294,1.975 2.262,3.387c-2.176,1.845 -4.668,-2.06 -2.262,-3.387Z" style="fill:#985071;fill-rule:nonzero;"/><path d="M463.186,553.228l0.677,0.475c3.084,2.478 5.922,5.23 8.804,7.94c1.83,-2.176 3.675,-4.309 5.562,-6.427c3.502,2.478 5.389,6.311 6.845,10.216c-0.548,0.822 -1.095,1.658 -1.643,2.493c0.677,0.101 2.003,0.303 2.666,0.418c-2.464,3.791 -4.496,7.868 -5.937,12.162c-2.248,-1.671 -4.208,-3.688 -5.764,-6.023c-0.432,-0.288 -1.297,-0.865 -1.744,-1.138c-0.706,-6.023 -5.317,-10.245 -9.121,-14.525c-0.014,-0.49 -0.043,-1.485 -0.058,-1.99c-0.879,-0.158 -2.637,-0.475 -3.516,-0.634c1.066,-0.995 2.147,-1.988 3.228,-2.968Zm17.09,6.24c4.813,0.028 -2.075,4.38 0,0Z" style="fill:#595758;fill-rule:nonzero;"/><path d="M534.631,555.103c3.069,0.145 6.139,0.101 9.208,0.086c3.429,6.269 9.842,10.693 17.047,11.11c5.159,6.917 5.836,15.606 5.518,23.935c-1.166,0.13 -3.516,0.404 -4.683,0.534c-1.355,-6.801 -3.602,-13.459 -7.363,-19.323c-2.478,-4.641 -6.08,-8.791 -11.125,-10.664c1.801,2.983 3.934,5.793 5.476,8.934c-0.303,2.19 -2.306,3.602 -3.473,5.36l-0.518,-0.692c-1.153,-1.485 -2.276,-2.925 -3.502,-4.294c0.75,-1.773 2.724,-3.502 1.788,-5.577c-2.061,-3.703 -5.36,-6.513 -8.373,-9.41Z" style="fill:#949495;fill-rule:nonzero;"/><path d="M618.077,556.543c0.475,-0.303 1.441,-0.894 1.917,-1.182c1.874,1.298 3.69,2.724 5.505,4.136c-0.648,3.257 -1.412,6.643 -3.444,9.353c-2.968,1.109 -6.21,0.547 -9.294,0.663c0.072,-2.249 0.143,-4.482 0.288,-6.7c0.331,-1.009 0.98,-2.998 1.296,-3.992c1.614,1.384 3.214,2.767 4.828,4.165c0.98,-0.404 1.974,-0.808 2.968,-1.197c-0.332,-1.195 -0.663,-2.377 -0.98,-3.545c-0.764,-0.432 -2.306,-1.282 -3.084,-1.7Z" style="fill:#848384;fill-rule:nonzero;"/><path d="M634.505,554.843c2.039,-0.693 2.66,0 1.85,2.083c-2.009,0.693 -2.631,0 -1.85,-2.083Z" style="fill:#35212b;fill-rule:nonzero;"/><path d="M675.009,556.976c4.222,-3.271 8.516,4.265 4.05,6.269c-3.862,2.767 -7.868,-3.805 -4.05,-6.269Z" style="fill:#77b1e1;fill-rule:nonzero;"/><path d="M650.124,559.339c0.446,-3.458 3.214,-2.478 5.116,-0.576c1.239,0.606 4.826,-0.547 3.573,2.104c-1.282,2.249 -2.535,4.626 0.086,6.471l1.024,-1.729c0.461,0.619 1.397,1.887 1.858,2.508c-2.464,0.215 -4.957,-0.015 -7.378,-0.448c-0.345,-0.404 -1.037,-1.21 -1.383,-1.599l-0.159,-0.98c0.793,-0.591 2.393,-1.757 3.185,-2.348c-1.887,-1.211 -4.971,-1.254 -5.922,-3.401Z" style="fill:#aa537b;fill-rule:nonzero;"/><path d="M480.277,559.467c4.828,0.029 -2.081,4.395 0,0Z" style="fill:#6f9dc4;fill-rule:nonzero;"/><path d="M632.286,559.311c2.067,-0.622 2.789,0.072 2.168,2.081c-2.052,0.578 -2.774,-0.116 -2.168,-2.081Z" style="fill:#3e2632;fill-rule:nonzero;"/><path d="M431.471,560.81c4.394,-2.125 0.087,4.784 0,0Z" style="fill:#5a595a;fill-rule:nonzero;"/><path d="M443.199,589.687c4.452,-2.009 -0.029,4.785 0,0Z" style="fill:#5a595a;fill-rule:nonzero;"/><path d="M493.664,561.398l1.225,-0.893c-5.778,8.415 -13.243,16.887 -13.012,27.768c1.902,0.389 3.689,-0.663 5.505,-1.081c-0.115,0.547 -0.36,1.658 -0.476,2.219c-0.504,2.276 -1.037,4.538 -1.556,6.816c-1.744,0.129 -3.487,0.259 -5.216,0.389c-0.086,-5.36 -0.749,-10.765 0.029,-16.11c1.441,-4.294 3.473,-8.373 5.937,-12.162c2.334,-2.522 4.755,-4.957 7.565,-6.946Z" style="fill:#c2c2c4;fill-rule:nonzero;"/><path d="M543.233,560.78c5.043,1.873 8.646,6.023 11.125,10.664c-0.029,2.636 0.303,5.287 1.34,7.752c-2.32,0.331 -6.456,-2.104 -7.018,1.368c-1.067,-1.887 -2.219,-3.717 -3.444,-5.489c1.167,-1.758 3.17,-3.17 3.473,-5.36c-1.542,-3.142 -3.675,-5.951 -5.476,-8.934Z" style="fill:#757475;fill-rule:nonzero;"/><path d="M636.753,560.866c2.11,-0.448 2.833,0.304 2.182,2.27c-2.067,0.405 -2.789,-0.347 -2.182,-2.27Z" style="fill:#39232e;fill-rule:nonzero;"/><path d="M684.232,568.286c-4.958,-1.369 -1.369,-8.588 2.781,-6.917c4.971,1.34 2.262,9.669 -2.753,7.234c0.635,3.227 2.205,8.386 -2.306,9.423c-5.375,1.527 -7.666,-6.009 -3.242,-8.617c1.946,0.49 3.848,-0.244 5.52,-1.124Zm-3.444,4.395c0.734,0.764 0.734,0.764 0,0Z" style="fill:#5ea6db;fill-rule:nonzero;"/><path d="M698.341,560.895c4.38,-2.168 0.159,4.771 0,0Z" style="fill:#315b76;fill-rule:nonzero;"/><path d="M649.104,561.919c0.622,0.159 1.865,0.477 2.501,0.636l0.376,1.199l-0.434,1.041c-0.622,-0.058 -1.865,-0.174 -2.486,-0.231l-0.664,-1.344l0.708,-1.302Z" style="fill:#ef6fa9;fill-rule:nonzero;"/><path d="M428.645,563.793c4.365,-2.154 0.173,4.784 0,0Z" style="fill:#616061;fill-rule:nonzero;"/><path d="M434.511,563.676c4.467,-1.938 -0.116,4.784 0,0Z" style="fill:#636163;fill-rule:nonzero;"/><path d="M437.278,566.618c4.279,-2.299 0.304,4.799 0,0Z" style="fill:#636163;fill-rule:nonzero;"/><path d="M566.131,565.578c0.922,-0.332 2.781,-0.98 3.703,-1.311c7.292,5.087 7.493,15.547 7.148,23.632c-1.946,0.619 -3.891,1.153 -5.85,1.599c0.446,-8.285 -0.49,-16.744 -5,-23.92Z" style="fill:#717172;fill-rule:nonzero;"/><path d="M642.632,567.517c-0.895,-2.27 2.79,-3.787 3.528,-1.344c-0.55,0.491 -1.677,1.474 -2.226,1.966l-1.302,-0.622Z" style="fill:#f06da8;fill-rule:nonzero;"/><path d="M409.944,572.048c4.611,0.303 5.36,-7.594 9.957,-5.893c3.055,1.08 6.297,1.628 9.539,1.225c-0.461,1.34 -0.951,2.666 -1.383,4.035c1.455,-0.461 2.911,-0.923 4.366,-1.369c-0.432,1.052 -0.864,2.104 -1.282,3.17c1.917,2.118 3.804,4.279 5.649,6.484c-0.634,0.534 -1.917,1.614 -2.551,2.148c1.671,1.758 3.372,3.516 4.943,5.39c0.014,0.604 0.014,1.801 0.029,2.405l-0.375,0.361c-0.648,0.648 -1.96,1.931 -2.608,2.579l-0.49,0.417c-0.692,0.62 -2.089,1.859 -2.796,2.48c-2.968,1.642 0.043,2.55 1.931,3.486l-0.721,1.599c1.009,0.303 3.041,0.938 4.049,1.239l-2.406,1.903c-3.069,-2.32 -5.62,-5.246 -8.199,-8.07c-0.202,-1.786 -0.648,-3.516 -1.081,-5.23c0.533,-1.24 1.917,-2.652 0.504,-3.848c-4.366,-4.943 -8.819,-10.794 -15.663,-12.105c-0.36,-0.606 -1.066,-1.801 -1.412,-2.407Zm6.989,-2.464c4.352,-2.19 0.202,4.74 0,0Zm16.009,20.447c0.764,0.721 0.764,0.721 0,0Zm-2.925,2.825c0.677,0.778 0.677,0.778 0,0Z" style="fill:#9b999b;fill-rule:nonzero;"/><path d="M481.879,652.945c4.813,0.449 -2.429,4.149 0,0Z" style="fill:#9b999b;fill-rule:nonzero;"/><path d="M484.513,656.044c4.322,-2.125 0.087,4.842 0,0Z" style="fill:#9b999b;fill-rule:nonzero;"/><path d="M431.571,566.747c4.322,-2.241 0.217,4.784 0,0Z" style="fill:#646364;fill-rule:nonzero;"/><path d="M560.886,566.299c1.758,-0.173 3.502,-0.418 5.245,-0.721c4.51,7.176 5.448,15.635 5,23.92c-1.181,0.189 -3.545,0.549 -4.726,0.736c0.316,-8.329 -0.361,-17.019 -5.52,-23.935Z" style="fill:#808182;fill-rule:nonzero;"/><path d="M647.102,566.731c2.11,-0.679 4.423,1.432 3.253,3.57c-2.039,1.619 -5.233,-1.749 -3.253,-3.57Z" style="fill:#db659c;fill-rule:nonzero;"/><path d="M663.267,567.048c2.104,-1.599 4.18,2.076 2.291,3.502c-2.19,1.845 -4.467,-2.104 -2.291,-3.502Z" style="fill:#9b4a70;fill-rule:nonzero;"/><path d="M692.303,566.76c4.496,-2.983 8.588,4.726 3.89,6.873c-4.265,2.55 -8.084,-4.496 -3.89,-6.873Z" style="fill:#399cd6;fill-rule:nonzero;"/><path d="M615.729,580.723c4.323,-4.136 6.095,-10.893 11.989,-13.243l-0.562,1.773c-0.231,0.777 -0.677,2.319 -0.894,3.097l-0.389,1.197c-1.34,3.486 -3.099,6.801 -4.9,10.073c-1.801,-0.231 -3.573,-0.433 -5.36,-0.635c0.029,-0.562 0.086,-1.685 0.116,-2.262Z" style="fill:#504e4e;fill-rule:nonzero;"/><path d="M416.931,569.587c4.365,-2.198 0.202,4.755 0,0Z" style="fill:#4f4f50;fill-rule:nonzero;"/><path d="M456.128,647.368c4.828,0.376 -2.27,4.265 0,0Z" style="fill:#4f4f50;fill-rule:nonzero;"/><path d="M440.189,569.527c4.322,-2.226 0.188,4.785 0,0Z" style="fill:#626162;fill-rule:nonzero;"/><path d="M430.017,592.857c0.679,0.78 0.679,0.78 0,0Z" style="fill:#626162;fill-rule:nonzero;"/><path d="M627.16,569.253c3.007,-0.333 1.445,4.943 -0.896,3.108c0.216,-0.781 0.665,-2.328 0.896,-3.108Z" style="fill:#905771;fill-rule:nonzero;"/><path d="M641.58,570.723c4.307,-2.284 0.246,4.857 0,0Z" style="fill:#b1517e;fill-rule:nonzero;"/><path d="M443.057,572.41c4.322,-2.27 0.275,4.769 0,0Z" style="fill:#5c5c5d;fill-rule:nonzero;"/><path d="M608.206,574.166c0.692,-0.648 2.104,-1.975 2.796,-2.623c1.6,3.04 3.214,6.082 4.727,9.179c-0.029,0.576 -0.086,1.701 -0.116,2.263c-0.98,1.513 -1.988,2.996 -3.04,4.452c-3.488,-1.426 -6.831,-3.17 -9.957,-5.259c2.348,-2.291 4.107,-5.087 5.59,-8.011Z" style="fill:#e6e6e7;fill-rule:nonzero;"/><path d="M680.788,572.682c0.738,0.766 0.738,0.766 0,0Z" style="fill:#3d5f77;fill-rule:nonzero;"/><path d="M625.875,573.547c1.887,1.325 4.092,1.557 6.368,1.47l-0.547,1.383c-0.85,1.801 -1.167,3.791 -1.34,5.764c-0.332,0.576 -0.995,1.729 -1.312,2.306c-0.173,0.907 -0.503,2.709 -0.663,3.602c-2.247,1.413 -4.351,-1.138 -6.455,-1.729c1.426,3.257 -0.145,6.139 -3.949,5.059l0.173,-2.45c0.923,-1.786 1.859,-3.574 2.825,-5.331c1.801,-3.271 3.559,-6.585 4.9,-10.073Zm-0.707,11.354c2.247,-0.922 1.397,-4.077 2.089,-5.95c-0.029,-0.202 -0.072,-0.591 -0.101,-0.793c-2.075,0.244 -5.188,5.821 -1.988,6.743Z" style="fill:#ce5790;fill-rule:nonzero;"/><path d="M700.891,573.806c2.363,-1.757 5.388,-0.143 4.006,2.911c-1.931,1.729 -5.533,-0.26 -4.006,-2.911Z" style="fill:#1e6891;fill-rule:nonzero;"/><path d="M440.304,575.408c4.221,-2.399 0.419,4.785 0,0Z" style="fill:#626163;fill-rule:nonzero;"/><path d="M455.896,575.421c4.307,-2.284 0.318,4.784 0,0Z" style="fill:#393a3b;fill-rule:nonzero;"/><path d="M686.639,575.477c2.219,-1.816 6.744,-0.648 6.542,2.594c0.966,3.141 -2.752,5.937 -5.533,4.409c-2.853,-1.024 -3.2,-5.144 -1.009,-7.003Z" style="fill:#3b9cd6;fill-rule:nonzero;"/><path d="M631.696,576.4l0.85,-0.029c1.369,2.076 0.044,4.497 -1.441,6.082l-0.749,-0.288c0.173,-1.974 0.49,-3.963 1.34,-5.764Z" style="fill:#bbadd3;fill-rule:nonzero;"/><path d="M632.546,576.372c2.017,-1.584 5.159,1.586 3.717,3.617c-0.894,2.493 -1.225,7.163 -5.188,5.461c0,-0.749 0.015,-2.247 0.029,-2.996c1.484,-1.586 2.809,-4.007 1.441,-6.082Z" style="fill:#c55e94;fill-rule:nonzero;"/><path d="M660.343,577.091c4.769,0.029 -2.053,4.424 0,0Z" style="fill:#492838;fill-rule:nonzero;"/><path d="M664.707,581.386c-2.825,-1.052 0.375,-5.318 2.421,-4.02c1.181,1.599 -0.303,4.553 -2.421,4.02Z" style="fill:#833b61;fill-rule:nonzero;"/><path d="M548.681,580.564c0.562,-3.472 4.698,-1.037 7.017,-1.368c0.347,0.907 1.039,2.752 1.399,3.674c-1.456,1.268 -2.882,2.537 -4.323,3.805c-1.384,-2.032 -2.781,-4.05 -4.092,-6.111Z" style="fill:#e3e2e4;fill-rule:nonzero;"/><path d="M505.998,589.325c2.781,-8.199 12.436,-11.354 20.087,-8.112c1.657,1.7 3.184,3.559 4.424,5.605c-0.519,-0.547 -1.556,-1.658 -2.075,-2.219c-0.893,-0.547 -2.709,-1.656 -3.602,-2.205l0.562,-0.259c-3.502,-0.375 -7.205,-1.138 -10.577,0.332c-4.136,1.383 -6.47,5.317 -9.294,8.329l0.475,-1.47Z" style="fill:#cecfd1;fill-rule:nonzero;"/><path d="M418.26,600.911c1.98,0.823 2.053,1.806 0.246,2.948c-1.937,-0.823 -2.009,-1.806 -0.246,-2.948Z" style="fill:#cecfd1;fill-rule:nonzero;"/><path d="M437.178,604.238c2.507,-1.181 5.288,-1.166 7.997,-1.225c-0.865,2.133 -1.715,4.281 -2.55,6.427c0.937,-0.244 2.81,-0.734 3.746,-0.993c-1.167,3.472 2.205,4.999 4.381,6.829c0.058,0.879 0.173,2.623 0.245,3.502c-1.383,-1.268 -2.594,-2.68 -3.646,-4.222c-0.389,0.764 -1.153,2.276 -1.542,3.026c-0.504,-0.143 -1.528,-0.446 -2.032,-0.606c1.052,-1.368 1.787,-2.895 2.205,-4.567c-1.628,0.635 -3.17,1.456 -4.726,2.219c-0.173,-0.274 -0.519,-0.821 -0.677,-1.096c1.038,-0.835 2.061,-1.671 3.069,-2.535c-2.19,-2.162 -5.144,-3.89 -6.47,-6.759Zm3.026,0.317c4.496,-2.017 -0.216,4.885 0,0Z" style="fill:#cecfd1;fill-rule:nonzero;"/><path d="M434.712,632.209c-0.663,-3.415 3.199,-4.178 5.13,-5.979c-0.836,1.917 -1.542,3.933 -1.614,6.051l-0.072,0.736c-0.591,0.215 -1.772,0.634 -2.363,0.835c-0.072,0.029 -0.23,0.086 -0.303,0.116l0.014,0c0.735,0.576 2.219,1.729 2.968,2.306l0.418,0.331l0.62,-0.23c1.614,0.072 3.17,0.634 4.539,1.498c-4.179,1.21 -0.504,6.801 2.162,3.343c0.908,2.017 1.989,3.949 3.055,5.893c-0.677,0.98 -1.34,1.96 -1.989,2.955c-0.937,2.291 -0.086,3.242 2.551,2.867c-0.086,0.778 -0.259,2.319 -0.331,3.084c0.864,-0.029 2.623,-0.101 3.502,-0.13c-0.519,1.584 -1.556,2.709 -3.113,3.372c-0.36,-0.75 -1.095,-2.234 -1.455,-2.983c-3.761,-0.894 0.101,3.156 1.398,3.789c-0.634,0.448 -1.902,1.355 -2.536,1.801c-0.576,-2.723 -2.046,-3.53 -4.424,-2.392c1.124,1.182 2.262,2.379 3.43,3.559c-2.017,-0.288 -7.003,-1.412 -5.663,-4.409c0.865,-0.288 2.608,-0.893 3.487,-1.195c-2.968,-2.377 -6.023,2.19 -7.637,4.38c-0.389,-0.418 -1.182,-1.268 -1.571,-1.685c1.96,-2.205 4.726,-4.079 5.159,-7.234c-1.787,1.34 -3.502,2.767 -5.361,4.006c-0.144,0.576 -0.432,1.758 -0.562,2.349c-0.648,-0.534 -1.931,-1.614 -2.565,-2.148c0.85,-0.663 2.551,-2.003 3.401,-2.666c-0.692,-0.475 -2.075,-1.426 -2.767,-1.902c-0.274,0.951 -0.836,2.853 -1.11,3.804c-3.357,-1.513 -1.527,-3.257 0.807,-4.683c-1.354,-1.296 -2.709,-2.55 -3.833,-4.05c-2.363,-1.397 -2.868,-3.573 0.519,-4.034c0.086,1.065 0.288,3.198 0.375,4.265c0.663,-0.692 1.989,-2.089 2.666,-2.796l-2.349,-1.195l-0.648,-3.156c1.888,0.993 3.689,2.161 5.548,3.227c-1.138,-3.113 -4.395,-4.799 -5.62,-7.796c0.821,-0.259 2.435,-0.792 3.242,-1.052c-0.274,-2.045 0.692,-2.996 2.896,-2.853Zm-2.248,4.107c0.922,0.865 0.922,0.865 0,0Zm3.257,2.68c0.764,0.894 0.764,0.894 0,0Zm-3.041,8.92c0.836,0.835 0.836,0.835 0,0Zm2.824,2.392c4.712,0.389 -2.392,4.279 0,0Zm8.761,2.926c4.755,0.043 -2.017,4.437 0,0Z" style="fill:#cecfd1;fill-rule:nonzero;"/><path d="M411.312,581.257c4.25,-2.371 0.39,4.784 0,0Z" style="fill:#7f7f80;fill-rule:nonzero;"/><path d="M445.938,581.056c4.337,-2.198 0.202,4.799 0,0Z" style="fill:#5e5c5e;fill-rule:nonzero;"/><path d="M526.085,581.212c6.7,0.534 14.093,-0.979 20.173,2.638c3.3,3.458 2.926,8.56 3.099,12.983c-4.54,-2.075 -9.237,-3.818 -14.078,-4.999c-0.187,1.08 -0.549,3.227 -0.721,4.309c-0.216,0.59 -0.634,1.743 -0.835,2.319c-0.533,-4.006 -1.383,-8.026 -3.213,-11.643c-1.239,-2.047 -2.767,-3.905 -4.424,-5.606Z" style="fill:#434342;fill-rule:nonzero;"/><path d="M548.681,580.564c1.311,2.061 2.708,4.079 4.092,6.109c1.441,-1.267 2.867,-2.535 4.323,-3.804c0.576,2.205 0.98,4.439 1.34,6.7c-1.887,0.663 -3.791,1.34 -5.663,2.017c-0.576,2.249 -1.138,4.51 -1.628,6.801c0.835,-6.038 0.187,-12.305 -2.464,-17.825Z" style="fill:#c5c3c5;fill-rule:nonzero;"/><path d="M658.701,581.604c4.799,0.288 -2.284,4.307 0,0Z" style="fill:#86335c;fill-rule:nonzero;"/><path d="M505.523,590.796c2.824,-3.012 5.159,-6.946 9.294,-8.329c3.372,-1.47 7.075,-0.707 10.577,-0.332l-0.562,0.259c-4.971,2.335 -11.038,4.064 -13.718,9.324c-1.672,4.755 4.078,7.622 8.069,6.7c3.847,-0.288 4.395,-4.799 5.879,-7.521l0.721,-1.628l0.749,0.518c0.764,5.144 -2.363,10.304 -7.623,11.225c-5.634,1.671 -8.747,-4.237 -11.888,-7.681c-1.153,3.703 -0.865,7.551 1.196,10.88l0.259,0.59c-3.17,4.482 -4.885,9.713 -5.706,15.102c-2.493,-1.946 -4.784,-4.165 -6.845,-6.572c2.421,-7.507 9.323,-13.631 8.415,-21.974c0.418,-0.505 1.239,-1.527 1.657,-2.032l-0.476,1.47Z" style="fill:#4f4f4f;fill-rule:nonzero;"/><path d="M661.58,582.049c2.579,0 5.115,0.288 4.726,3.574c1.009,-0.158 3.012,-0.505 4.02,-0.663c-0.404,0.778 -1.195,2.348 -1.599,3.126c-0.562,-0.49 -1.672,-1.484 -2.234,-1.988c-1.181,0.173 -3.53,0.49 -4.712,0.648c-0.057,-1.166 -0.143,-3.53 -0.201,-4.698Z" style="fill:#ac3b73;fill-rule:nonzero;"/><path d="M448.862,583.938c4.221,-2.415 0.434,4.769 0,0Z" style="fill:#666566;fill-rule:nonzero;"/><path d="M511.115,591.717c2.68,-5.259 8.747,-6.989 13.718,-9.324c0.562,2.45 1.182,4.914 1.7,7.392l-0.749,-0.518c-0.677,-0.389 -2.017,-1.153 -2.68,-1.542c-4.006,1.167 -7.536,3.602 -9.006,7.651c4.453,1.311 8.084,-1.426 10.966,-4.481c-1.484,2.723 -2.032,7.233 -5.879,7.521c-3.991,0.922 -9.741,-1.946 -8.069,-6.7Z" style="fill:#7d7576;fill-rule:nonzero;"/><path d="M524.832,582.394c0.893,0.549 2.709,1.658 3.602,2.205c0.879,5.649 1.282,12.523 -3.977,16.255c-4.467,4.265 -10.62,1.944 -15.62,0.215c-0.159,0.793 -0.461,2.364 -0.62,3.142c-2.061,-3.33 -2.349,-7.177 -1.196,-10.88c3.141,3.444 6.254,9.353 11.888,7.681c5.26,-0.922 8.386,-6.082 7.623,-11.225c-0.519,-2.478 -1.138,-4.942 -1.7,-7.392Z" style="fill:#2e2d2c;fill-rule:nonzero;"/><path d="M441.473,587.048c0.752,0.737 0.752,0.737 0,0Z" style="fill:#4b4a4b;fill-rule:nonzero;"/><path d="M453.186,598.519c0.737,0.752 0.737,0.752 0,0Z" style="fill:#4b4a4b;fill-rule:nonzero;"/><path d="M453.157,610.176c0.766,0.724 0.766,0.724 0,0Z" style="fill:#4b4a4b;fill-rule:nonzero;"/><path d="M445.967,586.92c4.192,-2.472 0.52,4.756 0,0Z" style="fill:#605f61;fill-rule:nonzero;"/><path d="M451.847,586.805c4.307,-2.241 0.246,4.784 0,0Z" style="fill:#656465;fill-rule:nonzero;"/><path d="M630.053,586.862c2.371,-0.752 3.15,0.058 2.371,2.4c-2.356,0.752 -3.152,-0.044 -2.371,-2.4Z" style="fill:#bc457f;fill-rule:nonzero;"/><path d="M514.096,595.376c1.47,-4.05 5,-6.484 9.006,-7.651c0.663,0.389 2.003,1.153 2.68,1.542l-0.72,1.628c-2.882,3.055 -6.513,5.792 -10.966,4.481Z" style="fill:#b1acad;fill-rule:nonzero;"/><path d="M432.941,590.031c0.766,0.723 0.766,0.723 0,0Z" style="fill:#595859;fill-rule:nonzero;"/><path d="M438.835,590.003l0.376,-0.361l0,0.752l-0.376,-0.39Z" style="fill:#4a494b;fill-rule:nonzero;"/><path d="M552.773,591.587c1.874,-0.677 3.776,-1.355 5.663,-2.017c0.116,2.249 0.143,4.496 0.116,6.744c-3.805,1.498 -6.153,4.726 -8.2,8.098l-0.114,-0.993c0.23,-1.268 0.677,-3.776 0.907,-5.03c0.49,-2.291 1.052,-4.553 1.628,-6.801Z" style="fill:#aca9aa;fill-rule:nonzero;"/><path d="M623.137,589.628c1.917,-2.55 5.562,1.312 3.099,3.17c-1.902,1.758 -4.683,-1.282 -3.099,-3.17Z" style="fill:#cd4688;fill-rule:nonzero;"/><path d="M423.907,592.899c4.799,-0.116 -1.923,4.452 0,0Z" style="fill:#848485;fill-rule:nonzero;"/><path d="M435.737,593.002l0.494,-0.421c-0.102,0.131 -0.32,0.392 -0.421,0.523l-0.073,-0.102Z" style="fill:#49494a;fill-rule:nonzero;"/><path d="M441.429,592.915c0.766,0.737 0.766,0.737 0,0Z" style="fill:#424243;fill-rule:nonzero;"/><path d="M493.564,605.767c1.96,-5.836 6.672,-10.073 10.778,-14.41c0.908,8.343 -5.994,14.468 -8.415,21.976c-1.282,-2.276 -3.357,-4.799 -2.363,-7.565Z" style="fill:#3a3939;fill-rule:nonzero;"/><path d="M535.28,591.833c4.841,1.182 9.539,2.926 14.078,5c0.143,2.205 0.505,4.395 0.879,6.585l0.114,0.995l-1.065,-0.475l-0.808,-0.505c-4.366,-2.925 -9.035,-5.36 -13.92,-7.292c0.174,-1.08 0.534,-3.227 0.721,-4.309Z" style="fill:#606060;fill-rule:nonzero;"/><path d="M619.07,592.395c2.867,2.176 3.027,5.649 3.271,8.948c-2.579,-0.604 -4.841,-1.83 -5.792,-4.452c0.85,-1.499 1.685,-2.998 2.521,-4.496Z" style="fill:#97c1e7;fill-rule:nonzero;"/><path d="M579.761,599.124c3.833,-1.384 7.42,-3.488 11.398,-4.51c3.415,0.619 6.311,2.809 9.351,4.38c-0.62,0.505 -1.859,1.498 -2.478,2.003c-4.035,2.392 -8.171,4.626 -12.508,6.456c-1.166,-3.271 -1.887,-7.292 -5.763,-8.329Z" style="fill:#989596;fill-rule:nonzero;"/><path d="M501.028,620.869c6.83,4.496 15.087,5.649 23.113,5.173l1.47,1.397c-0.893,0.288 -2.651,0.85 -3.545,1.125c-6.225,-0.015 -13.444,-3.17 -18.646,1.757c-2.32,-1.124 -4.554,-2.407 -6.7,-3.804c1.412,-0.995 2.824,-1.974 4.251,-2.939c0.014,-0.677 0.043,-2.032 0.058,-2.709Z" style="fill:#989596;fill-rule:nonzero;"/><path d="M420.217,595.594c2.024,-0.464 2.703,0.303 2.024,2.312c-2.038,0.448 -2.718,-0.317 -2.024,-2.312Z" style="fill:#777779;fill-rule:nonzero;"/><path d="M444.295,595.708c0.752,0.737 0.752,0.737 0,0Z" style="fill:#494849;fill-rule:nonzero;"/><path d="M458.893,604.327c0.665,0.81 0.665,0.81 0,0Z" style="fill:#494849;fill-rule:nonzero;"/><path d="M450.232,595.752c0.737,0.752 0.737,0.752 0,0Z" style="fill:#474747;fill-rule:nonzero;"/><path d="M534.559,596.141c4.885,1.931 9.553,4.366 13.919,7.292c-0.705,1.7 -1.355,3.429 -2.118,5.1c-3.804,-3.991 -8.731,-6.556 -14.323,-6.556c0.62,-1.153 1.182,-2.32 1.686,-3.517c0.202,-0.575 0.62,-1.728 0.836,-2.319Z" style="fill:#767677;fill-rule:nonzero;"/><path d="M550.352,604.412c2.047,-3.372 4.395,-6.601 8.199,-8.099c-0.028,0.908 -0.086,2.724 -0.101,3.631c-0.576,2.421 -1.138,4.87 -1.801,7.277c-2.06,0.231 -4.121,0.475 -6.181,0.721c-0.216,-0.721 -0.649,-2.177 -0.865,-2.911l-0.317,-1.094l1.067,0.475Z" style="fill:#8e8c8d;fill-rule:nonzero;"/><path d="M628.05,597.006c3.227,-0.418 6.095,1.239 6.455,4.654c-2.853,0.634 -5.259,2.377 -7.982,3.299c-0.36,-0.907 -1.096,-2.737 -1.47,-3.644c1.023,-1.441 2.017,-2.869 2.998,-4.309Z" style="fill:#85b8e3;fill-rule:nonzero;"/><path d="M458.864,598.605c0.741,0.726 0.741,0.726 0,0Z" style="fill:#585758;fill-rule:nonzero;"/><path d="M567.573,601.92c4.149,-0.418 8.3,-1.239 12.191,-2.796c3.876,1.037 4.597,5.058 5.763,8.329c-8.559,3.516 -17.853,4.525 -27.032,4.538c0.806,-3.257 1.671,-6.513 2.334,-9.812c2.234,-0.029 4.496,0 6.744,-0.259Zm9.322,-0.303c0.376,1.671 0.563,3.516 1.946,4.74c2.276,-0.677 1.917,-2.91 1.946,-4.799c-0.966,0.015 -2.911,0.044 -3.891,0.058Z" style="fill:#767373;fill-rule:nonzero;"/><path d="M429.856,601.588c0.766,0.867 0.766,0.867 0,0Z" style="fill:#8d8d8f;fill-rule:nonzero;"/><path d="M456.012,601.43c0.795,0.693 0.795,0.693 0,0Z" style="fill:#4a494a;fill-rule:nonzero;"/><path d="M461.774,601.501c0.766,0.737 0.766,0.737 0,0Z" style="fill:#424242;fill-rule:nonzero;"/><path d="M467.51,601.43c0.694,0.78 0.694,0.78 0,0Z" style="fill:#3c3c3d;fill-rule:nonzero;"/><path d="M473.347,601.515c0.723,0.766 0.723,0.766 0,0Z" style="fill:#353435;fill-rule:nonzero;"/><path d="M476.127,604.442c0.781,0.708 0.781,0.708 0,0Z" style="fill:#353435;fill-rule:nonzero;"/><path d="M532.038,601.976c5.591,0 10.52,2.565 14.324,6.557c-0.461,0.821 -1.399,2.449 -1.859,3.257c-4.51,-3.631 -10.014,-5.491 -15.764,-5.779c1.153,-1.298 2.248,-2.666 3.3,-4.035Z" style="fill:#8c8c8d;fill-rule:nonzero;"/><path d="M421.314,604.541c0.781,0.737 0.781,0.737 0,0Z" style="fill:#545455;fill-rule:nonzero;"/><path d="M470.435,604.369c0.723,0.766 0.723,0.766 0,0Z" style="fill:#343435;fill-rule:nonzero;"/><path d="M502.77,619.903c0.821,-5.39 2.536,-10.62 5.706,-15.101c2.406,1.527 4.914,2.925 7.551,4.02c-3.257,4.395 -5.245,9.553 -4.813,15.086c-2.911,-1.138 -5.721,-2.493 -8.444,-4.006Z" style="fill:#707071;fill-rule:nonzero;"/><path d="M549.602,605.032c0.216,0.734 0.648,2.19 0.865,2.911c-5.028,9.77 -14.007,17.219 -24.857,19.496l-1.47,-1.397c3.487,-0.692 6.859,-1.802 10.26,-2.854c0.303,-0.648 0.907,-1.917 1.21,-2.55c-3.53,1.614 -7.118,3.141 -10.922,3.905c-0.259,-0.806 -0.793,-2.434 -1.052,-3.242c2.061,-2.737 4.208,-6.844 8.343,-5.937c2.724,-0.202 3.848,2.335 4.092,4.655c0.475,0.518 0.951,1.023 1.441,1.541c4.799,-4.87 9.986,-9.798 12.09,-16.527Z" style="fill:#d4d8db;fill-rule:nonzero;"/><path d="M435.751,607.237c0.723,0.781 0.723,0.781 0,0Z" style="fill:#4d4c4d;fill-rule:nonzero;"/><path d="M468.086,607.222l0.13,0.088l0.116,0.231c-0.058,-0.073 -0.188,-0.231 -0.246,-0.319Z" style="fill:#383839;fill-rule:nonzero;"/><path d="M470.364,659.141c0.752,0.723 0.752,0.723 0,0Z" style="fill:#383839;fill-rule:nonzero;"/><path d="M473.331,607.208c0.766,0.695 0.766,0.695 0,0Z" style="fill:#464547;fill-rule:nonzero;"/><path d="M487.224,607.813l1.744,-1.109c1.599,3.646 3.732,7.075 6.528,9.943c-1.787,1.34 -3.574,2.695 -5.346,4.035c-1.873,-2.177 -3.631,-4.439 -5.216,-6.816c0.317,-2.162 1.21,-4.18 2.291,-6.052Z" style="fill:#e6e5e7;fill-rule:nonzero;"/><path d="M521.705,609.095c2.45,-0.734 4.813,-1.786 7.032,-3.084c5.749,0.288 11.254,2.148 15.765,5.779c-1.153,1.498 -2.364,2.983 -3.646,4.366c-3.833,-5.475 -12.465,-7.118 -17.191,-1.93c-3.242,2.565 -3.199,6.916 -3.689,10.648c-1.081,-0.057 -3.228,-0.187 -4.294,-0.244c-0.072,-5.836 2.234,-11.182 6.023,-15.534Z" style="fill:#a3a5a6;fill-rule:nonzero;"/><path d="M550.466,607.943c2.06,-0.246 4.121,-0.49 6.181,-0.721c-5.461,16.83 -23.473,28.272 -41.024,26.312c2.089,-1.715 4.208,-3.429 6.441,-4.971c0.893,-0.274 2.651,-0.835 3.545,-1.124c10.851,-2.276 19.828,-9.726 24.857,-19.496Z" style="fill:#636161;fill-rule:nonzero;"/><path d="M595.598,609.283c0.116,-0.85 0.347,-2.565 0.448,-3.415c0.764,1.283 1.454,2.767 2.031,4.178l0.029,0.433c0.49,5.418 0.288,10.879 0.721,16.298c1.484,0.49 2.996,0.878 4.553,1.166l0.547,0.116c-6.758,3.286 -14.51,2.709 -21.355,0.058c1.744,-0.663 3.502,-1.355 5.259,-2.032c-1.052,-0.562 -2.075,-1.11 -3.097,-1.643c4.351,0.922 8.746,0.317 12.867,-1.254c-3.271,-3.126 -4.121,-7.91 -1.239,-11.672c-1.325,-0.562 -2.622,-1.153 -3.905,-1.773c0.793,-0.114 2.349,-0.345 3.141,-0.461Z" style="fill:#f7b127;fill-rule:nonzero;"/><path d="M438.532,610.233c0.824,0.823 0.824,0.823 0,0Z" style="fill:#8a8a8c;fill-rule:nonzero;"/><path d="M458.965,610.12c0.741,0.741 0.741,0.741 0,0Z" style="fill:#474748;fill-rule:nonzero;"/><path d="M491.531,668.251c-0.043,-0.174 -0.101,-0.521 -0.145,-0.695l0.145,0.695Z" style="fill:#474748;fill-rule:nonzero;"/><path d="M464.73,610.076c0.737,0.765 0.737,0.765 0,0Z" style="fill:#3f3e3f;fill-rule:nonzero;"/><path d="M476.171,610.106c0.694,0.78 0.694,0.78 0,0Z" style="fill:#464647;fill-rule:nonzero;"/><path d="M511.215,623.908c-0.432,-5.533 1.556,-10.692 4.813,-15.086c1.888,0.158 3.775,0.244 5.677,0.274c-3.79,4.352 -6.095,9.698 -6.023,15.534c-1.124,-0.173 -3.343,-0.549 -4.467,-0.721Z" style="fill:#8f8f90;fill-rule:nonzero;"/><path d="M598.078,610.048c3.89,-1.197 8.876,-0.389 10.793,3.631c3.027,5.259 -0.215,11.975 -5.489,14.266c-1.557,-0.288 -3.069,-0.677 -4.554,-1.167c-0.432,-5.418 -0.23,-10.879 -0.72,-16.298l-0.029,-0.432Z" style="fill:#f8db24;fill-rule:nonzero;"/><path d="M523.665,614.226c4.726,-5.188 13.358,-3.545 17.191,1.93c-1.57,1.327 -3.156,2.608 -4.784,3.862c-0.244,-2.32 -1.369,-4.856 -4.092,-4.654c-4.136,-0.908 -6.283,3.198 -8.343,5.937c0.259,0.806 0.793,2.434 1.052,3.242c-1.182,0.086 -3.53,0.244 -4.712,0.331c0.49,-3.732 0.447,-8.083 3.689,-10.648Z" style="fill:#bcbec0;fill-rule:nonzero;"/><path d="M479.096,612.986c0.737,0.766 0.737,0.766 0,0Z" style="fill:#313132;fill-rule:nonzero;"/><path d="M473.331,624.5c0.752,0.752 0.752,0.752 0,0Z" style="fill:#313132;fill-rule:nonzero;"/><path d="M438.776,615.926c0.741,0.741 0.741,0.741 0,0Z" style="fill:#39393a;fill-rule:nonzero;"/><path d="M484.816,667.772c0.766,0.708 0.766,0.708 0,0Z" style="fill:#39393a;fill-rule:nonzero;"/><path d="M458.806,616.026c0.781,0.794 0.781,0.794 0,0Z" style="fill:#181819;fill-rule:nonzero;"/><path d="M464.585,616.288l-0.102,-0.232l0.436,0.494c-0.087,-0.073 -0.247,-0.19 -0.334,-0.262Z" style="fill:#2a2b2c;fill-rule:nonzero;"/><path d="M569.574,615.348c4.496,2.608 8.142,6.542 13.199,8.185c0.086,-0.922 0.274,-2.739 0.36,-3.66c0.058,1.268 0.116,2.537 0.174,3.818l1.426,0.749c1.023,0.534 2.045,1.081 3.097,1.643c-1.757,0.677 -3.516,1.369 -5.259,2.032c-5.375,-2.493 -9.021,-8.329 -12.997,-12.767Z" style="fill:#ad4e23;fill-rule:nonzero;"/><path d="M495.494,616.645c1.758,1.498 3.617,2.911 5.533,4.222c-0.014,0.677 -0.043,2.032 -0.058,2.709c-1.427,0.964 -2.839,1.944 -4.251,2.939c-2.349,-1.773 -4.568,-3.703 -6.571,-5.836c1.772,-1.34 3.559,-2.695 5.346,-4.035Z" style="fill:#b7b4b5;fill-rule:nonzero;"/><path d="M556.39,616.762c2.897,-0.029 5.85,-0.259 8.747,0.202c5.461,3.501 10.029,8.386 13.675,13.746c-2.867,0.347 -5.706,0.692 -8.545,1.052c-3.905,-5.649 -8.847,-10.389 -13.876,-15Z" style="fill:#ef4023;fill-rule:nonzero;"/><path d="M542.613,630.147c3.905,-2.867 7.018,-6.657 10.374,-10.115c1.11,4.366 2.566,8.632 4.251,12.81c0.922,-2.377 1.527,-8.602 5.23,-6.268c1.11,4.452 1.96,8.977 2.177,13.559c-1.874,-0.158 -3.761,-0.303 -5.621,-0.461c-1.225,-2.061 -2.478,-4.107 -3.775,-6.095c0.274,3.169 0.692,6.325 1.167,9.482c-0.404,-0.576 -1.211,-1.729 -1.614,-2.306c-1.917,-0.187 -3.833,-0.389 -5.75,-0.62c-3.344,1.067 -6.601,2.377 -9.742,3.934c-0.778,-0.36 -2.306,-1.052 -3.069,-1.397c-0.086,-1.658 -0.143,-3.271 -0.202,-4.9c-1.383,0.85 -2.795,1.671 -4.207,2.449c0.043,-1.902 0.101,-3.804 0.173,-5.691c3.559,-1.384 7.305,-2.407 10.606,-4.382Zm8.415,-5.375c-1.182,2.047 0.015,4.885 1.786,6.24c1.801,1.254 1.744,-1.542 1.009,-2.407c-0.764,-1.195 -0.966,-4.006 -2.796,-3.833Zm-3.126,10.102c-0.475,2.032 0.274,2.737 2.262,2.089c0.461,-2.045 -0.288,-2.737 -2.262,-2.089Zm-2.753,2.737c4.799,0.332 -2.319,4.222 0,0Z" style="fill:#3e4244;fill-rule:nonzero;"/><path d="M438.72,621.66c0.781,0.708 0.781,0.708 0,0Z" style="fill:#353536;fill-rule:nonzero;"/><path d="M444.483,621.719c0.781,0.838 0.781,0.838 0,0Z" style="fill:#818184;fill-rule:nonzero;"/><path d="M464.585,653.29c0.795,0.839 0.795,0.839 0,0Z" style="fill:#818184;fill-rule:nonzero;"/><path d="M450.218,621.776c0.853,0.854 0.853,0.854 0,0Z" style="fill:#9e9c9e;fill-rule:nonzero;"/><path d="M455.594,621.273c4.857,0.347 -2.327,4.263 0,0Z" style="fill:#7e7c7e;fill-rule:nonzero;"/><path d="M470.364,621.66c0.737,0.794 0.737,0.794 0,0Z" style="fill:#1a1a1b;fill-rule:nonzero;"/><path d="M476.171,621.719c0.752,0.708 0.752,0.708 0,0Z" style="fill:#444446;fill-rule:nonzero;"/><path d="M435.795,624.63c0.824,0.708 0.824,0.708 0,0Z" style="fill:#5a5b5c;fill-rule:nonzero;"/><path d="M441.486,624.687c0.882,0.794 0.882,0.794 0,0Z" style="fill:#949496;fill-rule:nonzero;"/><path d="M447.293,624.643c0.824,0.895 0.824,0.895 0,0Z" style="fill:#a4a3a5;fill-rule:nonzero;"/><path d="M453.013,624.643c0.81,0.867 0.81,0.867 0,0Z" style="fill:#9c9b9d;fill-rule:nonzero;"/><path d="M551.03,624.772c1.83,-0.173 2.032,2.636 2.796,3.833c0.734,0.865 0.793,3.66 -1.009,2.407c-1.772,-1.355 -2.968,-4.193 -1.786,-6.24Z" style="fill:#0d0f10;fill-rule:nonzero;"/><path d="M432.912,627.584c0.795,0.796 0.795,0.796 0,0Z" style="fill:#757577;fill-rule:nonzero;"/><path d="M443.158,629.068c0.202,-0.619 0.605,-1.858 0.807,-2.478l1.671,-0.043c0.202,0.749 0.576,2.233 0.764,2.968c0.576,-0.145 1.7,-0.418 2.262,-0.562c0.749,0.907 2.234,2.737 2.968,3.659c0.159,-0.979 0.461,-2.939 0.62,-3.919l2.205,0.865c0.144,0.62 0.447,1.845 0.591,2.45c0.864,0.187 2.579,0.547 3.444,0.72c-1.196,1.167 -2.551,2.118 -4.092,2.84c0.764,0.734 2.306,2.218 3.069,2.954c0.216,-0.908 0.634,-2.709 0.85,-3.603l2.507,0.534c-0.677,-2.003 -1.326,-3.992 -2.003,-5.966c2.277,1.355 4.237,3.358 6.657,4.439c0.836,-2.061 1.124,-4.251 -0.735,-5.836l0.49,0.547c0.49,0.576 1.499,1.715 2.003,2.276c0.014,0.044 0.058,0.101 0.072,0.145c0.461,1.08 0.937,2.233 1.455,3.314c0.346,0.461 1.038,1.412 1.383,1.887c-2.637,-1.037 -4.64,1.758 -7.277,1.34c0.231,0.736 0.692,2.19 0.922,2.926c-0.692,0.086 -2.09,0.259 -2.781,0.345c-0.043,0.778 -0.159,2.335 -0.202,3.113c-2.32,0.663 -3.502,-1.715 -5.044,-2.897c0,0.98 0.014,2.954 0.014,3.949c1.888,0.619 3.775,1.282 5.677,1.902l-1.21,2.434c1.34,0.475 2.68,0.98 4.02,1.485c-1.787,1.801 -3.646,3.53 -5.663,5.058c-0.259,-1.037 -0.764,-3.097 -1.009,-4.136c-3.631,1.715 -1.297,3.747 0.504,5.491c-1.225,3.387 -2.479,-0.734 -3.66,-1.456l-1.441,0.072c-0.879,0.029 -2.637,0.101 -3.502,0.13c0.072,-0.764 0.245,-2.306 0.331,-3.084c0.447,-2.247 -0.403,-3.213 -2.551,-2.867c0.649,-0.993 1.311,-1.974 1.989,-2.954c-1.066,-1.946 -2.147,-3.876 -3.055,-5.893c-0.548,-0.837 -1.628,-2.508 -2.161,-3.344c-1.369,-0.863 -2.925,-1.426 -4.539,-1.498c-0.259,-0.029 -0.778,-0.072 -1.038,-0.101c-0.749,-0.576 -2.234,-1.729 -2.968,-2.306l0.288,-0.116c0.591,-0.201 1.772,-0.619 2.363,-0.835l0.072,-0.734c1.167,-1.931 2.824,-3.012 4.928,-3.214Zm1.023,1.009c0.85,0.922 0.85,0.922 0,0Zm-2.695,3.011c0.865,0.908 0.865,0.908 0,0Zm5.764,0.029c0.922,0.865 0.922,0.865 0,0Zm6.009,-0.101c0.865,0.923 0.865,0.923 0,0Zm-3.055,3.04c0.879,0.822 0.879,0.822 0,0Zm11.715,-0.316c0.461,0.389 0.461,0.389 0,0Zm-8.79,3.112c0.807,0.85 0.807,0.85 0,0Zm6.066,0.145c0.908,0.835 0.908,0.835 0,0Zm-6.009,5.663c0.793,0.792 0.793,0.792 0,0Zm2.94,2.708c4.813,0.375 -2.262,4.251 0,0Zm-3.055,2.941c4.784,0.187 -2.075,4.366 0,0Zm5.98,0.331c0.375,0.375 0.375,0.375 0,0Z" style="fill:#b9b9ba;fill-rule:nonzero;"/><path d="M449.887,627.7c0.853,0.853 0.853,0.853 0,0Z" style="fill:#aaa9ab;fill-rule:nonzero;"/><path d="M455.737,627.714c0.911,0.794 0.911,0.794 0,0Z" style="fill:#a3a1a2;fill-rule:nonzero;"/><path d="M503.42,630.321c5.202,-4.929 12.421,-1.773 18.646,-1.758c-2.234,1.542 -4.352,3.257 -6.441,4.971c-4.193,-0.533 -8.343,-1.469 -12.205,-3.213Z" style="fill:#7d7a7a;fill-rule:nonzero;"/><path d="M430.059,630.479c0.838,0.781 0.838,0.781 0,0Z" style="fill:#818284;fill-rule:nonzero;"/><path d="M473.186,630.336c0.824,0.75 0.824,0.75 0,0Z" style="fill:#0c0c0c;fill-rule:nonzero;"/><path d="M570.266,631.762c2.838,-0.36 5.678,-0.705 8.545,-1.052l0.505,-0.057c8.905,14.308 15.865,32.076 10.951,48.965l-0.503,-2.349c1.441,-12.017 -2.278,-23.733 -7.033,-34.598c-0.734,-1.685 -1.859,-3.156 -2.882,-4.639c0.145,1.557 0.288,3.112 0.461,4.683c-1.023,0.173 -3.084,0.518 -4.121,0.677c-1.772,-3.978 -3.717,-7.883 -5.922,-11.629Z" style="fill:#f16424;fill-rule:nonzero;"/><path d="M427.133,633.363c0.795,0.78 0.795,0.78 0,0Z" style="fill:#737476;fill-rule:nonzero;"/><path d="M424.109,635.856c4.741,0.592 -2.53,4.164 0,0Z" style="fill:#3b3c3d;fill-rule:nonzero;"/><path d="M438.473,636.273c0.26,0.029 0.781,0.072 1.041,0.101l-0.622,0.231l-0.419,-0.332Z" style="fill:#303032;fill-rule:nonzero;"/><path d="M450.205,636.055c0.882,0.825 0.882,0.825 0,0Z" style="fill:#19191a;fill-rule:nonzero;"/><path d="M536.044,637.771c0.057,1.628 0.114,3.242 0.202,4.9c0.764,0.345 2.291,1.037 3.068,1.397c-0.274,4.395 -0.402,8.804 -0.331,13.228c-0.894,0.562 -1.786,1.138 -2.666,1.729c0.634,0.475 1.902,1.441 2.55,1.931c2.623,4.423 6.153,8.213 9.9,11.729c-1.138,0.98 -2.263,1.975 -3.372,2.968c-0.778,-1.225 -1.542,-2.449 -2.306,-3.66c-1.397,0.216 -2.796,0.448 -4.193,0.692c-0.246,-0.475 -0.764,-1.412 -1.009,-1.873c-4.495,0.734 -9.553,0.648 -13.156,3.861c-2.997,2.032 -4.222,5.534 -5.447,8.762c0.13,-2.651 0.187,-5.346 0.231,-7.983c-1.83,2.84 -3.041,5.995 -3.919,9.237c2.017,4.654 4.15,9.51 8.487,12.493c7.018,3.401 14.856,2.465 22.206,0.822c0.303,0.259 0.907,0.764 1.21,1.023c-7.681,1.83 -16.225,3.343 -23.531,-0.619c-8.631,-4.828 -14.986,-16.399 -10.029,-25.895c0.403,-0.663 1.21,-1.988 1.628,-2.651c-1.023,-0.663 -2.046,-1.298 -3.055,-1.931c2.536,-3.084 5.966,-6.254 5.26,-10.664c0.461,-4.683 -3.098,-7.997 -6.383,-10.664c0.548,0 1.657,0.015 2.205,0.015c3.199,-0.043 6.47,-0.129 9.554,-1.109c2.954,-1.672 5.591,-3.862 8.689,-5.289c1.412,-0.778 2.824,-1.599 4.208,-2.449Zm-12.35,9.294c-0.461,1.441 -0.922,2.882 -1.383,4.322c1.931,-0.086 4.049,0.376 5.721,-0.863c0.418,-3.128 -1.845,-3.559 -4.337,-3.458Zm1.628,17.796c0.706,0.778 0.706,0.778 0,0Zm5.576,-0.187c4.323,-2.177 0.173,4.784 0,0Zm5.923,0.244c-0.288,2.276 3.04,4.367 5.144,4.02c0.086,-2.291 -2.968,-4.597 -5.144,-4.02Zm-14.454,2.954c0.749,0.778 0.749,0.778 0,0Zm19.598,1.067c0.98,0.808 0.98,0.808 0,0Zm-22.609,0.433c-1.138,1.426 -2.565,2.521 -4.251,3.257c-0.389,2.132 -3.285,7.334 0.245,7.507c1.196,-3.646 4.222,-6.427 5.548,-9.827l-1.542,-0.936Z" style="fill:#59585a;fill-rule:nonzero;"/><path d="M545.151,637.612c4.813,0.332 -2.328,4.235 0,0Z" style="fill:#1b1b1c;fill-rule:nonzero;"/><path d="M426.891,638.735c4.785,0.086 -2.11,4.452 0,0Z" style="fill:#999c9e;fill-rule:nonzero;"/><path d="M435.722,638.997c0.766,0.895 0.766,0.895 0,0Z" style="fill:#3c3d3e;fill-rule:nonzero;"/><path d="M467.741,639.139c0.867,0.809 0.867,0.809 0,0Z" style="fill:#9d9b9c;fill-rule:nonzero;"/><path d="M579.848,638.031c1.023,1.485 2.146,2.954 2.882,4.641c1.441,4.769 2.939,9.524 4.38,14.308c-2.594,0.433 -5.173,0.879 -7.752,1.34c-0.576,-5.072 -1.571,-10.071 -3.17,-14.928c1.037,-0.158 3.099,-0.503 4.121,-0.677c-0.173,-1.57 -0.317,-3.126 -0.461,-4.683Z" style="fill:#f38625;fill-rule:nonzero;"/><path d="M559.028,639.673c1.859,0.158 3.747,0.301 5.619,0.461c-0.173,1.902 -0.404,3.818 -0.705,5.72c-2.335,-0.029 -4.67,-0.057 -7.003,-0.116c0.677,-2.032 1.368,-4.063 2.089,-6.066Z" style="fill:#485e72;fill-rule:nonzero;"/><path d="M424.295,642.021c0.838,0.781 0.838,0.781 0,0Z" style="fill:#7a7c7e;fill-rule:nonzero;"/><path d="M464.902,641.69c0.853,0.867 0.853,0.867 0,0Z" style="fill:#a3a2a3;fill-rule:nonzero;"/><path d="M502.066,641.806c0.824,0.752 0.824,0.752 0,0Z" style="fill:#050506;fill-rule:nonzero;"/><path d="M540.091,649.775c1.514,-7.94 11.514,-8.371 16.846,-4.035c2.334,0.058 4.668,0.086 7.002,0.116c-0.402,1.527 -0.85,3.055 -1.353,4.567c-7.493,-0.461 -15.001,-0.648 -22.495,-0.648Z" style="fill:#526f8b;fill-rule:nonzero;"/><path d="M453.186,644.659c0.795,0.796 0.795,0.796 0,0Z" style="fill:#434446;fill-rule:nonzero;"/><path d="M461.213,644.631c4.235,-2.5 0.448,4.857 0,0Z" style="fill:#898889;fill-rule:nonzero;"/><path d="M476.113,653.333c0.867,0.781 0.867,0.781 0,0Z" style="fill:#898889;fill-rule:nonzero;"/><path d="M467.552,644.789c0.824,0.853 0.824,0.853 0,0Z" style="fill:#9b9a9b;fill-rule:nonzero;"/><path d="M479.008,644.818c0.853,0.737 0.853,0.737 0,0Z" style="fill:#767475;fill-rule:nonzero;"/><path d="M424.05,647.411c4.756,0.159 -2.125,4.408 0,0Z" style="fill:#525455;fill-rule:nonzero;"/><path d="M432.682,647.917c0.838,0.838 0.838,0.838 0,0Z" style="fill:#333334;fill-rule:nonzero;"/><path d="M475.955,647.484c4.785,0.26 -2.269,4.307 0,0Z" style="fill:#282829;fill-rule:nonzero;"/><path d="M523.693,647.065c2.493,-0.101 4.755,0.331 4.337,3.458c-1.671,1.239 -3.79,0.778 -5.721,0.865c0.461,-1.441 0.922,-2.882 1.383,-4.323Z" style="fill:#151718;fill-rule:nonzero;"/><path d="M426.962,650.294c4.741,-0.029 -1.937,4.538 0,0Z" style="fill:#939697;fill-rule:nonzero;"/><path d="M435.506,650.307c4.727,0.39 -2.399,4.294 0,0Z" style="fill:#5b5d5e;fill-rule:nonzero;"/><path d="M447.28,650.064c2.154,-0.348 3.007,0.62 2.559,2.875c-2.645,0.376 -3.498,-0.578 -2.559,-2.875Z" style="fill:#0e0f10;fill-rule:nonzero;"/><path d="M453.072,650.307c4.799,0.189 -2.081,4.38 0,0Z" style="fill:#5b5c5d;fill-rule:nonzero;"/><path d="M441.544,664.921c0.766,0.765 0.766,0.765 0,0Z" style="fill:#5b5c5d;fill-rule:nonzero;"/><path d="M467.496,650.423c0.838,0.796 0.838,0.796 0,0Z" style="fill:#878789;fill-rule:nonzero;"/><path d="M473.274,650.496c0.867,0.781 0.867,0.781 0,0Z" style="fill:#89898b;fill-rule:nonzero;"/><path d="M478.981,650.452c0.896,0.781 0.896,0.781 0,0Z" style="fill:#989597;fill-rule:nonzero;"/><path d="M484.903,650.739c0.853,0.796 0.853,0.796 0,0Z" style="fill:#908d8f;fill-rule:nonzero;"/><path d="M424.095,653.204c4.756,0.519 -2.486,4.192 0,0Z" style="fill:#2b2c2d;fill-rule:nonzero;"/><path d="M444.268,653.233c4.77,0.043 -2.024,4.452 0,0Z" style="fill:#6b6c6d;fill-rule:nonzero;"/><path d="M470.435,653.362c0.766,0.81 0.766,0.81 0,0Z" style="fill:#6c6c6e;fill-rule:nonzero;"/><path d="M426.92,656.057c4.77,0.216 -2.212,4.351 0,0Z" style="fill:#424244;fill-rule:nonzero;"/><path d="M461.674,656.188c0.81,0.823 0.81,0.823 0,0Z" style="fill:#848487;fill-rule:nonzero;"/><path d="M467.538,656.287c0.824,0.651 0.824,0.651 0,0Z" style="fill:#414142;fill-rule:nonzero;"/><path d="M473.318,656.202c0.752,0.78 0.752,0.78 0,0Z" style="fill:#575658;fill-rule:nonzero;"/><path d="M479.008,656.214c0.867,0.766 0.867,0.766 0,0Z" style="fill:#878688;fill-rule:nonzero;"/><path d="M490.566,656.548c0.275,0.044 0.838,0.145 1.113,0.187l-0.593,0.491l-0.52,-0.679Z" style="fill:#8d8a8c;fill-rule:nonzero;"/><path d="M569.964,657.758c1.801,1.743 1.902,7.651 -0.072,9.28c-1.052,-2.867 -0.951,-6.427 0.072,-9.28Z" style="fill:#4b4c4f;fill-rule:nonzero;"/><path d="M579.359,658.32c2.579,-0.462 5.159,-0.908 7.752,-1.34c1.21,4.265 1.656,8.703 1.498,13.141c-3.559,0.073 -7.119,0.159 -10.677,0.26c0.821,-3.978 1.254,-8.013 1.426,-12.061Z" style="fill:#f6a927;fill-rule:nonzero;"/><path d="M429.816,658.952c4.756,0.101 -2.11,4.409 0,0Z" style="fill:#484a4b;fill-rule:nonzero;"/><path d="M453.015,658.895c4.77,0.232 -2.197,4.409 0,0Z" style="fill:#979799;fill-rule:nonzero;"/><path d="M458.791,659.155c0.81,0.794 0.81,0.794 0,0Z" style="fill:#808082;fill-rule:nonzero;"/><path d="M476.186,659.17c0.795,0.737 0.795,0.737 0,0Z" style="fill:#565657;fill-rule:nonzero;"/><path d="M481.933,659.04c0.824,0.781 0.824,0.781 0,0Z" style="fill:#787779;fill-rule:nonzero;"/><path d="M487.813,659.097c0.781,0.838 0.781,0.838 0,0Z" style="fill:#868586;fill-rule:nonzero;"/><path d="M493.477,659.097l0.578,0.072l0.058,0.491l-0.506,0.29l-0.13,-0.853Z" style="fill:#868486;fill-rule:nonzero;"/><path d="M433.013,662.095c0.737,0.796 0.737,0.796 0,0Z" style="fill:#5b5c5e;fill-rule:nonzero;"/><path d="M455.925,662.008c0.795,0.737 0.795,0.737 0,0Z" style="fill:#585858;fill-rule:nonzero;"/><path d="M473.301,662.022c0.723,0.766 0.723,0.766 0,0Z" style="fill:#373838;fill-rule:nonzero;"/><path d="M479.111,661.994c0.679,0.825 0.679,0.825 0,0Z" style="fill:#4b4b4c;fill-rule:nonzero;"/><path d="M484.903,661.994c0.737,0.796 0.737,0.796 0,0Z" style="fill:#585859;fill-rule:nonzero;"/><path d="M491.416,662.775c0.029,-0.187 0.072,-0.549 0.101,-0.737l-0.101,0.737Z" style="fill:#777678;fill-rule:nonzero;"/><path d="M435.78,665.063c0.781,0.693 0.781,0.693 0,0Z" style="fill:#353637;fill-rule:nonzero;"/><path d="M453.1,664.921c0.766,0.723 0.766,0.723 0,0Z" style="fill:#39393b;fill-rule:nonzero;"/><path d="M481.919,664.877c0.708,0.794 0.708,0.794 0,0Z" style="fill:#4a4a4b;fill-rule:nonzero;"/><path d="M487.784,664.848c0.766,0.752 0.766,0.752 0,0Z" style="fill:#535253;fill-rule:nonzero;"/><path d="M525.323,664.862c0.708,0.78 0.708,0.78 0,0Z" style="fill:#1e1e1e;fill-rule:nonzero;"/><path d="M530.899,664.675c4.336,-2.183 0.173,4.799 0,0Z" style="fill:#282728;fill-rule:nonzero;"/><path d="M505.135,670.625c0.752,0.752 0.752,0.752 0,0Z" style="fill:#464546;fill-rule:nonzero;"/><path d="M519.357,669.37l1.542,0.938c-1.326,3.4 -4.352,6.181 -5.548,9.827c-3.53,-0.173 -0.634,-5.375 -0.245,-7.508c1.686,-0.734 3.113,-1.829 4.251,-3.257Z" style="fill:#0a0a0b;fill-rule:nonzero;"/><path d="M546.087,684.717c0.792,-8.718 8.703,-15.879 17.55,-15.231c4.971,-0.173 8.733,3.33 11.571,6.974c0.908,-2.031 1.817,-4.063 2.724,-6.08c3.559,-0.101 7.119,-0.187 10.678,-0.259c-0.361,3.358 -0.879,6.715 -1.773,9.972c-13.675,0.547 -27.162,3.169 -40.75,4.625Z" style="fill:#f8c125;fill-rule:nonzero;"/><path d="M577.918,697.499c6.44,-4.957 9.135,-12.911 11.844,-20.232l0.505,2.349c-1.542,7.969 -6.168,15.606 -12.767,20.447c-9.452,6.845 -24.597,3.704 -29.8,-7.017c11.644,-1.801 23.387,-3.141 35.131,-4.222c-3.846,6.066 -10.172,11.312 -17.724,10.98c-4.913,0.274 -9.092,-2.478 -13.127,-4.841c6.111,7.709 18.46,8.876 25.939,2.535Z" style="fill:#f8eb25;fill-rule:nonzero;"/><path d="M546.087,684.718c13.588,-1.456 27.076,-4.079 40.75,-4.626c-1.081,3.011 -2.421,5.951 -4.006,8.733c-11.743,1.08 -23.488,2.42 -35.131,4.222c-0.663,-2.753 -1.167,-5.548 -1.614,-8.329Z" style="fill:#f8d623;fill-rule:nonzero;"/><path d="M845.634,405.489c19.716,-0.47 39.447,0.059 59.178,-0.279c0.147,4.216 0.177,8.433 0.162,12.65c-7.552,0.015 -15.104,0 -22.641,0.015c-0.088,19.863 0.029,39.727 -0.044,59.59c-4.613,-0.044 -9.197,-0.029 -13.781,0.073c-0.352,-19.878 -0.058,-39.771 -0.162,-59.663c-7.536,-0.015 -15.073,-0.015 -22.61,0c-0.029,-4.128 -0.058,-8.257 -0.102,-12.385Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M912.157,405.24c4.216,0.059 8.447,0.088 12.678,0.088c-0.073,9.021 0.044,18.027 -0.088,27.033c9.27,-9.976 29.074,-8.83 34.261,4.775c4.421,12.855 1.322,26.828 2.307,40.168c-4.188,-0.015 -8.507,0.749 -12.605,-0.294c-0.545,-9.903 0.425,-19.849 -0.441,-29.722c-0.501,-4.907 -4.452,-9.388 -9.565,-9.491c-7.257,-1.146 -13.766,5.392 -13.781,12.473c-0.323,9.05 0.029,18.1 -0.146,27.165c-4.305,0.06 -8.596,0.031 -12.886,-0.146c0.25,-24.007 -0.279,-48.042 0.266,-72.049Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M981.794,428.57c9.946,-5.348 23.405,-5.069 32.411,2.086c7.477,6.244 8.58,16.705 8.139,25.799c-13.267,0.22 -26.534,0.059 -39.785,0.088c0.793,3.144 2.276,6.317 5.23,7.977c7.228,4.394 16.763,2.733 23.432,-1.998c2.719,2.571 5.569,5.024 7.949,7.933c-10.314,10.094 -28.371,11.093 -40.094,2.865c-14.016,-10.696 -12.988,-36.068 2.717,-44.751Zm0.823,17.145c9.123,0.309 18.262,0.162 27.4,0.103c-2.806,-13.12 -23.595,-12.135 -27.4,-0.103Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M778.768,460.819c9.593,0.015 19.188,-0.029 28.781,0.029c20.76,46.514 41.696,92.94 62.484,139.425c-9.579,0.058 -19.143,-0.073 -28.708,0.088c-4.113,-8.711 -7.991,-17.526 -11.811,-26.341c-24.227,-0.177 -48.453,-0.045 -72.68,-0.074c-3.953,8.771 -7.993,17.513 -11.916,26.299c-9.579,0.088 -19.157,0.029 -28.737,0.029c20.877,-46.471 41.696,-92.985 62.588,-139.454Zm14.207,30.118c-8.594,19.436 -17.014,38.963 -25.55,58.429c17.13,0 34.246,0.015 51.363,-0.015c-8.683,-19.437 -16.647,-39.214 -25.814,-58.415Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M874.31,501.94c8.153,0.029 16.307,0.015 24.476,0.015c0,17.014 0,34.013 -0.015,51.025c-0.104,7.155 1.777,14.721 6.861,20.011c6.728,7.007 18.027,8.285 26.694,4.407c10.329,-4.598 15.603,-16.396 14.956,-27.283c-0.029,-16.057 0,-32.101 -0.015,-48.16c8.037,-0.015 16.073,0.045 24.11,-0.029c0.175,32.821 0.029,65.644 0.073,98.465c-7.258,0 -14.53,-0.015 -21.788,0c-0.572,-4.348 -1.086,-8.698 -1.484,-13.061c-7.464,7.038 -16.485,13.149 -26.96,14.104c-14.06,2.116 -29.486,-3.202 -37.698,-15.146c-8.478,-11.607 -9.726,-26.548 -9.212,-40.462c0,-14.618 -0.015,-29.251 0,-43.885Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M1070.255,461.171c8.183,-0.058 16.381,-0.175 24.579,0.074c-0.147,46.352 -0.029,92.705 -0.073,139.042c-7.67,-0.102 -15.353,0.266 -23.007,-0.25c-0.397,-4.334 -0.94,-8.667 -1.308,-13.001c-5.127,7.242 -12.811,12.635 -21.582,14.339c-16.101,3.071 -34.113,-0.822 -46.088,-12.429c-9.329,-8.889 -14.001,-21.788 -14.486,-34.497c-0.66,-13.384 2.087,-27.606 10.77,-38.183c8.432,-10.535 21.934,-15.927 35.187,-16.514c13.398,-0.808 27.87,4.099 36.2,15.044c-0.381,-17.88 -0.015,-35.744 -0.19,-53.625Zm-36.686,62.059c-6.318,1.44 -12.179,5.127 -15.853,10.504c-8.241,12.033 -6.508,30.412 4.981,39.815c10.094,8.447 26.107,8.139 36.098,-0.309c12.135,-9.887 13.252,-30.029 2.763,-41.489c-6.832,-7.728 -18.042,-10.858 -27.988,-8.521Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M1115.565,501.779c8.14,-0.235 16.308,-0.073 24.462,-0.117l0,98.626c-8.153,-0.058 -16.292,0.133 -24.417,-0.131c0.029,-32.792 0.117,-65.585 -0.045,-98.377Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M1121.661,461.421c9.27,-4.481 21.817,2.894 20.613,13.649c0.381,10.666 -12.532,16.968 -21.392,12.135c-10.328,-4.599 -9.901,-21.847 0.78,-25.785Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M1198.057,501.236c16.558,-3.452 35.172,0.22 47.675,12.062c19.261,17.836 20.921,50.569 4.848,70.976c-17.395,22.038 -53.565,24.11 -74.575,6.2c-17.777,-15.499 -21.303,-43.385 -11.004,-64.042c6.346,-12.943 18.967,-22.39 33.056,-25.196Zm4.569,22.067c-21.45,6.098 -25.196,38.478 -8.33,51.539c10.49,7.905 27.268,6.686 35.79,-3.614c9.872,-12.048 8.859,-31.719 -2.719,-42.341c-6.493,-6.127 -16.249,-8.037 -24.741,-5.584Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M1277.937,460.73c19.173,0.102 38.361,-0.147 57.549,0.117c17.923,0.279 36.304,6.626 48.659,20.01c15.837,16.911 20.965,41.798 17.087,64.219c-2.91,16.939 -11.931,33.189 -26.226,43.076c-11.915,8.419 -26.68,11.96 -41.122,12.106c-18.674,0.235 -37.346,0.06 -56.005,0.089c0.015,-46.544 -0.102,-93.073 0.058,-139.616Zm26.592,25.152c-0.294,29.618 -0.308,59.251 0.015,88.856c10.271,-0.177 20.554,0.058 30.838,-0.104c12.62,-0.294 25.593,-5.832 32.777,-16.542c9.917,-14.707 10.431,-34.835 2.675,-50.584c-5.54,-11.475 -17.19,-19.321 -29.752,-20.921c-12.121,-1.425 -24.373,-0.251 -36.553,-0.705Z" style="fill:#6599cd;fill-rule:nonzero;"/><path d="M1418.773,460.701c22.2,0.133 44.399,-0.044 66.599,0.089c12.516,0.175 25.872,2.688 35.569,11.195c10.298,8.903 13.604,23.83 11.033,36.818c-1.866,8.403 -8.228,14.854 -15.588,18.849c1.161,2.322 4.423,2.646 6.347,4.306c18.834,13.03 18.865,44.383 0.838,58.12c-11.269,8.551 -26.005,10.065 -39.698,10.24c-21.728,0.104 -43.444,0.045 -65.172,0.031c0.015,-46.544 -0.133,-93.103 0.073,-139.647Zm25.799,55.697c14.501,0.337 29.016,0.191 43.531,0.102c7.552,-0.029 16.91,-3.555 18.174,-12.017c1.866,-10.564 -8.374,-19.159 -18.291,-19.35c-14.355,-0.514 -28.737,0.104 -43.092,-0.294c-0.602,10.504 -0.058,21.039 -0.323,31.558Zm0.044,23.757c0.177,11.856 -0.308,23.727 0.264,35.569c13.443,-0.308 26.9,0.044 40.344,-0.162c7.508,-0.337 15.749,-1.527 21.435,-6.92c5.113,-4.745 5.245,-13.311 0.896,-18.599c-5.642,-6.965 -14.971,-10.065 -23.713,-9.932c-13.076,0.029 -26.151,-0.015 -39.227,0.044Z" style="fill:#6599cd;fill-rule:nonzero;"/></svg> \ No newline at end of file
diff --git a/MediaBrowser.Providers/Plugins/ListenBrainz/Api/ListenBrainzLabsClient.cs b/MediaBrowser.Providers/Plugins/ListenBrainz/Api/ListenBrainzLabsClient.cs
new file mode 100644
index 0000000000..e080370b8c
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/ListenBrainz/Api/ListenBrainzLabsClient.cs
@@ -0,0 +1,128 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Providers.Plugins.ListenBrainz.Api.Models;
+using MediaBrowser.Providers.Plugins.ListenBrainz.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Plugins.ListenBrainz.Api;
+
+/// <summary>
+/// Client for the ListenBrainz Labs API.
+/// </summary>
+public class ListenBrainzLabsClient : IDisposable
+{
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly ILogger<ListenBrainzLabsClient> _logger;
+ private readonly SemaphoreSlim _rateLimitLock = new(1, 1);
+
+ private DateTime _lastRequestTime = DateTime.MinValue;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ListenBrainzLabsClient"/> class.
+ /// </summary>
+ /// <param name="httpClientFactory">The HTTP client factory.</param>
+ /// <param name="logger">The logger.</param>
+ public ListenBrainzLabsClient(
+ IHttpClientFactory httpClientFactory,
+ ILogger<ListenBrainzLabsClient> logger)
+ {
+ _httpClientFactory = httpClientFactory;
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// Gets similar artists for the given MusicBrainz artist ID.
+ /// </summary>
+ /// <param name="artistMbid">The MusicBrainz artist ID.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>A list of similar artist MusicBrainz IDs ordered by similarity score.</returns>
+ public async Task<IReadOnlyList<Guid>> GetSimilarArtistsAsync(
+ Guid artistMbid,
+ CancellationToken cancellationToken)
+ {
+ var config = ListenBrainzPlugin.Instance?.Configuration;
+ var baseUrl = config?.LabsServer ?? PluginConfiguration.DefaultLabsServer;
+ var algorithm = config?.AlgorithmString ?? new PluginConfiguration().AlgorithmString;
+ var rateLimit = config?.RateLimit ?? PluginConfiguration.DefaultRateLimit;
+
+ // Enforce rate limit
+ await EnforceRateLimitAsync(rateLimit, cancellationToken).ConfigureAwait(false);
+
+ var url = $"{baseUrl}/similar-artists/json?artist_mbids={artistMbid}&algorithm={algorithm}";
+
+ _logger.LogDebug("Fetching similar artists from ListenBrainz Labs: {Url}", url);
+
+ try
+ {
+ var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
+ var response = await httpClient.GetFromJsonAsync<List<SimilarArtistData>>(url, cancellationToken).ConfigureAwait(false);
+
+ if (response is null || response.Count == 0)
+ {
+ _logger.LogDebug("No similar artists found for {ArtistMbid}", artistMbid);
+ return [];
+ }
+
+ var similarMbids = response
+ .Where(a => !a.ArtistMbid.Equals(artistMbid)) // Exclude the source artist
+ .OrderByDescending(a => a.Score)
+ .Select(a => a.ArtistMbid)
+ .ToList();
+
+ _logger.LogDebug("Found {Count} similar artists for {ArtistMbid}", similarMbids.Count, artistMbid);
+
+ return similarMbids;
+ }
+ catch (HttpRequestException ex)
+ {
+ _logger.LogWarning(ex, "Failed to fetch similar artists from ListenBrainz Labs for {ArtistMbid}", artistMbid);
+ return [];
+ }
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _rateLimitLock.Dispose();
+ }
+ }
+
+ private async Task EnforceRateLimitAsync(double rateLimitSeconds, CancellationToken cancellationToken)
+ {
+ await _rateLimitLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ var timeSinceLastRequest = DateTime.UtcNow - _lastRequestTime;
+ var requiredDelay = TimeSpan.FromSeconds(rateLimitSeconds) - timeSinceLastRequest;
+
+ if (requiredDelay > TimeSpan.Zero)
+ {
+ await Task.Delay(requiredDelay, cancellationToken).ConfigureAwait(false);
+ }
+
+ _lastRequestTime = DateTime.UtcNow;
+ }
+ finally
+ {
+ _rateLimitLock.Release();
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/ListenBrainz/Api/Models/SimilarArtistData.cs b/MediaBrowser.Providers/Plugins/ListenBrainz/Api/Models/SimilarArtistData.cs
new file mode 100644
index 0000000000..237f33ee3a
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/ListenBrainz/Api/Models/SimilarArtistData.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Providers.Plugins.ListenBrainz.Api.Models;
+
+/// <summary>
+/// A similar artist data entry from the ListenBrainz Labs API.
+/// </summary>
+public class SimilarArtistData
+{
+ /// <summary>
+ /// Gets or sets the MusicBrainz artist ID.
+ /// </summary>
+ [JsonPropertyName("artist_mbid")]
+ public Guid ArtistMbid { get; set; }
+
+ /// <summary>
+ /// Gets or sets the artist name.
+ /// </summary>
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the similarity score.
+ /// </summary>
+ [JsonPropertyName("score")]
+ public double Score { get; set; }
+}
diff --git a/MediaBrowser.Providers/Plugins/ListenBrainz/Api/Models/SimilarArtistsResponse.cs b/MediaBrowser.Providers/Plugins/ListenBrainz/Api/Models/SimilarArtistsResponse.cs
new file mode 100644
index 0000000000..12e8f25dcc
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/ListenBrainz/Api/Models/SimilarArtistsResponse.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Providers.Plugins.ListenBrainz.Api.Models;
+
+/// <summary>
+/// Response from ListenBrainz Labs similar-artists endpoint.
+/// </summary>
+public class SimilarArtistsResponse
+{
+ /// <summary>
+ /// Gets or sets the list of similar artists.
+ /// </summary>
+ [JsonPropertyName("data")]
+ public IReadOnlyList<SimilarArtistData>? Data { get; set; }
+}
diff --git a/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/ListenBrainz_logo.svg b/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/ListenBrainz_logo.svg
new file mode 100644
index 0000000000..416a097f9c
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/ListenBrainz_logo.svg
@@ -0,0 +1,60 @@
+<svg version="1.2" baseProfile="tiny-ps" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450" width="800" height="450">
+ <title>ListenBrainz_logo-svg</title>
+ <defs>
+ <image width="800" height="450" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyAAAAHCAQMAAAAtrT+LAAAAAXNSR0IB2cksfwAAAANQTFRF9tmZzxpnDgAAAENJREFUeJztwYEAAAAAw6D5U1/hAFUBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALwGsYoAAQbjkYoAAAAASUVORK5CYII="/>
+ <clipPath clipPathUnits="userSpaceOnUse" id="cp1">
+ <path d="M173.35 178.24L224.79 178.24L224.79 269.03L173.35 269.03L173.35 178.24Z" />
+ </clipPath>
+ <clipPath clipPathUnits="userSpaceOnUse" id="cp2">
+ <path d="M173.35 178.24L224.79 178.24L224.79 269.03L173.35 269.03L173.35 178.24Z" />
+ </clipPath>
+ <clipPath clipPathUnits="userSpaceOnUse" id="cp3">
+ <path d="M173.35 178.24L224.79 178.24L224.79 269.03L173.35 269.03L173.35 178.24Z" />
+ </clipPath>
+ </defs>
+ <style>
+ tspan { white-space:pre }
+ .shp0 { fill: #eb743b }
+ .shp1 { fill: #353070 }
+ .shp2 { fill: #000000 }
+ .shp3 { fill: #d3562c }
+ .shp4 { fill: #fffedb }
+ .shp5 { opacity: 0.251;fill: #000000 }
+ </style>
+ <use id="Background" href="#img1" x="0" y="0" />
+ <path id="Layer" class="shp0" d="M173.35 152.82L173.35 296.82L234.35 261.82L234.35 187.82L173.35 152.82Z" />
+ <path id="Layer" class="shp1" d="M168.35 152.82L107.35 187.82L107.35 261.82L168.35 296.82L168.35 152.82Z" />
+ <g id="Layer" style="opacity: 0.071">
+ <g id="Clip-Path" clip-path="url(#cp1)">
+ <path id="Layer" class="shp2" d="M190.66 244.56C190.75 244.4 190.86 244.26 190.99 244.13C191.12 244 191.27 243.89 191.43 243.8C191.59 243.71 191.76 243.64 191.93 243.59C192.11 243.55 192.29 243.53 192.48 243.53C192.85 243.53 193.23 243.63 193.55 243.82C193.79 243.97 194 244.15 194.17 244.38C194.33 244.6 194.45 244.85 194.52 245.12C194.59 245.39 194.6 245.67 194.56 245.94C194.52 246.22 194.43 246.48 194.29 246.72C194.2 246.88 194.08 247.02 193.95 247.15C193.82 247.28 193.68 247.39 193.52 247.48C193.36 247.57 193.19 247.64 193.01 247.69C192.84 247.73 192.65 247.75 192.47 247.75C192.09 247.75 191.72 247.65 191.39 247.45C191.27 247.38 191.16 247.3 191.05 247.21C190.95 247.12 190.86 247.01 190.78 246.9C190.69 246.79 190.62 246.67 190.56 246.55C190.5 246.42 190.46 246.29 190.42 246.16C190.39 246.02 190.37 245.89 190.36 245.75C190.35 245.61 190.36 245.47 190.38 245.33C190.4 245.2 190.43 245.06 190.48 244.93C190.53 244.8 190.59 244.68 190.66 244.56M203.26 266.92C203.05 267.1 202.8 267.24 202.54 267.32C202.28 267.41 202 267.44 201.72 267.42C201.45 267.4 201.18 267.33 200.93 267.2C200.68 267.08 200.46 266.9 200.28 266.69C200.1 266.48 199.96 266.24 199.87 265.97C199.79 265.71 199.76 265.43 199.78 265.16C199.8 264.88 199.87 264.61 200 264.36C200.13 264.11 200.3 263.89 200.51 263.71C200.6 263.63 200.7 263.56 200.81 263.49C200.92 263.43 201.03 263.38 201.15 263.33C201.27 263.29 201.39 263.26 201.51 263.23C201.63 263.21 201.76 263.2 201.88 263.2C202.17 263.2 202.46 263.26 202.73 263.38C203 263.5 203.24 263.67 203.43 263.88C203.63 264.09 203.78 264.35 203.88 264.62C203.98 264.9 204.01 265.19 203.99 265.48C203.98 265.62 203.96 265.75 203.92 265.89C203.88 266.02 203.83 266.15 203.77 266.27C203.71 266.4 203.63 266.51 203.55 266.62C203.46 266.73 203.36 266.83 203.26 266.92M193.73 208.08C193.88 207.85 194.08 207.65 194.3 207.49C194.53 207.33 194.79 207.21 195.06 207.15C195.33 207.09 195.61 207.09 195.88 207.13C196.15 207.18 196.41 207.28 196.65 207.42C196.88 207.57 197.09 207.77 197.25 207.99C197.41 208.22 197.52 208.48 197.58 208.75C197.64 209.02 197.65 209.3 197.6 209.57C197.56 209.85 197.46 210.11 197.31 210.34C197.16 210.58 196.97 210.78 196.74 210.94C196.51 211.1 196.26 211.22 195.99 211.28C195.72 211.34 195.44 211.35 195.16 211.3C194.89 211.25 194.63 211.15 194.39 211C194.16 210.85 193.95 210.66 193.79 210.43C193.63 210.21 193.52 209.95 193.46 209.68C193.4 209.41 193.39 209.13 193.44 208.85C193.49 208.58 193.59 208.32 193.73 208.08ZM200.11 187.89C199.51 188.84 198.18 189.16 197.2 188.55L197.1 188.48C196.89 188.33 196.7 188.13 196.55 187.91C196.41 187.68 196.31 187.43 196.26 187.17C196.21 186.91 196.2 186.64 196.25 186.37C196.3 186.11 196.4 185.86 196.55 185.63C196.64 185.48 196.76 185.35 196.88 185.22C197.01 185.1 197.16 185 197.31 184.91C197.47 184.83 197.63 184.76 197.81 184.72C197.98 184.67 198.15 184.65 198.33 184.65C198.71 184.65 199.08 184.75 199.41 184.95C199.73 185.14 200 185.42 200.18 185.75C200.36 186.08 200.45 186.45 200.44 186.83C200.43 187.21 200.32 187.57 200.11 187.89M216.88 199.49C216.97 199.34 217.09 199.2 217.22 199.08C217.35 198.96 217.49 198.86 217.64 198.77C217.8 198.68 217.97 198.62 218.14 198.58C218.31 198.53 218.49 198.51 218.66 198.51C219.06 198.51 219.45 198.62 219.79 198.84C220.27 199.14 220.6 199.61 220.72 200.17C220.76 200.3 220.77 200.44 220.78 200.58C220.78 200.71 220.77 200.85 220.74 200.99C220.72 201.12 220.68 201.25 220.63 201.38C220.58 201.51 220.52 201.63 220.44 201.75C219.85 202.7 218.51 203.02 217.53 202.4L217.44 202.34C217.22 202.19 217.03 201.99 216.89 201.77C216.74 201.54 216.64 201.29 216.59 201.03C216.54 200.77 216.54 200.49 216.59 200.23C216.64 199.97 216.74 199.72 216.88 199.49M216.88 222.95C216.97 222.8 217.09 222.67 217.22 222.54C217.35 222.42 217.49 222.32 217.64 222.23C217.8 222.15 217.97 222.08 218.14 222.04C218.31 222 218.49 221.97 218.66 221.97C219.06 221.97 219.45 222.09 219.79 222.3C220.27 222.6 220.6 223.07 220.72 223.63C220.76 223.76 220.77 223.9 220.78 224.04C220.78 224.18 220.77 224.31 220.74 224.45C220.72 224.59 220.68 224.72 220.63 224.85C220.58 224.98 220.52 225.1 220.44 225.21C219.85 226.16 218.51 226.48 217.53 225.87L217.44 225.8C217.22 225.65 217.03 225.45 216.89 225.23C216.74 225 216.64 224.75 216.59 224.49C216.54 224.23 216.54 223.96 216.59 223.69C216.64 223.43 216.74 223.18 216.88 222.95M216.88 249.26C216.97 249.11 217.09 248.97 217.22 248.85C217.35 248.73 217.49 248.62 217.64 248.54C217.8 248.45 217.97 248.39 218.14 248.34C218.31 248.3 218.49 248.28 218.66 248.28C219.06 248.28 219.45 248.39 219.79 248.6C220.27 248.91 220.6 249.38 220.72 249.93C220.76 250.07 220.77 250.2 220.78 250.34C220.78 250.48 220.77 250.62 220.74 250.75C220.72 250.89 220.68 251.02 220.63 251.15C220.58 251.28 220.52 251.4 220.44 251.52C219.85 252.47 218.51 252.79 217.53 252.17L217.44 252.11C217.22 251.95 217.03 251.76 216.89 251.53C216.74 251.31 216.64 251.06 216.59 250.79C216.54 250.53 216.54 250.26 216.59 250C216.64 249.73 216.74 249.48 216.88 249.26" />
+ </g>
+ <g id="Clip-Path" clip-path="url(#cp2)">
+ <path id="Layer" fill-rule="evenodd" class="shp2" d="M206.53 258.93C206.79 259.23 207.02 259.56 207.22 259.91C207.42 260.26 207.58 260.63 207.71 261.01C207.83 261.39 207.92 261.78 207.96 262.18C208.01 262.58 208.02 262.98 207.99 263.38C207.96 263.78 207.89 264.17 207.78 264.56C207.67 264.95 207.53 265.32 207.35 265.68C207.17 266.04 206.95 266.38 206.7 266.69C206.45 267.01 206.17 267.3 205.87 267.56C205.59 267.79 205.3 268 204.99 268.19C204.68 268.37 204.35 268.52 204.01 268.65C203.67 268.78 203.32 268.87 202.96 268.94C202.61 269 202.25 269.03 201.89 269.03C201.49 269.03 201.1 268.99 200.71 268.92C200.32 268.84 199.94 268.73 199.57 268.58C199.21 268.43 198.85 268.24 198.52 268.02C198.19 267.81 197.88 267.56 197.6 267.28C195.75 267.98 192.69 269 190.58 269C190.48 269 190.37 269 190.27 269C188.3 268.89 187.03 268.03 186.01 267.34C184.44 266.28 182.95 265.27 177.6 266.63C175.24 267.71 173.63 267.79 173.36 267.82L173.36 263.82C173.86 263.72 174.73 263.55 175.91 263.01C175.94 263 175.97 262.98 176 262.97C176.03 262.95 176.06 262.94 176.09 262.92C176.12 262.91 176.15 262.89 176.18 262.88C176.21 262.87 176.24 262.86 176.27 262.85C179.62 261.24 184.79 257.38 188.84 248.16C188.56 247.95 188.29 247.72 188.05 247.47C187.8 247.22 187.58 246.94 187.39 246.65C187.19 246.36 187.02 246.05 186.88 245.73C186.74 245.41 186.62 245.07 186.54 244.73C186.44 244.34 186.38 243.95 186.36 243.55C186.33 243.15 186.35 242.74 186.41 242.35C186.47 241.95 186.57 241.56 186.7 241.18C186.84 240.8 187.01 240.44 187.22 240.1C187.49 239.64 187.82 239.23 188.19 238.86C188.57 238.49 188.99 238.17 189.45 237.91C189.91 237.65 190.4 237.45 190.91 237.32C191.42 237.18 191.95 237.11 192.48 237.11C192.75 237.11 193.02 237.13 193.3 237.17C193.57 237.2 193.84 237.26 194.1 237.33C194.37 237.4 194.62 237.5 194.88 237.6C195.13 237.71 195.37 237.83 195.61 237.97C198.51 239.71 199.46 243.47 197.73 246.37C197.47 246.8 197.16 247.21 196.79 247.56C196.43 247.92 196.03 248.23 195.59 248.49C195.15 248.75 194.68 248.95 194.2 249.1C193.71 249.24 193.2 249.32 192.69 249.34C190.12 255.34 187.05 259.41 184.11 262.18C185.84 262.51 187.09 263.23 188.26 264.02C189.18 264.65 189.66 264.95 190.48 264.99C191.55 265.03 193.86 264.36 195.81 263.66C195.75 263.16 195.75 262.66 195.81 262.16C195.87 261.66 196 261.17 196.18 260.7C196.36 260.23 196.6 259.79 196.89 259.38C197.18 258.97 197.52 258.59 197.9 258.26C198.51 257.74 199.22 257.34 199.98 257.09C200.74 256.84 201.55 256.74 202.35 256.8C203.15 256.87 203.94 257.08 204.65 257.45C205.37 257.81 206.01 258.32 206.53 258.93ZM190.66 242.15C190.59 242.27 190.53 242.4 190.48 242.53C190.43 242.66 190.4 242.79 190.38 242.93C190.36 243.07 190.35 243.2 190.36 243.34C190.37 243.48 190.39 243.62 190.42 243.75C190.46 243.89 190.5 244.02 190.56 244.14C190.62 244.27 190.69 244.39 190.78 244.5C190.86 244.61 190.95 244.71 191.05 244.8C191.16 244.89 191.27 244.98 191.39 245.05C191.72 245.24 192.09 245.35 192.47 245.35C192.65 245.35 192.84 245.33 193.01 245.28C193.19 245.23 193.36 245.16 193.52 245.07C193.68 244.98 193.82 244.87 193.95 244.75C194.08 244.62 194.2 244.47 194.29 244.32C194.58 243.83 194.66 243.26 194.52 242.71C194.38 242.17 194.04 241.7 193.56 241.42C193.07 241.13 192.5 241.05 191.95 241.18C191.41 241.32 190.94 241.67 190.66 242.15ZM203.26 264.51C203.36 264.42 203.46 264.32 203.55 264.22C203.63 264.11 203.71 263.99 203.77 263.87C203.83 263.74 203.88 263.62 203.92 263.48C203.96 263.35 203.98 263.21 203.99 263.07C204.01 262.78 203.98 262.49 203.88 262.22C203.78 261.94 203.63 261.69 203.43 261.47C203.24 261.26 203 261.09 202.73 260.97C202.46 260.86 202.17 260.8 201.88 260.8C201.76 260.8 201.63 260.81 201.51 260.83C201.39 260.85 201.27 260.88 201.15 260.93C201.03 260.97 200.92 261.02 200.81 261.09C200.7 261.15 200.6 261.23 200.51 261.31C200.3 261.49 200.13 261.71 200 261.96C199.87 262.2 199.8 262.47 199.78 262.75C199.76 263.03 199.79 263.31 199.87 263.57C199.96 263.83 200.1 264.08 200.28 264.29C200.46 264.5 200.68 264.67 200.93 264.8C201.18 264.92 201.45 265 201.72 265.02C202 265.04 202.28 265 202.54 264.92C202.8 264.83 203.05 264.69 203.26 264.51ZM204.3 183.03C204.39 183.42 204.44 183.82 204.45 184.22C204.46 184.62 204.43 185.02 204.36 185.41C204.3 185.8 204.19 186.19 204.04 186.56C203.9 186.94 203.72 187.29 203.5 187.63C203.23 188.07 202.9 188.47 202.52 188.82C202.15 189.17 201.73 189.48 201.28 189.72C200.83 189.97 200.35 190.16 199.85 190.29C199.36 190.42 198.84 190.48 198.33 190.48C198.18 190.48 198.03 190.47 197.88 190.46C197.73 190.45 197.58 190.43 197.43 190.41C197.29 190.39 197.14 190.36 196.99 190.33C196.85 190.29 196.7 190.26 196.56 190.21C194.18 193.04 191.3 194.53 188.78 195.31C190.8 196.86 192.59 198.72 194.12 200.88C195.7 200.52 197.41 200.76 198.79 201.63C201.65 203.44 202.5 207.23 200.7 210.08C200.42 210.52 200.09 210.92 199.72 211.27C199.34 211.63 198.93 211.93 198.47 212.18C198.02 212.43 197.54 212.62 197.04 212.74C196.54 212.87 196.03 212.94 195.52 212.93C194.36 212.93 193.23 212.61 192.25 211.98C189.4 210.18 188.55 206.39 190.34 203.54C190.37 203.49 190.4 203.45 190.43 203.4C190.46 203.36 190.49 203.31 190.53 203.27C190.56 203.22 190.59 203.18 190.62 203.13C190.66 203.09 190.69 203.04 190.72 203C188.45 199.89 185.56 197.52 182.12 195.96C182.11 195.96 182.11 195.96 182.1 195.96C178.91 194.5 175.8 193.96 173.35 193.82L173.35 189.82C176.13 189.98 179.67 190.54 183.36 192.14C184.59 192.16 189.66 191.97 193.3 187.85C192.96 187.36 192.69 186.82 192.51 186.25C192.32 185.68 192.22 185.09 192.21 184.49C192.2 183.89 192.27 183.3 192.43 182.72C192.59 182.14 192.84 181.59 193.16 181.09C193.43 180.65 193.76 180.25 194.13 179.9C194.51 179.55 194.92 179.24 195.38 178.99C195.83 178.74 196.31 178.55 196.81 178.43C197.3 178.3 197.82 178.24 198.33 178.24C198.62 178.24 198.91 178.26 199.19 178.3C199.47 178.34 199.76 178.4 200.03 178.48C200.31 178.56 200.58 178.66 200.84 178.78C201.1 178.89 201.35 179.03 201.6 179.18C201.94 179.4 202.26 179.64 202.55 179.92C202.84 180.2 203.1 180.5 203.34 180.83C203.57 181.16 203.77 181.51 203.93 181.88C204.09 182.25 204.22 182.63 204.3 183.03ZM193.73 205.67C193.59 205.91 193.49 206.17 193.44 206.45C193.39 206.72 193.4 207 193.46 207.27C193.52 207.54 193.63 207.8 193.79 208.03C193.95 208.25 194.16 208.45 194.39 208.6C194.63 208.75 194.89 208.85 195.16 208.89C195.44 208.94 195.72 208.93 195.99 208.87C196.26 208.81 196.51 208.7 196.74 208.54C196.97 208.38 197.16 208.17 197.31 207.94C197.46 207.7 197.56 207.44 197.6 207.17C197.65 206.89 197.64 206.61 197.58 206.34C197.52 206.07 197.41 205.81 197.25 205.59C197.09 205.36 196.88 205.17 196.65 205.02C196.41 204.87 196.15 204.77 195.88 204.73C195.61 204.68 195.33 204.69 195.06 204.75C194.79 204.81 194.53 204.92 194.3 205.08C194.08 205.24 193.88 205.44 193.73 205.67ZM200.11 185.49C200.32 185.17 200.43 184.8 200.44 184.42C200.45 184.05 200.36 183.67 200.18 183.34C200 183.01 199.73 182.73 199.41 182.54C199.08 182.35 198.71 182.25 198.33 182.25C198.15 182.25 197.98 182.27 197.81 182.31C197.63 182.35 197.47 182.42 197.31 182.51C197.16 182.59 197.01 182.7 196.88 182.82C196.76 182.94 196.64 183.08 196.55 183.23C196.4 183.45 196.3 183.7 196.25 183.97C196.2 184.23 196.21 184.5 196.26 184.76C196.31 185.03 196.41 185.28 196.55 185.5C196.7 185.73 196.89 185.92 197.11 186.08L197.2 186.14C198.18 186.76 199.51 186.44 200.11 185.49Z" />
+ </g>
+ <g id="Clip-Path" clip-path="url(#cp3)">
+ <path id="Layer" fill-rule="evenodd" class="shp2" d="M213 224.01C210.76 224.39 209.57 225.15 208.14 226.08C206.98 226.83 205.74 227.64 203.93 228.27C208.47 231.6 214.56 237.87 217.12 242.06C217.52 241.95 217.94 241.89 218.35 241.87C218.77 241.85 219.19 241.87 219.6 241.93C220.01 242 220.42 242.11 220.81 242.25C221.2 242.4 221.58 242.58 221.93 242.81C222.27 243.02 222.59 243.27 222.88 243.55C223.17 243.82 223.44 244.13 223.67 244.46C223.9 244.79 224.1 245.14 224.26 245.51C224.43 245.88 224.55 246.26 224.64 246.65C224.73 247.04 224.78 247.44 224.79 247.84C224.8 248.24 224.77 248.64 224.7 249.03C224.63 249.43 224.52 249.81 224.38 250.19C224.23 250.56 224.05 250.92 223.83 251.25C223.56 251.69 223.23 252.09 222.86 252.44C222.48 252.8 222.07 253.1 221.62 253.35C221.16 253.6 220.68 253.79 220.19 253.92C219.69 254.04 219.17 254.11 218.66 254.1C217.5 254.1 216.37 253.78 215.39 253.16C215.26 253.07 215.18 253.02 215.11 252.97C212.46 251.1 211.75 247.47 213.49 244.71C213.59 244.56 213.69 244.42 213.8 244.28C211.13 239.63 201.64 230.6 199.03 229.96C195.63 229.11 193.67 229.18 191.18 229.27C189.73 229.32 188.08 229.39 185.97 229.27C182.89 229.11 181.2 227.19 179.71 225.49C178.15 223.71 176.65 222.12 173.36 221.82L173.36 217.82C178.5 218.2 180.92 220.79 182.72 222.85C184.08 224.39 184.85 225.2 186.18 225.27C188.12 225.37 189.6 225.32 191.03 225.27C192.77 225.2 194.36 225.15 196.32 225.38C202 225.29 203.92 224.04 205.96 222.72C207.58 221.66 209.41 220.48 212.78 219.99C212.82 219.85 212.87 219.71 212.92 219.58C212.97 219.44 213.02 219.31 213.08 219.17C213.14 219.04 213.2 218.91 213.27 218.78C213.34 218.66 213.41 218.53 213.49 218.41C213.54 218.34 213.59 218.27 213.64 218.19C213.69 218.12 213.74 218.05 213.79 217.98C213.84 217.92 213.9 217.85 213.95 217.78C214.01 217.71 214.07 217.65 214.12 217.58C214.26 217.44 214.41 217.32 214.55 217.19C213.44 213.09 213.24 207.29 214.59 202.77C214.56 202.75 214.54 202.73 214.51 202.71C213.99 202.23 213.56 201.66 213.23 201.03C212.9 200.4 212.69 199.72 212.59 199.01C212.5 198.31 212.53 197.59 212.68 196.9C212.84 196.21 213.11 195.55 213.49 194.95C213.76 194.51 214.09 194.11 214.46 193.76C214.84 193.4 215.26 193.1 215.71 192.85C216.16 192.6 216.64 192.41 217.14 192.28C217.64 192.16 218.15 192.09 218.66 192.1C218.95 192.1 219.24 192.12 219.52 192.16C219.81 192.2 220.09 192.26 220.36 192.34C220.64 192.42 220.91 192.52 221.17 192.63C221.43 192.75 221.69 192.89 221.93 193.04C222.27 193.25 222.59 193.5 222.88 193.78C223.17 194.06 223.44 194.36 223.67 194.69C223.9 195.02 224.1 195.37 224.26 195.74C224.42 196.11 224.55 196.49 224.64 196.88C224.73 197.27 224.78 197.67 224.79 198.07C224.8 198.47 224.77 198.87 224.7 199.27C224.63 199.66 224.52 200.05 224.38 200.42C224.23 200.79 224.05 201.15 223.83 201.49C223.56 201.92 223.23 202.32 222.86 202.68C222.48 203.03 222.07 203.33 221.62 203.58C221.16 203.83 220.68 204.02 220.19 204.15C219.69 204.27 219.17 204.34 218.66 204.34C218.55 204.34 218.44 204.32 218.32 204.31C217.37 207.81 217.58 212.42 218.29 215.58C218.41 215.57 218.54 215.56 218.66 215.56C218.95 215.56 219.24 215.58 219.52 215.62C219.81 215.66 220.09 215.72 220.36 215.8C220.64 215.88 220.91 215.98 221.17 216.1C221.43 216.21 221.69 216.35 221.93 216.5C222.27 216.72 222.59 216.96 222.88 217.24C223.17 217.52 223.44 217.82 223.67 218.15C223.9 218.48 224.1 218.83 224.26 219.2C224.42 219.57 224.55 219.95 224.64 220.35C224.73 220.74 224.78 221.14 224.79 221.54C224.8 221.94 224.77 222.34 224.7 222.73C224.63 223.12 224.52 223.51 224.38 223.88C224.23 224.26 224.05 224.61 223.83 224.95C223.56 225.39 223.23 225.79 222.86 226.14C222.48 226.49 222.07 226.8 221.62 227.05C221.16 227.29 220.68 227.48 220.19 227.61C219.69 227.74 219.17 227.8 218.66 227.8C218.34 227.8 218.03 227.78 217.72 227.73C217.4 227.68 217.1 227.6 216.8 227.51C216.49 227.41 216.2 227.29 215.92 227.15C215.64 227.01 215.37 226.84 215.11 226.66C214.88 226.5 214.65 226.31 214.45 226.12C214.24 225.92 214.04 225.71 213.87 225.49C213.69 225.26 213.52 225.03 213.38 224.78C213.24 224.53 213.11 224.28 213 224.01ZM216.88 197.09C216.74 197.31 216.64 197.56 216.59 197.83C216.54 198.09 216.54 198.36 216.59 198.62C216.64 198.88 216.74 199.14 216.89 199.36C217.03 199.59 217.22 199.78 217.44 199.94L217.53 200C218.51 200.62 219.85 200.3 220.44 199.34C220.52 199.23 220.58 199.11 220.63 198.98C220.68 198.85 220.72 198.72 220.74 198.58C220.77 198.44 220.78 198.31 220.78 198.17C220.77 198.03 220.76 197.89 220.73 197.76C220.6 197.2 220.27 196.73 219.79 196.43C219.45 196.22 219.06 196.1 218.66 196.1C218.49 196.1 218.31 196.13 218.14 196.17C217.97 196.21 217.8 196.28 217.64 196.36C217.49 196.45 217.35 196.55 217.22 196.68C217.09 196.8 216.97 196.93 216.88 197.09ZM216.88 220.55C216.74 220.77 216.64 221.03 216.59 221.29C216.54 221.55 216.54 221.82 216.59 222.09C216.64 222.35 216.74 222.6 216.89 222.82C217.03 223.05 217.22 223.24 217.44 223.4L217.53 223.46C218.51 224.08 219.85 223.76 220.44 222.81C220.52 222.69 220.58 222.57 220.63 222.44C220.68 222.31 220.72 222.18 220.74 222.04C220.77 221.91 220.78 221.77 220.78 221.63C220.77 221.49 220.76 221.36 220.72 221.22C220.6 220.67 220.27 220.2 219.79 219.89C219.45 219.68 219.06 219.57 218.66 219.57C218.49 219.57 218.31 219.59 218.14 219.63C217.97 219.68 217.8 219.74 217.64 219.83C217.49 219.91 217.35 220.02 217.22 220.14C217.09 220.26 216.97 220.4 216.88 220.55ZM216.88 246.85C216.74 247.08 216.64 247.33 216.59 247.59C216.54 247.86 216.54 248.13 216.59 248.39C216.64 248.65 216.74 248.9 216.89 249.13C217.03 249.35 217.22 249.55 217.44 249.7L217.53 249.76C218.51 250.38 219.85 250.06 220.44 249.11C220.52 249 220.58 248.87 220.63 248.74C220.68 248.62 220.72 248.48 220.74 248.35C220.77 248.21 220.78 248.07 220.78 247.94C220.77 247.8 220.76 247.66 220.72 247.53C220.6 246.97 220.27 246.5 219.79 246.2C219.45 245.98 219.06 245.87 218.66 245.87C218.49 245.87 218.31 245.89 218.14 245.94C217.97 245.98 217.8 246.05 217.64 246.13C217.49 246.22 217.35 246.32 217.22 246.44C217.09 246.56 216.97 246.7 216.88 246.85Z" />
+ </g>
+ </g>
+ <path id="Layer" fill-rule="evenodd" class="shp3" d="M206.53 261.93C206.79 262.23 207.02 262.56 207.22 262.91C207.42 263.26 207.58 263.63 207.71 264.01C207.83 264.39 207.92 264.78 207.96 265.18C208.01 265.58 208.02 265.98 207.99 266.38C207.96 266.78 207.89 267.17 207.78 267.56C207.67 267.95 207.53 268.32 207.35 268.68C207.17 269.04 206.95 269.38 206.7 269.69C206.45 270.01 206.17 270.3 205.87 270.56C205.59 270.79 205.3 271 204.99 271.19C204.68 271.37 204.35 271.52 204.01 271.65C203.67 271.78 203.32 271.87 202.96 271.94C202.61 272 202.25 272.03 201.89 272.03C201.49 272.03 201.1 271.99 200.71 271.92C200.32 271.84 199.94 271.73 199.57 271.58C199.21 271.43 198.85 271.24 198.52 271.02C198.19 270.81 197.88 270.56 197.6 270.28C195.75 270.98 192.69 272 190.58 272C190.48 272 190.37 272 190.27 272C188.3 271.89 187.03 271.03 186.01 270.34C184.44 269.28 182.95 268.27 177.6 269.63C175.24 270.71 173.63 270.79 173.36 270.82L173.36 266.82C173.86 266.72 174.73 266.55 175.91 266.01C175.94 266 175.97 265.98 176 265.97C176.03 265.95 176.06 265.94 176.09 265.92C176.12 265.91 176.15 265.89 176.18 265.88C176.21 265.87 176.24 265.86 176.27 265.85C179.62 264.24 184.79 260.38 188.84 251.16C188.56 250.95 188.29 250.72 188.05 250.47C187.8 250.22 187.58 249.94 187.39 249.65C187.19 249.36 187.02 249.05 186.88 248.73C186.74 248.41 186.62 248.07 186.54 247.73C186.44 247.34 186.38 246.95 186.36 246.55C186.33 246.15 186.35 245.74 186.41 245.35C186.47 244.95 186.57 244.56 186.7 244.18C186.84 243.8 187.01 243.44 187.22 243.1C187.49 242.64 187.82 242.23 188.19 241.86C188.57 241.49 188.99 241.17 189.45 240.91C189.91 240.65 190.4 240.45 190.91 240.32C191.42 240.18 191.95 240.11 192.48 240.11C192.75 240.11 193.02 240.13 193.3 240.17C193.57 240.2 193.84 240.26 194.1 240.33C194.37 240.4 194.62 240.5 194.88 240.6C195.13 240.71 195.37 240.83 195.61 240.97C198.51 242.71 199.46 246.47 197.73 249.37C197.47 249.8 197.16 250.21 196.79 250.56C196.43 250.92 196.03 251.23 195.59 251.49C195.15 251.75 194.68 251.95 194.2 252.1C193.71 252.24 193.2 252.32 192.69 252.34C190.12 258.35 187.05 262.41 184.11 265.18C185.84 265.51 187.09 266.23 188.26 267.02C189.18 267.65 189.66 267.95 190.48 267.99C191.55 268.03 193.86 267.36 195.81 266.66C195.75 266.16 195.75 265.66 195.81 265.16C195.87 264.66 196 264.17 196.18 263.7C196.36 263.23 196.6 262.79 196.89 262.38C197.18 261.97 197.52 261.59 197.9 261.26C198.51 260.74 199.22 260.34 199.98 260.09C200.74 259.84 201.55 259.74 202.35 259.8C203.15 259.87 203.94 260.08 204.65 260.45C205.37 260.81 206.01 261.32 206.53 261.93ZM190.66 245.15C190.59 245.27 190.53 245.4 190.48 245.53C190.43 245.66 190.4 245.79 190.38 245.93C190.36 246.07 190.35 246.2 190.36 246.34C190.37 246.48 190.39 246.62 190.42 246.75C190.46 246.89 190.5 247.02 190.56 247.14C190.62 247.27 190.69 247.39 190.78 247.5C190.86 247.61 190.95 247.71 191.05 247.8C191.16 247.89 191.27 247.98 191.39 248.05C191.72 248.24 192.09 248.35 192.47 248.35C192.65 248.35 192.84 248.33 193.01 248.28C193.19 248.23 193.36 248.16 193.52 248.07C193.68 247.98 193.82 247.87 193.95 247.75C194.08 247.62 194.2 247.47 194.29 247.32C194.58 246.83 194.66 246.26 194.52 245.71C194.38 245.17 194.04 244.7 193.56 244.42C193.07 244.13 192.5 244.05 191.95 244.18C191.41 244.32 190.94 244.67 190.66 245.15ZM203.26 267.51C203.36 267.42 203.46 267.32 203.55 267.22C203.63 267.11 203.71 266.99 203.77 266.87C203.83 266.74 203.88 266.62 203.92 266.48C203.96 266.35 203.98 266.21 203.99 266.07C204.01 265.78 203.98 265.49 203.88 265.22C203.78 264.94 203.63 264.69 203.43 264.47C203.24 264.26 203 264.09 202.73 263.97C202.46 263.86 202.17 263.8 201.88 263.8C201.76 263.8 201.63 263.81 201.51 263.83C201.39 263.85 201.27 263.88 201.15 263.93C201.03 263.97 200.92 264.02 200.81 264.09C200.7 264.15 200.6 264.23 200.51 264.31C200.3 264.49 200.13 264.71 200 264.96C199.87 265.2 199.8 265.47 199.78 265.75C199.76 266.03 199.79 266.31 199.87 266.57C199.96 266.83 200.1 267.08 200.28 267.29C200.46 267.5 200.68 267.67 200.93 267.8C201.18 267.92 201.45 268 201.72 268.02C202 268.04 202.28 268 202.54 267.92C202.8 267.83 203.05 267.69 203.26 267.51Z" />
+ <path id="Layer" class="shp2" d="" />
+ <path id="Layer" fill-rule="evenodd" class="shp3" d="M204.3 185.03C204.39 185.42 204.44 185.82 204.45 186.22C204.46 186.62 204.43 187.02 204.36 187.41C204.3 187.81 204.19 188.19 204.04 188.57C203.9 188.94 203.72 189.3 203.5 189.63C203.23 190.07 202.9 190.47 202.53 190.82C202.15 191.18 201.73 191.48 201.28 191.73C200.83 191.98 200.35 192.17 199.85 192.29C199.36 192.42 198.84 192.48 198.33 192.48C198.18 192.48 198.03 192.48 197.88 192.46C197.73 192.45 197.58 192.44 197.43 192.41C197.29 192.39 197.14 192.36 196.99 192.33C196.85 192.3 196.7 192.26 196.56 192.22C194.18 195.04 191.3 196.53 188.78 197.31C190.8 198.86 192.59 200.72 194.12 202.88C195.7 202.52 197.41 202.76 198.79 203.63C201.65 205.44 202.5 209.24 200.7 212.09C200.42 212.52 200.09 212.92 199.72 213.28C199.34 213.63 198.93 213.93 198.47 214.18C198.02 214.43 197.54 214.62 197.04 214.75C196.54 214.88 196.03 214.94 195.52 214.94C194.36 214.94 193.23 214.61 192.25 213.99C189.4 212.19 188.55 208.4 190.34 205.54C190.37 205.5 190.4 205.45 190.43 205.4C190.46 205.36 190.49 205.31 190.53 205.27C190.56 205.22 190.59 205.18 190.62 205.14C190.66 205.09 190.69 205.05 190.72 205C188.45 201.89 185.56 199.53 182.12 197.96C182.11 197.96 182.11 197.96 182.1 197.96C178.91 196.51 175.8 195.97 173.35 195.82L173.35 191.83C176.13 191.98 179.67 192.54 183.36 194.14C184.59 194.16 189.66 193.97 193.3 189.85C192.96 189.36 192.69 188.82 192.51 188.25C192.32 187.68 192.22 187.09 192.21 186.49C192.2 185.9 192.27 185.3 192.43 184.72C192.59 184.15 192.84 183.6 193.16 183.09C193.43 182.66 193.76 182.26 194.13 181.9C194.51 181.55 194.92 181.25 195.38 181C195.83 180.75 196.31 180.56 196.81 180.43C197.3 180.3 197.82 180.24 198.33 180.24C198.62 180.24 198.91 180.26 199.19 180.3C199.47 180.34 199.76 180.4 200.03 180.48C200.31 180.56 200.58 180.66 200.84 180.78C201.1 180.9 201.35 181.03 201.6 181.19C201.94 181.4 202.26 181.65 202.55 181.93C202.84 182.2 203.1 182.51 203.34 182.84C203.57 183.17 203.77 183.52 203.93 183.89C204.09 184.25 204.22 184.64 204.3 185.03ZM193.73 207.68C193.59 207.91 193.49 208.18 193.44 208.45C193.39 208.72 193.4 209 193.46 209.28C193.52 209.55 193.63 209.8 193.79 210.03C193.95 210.26 194.16 210.45 194.39 210.6C194.63 210.75 194.89 210.85 195.16 210.9C195.44 210.95 195.72 210.94 195.99 210.88C196.26 210.82 196.51 210.7 196.74 210.54C196.97 210.38 197.16 210.18 197.31 209.94C197.46 209.71 197.56 209.44 197.6 209.17C197.65 208.9 197.64 208.62 197.58 208.35C197.52 208.07 197.41 207.82 197.25 207.59C197.09 207.36 196.88 207.17 196.65 207.02C196.41 206.88 196.15 206.78 195.88 206.73C195.61 206.68 195.33 206.69 195.06 206.75C194.79 206.81 194.53 206.93 194.3 207.08C194.08 207.24 193.88 207.44 193.73 207.68ZM200.11 187.49C200.32 187.17 200.43 186.8 200.44 186.43C200.45 186.05 200.36 185.68 200.18 185.34C200 185.01 199.73 184.74 199.41 184.55C199.08 184.35 198.71 184.25 198.33 184.25C198.15 184.25 197.98 184.27 197.81 184.32C197.63 184.36 197.47 184.42 197.31 184.51C197.16 184.59 197.01 184.7 196.88 184.82C196.76 184.94 196.64 185.08 196.55 185.23C196.4 185.46 196.3 185.71 196.25 185.97C196.2 186.23 196.21 186.5 196.26 186.77C196.31 187.03 196.41 187.28 196.55 187.51C196.7 187.73 196.89 187.93 197.11 188.08L197.2 188.14C198.18 188.76 199.51 188.44 200.11 187.49Z" />
+ <path id="Layer" fill-rule="evenodd" class="shp3" d="M213 227.01C210.76 227.39 209.57 228.15 208.14 229.08C206.98 229.83 205.74 230.64 203.93 231.26C208.47 234.6 214.56 240.87 217.12 245.06C217.52 244.95 217.94 244.89 218.35 244.87C218.77 244.85 219.19 244.87 219.6 244.93C220.01 245 220.42 245.1 220.81 245.25C221.2 245.4 221.58 245.58 221.93 245.81C222.27 246.02 222.59 246.27 222.88 246.55C223.17 246.82 223.44 247.13 223.67 247.46C223.9 247.79 224.1 248.14 224.26 248.51C224.43 248.87 224.55 249.26 224.64 249.65C224.73 250.04 224.78 250.44 224.79 250.84C224.8 251.24 224.77 251.64 224.7 252.03C224.63 252.43 224.52 252.81 224.38 253.19C224.23 253.56 224.05 253.92 223.83 254.25C223.56 254.69 223.23 255.09 222.86 255.44C222.48 255.8 222.07 256.1 221.62 256.35C221.16 256.6 220.68 256.79 220.19 256.91C219.69 257.04 219.17 257.1 218.66 257.1C217.5 257.1 216.37 256.78 215.39 256.15C215.26 256.07 215.18 256.02 215.11 255.96C212.46 254.1 211.75 250.47 213.49 247.71C213.59 247.56 213.69 247.42 213.8 247.28C211.13 242.63 201.64 233.6 199.03 232.96C195.63 232.11 193.67 232.18 191.18 232.27C189.73 232.32 188.08 232.39 185.97 232.27C182.89 232.1 181.2 230.18 179.71 228.49C178.15 226.71 176.65 225.12 173.36 224.82L173.36 220.82C178.5 221.2 180.92 223.79 182.72 225.84C184.08 227.39 184.85 228.2 186.18 228.27C188.12 228.37 189.6 228.32 191.03 228.27C192.77 228.2 194.36 228.15 196.32 228.38C202 228.29 203.92 227.04 205.96 225.72C207.58 224.66 209.41 223.48 212.78 222.99C212.82 222.85 212.87 222.71 212.92 222.58C212.97 222.44 213.02 222.31 213.08 222.17C213.14 222.04 213.2 221.91 213.27 221.78C213.34 221.66 213.41 221.53 213.49 221.41C213.54 221.34 213.59 221.27 213.64 221.19C213.69 221.12 213.74 221.05 213.79 220.98C213.84 220.92 213.9 220.85 213.95 220.78C214.01 220.71 214.07 220.65 214.12 220.58C214.26 220.44 214.41 220.32 214.55 220.19C213.44 216.09 213.24 210.29 214.59 205.77C214.56 205.75 214.54 205.73 214.51 205.71C213.99 205.23 213.56 204.66 213.23 204.03C212.9 203.4 212.69 202.72 212.59 202.01C212.5 201.31 212.53 200.59 212.68 199.9C212.84 199.21 213.11 198.55 213.49 197.95C213.76 197.51 214.09 197.11 214.46 196.76C214.84 196.4 215.26 196.1 215.71 195.85C216.16 195.6 216.64 195.41 217.14 195.28C217.64 195.16 218.15 195.09 218.66 195.1C218.95 195.1 219.24 195.12 219.52 195.16C219.81 195.2 220.09 195.26 220.36 195.34C220.64 195.42 220.91 195.52 221.17 195.63C221.43 195.75 221.69 195.89 221.93 196.04C222.27 196.25 222.59 196.5 222.88 196.78C223.17 197.06 223.44 197.36 223.67 197.69C223.9 198.02 224.1 198.37 224.26 198.74C224.42 199.11 224.55 199.49 224.64 199.88C224.73 200.27 224.78 200.67 224.79 201.07C224.8 201.47 224.77 201.87 224.7 202.27C224.63 202.66 224.52 203.05 224.38 203.42C224.23 203.79 224.05 204.15 223.83 204.49C223.56 204.92 223.23 205.32 222.86 205.68C222.48 206.03 222.07 206.33 221.62 206.58C221.16 206.83 220.68 207.02 220.19 207.15C219.69 207.27 219.17 207.34 218.66 207.34C218.55 207.34 218.44 207.32 218.32 207.31C217.37 210.81 217.58 215.42 218.29 218.58C218.41 218.57 218.54 218.56 218.66 218.56C218.95 218.56 219.24 218.58 219.52 218.62C219.81 218.66 220.09 218.72 220.36 218.8C220.64 218.88 220.91 218.98 221.17 219.1C221.43 219.21 221.69 219.35 221.93 219.5C222.27 219.72 222.59 219.96 222.88 220.24C223.17 220.52 223.44 220.82 223.67 221.15C223.9 221.48 224.1 221.83 224.26 222.2C224.42 222.57 224.55 222.95 224.64 223.35C224.73 223.74 224.78 224.14 224.79 224.54C224.8 224.94 224.77 225.34 224.7 225.73C224.63 226.12 224.52 226.51 224.38 226.88C224.23 227.26 224.05 227.61 223.83 227.95C223.56 228.39 223.23 228.79 222.86 229.14C222.48 229.49 222.07 229.8 221.62 230.05C221.16 230.29 220.68 230.48 220.19 230.61C219.69 230.74 219.17 230.8 218.66 230.8C218.34 230.8 218.03 230.78 217.72 230.73C217.4 230.68 217.1 230.6 216.8 230.51C216.49 230.41 216.2 230.29 215.92 230.15C215.64 230.01 215.37 229.84 215.11 229.66C214.88 229.5 214.65 229.31 214.45 229.12C214.24 228.92 214.04 228.71 213.87 228.49C213.69 228.26 213.52 228.03 213.38 227.78C213.24 227.53 213.11 227.28 213 227.01ZM216.88 200.09C216.74 200.31 216.64 200.56 216.59 200.83C216.54 201.09 216.54 201.36 216.59 201.62C216.64 201.88 216.74 202.14 216.89 202.36C217.03 202.59 217.22 202.78 217.44 202.94L217.53 203C218.51 203.62 219.85 203.3 220.44 202.34C220.52 202.23 220.58 202.11 220.63 201.98C220.68 201.85 220.72 201.72 220.74 201.58C220.77 201.44 220.78 201.31 220.78 201.17C220.77 201.03 220.76 200.89 220.73 200.76C220.6 200.2 220.27 199.73 219.79 199.43C219.45 199.22 219.06 199.1 218.66 199.1C218.49 199.1 218.31 199.13 218.14 199.17C217.97 199.21 217.8 199.28 217.64 199.36C217.49 199.45 217.35 199.55 217.22 199.68C217.09 199.8 216.97 199.93 216.88 200.09ZM216.88 223.55C216.74 223.77 216.64 224.03 216.59 224.29C216.54 224.55 216.54 224.82 216.59 225.09C216.64 225.35 216.74 225.6 216.89 225.82C217.03 226.05 217.22 226.24 217.44 226.4L217.53 226.46C218.51 227.08 219.85 226.76 220.44 225.81C220.52 225.69 220.58 225.57 220.63 225.44C220.68 225.31 220.72 225.18 220.74 225.04C220.77 224.91 220.78 224.77 220.78 224.63C220.77 224.49 220.76 224.36 220.72 224.22C220.6 223.67 220.27 223.2 219.79 222.89C219.45 222.68 219.06 222.57 218.66 222.57C218.49 222.57 218.31 222.59 218.14 222.63C217.97 222.68 217.8 222.74 217.64 222.83C217.49 222.91 217.35 223.02 217.22 223.14C217.09 223.26 216.97 223.4 216.88 223.55ZM216.88 249.85C216.74 250.08 216.64 250.33 216.59 250.59C216.54 250.86 216.54 251.13 216.59 251.39C216.64 251.65 216.74 251.9 216.89 252.13C217.03 252.35 217.22 252.55 217.44 252.7L217.53 252.76C218.51 253.38 219.85 253.06 220.44 252.11C220.52 252 220.58 251.87 220.63 251.74C220.68 251.62 220.72 251.48 220.74 251.35C220.77 251.21 220.78 251.07 220.78 250.94C220.77 250.8 220.76 250.66 220.72 250.53C220.6 249.97 220.27 249.5 219.79 249.2C219.45 248.98 219.06 248.87 218.66 248.87C218.49 248.87 218.31 248.89 218.14 248.94C217.97 248.98 217.8 249.05 217.64 249.13C217.49 249.22 217.35 249.32 217.22 249.44C217.09 249.56 216.97 249.7 216.88 249.85Z" />
+ <path id="Layer" fill-rule="evenodd" class="shp4" d="M206.53 258.93C206.79 259.23 207.02 259.56 207.22 259.91C207.42 260.26 207.58 260.63 207.71 261.01C207.83 261.39 207.92 261.78 207.96 262.18C208.01 262.58 208.02 262.98 207.99 263.38C207.96 263.78 207.89 264.17 207.78 264.56C207.67 264.95 207.53 265.32 207.35 265.68C207.17 266.04 206.95 266.38 206.7 266.69C206.45 267.01 206.17 267.3 205.87 267.56C205.59 267.79 205.3 268 204.99 268.19C204.68 268.37 204.35 268.52 204.01 268.65C203.67 268.78 203.32 268.87 202.96 268.94C202.61 269 202.25 269.03 201.89 269.03C201.49 269.03 201.1 268.99 200.71 268.92C200.32 268.84 199.94 268.73 199.57 268.58C199.21 268.43 198.85 268.24 198.52 268.02C198.19 267.81 197.88 267.56 197.6 267.28C195.75 267.98 192.69 269 190.58 269C190.48 269 190.37 269 190.27 269C188.3 268.89 187.03 268.03 186.01 267.34C184.44 266.28 182.95 265.27 177.6 266.63C175.24 267.71 173.63 267.79 173.36 267.82L173.36 263.82C173.86 263.72 174.73 263.55 175.91 263.01C175.94 263 175.97 262.98 176 262.97C176.03 262.95 176.06 262.94 176.09 262.92C176.12 262.91 176.15 262.89 176.18 262.88C176.21 262.87 176.24 262.86 176.27 262.85C179.62 261.24 184.79 257.38 188.84 248.16C188.56 247.95 188.29 247.72 188.05 247.47C187.8 247.22 187.58 246.94 187.39 246.65C187.19 246.36 187.02 246.05 186.88 245.73C186.74 245.41 186.62 245.07 186.54 244.73C186.44 244.34 186.38 243.95 186.36 243.55C186.33 243.15 186.35 242.74 186.41 242.35C186.47 241.95 186.57 241.56 186.7 241.18C186.84 240.8 187.01 240.44 187.22 240.1C187.49 239.64 187.82 239.23 188.19 238.86C188.57 238.49 188.99 238.17 189.45 237.91C189.91 237.65 190.4 237.45 190.91 237.32C191.42 237.18 191.95 237.11 192.48 237.11C192.75 237.11 193.02 237.13 193.3 237.17C193.57 237.2 193.84 237.26 194.1 237.33C194.37 237.4 194.62 237.5 194.88 237.6C195.13 237.71 195.37 237.83 195.61 237.97C198.51 239.71 199.46 243.47 197.73 246.37C197.47 246.8 197.16 247.21 196.79 247.56C196.43 247.92 196.03 248.23 195.59 248.49C195.15 248.75 194.68 248.95 194.2 249.1C193.71 249.24 193.2 249.32 192.69 249.34C190.12 255.34 187.05 259.41 184.11 262.18C185.84 262.51 187.09 263.23 188.26 264.02C189.18 264.65 189.66 264.95 190.48 264.99C191.55 265.03 193.86 264.36 195.81 263.66C195.75 263.16 195.75 262.66 195.81 262.16C195.87 261.66 196 261.17 196.18 260.7C196.36 260.23 196.6 259.79 196.89 259.38C197.18 258.97 197.52 258.59 197.9 258.26C198.51 257.74 199.22 257.34 199.98 257.09C200.74 256.84 201.55 256.74 202.35 256.8C203.15 256.87 203.94 257.08 204.65 257.45C205.37 257.81 206.01 258.32 206.53 258.93ZM190.66 242.15C190.59 242.27 190.53 242.4 190.48 242.53C190.43 242.66 190.4 242.79 190.38 242.93C190.36 243.07 190.35 243.2 190.36 243.34C190.37 243.48 190.39 243.62 190.42 243.75C190.46 243.89 190.5 244.02 190.56 244.14C190.62 244.27 190.69 244.39 190.78 244.5C190.86 244.61 190.95 244.71 191.05 244.8C191.16 244.89 191.27 244.98 191.39 245.05C191.72 245.24 192.09 245.35 192.47 245.35C192.65 245.35 192.84 245.33 193.01 245.28C193.19 245.23 193.36 245.16 193.52 245.07C193.68 244.98 193.82 244.87 193.95 244.75C194.08 244.62 194.2 244.47 194.29 244.32C194.58 243.83 194.66 243.26 194.52 242.71C194.38 242.17 194.04 241.7 193.56 241.42C193.07 241.13 192.5 241.05 191.95 241.18C191.41 241.32 190.94 241.67 190.66 242.15ZM203.26 264.51C203.36 264.42 203.46 264.32 203.55 264.22C203.63 264.11 203.71 263.99 203.77 263.87C203.83 263.74 203.88 263.62 203.92 263.48C203.96 263.35 203.98 263.21 203.99 263.07C204.01 262.78 203.98 262.49 203.88 262.22C203.78 261.94 203.63 261.69 203.43 261.47C203.24 261.26 203 261.09 202.73 260.97C202.46 260.86 202.17 260.8 201.88 260.8C201.76 260.8 201.63 260.81 201.51 260.83C201.39 260.85 201.27 260.88 201.15 260.93C201.03 260.97 200.92 261.02 200.81 261.09C200.7 261.15 200.6 261.23 200.51 261.31C200.3 261.49 200.13 261.71 200 261.96C199.87 262.2 199.8 262.47 199.78 262.75C199.76 263.03 199.79 263.31 199.87 263.57C199.96 263.83 200.1 264.08 200.28 264.29C200.46 264.5 200.68 264.67 200.93 264.8C201.18 264.92 201.45 265 201.72 265.02C202 265.04 202.28 265 202.54 264.92C202.8 264.83 203.05 264.69 203.26 264.51ZM204.3 183.03C204.39 183.42 204.44 183.82 204.45 184.22C204.46 184.62 204.43 185.02 204.36 185.41C204.3 185.8 204.19 186.19 204.04 186.56C203.9 186.94 203.72 187.29 203.5 187.63C203.23 188.07 202.9 188.47 202.52 188.82C202.15 189.17 201.73 189.48 201.28 189.72C200.83 189.97 200.35 190.16 199.85 190.29C199.36 190.42 198.84 190.48 198.33 190.48C198.18 190.48 198.03 190.47 197.88 190.46C197.73 190.45 197.58 190.43 197.43 190.41C197.29 190.39 197.14 190.36 196.99 190.33C196.85 190.29 196.7 190.26 196.56 190.21C194.18 193.04 191.3 194.53 188.78 195.31C190.8 196.86 192.59 198.72 194.12 200.88C195.7 200.52 197.41 200.76 198.79 201.63C201.65 203.44 202.5 207.23 200.7 210.08C200.42 210.52 200.09 210.92 199.72 211.27C199.34 211.63 198.93 211.93 198.47 212.18C198.02 212.43 197.54 212.62 197.04 212.74C196.54 212.87 196.03 212.94 195.52 212.93C194.36 212.93 193.23 212.61 192.25 211.98C189.4 210.18 188.55 206.39 190.34 203.54C190.37 203.49 190.4 203.45 190.43 203.4C190.46 203.36 190.49 203.31 190.53 203.27C190.56 203.22 190.59 203.18 190.62 203.13C190.66 203.09 190.69 203.04 190.72 203C188.45 199.89 185.56 197.52 182.12 195.96C182.11 195.96 182.11 195.96 182.1 195.96C178.91 194.5 175.8 193.96 173.35 193.82L173.35 189.82C176.13 189.98 179.67 190.54 183.36 192.14C184.59 192.16 189.66 191.97 193.3 187.85C192.96 187.36 192.69 186.82 192.51 186.25C192.32 185.68 192.22 185.09 192.21 184.49C192.2 183.89 192.27 183.3 192.43 182.72C192.59 182.14 192.84 181.59 193.16 181.09C193.43 180.65 193.76 180.25 194.13 179.9C194.51 179.55 194.92 179.24 195.38 178.99C195.83 178.74 196.31 178.55 196.81 178.43C197.3 178.3 197.82 178.24 198.33 178.24C198.62 178.24 198.91 178.26 199.19 178.3C199.47 178.34 199.76 178.4 200.03 178.48C200.31 178.56 200.58 178.66 200.84 178.78C201.1 178.89 201.35 179.03 201.6 179.18C201.94 179.4 202.26 179.64 202.55 179.92C202.84 180.2 203.1 180.5 203.34 180.83C203.57 181.16 203.77 181.51 203.93 181.88C204.09 182.25 204.22 182.63 204.3 183.03ZM193.73 205.67C193.59 205.91 193.49 206.17 193.44 206.45C193.39 206.72 193.4 207 193.46 207.27C193.52 207.54 193.63 207.8 193.79 208.03C193.95 208.25 194.16 208.45 194.39 208.6C194.63 208.75 194.89 208.85 195.16 208.89C195.44 208.94 195.72 208.93 195.99 208.87C196.26 208.81 196.51 208.7 196.74 208.54C196.97 208.38 197.16 208.17 197.31 207.94C197.46 207.7 197.56 207.44 197.6 207.17C197.65 206.89 197.64 206.61 197.58 206.34C197.52 206.07 197.41 205.81 197.25 205.59C197.09 205.36 196.88 205.17 196.65 205.02C196.41 204.87 196.15 204.77 195.88 204.73C195.61 204.68 195.33 204.69 195.06 204.75C194.79 204.81 194.53 204.92 194.3 205.08C194.08 205.24 193.88 205.44 193.73 205.67ZM200.11 185.49C200.32 185.17 200.43 184.8 200.44 184.42C200.45 184.05 200.36 183.67 200.18 183.34C200 183.01 199.73 182.73 199.41 182.54C199.08 182.35 198.71 182.25 198.33 182.25C198.15 182.25 197.98 182.27 197.81 182.31C197.63 182.35 197.47 182.42 197.31 182.51C197.16 182.59 197.01 182.7 196.88 182.82C196.76 182.94 196.64 183.08 196.55 183.23C196.4 183.45 196.3 183.7 196.25 183.97C196.2 184.23 196.21 184.5 196.26 184.76C196.31 185.03 196.41 185.28 196.55 185.5C196.7 185.73 196.89 185.92 197.11 186.08L197.2 186.14C198.18 186.76 199.51 186.44 200.11 185.49Z" />
+ <path id="Layer" fill-rule="evenodd" class="shp4" d="M213 224.01C210.76 224.39 209.57 225.15 208.14 226.08C206.98 226.83 205.74 227.64 203.93 228.27C208.47 231.6 214.56 237.87 217.12 242.06C217.52 241.95 217.94 241.89 218.35 241.87C218.77 241.85 219.19 241.87 219.6 241.93C220.01 242 220.42 242.11 220.81 242.25C221.2 242.4 221.58 242.58 221.93 242.81C222.27 243.02 222.59 243.27 222.88 243.55C223.17 243.82 223.44 244.13 223.67 244.46C223.9 244.79 224.1 245.14 224.26 245.51C224.43 245.88 224.55 246.26 224.64 246.65C224.73 247.04 224.78 247.44 224.79 247.84C224.8 248.24 224.77 248.64 224.7 249.03C224.63 249.43 224.52 249.81 224.38 250.19C224.23 250.56 224.05 250.92 223.83 251.25C223.56 251.69 223.23 252.09 222.86 252.44C222.48 252.8 222.07 253.1 221.62 253.35C221.16 253.6 220.68 253.79 220.19 253.92C219.69 254.04 219.17 254.11 218.66 254.1C217.5 254.1 216.37 253.78 215.39 253.16C215.26 253.07 215.18 253.02 215.11 252.97C212.46 251.1 211.75 247.47 213.49 244.71C213.59 244.56 213.69 244.42 213.8 244.28C211.13 239.63 201.64 230.6 199.03 229.96C195.63 229.11 193.67 229.18 191.18 229.27C189.73 229.32 188.08 229.39 185.97 229.27C182.89 229.11 181.2 227.19 179.71 225.49C178.15 223.71 176.65 222.12 173.36 221.82L173.36 217.82C178.5 218.2 180.92 220.79 182.72 222.85C184.08 224.39 184.85 225.2 186.18 225.27C188.12 225.37 189.6 225.32 191.03 225.27C192.77 225.2 194.36 225.15 196.32 225.38C202 225.29 203.92 224.04 205.96 222.72C207.58 221.66 209.41 220.48 212.78 219.99C212.82 219.85 212.87 219.71 212.92 219.58C212.97 219.44 213.02 219.31 213.08 219.17C213.14 219.04 213.2 218.91 213.27 218.78C213.34 218.66 213.41 218.53 213.49 218.41C213.54 218.34 213.59 218.27 213.64 218.19C213.69 218.12 213.74 218.05 213.79 217.98C213.84 217.92 213.9 217.85 213.95 217.78C214.01 217.71 214.07 217.65 214.12 217.58C214.26 217.44 214.41 217.32 214.55 217.19C213.44 213.09 213.24 207.29 214.59 202.77C214.56 202.75 214.54 202.73 214.51 202.71C213.99 202.23 213.56 201.66 213.23 201.03C212.9 200.4 212.69 199.72 212.59 199.01C212.5 198.31 212.53 197.59 212.68 196.9C212.84 196.21 213.11 195.55 213.49 194.95C213.76 194.51 214.09 194.11 214.46 193.76C214.84 193.4 215.26 193.1 215.71 192.85C216.16 192.6 216.64 192.41 217.14 192.28C217.64 192.16 218.15 192.09 218.66 192.1C218.95 192.1 219.24 192.12 219.52 192.16C219.81 192.2 220.09 192.26 220.36 192.34C220.64 192.42 220.91 192.52 221.17 192.63C221.43 192.75 221.69 192.89 221.93 193.04C222.27 193.25 222.59 193.5 222.88 193.78C223.17 194.06 223.44 194.36 223.67 194.69C223.9 195.02 224.1 195.37 224.26 195.74C224.42 196.11 224.55 196.49 224.64 196.88C224.73 197.27 224.78 197.67 224.79 198.07C224.8 198.47 224.77 198.87 224.7 199.27C224.63 199.66 224.52 200.05 224.38 200.42C224.23 200.79 224.05 201.15 223.83 201.49C223.56 201.92 223.23 202.32 222.86 202.68C222.48 203.03 222.07 203.33 221.62 203.58C221.16 203.83 220.68 204.02 220.19 204.15C219.69 204.27 219.17 204.34 218.66 204.34C218.55 204.34 218.44 204.32 218.32 204.31C217.37 207.81 217.58 212.42 218.29 215.58C218.41 215.57 218.54 215.56 218.66 215.56C218.95 215.56 219.24 215.58 219.52 215.62C219.81 215.66 220.09 215.72 220.36 215.8C220.64 215.88 220.91 215.98 221.17 216.1C221.43 216.21 221.69 216.35 221.93 216.5C222.27 216.72 222.59 216.96 222.88 217.24C223.17 217.52 223.44 217.82 223.67 218.15C223.9 218.48 224.1 218.83 224.26 219.2C224.42 219.57 224.55 219.95 224.64 220.35C224.73 220.74 224.78 221.14 224.79 221.54C224.8 221.94 224.77 222.34 224.7 222.73C224.63 223.12 224.52 223.51 224.38 223.88C224.23 224.26 224.05 224.61 223.83 224.95C223.56 225.39 223.23 225.79 222.86 226.14C222.48 226.49 222.07 226.8 221.62 227.05C221.16 227.29 220.68 227.48 220.19 227.61C219.69 227.74 219.17 227.8 218.66 227.8C218.34 227.8 218.03 227.78 217.72 227.73C217.4 227.68 217.1 227.6 216.8 227.51C216.49 227.41 216.2 227.29 215.92 227.15C215.64 227.01 215.37 226.84 215.11 226.66C214.88 226.5 214.65 226.31 214.45 226.12C214.24 225.92 214.04 225.71 213.87 225.49C213.69 225.26 213.52 225.03 213.38 224.78C213.24 224.53 213.11 224.28 213 224.01ZM216.88 197.09C216.74 197.31 216.64 197.56 216.59 197.83C216.54 198.09 216.54 198.36 216.59 198.62C216.64 198.88 216.74 199.14 216.89 199.36C217.03 199.59 217.22 199.78 217.44 199.94L217.53 200C218.51 200.62 219.85 200.3 220.44 199.34C220.52 199.23 220.58 199.11 220.63 198.98C220.68 198.85 220.72 198.72 220.74 198.58C220.77 198.44 220.78 198.31 220.78 198.17C220.77 198.03 220.76 197.89 220.73 197.76C220.6 197.2 220.27 196.73 219.79 196.43C219.45 196.22 219.06 196.1 218.66 196.1C218.49 196.1 218.31 196.13 218.14 196.17C217.97 196.21 217.8 196.28 217.64 196.36C217.49 196.45 217.35 196.55 217.22 196.68C217.09 196.8 216.97 196.93 216.88 197.09ZM216.88 220.55C216.74 220.77 216.64 221.03 216.59 221.29C216.54 221.55 216.54 221.82 216.59 222.09C216.64 222.35 216.74 222.6 216.89 222.82C217.03 223.05 217.22 223.24 217.44 223.4L217.53 223.46C218.51 224.08 219.85 223.76 220.44 222.81C220.52 222.69 220.58 222.57 220.63 222.44C220.68 222.31 220.72 222.18 220.74 222.04C220.77 221.91 220.78 221.77 220.78 221.63C220.77 221.49 220.76 221.36 220.72 221.22C220.6 220.67 220.27 220.2 219.79 219.89C219.45 219.68 219.06 219.57 218.66 219.57C218.49 219.57 218.31 219.59 218.14 219.63C217.97 219.68 217.8 219.74 217.64 219.83C217.49 219.91 217.35 220.02 217.22 220.14C217.09 220.26 216.97 220.4 216.88 220.55ZM216.88 246.85C216.74 247.08 216.64 247.33 216.59 247.59C216.54 247.86 216.54 248.13 216.59 248.39C216.64 248.65 216.74 248.9 216.89 249.13C217.03 249.35 217.22 249.55 217.44 249.7L217.53 249.76C218.51 250.38 219.85 250.06 220.44 249.11C220.52 249 220.58 248.87 220.63 248.74C220.68 248.62 220.72 248.48 220.74 248.35C220.77 248.21 220.78 248.07 220.78 247.94C220.77 247.8 220.76 247.66 220.72 247.53C220.6 246.97 220.27 246.5 219.79 246.2C219.45 245.98 219.06 245.87 218.66 245.87C218.49 245.87 218.31 245.89 218.14 245.94C217.97 245.98 217.8 246.05 217.64 246.13C217.49 246.22 217.35 246.32 217.22 246.44C217.09 246.56 216.97 246.7 216.88 246.85Z" />
+ <g id="Layer" style="opacity: 0.251">
+ <path id="Layer" class="shp2" d="M151.78 249.54C151.54 249.54 151.3 249.47 151.09 249.32C150.96 249.23 150.85 249.11 150.77 248.98C150.68 248.84 150.63 248.69 150.6 248.54C150.57 248.38 150.58 248.22 150.61 248.06C150.65 247.91 150.71 247.76 150.8 247.63C151.07 247.25 151.32 246.84 151.53 246.41C151.88 245.71 152.14 244.96 152.3 244.2C152.46 243.43 152.52 242.64 152.47 241.86C152.43 241.08 152.28 240.3 152.04 239.56C151.8 238.81 151.46 238.1 151.03 237.45C150.86 237.18 150.8 236.85 150.87 236.54C150.93 236.22 151.12 235.95 151.39 235.78C151.66 235.6 151.99 235.54 152.3 235.61C152.62 235.68 152.89 235.87 153.06 236.14C155.24 239.53 155.48 243.88 153.7 247.48C153.63 247.61 153.56 247.75 153.49 247.88C153.42 248.01 153.34 248.14 153.27 248.27C153.19 248.4 153.11 248.53 153.03 248.65C152.95 248.78 152.86 248.9 152.78 249.03C152.72 249.11 152.66 249.18 152.58 249.24C152.51 249.3 152.43 249.36 152.34 249.4C152.26 249.45 152.16 249.48 152.07 249.5C151.98 249.53 151.88 249.54 151.78 249.54M157.34 252.41C157.12 252.41 156.9 252.35 156.71 252.23C156.52 252.12 156.37 251.95 156.27 251.75C156.17 251.55 156.12 251.33 156.14 251.11C156.16 250.88 156.24 250.67 156.37 250.49C156.82 249.88 157.22 249.21 157.57 248.52C158.11 247.43 158.51 246.26 158.73 245.06C158.96 243.86 159.02 242.64 158.91 241.42C158.8 240.2 158.52 239.01 158.08 237.87C157.64 236.73 157.05 235.65 156.32 234.67C156.13 234.42 156.05 234.09 156.09 233.78C156.14 233.46 156.31 233.18 156.57 232.98C156.82 232.79 157.15 232.71 157.46 232.76C157.78 232.8 158.06 232.98 158.25 233.23C159.12 234.39 159.82 235.66 160.34 237C160.86 238.35 161.18 239.76 161.31 241.2C161.44 242.64 161.37 244.09 161.11 245.5C160.84 246.92 160.38 248.3 159.73 249.59C159.33 250.41 158.85 251.19 158.32 251.92C158.26 252 158.2 252.07 158.13 252.13C158.05 252.19 157.97 252.24 157.89 252.28C157.8 252.33 157.71 252.36 157.62 252.38C157.53 252.4 157.43 252.41 157.34 252.41ZM146.34 246.49C146.13 246.48 145.93 246.43 145.75 246.32C145.57 246.22 145.41 246.06 145.31 245.88C145.2 245.7 145.15 245.49 145.15 245.28C145.14 245.07 145.2 244.87 145.3 244.68C145.49 244.35 145.63 244 145.74 243.64C145.84 243.27 145.9 242.9 145.92 242.52C145.93 242.14 145.9 241.76 145.83 241.39C145.76 241.02 145.64 240.66 145.48 240.31C145.36 240.03 145.36 239.7 145.47 239.41C145.58 239.12 145.81 238.88 146.09 238.75C146.37 238.62 146.7 238.61 146.99 238.71C147.29 238.81 147.53 239.03 147.67 239.31C147.91 239.82 148.09 240.37 148.2 240.92C148.31 241.48 148.35 242.05 148.33 242.62C148.31 243.19 148.21 243.76 148.06 244.3C147.9 244.85 147.68 245.38 147.4 245.88C147.35 245.97 147.28 246.05 147.21 246.13C147.13 246.2 147.05 246.27 146.96 246.32C146.86 246.38 146.76 246.42 146.66 246.45C146.56 246.47 146.45 246.49 146.34 246.49" />
+ </g>
+ <g id="Layer">
+ <path id="Layer" class="shp0" d="M151.78 246.54C151.54 246.54 151.3 246.47 151.09 246.32C150.96 246.23 150.85 246.11 150.77 245.98C150.68 245.84 150.63 245.69 150.6 245.54C150.57 245.38 150.58 245.22 150.61 245.06C150.65 244.91 150.71 244.76 150.8 244.63C151.07 244.25 151.32 243.84 151.53 243.41C151.88 242.71 152.14 241.96 152.3 241.2C152.46 240.43 152.52 239.64 152.47 238.86C152.43 238.08 152.28 237.3 152.04 236.56C151.8 235.81 151.46 235.1 151.03 234.45C150.86 234.18 150.8 233.85 150.87 233.54C150.93 233.22 151.12 232.95 151.39 232.78C151.66 232.6 151.99 232.54 152.3 232.61C152.62 232.68 152.89 232.87 153.06 233.14C155.24 236.53 155.48 240.88 153.7 244.48C153.63 244.61 153.56 244.75 153.49 244.88C153.42 245.01 153.34 245.14 153.27 245.27C153.19 245.4 153.11 245.53 153.03 245.65C152.95 245.78 152.86 245.9 152.78 246.03C152.72 246.11 152.66 246.18 152.58 246.24C152.51 246.3 152.43 246.36 152.34 246.4C152.26 246.45 152.16 246.48 152.07 246.5C151.98 246.53 151.88 246.54 151.78 246.54M157.34 249.41C157.12 249.41 156.9 249.35 156.71 249.23C156.52 249.12 156.37 248.95 156.27 248.75C156.17 248.55 156.12 248.33 156.14 248.11C156.16 247.88 156.24 247.67 156.37 247.49C156.82 246.88 157.22 246.21 157.57 245.52C158.11 244.43 158.51 243.26 158.73 242.06C158.96 240.86 159.02 239.64 158.91 238.42C158.8 237.2 158.52 236.01 158.08 234.87C157.64 233.73 157.05 232.65 156.32 231.67C156.13 231.42 156.05 231.09 156.09 230.78C156.14 230.46 156.31 230.18 156.57 229.98C156.82 229.79 157.15 229.71 157.46 229.76C157.78 229.8 158.06 229.98 158.25 230.23C159.12 231.39 159.82 232.66 160.34 234C160.86 235.35 161.18 236.76 161.31 238.2C161.44 239.64 161.37 241.09 161.11 242.5C160.84 243.92 160.38 245.3 159.73 246.59C159.33 247.41 158.85 248.19 158.32 248.92C158.26 249 158.2 249.07 158.13 249.13C158.05 249.19 157.97 249.24 157.89 249.28C157.8 249.33 157.71 249.36 157.62 249.38C157.53 249.4 157.43 249.41 157.34 249.41ZM146.34 243.49C146.13 243.48 145.93 243.43 145.75 243.32C145.57 243.22 145.41 243.06 145.31 242.88C145.2 242.7 145.15 242.49 145.15 242.28C145.14 242.07 145.2 241.87 145.3 241.68C145.49 241.35 145.63 241 145.74 240.64C145.84 240.27 145.9 239.9 145.92 239.52C145.93 239.14 145.9 238.76 145.83 238.39C145.76 238.02 145.64 237.66 145.48 237.31C145.36 237.03 145.36 236.7 145.47 236.41C145.58 236.12 145.81 235.88 146.09 235.75C146.37 235.62 146.7 235.61 146.99 235.71C147.29 235.81 147.53 236.03 147.67 236.31C147.91 236.82 148.09 237.37 148.2 237.92C148.31 238.48 148.35 239.05 148.33 239.62C148.31 240.19 148.21 240.76 148.06 241.3C147.9 241.85 147.68 242.38 147.4 242.88C147.35 242.97 147.28 243.05 147.21 243.13C147.13 243.2 147.05 243.27 146.96 243.32C146.86 243.38 146.76 243.42 146.66 243.45C146.56 243.47 146.45 243.49 146.34 243.49" />
+ </g>
+ <g id="Layer">
+ <path id="Layer" fill-rule="evenodd" class="shp5" d="M164.08 192.24C165.2 192.24 167.16 192.28 168.36 192.39L168.36 195.88C167.32 195.78 165.12 195.74 164.08 195.74C154.95 195.74 145.38 199.12 139.72 204.35C135.15 208.57 132.19 214.61 131.03 221.84C130.8 223.29 130.66 225.08 130.6 226.94C130.89 225.07 132.31 223.65 134.03 223.65L135.93 223.65C137.86 223.65 139.42 225.44 139.42 227.65L139.42 257.32C139.42 259.53 137.86 261.32 135.93 261.32L134.03 261.32C132.1 261.32 130.53 259.53 130.53 257.32L130.53 255.72C130.24 255.79 129.94 255.83 129.62 255.83L127.59 255.83C127.06 255.83 126.54 255.73 126.06 255.53C125.57 255.33 125.13 255.03 124.76 254.66C124.39 254.29 124.09 253.85 123.89 253.36C123.69 252.88 123.59 252.36 123.59 251.83L123.59 233.71C123.59 233.24 123.67 232.77 123.84 232.32C124.01 231.88 124.25 231.47 124.56 231.11C124.87 230.75 125.24 230.45 125.66 230.22C126.08 229.99 126.53 229.84 127 229.77C126.82 218.03 130.46 208.14 137.35 201.78C143.72 195.9 153.96 192.24 164.08 192.24ZM129.62 251.43L130.54 251.43L130.54 234.12L129.62 234.12C129.27 234.12 128.93 234.18 128.61 234.32C128.29 234.45 127.99 234.65 127.75 234.89C127.5 235.14 127.31 235.43 127.17 235.75C127.04 236.07 126.97 236.42 126.97 236.77L126.97 248.78C126.97 249.13 127.04 249.47 127.17 249.79C127.31 250.11 127.5 250.41 127.75 250.65C128 250.9 128.29 251.09 128.61 251.23C128.93 251.36 129.27 251.43 129.62 251.43Z" />
+ <path id="Layer" fill-rule="evenodd" class="shp4" d="M164.08 189.24C165.2 189.24 167.16 189.28 168.36 189.39L168.36 192.88C167.32 192.78 165.12 192.74 164.08 192.74C154.95 192.74 145.38 196.12 139.72 201.35C135.15 205.57 132.19 211.61 131.03 218.84C130.8 220.29 130.66 222.08 130.6 223.94C130.89 222.07 132.31 220.65 134.03 220.65L135.93 220.65C137.86 220.65 139.42 222.44 139.42 224.65L139.42 254.32C139.42 256.53 137.86 258.32 135.93 258.32L134.03 258.32C132.1 258.32 130.53 256.53 130.53 254.32L130.53 252.72C130.24 252.79 129.94 252.83 129.62 252.83L127.59 252.83C127.06 252.83 126.54 252.73 126.06 252.53C125.57 252.33 125.13 252.03 124.76 251.66C124.39 251.29 124.09 250.85 123.89 250.36C123.69 249.88 123.59 249.36 123.59 248.83L123.59 230.71C123.59 230.24 123.67 229.77 123.84 229.32C124.01 228.88 124.25 228.47 124.56 228.11C124.87 227.75 125.24 227.45 125.66 227.22C126.08 226.99 126.53 226.84 127 226.77C126.82 215.03 130.46 205.14 137.35 198.78C143.72 192.9 153.96 189.24 164.08 189.24ZM129.62 248.43L130.54 248.43L130.54 231.12L129.62 231.12C129.27 231.12 128.93 231.18 128.61 231.32C128.29 231.45 127.99 231.65 127.75 231.89C127.5 232.14 127.31 232.43 127.17 232.75C127.04 233.07 126.97 233.42 126.97 233.77L126.97 245.78C126.97 246.13 127.04 246.47 127.17 246.79C127.31 247.11 127.5 247.41 127.75 247.65C128 247.9 128.29 248.09 128.61 248.23C128.93 248.36 129.27 248.43 129.62 248.43Z" />
+ </g>
+ <g id="Layer">
+ <path id="Layer" fill-rule="evenodd" class="shp1" d="M295.75 242.67L295.75 252.82L259.58 252.82L259.58 198.08L272.78 198.08L272.78 242.67L295.75 242.67ZM300.63 201.65C300.63 199.85 301.29 198.37 302.59 197.22C303.89 196.06 305.59 195.49 307.67 195.49C309.75 195.49 311.44 196.06 312.74 197.22C314.04 198.37 314.7 199.85 314.7 201.65C314.7 203.46 314.05 204.94 312.74 206.09C311.44 207.24 309.75 207.82 307.67 207.82C305.59 207.82 303.89 207.24 302.59 206.09C301.29 204.94 300.63 203.46 300.63 201.65ZM314.1 252.82L301.39 252.82L301.39 212.14L314.1 212.14L314.1 252.82ZM341.5 238.8C340.38 238.15 338.23 237.47 335.07 236.77C331.92 236.07 329.31 235.15 327.25 234.01C325.2 232.87 323.63 231.48 322.55 229.85C321.48 228.22 320.94 226.36 320.94 224.25C320.94 220.51 322.48 217.44 325.56 215.02C328.64 212.6 332.68 211.39 337.67 211.39C343.03 211.39 347.34 212.61 350.6 215.04C353.86 217.47 355.49 220.66 355.49 224.63L342.78 224.63C342.78 221.37 341.06 219.74 337.63 219.74C336.3 219.74 335.19 220.11 334.28 220.85C333.38 221.58 332.93 222.51 332.93 223.61C332.93 224.74 333.48 225.65 334.58 226.35C335.69 227.06 337.45 227.63 339.87 228.08C342.29 228.53 344.41 229.07 346.24 229.7C352.36 231.8 355.41 235.58 355.41 241.02C355.41 244.73 353.77 247.75 350.47 250.08C347.17 252.41 342.91 253.57 337.67 253.57C334.18 253.57 331.08 252.95 328.34 251.69C325.61 250.44 323.48 248.74 321.95 246.58C320.42 244.43 319.66 242.16 319.66 239.77L331.5 239.77C331.55 241.66 332.18 243.03 333.38 243.89C334.58 244.76 336.13 245.19 338.01 245.19C339.74 245.19 341.03 244.84 341.9 244.14C342.76 243.44 343.19 242.52 343.19 241.39C343.19 240.32 342.63 239.45 341.5 238.8ZM376.09 212.14L382.79 212.14L382.79 220.94L376.09 220.94L376.09 239.55C376.09 241.08 376.37 242.15 376.92 242.75C377.47 243.35 378.56 243.65 380.19 243.65C381.44 243.65 382.5 243.57 383.35 243.42L383.35 252.49C382.77 252.67 382.19 252.83 381.6 252.96C381.01 253.1 380.42 253.22 379.82 253.31C379.22 253.4 378.62 253.47 378.02 253.51C377.41 253.56 376.81 253.58 376.21 253.57C371.84 253.57 368.62 252.55 366.54 250.49C364.46 248.44 363.42 245.32 363.42 241.13L363.42 220.94L358.23 220.94L358.23 212.14L363.42 212.14L363.42 202.03L376.09 202.03L376.09 212.14ZM392.3 247.99C388.44 244.27 386.51 239.43 386.51 233.46L386.51 232.41C386.51 228.25 387.28 224.58 388.82 221.39C390.36 218.21 392.61 215.75 395.57 214C398.53 212.26 402.04 211.39 406.1 211.39C411.81 211.39 416.32 213.16 419.63 216.71C422.94 220.26 424.59 225.2 424.59 231.54L424.59 236.47L399.41 236.47C399.86 238.75 400.85 240.54 402.38 241.84C403.9 243.15 405.88 243.8 408.32 243.8C412.33 243.8 415.46 242.4 417.72 239.59L423.5 246.43C421.93 248.61 419.69 250.35 416.79 251.64C413.9 252.93 410.78 253.57 407.45 253.57C401.21 253.57 396.16 251.71 392.3 247.99ZM399.4 228.53L412.19 228.53L412.19 227.56C412.24 225.53 411.72 223.95 410.65 222.84C409.57 221.72 408.03 221.17 406.02 221.17C402.31 221.17 400.11 223.62 399.4 228.53ZM441.74 216.92C444.55 213.23 448.42 211.39 453.36 211.39C457.59 211.39 460.76 212.66 462.85 215.19C464.94 217.72 466.03 221.53 466.1 226.62L466.1 252.82L453.39 252.82L453.39 227.14C453.39 225.09 452.98 223.58 452.15 222.61C451.33 221.65 449.82 221.17 447.64 221.17C445.16 221.17 443.32 222.14 442.11 224.1L442.11 252.82L429.44 252.82L429.44 212.14L441.32 212.14L441.74 216.92Z" />
+ </g>
+ <g id="Layer">
+ <path id="Layer" fill-rule="evenodd" class="shp0" d="M473.64 198.08L493.3 198.08C500.34 198.08 505.7 199.37 509.37 201.95C513.05 204.54 514.88 208.28 514.88 213.2C514.88 216.03 514.23 218.45 512.93 220.45C511.62 222.46 509.71 223.94 507.18 224.89C510.03 225.64 512.23 227.05 513.75 229.1C515.28 231.15 516.05 233.66 516.05 236.62C516.05 241.98 514.35 246.01 510.95 248.71C507.56 251.4 502.52 252.77 495.86 252.82L473.64 252.82L473.64 198.08ZM486.83 220.9L493.72 220.9C496.6 220.88 498.65 220.35 499.88 219.32C501.11 218.29 501.72 216.77 501.72 214.76C501.72 212.44 501.06 210.78 499.73 209.76C498.4 208.74 496.26 208.23 493.3 208.23L486.83 208.23L486.83 220.9ZM486.83 229.55L486.83 242.67L495.48 242.67C497.86 242.67 499.69 242.13 500.97 241.04C502.25 239.95 502.89 238.41 502.89 236.43C502.89 231.87 500.62 229.58 496.08 229.55L486.83 229.55ZM547.33 223.61L543.15 223.31C539.17 223.31 536.61 224.56 535.48 227.07L535.48 252.82L522.81 252.82L522.81 212.14L534.69 212.14L535.11 217.37C537.24 213.38 540.21 211.39 544.02 211.39C545.37 211.39 546.55 211.54 547.55 211.84L547.33 223.61ZM573.73 252.82C573.28 252 572.88 250.78 572.53 249.18C570.2 252.11 566.94 253.57 562.76 253.57C558.92 253.57 555.66 252.42 552.98 250.1C550.3 247.78 548.96 244.87 548.96 241.36C548.96 236.94 550.59 233.61 553.85 231.35C557.11 229.1 561.84 227.97 568.06 227.97L571.97 227.97L571.97 225.82C571.97 222.06 570.35 220.19 567.12 220.19C564.11 220.19 562.61 221.67 562.61 224.65L549.94 224.65C549.94 220.72 551.61 217.52 554.96 215.07C558.3 212.62 562.57 211.39 567.76 211.39C572.95 211.39 577.04 212.66 580.05 215.19C583.06 217.72 584.6 221.19 584.68 225.6L584.68 243.61C584.73 247.34 585.3 250.2 586.41 252.18L586.41 252.82L573.73 252.82L573.73 252.82ZM569.73 243.54C570.77 242.86 571.52 242.1 571.97 241.24L571.97 234.74L568.28 234.74C563.87 234.74 561.67 236.72 561.67 240.68C561.67 241.83 562.05 242.76 562.83 243.48C563.61 244.19 564.6 244.55 565.8 244.55C567.38 244.55 568.69 244.21 569.73 243.54ZM591.59 201.65C591.59 199.85 592.25 198.37 593.55 197.22C594.85 196.06 596.54 195.49 598.63 195.49C600.7 195.49 602.4 196.06 603.7 197.22C605 198.37 605.65 199.85 605.65 201.65C605.65 203.46 605 204.94 603.7 206.09C602.4 207.24 600.7 207.82 598.63 207.82C596.54 207.82 594.85 207.24 593.55 206.09C592.25 204.94 591.59 203.46 591.59 201.65ZM605.05 252.82L592.35 252.82L592.35 212.14L605.05 212.14L605.05 252.82ZM624.49 212.14L624.91 216.92C627.71 213.23 631.58 211.39 636.52 211.39C640.76 211.39 643.92 212.66 646.02 215.19C648.11 217.72 649.19 221.53 649.27 226.62L649.27 252.82L636.56 252.82L636.56 227.14C636.56 225.09 636.15 223.58 635.32 222.61C634.49 221.65 632.99 221.17 630.81 221.17C628.33 221.17 626.48 222.14 625.28 224.1L625.28 252.82L612.61 252.82L612.61 212.14L624.49 212.14ZM689.76 243.05L689.76 252.82L655.21 252.82L655.21 245.75L673.26 221.92L656 221.92L656 212.14L689.35 212.14L689.35 218.99L671.23 243.05L689.76 243.05Z" />
+ </g>
+</svg> \ No newline at end of file
diff --git a/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/NOTICE.md b/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/NOTICE.md
new file mode 100644
index 0000000000..774e383f8b
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/NOTICE.md
@@ -0,0 +1,23 @@
+# ListenBrainz logo attribution
+
+The file `ListenBrainz_logo.svg` shipped alongside this plugin is a derivative
+work used here under the terms of the Creative Commons Attribution-ShareAlike
+4.0 International license (CC BY-SA 4.0).
+
+## Attribution chain
+
+1. Original work: [ListenBrainz logo](https://github.com/metabrainz/metabrainz-logos/commit/10127d3e84e5bb7e1c8509f1da12223d19581e18)
+ by [MonkeyDo](https://github.com/metabrainz/metabrainz-logos/commits?author=MonkeyDo)
+ at the [MetaBrainz Foundation](https://github.com/metabrainz), licensed under
+ CC BY-SA 4.0.
+2. "ListenBrainz logo for Jellyfin plugin" — derivative by
+ [lyarenei](https://github.com/lyarenei), distributed in
+ [jellyfin-plugin-listenbrainz](https://github.com/lyarenei/jellyfin-plugin-listenbrainz/tree/main/res/listenbrainz)
+ under CC BY-SA 4.0.
+3. This redistribution within Jellyfin retains the work unmodified and remains
+ licensed under CC BY-SA 4.0 per the license's ShareAlike requirement.
+
+## License
+
+A full copy of the CC BY-SA 4.0 license is available at
+<https://creativecommons.org/licenses/by-sa/4.0/legalcode>.
diff --git a/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/PluginConfiguration.cs
new file mode 100644
index 0000000000..6f60d18c33
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/PluginConfiguration.cs
@@ -0,0 +1,65 @@
+using MediaBrowser.Model.Plugins;
+
+namespace MediaBrowser.Providers.Plugins.ListenBrainz.Configuration;
+
+/// <summary>
+/// ListenBrainz plugin configuration.
+/// </summary>
+public class PluginConfiguration : BasePluginConfiguration
+{
+ /// <summary>
+ /// The default Labs API server URL.
+ /// </summary>
+ public const string DefaultLabsServer = "https://labs.api.listenbrainz.org";
+
+ /// <summary>
+ /// The default rate limit in seconds.
+ /// </summary>
+ public const double DefaultRateLimit = 1.0;
+
+ private string _labsServer = DefaultLabsServer;
+ private double _rateLimit = DefaultRateLimit;
+
+ /// <summary>
+ /// Gets or sets the Labs API server URL.
+ /// </summary>
+ public string LabsServer
+ {
+ get => _labsServer;
+ set => _labsServer = string.IsNullOrWhiteSpace(value) ? DefaultLabsServer : value.TrimEnd('/');
+ }
+
+ /// <summary>
+ /// Gets or sets the similarity algorithm.
+ /// </summary>
+ public SimilarityAlgorithm Algorithm { get; set; } = SimilarityAlgorithm.SessionBased1825Days;
+
+ /// <summary>
+ /// Gets or sets the rate limit in seconds.
+ /// </summary>
+ public double RateLimit
+ {
+ get => _rateLimit;
+ set
+ {
+ if (value < DefaultRateLimit && _labsServer == DefaultLabsServer)
+ {
+ _rateLimit = DefaultRateLimit;
+ }
+ else
+ {
+ _rateLimit = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the cache duration in days for similar item results. A value of 0 disables caching.
+ /// </summary>
+ public int SimilarItemsCacheDays { get; set; } = 14;
+
+ /// <summary>
+ /// Gets the algorithm string for the API call.
+ /// </summary>
+ public string AlgorithmString => Algorithm.ToApiString();
+}
diff --git a/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/SimilarityAlgorithm.cs b/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/SimilarityAlgorithm.cs
new file mode 100644
index 0000000000..f297d99f6d
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/SimilarityAlgorithm.cs
@@ -0,0 +1,37 @@
+namespace MediaBrowser.Providers.Plugins.ListenBrainz.Configuration;
+
+/// <summary>
+/// Available similarity algorithms for ListenBrainz Labs API.
+/// </summary>
+public enum SimilarityAlgorithm
+{
+ /// <summary>
+ /// Session-based algorithm analyzing ~5 years of listening data.
+ /// </summary>
+ SessionBased1825Days = 0,
+
+ /// <summary>
+ /// Session-based algorithm analyzing ~5 years of listening data (alternate).
+ /// </summary>
+ SessionBased1800Days = 1,
+
+ /// <summary>
+ /// Session-based algorithm analyzing ~20 years of listening data.
+ /// </summary>
+ SessionBased7500Days = 2,
+
+ /// <summary>
+ /// Session-based algorithm analyzing ~20 years with higher contribution threshold.
+ /// </summary>
+ SessionBased7500DaysHighContribution = 3,
+
+ /// <summary>
+ /// Session-based algorithm analyzing ~25 years of listening data.
+ /// </summary>
+ SessionBased9000Days = 4,
+
+ /// <summary>
+ /// Session-based algorithm analyzing ~75 days of recent listening data.
+ /// </summary>
+ SessionBased75Days = 5
+}
diff --git a/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/SimilarityAlgorithmExtensions.cs b/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/SimilarityAlgorithmExtensions.cs
new file mode 100644
index 0000000000..f7874dbae8
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/SimilarityAlgorithmExtensions.cs
@@ -0,0 +1,23 @@
+namespace MediaBrowser.Providers.Plugins.ListenBrainz.Configuration;
+
+/// <summary>
+/// Extension methods for <see cref="SimilarityAlgorithm"/>.
+/// </summary>
+public static class SimilarityAlgorithmExtensions
+{
+ /// <summary>
+ /// Gets the API string value for the algorithm.
+ /// </summary>
+ /// <param name="algorithm">The algorithm.</param>
+ /// <returns>The API string value.</returns>
+ public static string ToApiString(this SimilarityAlgorithm algorithm) => algorithm switch
+ {
+ SimilarityAlgorithm.SessionBased1825Days => "session_based_days_1825_session_300_contribution_3_threshold_10_limit_100_filter_True_skip_30",
+ SimilarityAlgorithm.SessionBased1800Days => "session_based_days_1800_session_300_contribution_3_threshold_10_limit_100_filter_True_skip_30",
+ SimilarityAlgorithm.SessionBased7500Days => "session_based_days_7500_session_300_contribution_3_threshold_10_limit_100_filter_True_skip_30",
+ SimilarityAlgorithm.SessionBased7500DaysHighContribution => "session_based_days_7500_session_300_contribution_5_threshold_10_limit_100_filter_True_skip_30",
+ SimilarityAlgorithm.SessionBased9000Days => "session_based_days_9000_session_300_contribution_5_threshold_15_limit_50_skip_30",
+ SimilarityAlgorithm.SessionBased75Days => "session_based_days_75_session_300_contribution_5_threshold_10_limit_100_filter_True_skip_30",
+ _ => "session_based_days_1825_session_300_contribution_3_threshold_10_limit_100_filter_True_skip_30"
+ };
+}
diff --git a/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/config.html
new file mode 100644
index 0000000000..dec21d1b42
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/config.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>ListenBrainz</title>
+</head>
+<body>
+ <div id="configPage" data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-select">
+ <div data-role="content">
+ <div class="content-primary">
+ <img id="listenBrainzLogo" alt="ListenBrainz" style="max-width:240px;display:block;margin:0 auto 1em;" />
+ <h1>ListenBrainz</h1>
+ <p>Get similar artist recommendations from ListenBrainz Labs.</p>
+ <form class="configForm">
+ <div class="inputContainer">
+ <input is="emby-input" type="text" id="labsServer" required label="Labs API Server" />
+ <div class="fieldDescription">The ListenBrainz Labs API server URL. Default: https://labs.api.listenbrainz.org</div>
+ </div>
+ <div class="selectContainer">
+ <label class="selectLabel" for="algorithm">Similarity Algorithm</label>
+ <select is="emby-select" id="algorithm" class="emby-select-withcolor">
+ <option value="0" selected>~5 years / 1825 days (Recommended)</option>
+ <option value="1">~5 years / 1800 days</option>
+ <option value="2">~20 years / 7500 days</option>
+ <option value="3">~20 years / 7500 days (high contribution)</option>
+ <option value="4">~25 years / 9000 days</option>
+ <option value="5">~75 days (recent)</option>
+ </select>
+ <div class="fieldDescription">The algorithm used for artist similarity calculation.</div>
+ </div>
+ <div class="inputContainer">
+ <input is="emby-input" type="number" id="rateLimit" required pattern="[0-9]*" min="0" max="10" step=".01" label="Rate Limit (seconds)" />
+ <div class="fieldDescription">Span of time between requests in seconds. The official server is rate limited to one request per second.</div>
+ </div>
+ <div class="inputContainer">
+ <input is="emby-input" type="number" id="similarItemsCacheDays" required pattern="[0-9]*" min="0" max="365" label="Cache duration (days)" />
+ <div class="fieldDescription">Number of days to cache similar artist results from ListenBrainz. Set to 0 to disable caching.</div>
+ </div>
+ <br />
+ <div>
+ <button is="emby-button" type="submit" class="raised button-submit block"><span>Save</span></button>
+ </div>
+ </form>
+ <div class="verticalSection" style="margin-top:2em;font-size:0.85em;opacity:0.8;">
+ <p>The ListenBrainz logo is &copy; the MetaBrainz Foundation (by MonkeyDo),
+ adapted for Jellyfin plugin use by
+ <a href="https://github.com/lyarenei" target="_blank" rel="noopener">lyarenei</a>,
+ and redistributed here under
+ <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener">CC BY-SA 4.0</a>.
+ Full attribution notice is shipped alongside the plugin in <code>NOTICE.md</code>.</p>
+ </div>
+ </div>
+ </div>
+ <script type="text/javascript">
+ var ListenBrainzPluginConfig = {
+ uniquePluginId: "a5b2e8c1-9d4f-4a3b-8c7e-6f1a2b3c4d5e"
+ };
+
+ document.querySelector('.configPage')
+ .addEventListener('pageshow', function () {
+ Dashboard.showLoadingMsg();
+ document.querySelector('#listenBrainzLogo').src = ApiClient.getUrl('web/ConfigurationPage', { name: 'ListenBrainzLogo' });
+ ApiClient.getPluginConfiguration(ListenBrainzPluginConfig.uniquePluginId).then(function (config) {
+ var labsServer = document.querySelector('#labsServer');
+ labsServer.value = config.LabsServer;
+ labsServer.dispatchEvent(new Event('change', {
+ bubbles: true,
+ cancelable: false
+ }));
+
+ document.querySelector('#algorithm').value = config.Algorithm;
+
+ var rateLimit = document.querySelector('#rateLimit');
+ rateLimit.value = config.RateLimit;
+ rateLimit.dispatchEvent(new Event('change', {
+ bubbles: true,
+ cancelable: false
+ }));
+
+ var similarItemsCacheDays = document.querySelector('#similarItemsCacheDays');
+ similarItemsCacheDays.value = config.SimilarItemsCacheDays;
+ similarItemsCacheDays.dispatchEvent(new Event('change', {
+ bubbles: true,
+ cancelable: false
+ }));
+
+ Dashboard.hideLoadingMsg();
+ });
+ });
+
+ document.querySelector('.configForm')
+ .addEventListener('submit', function (e) {
+ Dashboard.showLoadingMsg();
+
+ ApiClient.getPluginConfiguration(ListenBrainzPluginConfig.uniquePluginId).then(function (config) {
+ config.LabsServer = document.querySelector('#labsServer').value;
+ config.Algorithm = parseInt(document.querySelector('#algorithm').value, 10);
+ config.RateLimit = document.querySelector('#rateLimit').value;
+ config.SimilarItemsCacheDays = parseInt(document.querySelector('#similarItemsCacheDays').value, 10);
+
+ ApiClient.updatePluginConfiguration(ListenBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+ });
+
+ e.preventDefault();
+ return false;
+ });
+ </script>
+ </div>
+</body>
+</html>
diff --git a/MediaBrowser.Providers/Plugins/ListenBrainz/ListenBrainzPlugin.cs b/MediaBrowser.Providers/Plugins/ListenBrainz/ListenBrainzPlugin.cs
new file mode 100644
index 0000000000..1681f0334d
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/ListenBrainz/ListenBrainzPlugin.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Controller.Plugins;
+using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Plugins.ListenBrainz.Configuration;
+
+namespace MediaBrowser.Providers.Plugins.ListenBrainz;
+
+/// <summary>
+/// ListenBrainz plugin instance.
+/// </summary>
+public class ListenBrainzPlugin : BasePlugin<PluginConfiguration>, IHasWebPages, IHasEmbeddedImage
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ListenBrainzPlugin"/> class.
+ /// </summary>
+ /// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
+ /// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
+ public ListenBrainzPlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ : base(applicationPaths, xmlSerializer)
+ {
+ Instance = this;
+ }
+
+ /// <summary>
+ /// Gets the current plugin instance.
+ /// </summary>
+ public static ListenBrainzPlugin? Instance { get; private set; }
+
+ /// <inheritdoc />
+ public override Guid Id => new("a5b2e8c1-9d4f-4a3b-8c7e-6f1a2b3c4d5e");
+
+ /// <inheritdoc />
+ public override string Name => "ListenBrainz Similarity Provider";
+
+ /// <inheritdoc />
+ public override string Description => "Get similar artist recommendations from ListenBrainz Labs.";
+
+ /// <inheritdoc />
+ public override string ConfigurationFileName => "Jellyfin.Plugin.ListenBrainz.xml";
+
+ /// <inheritdoc />
+ public string ImageResourceName => GetType().Namespace + ".Configuration.ListenBrainz_logo.svg";
+
+ /// <inheritdoc />
+ public IEnumerable<PluginPageInfo> GetPages()
+ {
+ var resourcePrefix = GetType().Namespace + ".Configuration.";
+ yield return new PluginPageInfo
+ {
+ Name = Name,
+ EmbeddedResourcePath = resourcePrefix + "config.html"
+ };
+ yield return new PluginPageInfo
+ {
+ Name = Name + "Notice",
+ EmbeddedResourcePath = resourcePrefix + "NOTICE.md"
+ };
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/ListenBrainz/ListenBrainzSimilarArtistProvider.cs b/MediaBrowser.Providers/Plugins/ListenBrainz/ListenBrainzSimilarArtistProvider.cs
new file mode 100644
index 0000000000..3dca748d06
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/ListenBrainz/ListenBrainzSimilarArtistProvider.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Providers.Plugins.ListenBrainz.Api;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Plugins.ListenBrainz;
+
+/// <summary>
+/// ListenBrainz-based similar items provider for music artists.
+/// </summary>
+public class ListenBrainzSimilarArtistProvider : IRemoteSimilarItemsProvider<MusicArtist>
+{
+ private readonly ListenBrainzLabsClient _labsClient;
+ private readonly ILogger<ListenBrainzSimilarArtistProvider> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ListenBrainzSimilarArtistProvider"/> class.
+ /// </summary>
+ /// <param name="labsClient">The ListenBrainz Labs API client.</param>
+ /// <param name="logger">The logger.</param>
+ public ListenBrainzSimilarArtistProvider(
+ ListenBrainzLabsClient labsClient,
+ ILogger<ListenBrainzSimilarArtistProvider> logger)
+ {
+ _labsClient = labsClient;
+ _logger = logger;
+ }
+
+ /// <inheritdoc/>
+ public string Name => "ListenBrainz";
+
+ /// <inheritdoc/>
+ public MetadataPluginType Type => MetadataPluginType.SimilarityProvider;
+
+ /// <inheritdoc/>
+ public TimeSpan? CacheDuration
+ {
+ get
+ {
+ var days = ListenBrainzPlugin.Instance?.Configuration.SimilarItemsCacheDays ?? 0;
+ return days > 0 ? TimeSpan.FromDays(days) : null;
+ }
+ }
+
+ /// <inheritdoc/>
+ public async IAsyncEnumerable<SimilarItemReference> GetSimilarItemsAsync(
+ MusicArtist item,
+ SimilarItemsQuery query,
+ [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(item);
+ ArgumentNullException.ThrowIfNull(query);
+
+ if (!item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var mbidStr) || !Guid.TryParse(mbidStr, out var mbid))
+ {
+ _logger.LogDebug("No MusicBrainz Artist ID found for {ArtistName}", item.Name);
+ yield break;
+ }
+
+ IReadOnlyList<Guid> similarMbids;
+ try
+ {
+ similarMbids = await _labsClient.GetSimilarArtistsAsync(mbid, cancellationToken).ConfigureAwait(false);
+ }
+ catch (HttpRequestException ex)
+ {
+ _logger.LogWarning(ex, "Failed to fetch similar artists from ListenBrainz for {ArtistMbid}", mbid);
+ yield break;
+ }
+
+ var providerName = MetadataProvider.MusicBrainzArtist.ToString();
+
+ foreach (var similarMbid in similarMbids)
+ {
+ yield return new SimilarItemReference
+ {
+ ProviderName = providerName,
+ ProviderId = similarMbid.ToString()
+ };
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs
index dd0a939f72..f7c570692d 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs
@@ -19,7 +19,7 @@ public class MusicBrainzReleaseGroupExternalUrlProvider : IExternalUrlProvider
{
if (item is MusicAlbum)
{
- if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out var externalId))
+ if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out var externalId))
{
yield return Plugin.Instance!.Configuration.Server + $"/release-group/{externalId}";
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs
index 59e6f42b19..c2bbd8ba86 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs
@@ -19,7 +19,7 @@ public class MusicBrainzTrackExternalUrlProvider : IExternalUrlProvider
{
if (item is Audio)
{
- if (item.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out var externalId))
+ if (item.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out var externalId))
{
yield return Plugin.Instance!.Configuration.Server + $"/track/{externalId}";
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
index 69225d0b95..f448e6b20c 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
@@ -6,6 +6,7 @@ using System.Threading;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
+using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
@@ -17,7 +18,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz;
/// <summary>
/// Plugin instance.
/// </summary>
-public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages, IDisposable
+public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages, IHasEmbeddedImage, IDisposable
{
private readonly ILogger<Plugin> _logger;
private readonly Lock _queryLock = new();
@@ -66,6 +67,9 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages, IDisposable
// TODO remove when plugin removed from server.
public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml";
+ /// <inheritdoc />
+ public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-musicbrainz.svg";
+
/// <summary>
/// Gets the current MusicBrainz query client.
/// </summary>
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/jellyfin-plugin-musicbrainz.svg b/MediaBrowser.Providers/Plugins/MusicBrainz/jellyfin-plugin-musicbrainz.svg
new file mode 100644
index 0000000000..8074d59d7b
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/jellyfin-plugin-musicbrainz.svg
@@ -0,0 +1,36 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1920" height="1080" viewBox="0 0 508 285.75" version="1.1" id="svg8">
+ <defs id="defs2">
+ <linearGradient id="linearGradient851">
+ <stop style="stop-color:#fffedb;stop-opacity:1" offset="0" id="stop847"/>
+ <stop style="stop-color:#ffeec2;stop-opacity:1" offset="1" id="stop849"/>
+ </linearGradient>
+ <linearGradient id="linear-gradient" y1="40.76" x2="190.24" y2="40.76" gradientUnits="userSpaceOnUse" gradientTransform="translate(95.25 74.882) scale(1.66894)">
+ <stop offset="0" stop-color="#90cea1" id="stop916"/>
+ <stop offset=".56" stop-color="#3cbec9" id="stop918"/>
+ <stop offset="1" stop-color="#00b3e5" id="stop920"/>
+ </linearGradient>
+ <style id="style914"/>
+ <style id="style1359">
+ .a{fill:#ba478f}.b{fill:#eb743b}
+ </style>
+ <radialGradient xlink:href="#linearGradient851" id="radialGradient853" cx="125.255" cy="16.735" fx="125.255" fy="16.735" r="254" gradientTransform="matrix(0 1.06475 -1.74952 0 154.532 -61.444)" gradientUnits="userSpaceOnUse"/>
+ </defs>
+ <g id="layer1">
+ <path style="opacity:.98;fill:url(#radialGradient853);stroke-width:2.64583;fill-opacity:1" id="rect845" d="M0 0h508v285.75H0z"/>
+ <g id="g1409" transform="translate(95.25 119.153) scale(1.69442)">
+ <path class="a" id="polygon1363" d="M0 7v14l12 7V0z"/>
+ <path class="b" id="polygon1365" d="M25 7v14l-12 7V0z"/>
+ <path class="a" d="m40.2 5.52 4.44 13.85 4.43-13.85h6.32v19.91h-4.82v-4.65l.43-9.51-4.77 14.16h-3.18l-4.82-14.18.46 9.53v4.65h-4.8V5.52h6.3z" transform="translate(-2 -1)" id="path1367"/>
+ <path class="a" d="M67 23.83a4.9 4.9 0 0 1-1.68 1.38 5 5 0 0 1-2.27.49 6.22 6.22 0 0 1-2-.31 3.89 3.89 0 0 1-1.56-1 4.51 4.51 0 0 1-1-1.71 7.7 7.7 0 0 1-.36-2.51v-9.53h4.61v9.6a2 2 0 0 0 .47 1.45 1.88 1.88 0 0 0 1.37.46 2.87 2.87 0 0 0 1.4-.3 2.24 2.24 0 0 0 .88-.85V10.64h4.63v14.79h-4.32z" transform="translate(-2 -1)" id="path1369"/>
+ <path class="a" d="M81.76 21.27a1 1 0 0 0-.13-.51 1.3 1.3 0 0 0-.46-.42 4.21 4.21 0 0 0-.89-.34q-.56-.18-1.42-.37a12.89 12.89 0 0 1-2-.61 6.57 6.57 0 0 1-1.65-.93 4.14 4.14 0 0 1-1.11-1.31 3.59 3.59 0 0 1-.4-1.72 4.12 4.12 0 0 1 .4-1.79 4.34 4.34 0 0 1 1.18-1.49 5.88 5.88 0 0 1 1.91-1 8.2 8.2 0 0 1 2.58-.38 9.48 9.48 0 0 1 2.69.36 6.34 6.34 0 0 1 2 1 4.41 4.41 0 0 1 1.29 1.52 4.21 4.21 0 0 1 .45 1.95h-4.59a1.88 1.88 0 0 0-.42-1.31 1.89 1.89 0 0 0-1.45-.46 2.08 2.08 0 0 0-.66.1 1.75 1.75 0 0 0-.54.29 1.38 1.38 0 0 0-.37.44 1.22 1.22 0 0 0-.14.57 1.14 1.14 0 0 0 .58 1 5.41 5.41 0 0 0 1.88.63 18.45 18.45 0 0 1 2.15.53 6.79 6.79 0 0 1 1.83.86 4.15 4.15 0 0 1 1.26 1.35 3.87 3.87 0 0 1 .47 2 3.76 3.76 0 0 1-.45 1.77 4.31 4.31 0 0 1-1.29 1.44 6.61 6.61 0 0 1-2 1 9.52 9.52 0 0 1-2.68.35 8.14 8.14 0 0 1-2.82-.45 6.56 6.56 0 0 1-2.05-1.17 4.92 4.92 0 0 1-1.25-1.61 4.14 4.14 0 0 1-.42-1.78h4.31a1.78 1.78 0 0 0 .68 1.5 2.81 2.81 0 0 0 1.68.47 2.21 2.21 0 0 0 1.42-.38 1.22 1.22 0 0 0 .43-1.1z" transform="translate(-2 -1)" id="path1371"/>
+ <path class="a" d="M88.32 6.82a2.17 2.17 0 0 1 .18-.9 2.07 2.07 0 0 1 .5-.71 2.44 2.44 0 0 1 .81-.46 3.37 3.37 0 0 1 2.08 0 2.44 2.44 0 0 1 .81.46 2.07 2.07 0 0 1 .53.71 2.3 2.3 0 0 1 0 1.8 2.07 2.07 0 0 1-.53.71 2.44 2.44 0 0 1-.81.46 3.37 3.37 0 0 1-2.08 0 2.44 2.44 0 0 1-.81-.45 2.07 2.07 0 0 1-.53-.71 2.17 2.17 0 0 1-.15-.91zm4.89 18.61H88.6V10.64h4.62v14.79z" transform="translate(-2 -1)" id="path1373"/>
+ <path class="a" d="M102.31 22.15a2.05 2.05 0 0 0 1.5-.53 1.93 1.93 0 0 0 .52-1.47h4.32a5.35 5.35 0 0 1-.47 2.28 5.22 5.22 0 0 1-1.31 1.75 5.88 5.88 0 0 1-2 1.13 8.13 8.13 0 0 1-5.51-.18 6 6 0 0 1-2.17-1.58 6.7 6.7 0 0 1-1.31-2.38 9.74 9.74 0 0 1-.44-3v-.29a9.81 9.81 0 0 1 .44-3 6.75 6.75 0 0 1 1.3-2.39 6 6 0 0 1 2.15-1.58 7.37 7.37 0 0 1 3-.57 7.81 7.81 0 0 1 2.55.4 5.57 5.57 0 0 1 2 1.16 5.18 5.18 0 0 1 1.29 1.87 6.53 6.53 0 0 1 .46 2.52h-4.32a3.62 3.62 0 0 0-.12-.93 2 2 0 0 0-.38-.76 1.83 1.83 0 0 0-.65-.51 2.14 2.14 0 0 0-.92-.18 1.87 1.87 0 0 0-1.14.32 2.07 2.07 0 0 0-.66.86 4.3 4.3 0 0 0-.31 1.26 14.81 14.81 0 0 0-.08 1.52v.33a14.89 14.89 0 0 0 .08 1.54 4.3 4.3 0 0 0 .31 1.26 1.93 1.93 0 0 0 .68.85 2 2 0 0 0 1.19.3z" transform="translate(-2 -1)" id="path1375"/>
+ <path class="b" d="M110.81 25.43V5.52H118a14.77 14.77 0 0 1 3.3.33 7.29 7.29 0 0 1 2.47 1 4.6 4.6 0 0 1 1.54 1.72 5.22 5.22 0 0 1 .53 2.43 5.69 5.69 0 0 1-.15 1.32 4.21 4.21 0 0 1-.49 1.2 3.92 3.92 0 0 1-.87 1 4.45 4.45 0 0 1-1.3.73 4.56 4.56 0 0 1 1.5.67 3.89 3.89 0 0 1 1 1 4 4 0 0 1 .55 1.24 5.36 5.36 0 0 1 .17 1.35 5.26 5.26 0 0 1-1.9 4.49 9 9 0 0 1-5.59 1.47h-7.94zm4.8-11.61h2.5a3.56 3.56 0 0 0 2.24-.57 2 2 0 0 0 .67-1.65 2.14 2.14 0 0 0-.72-1.81 3.89 3.89 0 0 0-2.3-.56h-2.35v4.59zm0 3.14v4.77h3.14a3.8 3.8 0 0 0 1.24-.18 2.25 2.25 0 0 0 .83-.49 1.88 1.88 0 0 0 .47-.72 2.55 2.55 0 0 0 .15-.89 3.69 3.69 0 0 0-.14-1 1.92 1.92 0 0 0-.44-.79 2 2 0 0 0-.78-.5 3.36 3.36 0 0 0-1.16-.18h-3.32z" transform="translate(-2 -1)" id="path1377"/>
+ <path class="b" d="M137.61 14.81h-1.52a4.09 4.09 0 0 0-1.79.33 2.06 2.06 0 0 0-1 1v9.37h-4.6V10.64h4.3l.15 1.9a4.51 4.51 0 0 1 1.36-1.6 3.18 3.18 0 0 1 1.88-.57 5.57 5.57 0 0 1 .68 0 3.68 3.68 0 0 1 .61.12z" transform="translate(-2 -1)" id="path1379"/>
+ <path class="b" d="M147.19 25.43a3.19 3.19 0 0 1-.25-.61q-.1-.34-.18-.72a4.24 4.24 0 0 1-3.55 1.6 5.66 5.66 0 0 1-1.93-.33 5.11 5.11 0 0 1-1.59-.91 4.31 4.31 0 0 1-1.09-1.4 4 4 0 0 1-.4-1.8 4.83 4.83 0 0 1 .42-2.05 3.89 3.89 0 0 1 1.27-1.52 6.3 6.3 0 0 1 2.16-1 12.56 12.56 0 0 1 3.1-.33h1.42v-.75a2.43 2.43 0 0 0-.41-1.49 1.58 1.58 0 0 0-1.35-.55 1.71 1.71 0 0 0-1.22.4 1.59 1.59 0 0 0-.42 1.22h-4.61a4.11 4.11 0 0 1 .46-1.91 4.49 4.49 0 0 1 1.31-1.53 6.54 6.54 0 0 1 2-1 9 9 0 0 1 2.67-.37 8.76 8.76 0 0 1 2.45.33 5.5 5.5 0 0 1 1.95 1 4.61 4.61 0 0 1 1.29 1.65 5.38 5.38 0 0 1 .46 2.3v7.3a7.71 7.71 0 0 0 .12.94 5 5 0 0 0 .2.72 4.26 4.26 0 0 0 .27.59v.23h-4.61zm-2.88-3a2.57 2.57 0 0 0 1.43-.37 2.32 2.32 0 0 0 .81-.83v-2.38h-1.45a2.94 2.94 0 0 0-1.1.15 1.85 1.85 0 0 0-.71.48 1.8 1.8 0 0 0-.38.69 2.84 2.84 0 0 0-.12.81 1.32 1.32 0 0 0 .42 1 1.53 1.53 0 0 0 1.1.44z" transform="translate(-2 -1)" id="path1381"/>
+ <path class="b" d="M153.69 6.82a2.17 2.17 0 0 1 .18-.9 2.07 2.07 0 0 1 .53-.71 2.44 2.44 0 0 1 .81-.46 3.37 3.37 0 0 1 2.08 0 2.44 2.44 0 0 1 .81.46 2.07 2.07 0 0 1 .53.71 2.3 2.3 0 0 1 0 1.8 2.07 2.07 0 0 1-.53.71 2.44 2.44 0 0 1-.81.46 3.37 3.37 0 0 1-2.08 0 2.44 2.44 0 0 1-.81-.46 2.07 2.07 0 0 1-.53-.71 2.17 2.17 0 0 1-.18-.9zm4.89 18.61H154V10.64h4.62v14.79z" transform="translate(-2 -1)" id="path1383"/>
+ <path class="b" d="m165.65 10.64.15 1.74a5 5 0 0 1 1.83-1.5 5.5 5.5 0 0 1 2.4-.51 5.74 5.74 0 0 1 1.88.29 3.5 3.5 0 0 1 1.47 1 4.59 4.59 0 0 1 1 1.78 9.43 9.43 0 0 1 .33 2.71v9.31H170v-9.35a3.48 3.48 0 0 0-.1-1.11 1.58 1.58 0 0 0-.41-.67 1.41 1.41 0 0 0-.66-.33 4 4 0 0 0-.88-.09 2.37 2.37 0 0 0-1.22.29 2.26 2.26 0 0 0-.79.78v10.45h-4.61V10.64z" transform="translate(-2 -1)" id="path1385"/>
+ <path class="b" d="M182.64 21.88h6.74v3.55h-12.56v-2.57l6.56-8.67h-6.28v-3.55h12.13v2.49z" transform="translate(-2 -1)" id="path1387"/>
+ </g>
+ </g>
+</svg>
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index 4882822766..f562d64ddd 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -413,7 +413,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
item.Overview = result.Plot;
- item.OriginalLanguage = result.Language;
+ item.OriginalLanguage = result.Language?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).FirstOrDefault();
if (!Plugin.Instance.Configuration.CastAndCrew)
{
diff --git a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
index a0fba48f05..9066ff8523 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
@@ -5,12 +5,13 @@ using System;
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
+using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Providers.Plugins.Omdb
{
- public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
+ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages, IHasEmbeddedImage
{
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
@@ -29,6 +30,8 @@ namespace MediaBrowser.Providers.Plugins.Omdb
// TODO remove when plugin removed from server.
public override string ConfigurationFileName => "Jellyfin.Plugin.Omdb.xml";
+ public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-omdb.png";
+
public IEnumerable<PluginPageInfo> GetPages()
{
yield return new PluginPageInfo
diff --git a/MediaBrowser.Providers/Plugins/Omdb/jellyfin-plugin-omdb.png b/MediaBrowser.Providers/Plugins/Omdb/jellyfin-plugin-omdb.png
new file mode 100644
index 0000000000..a4bb7f7d5f
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Omdb/jellyfin-plugin-omdb.png
Binary files differ
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs
index 28f8c0c617..167959967d 100644
--- a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
+using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Providers.Plugins.StudioImages.Configuration;
@@ -13,7 +14,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages
/// <summary>
/// Artwork Plugin class.
/// </summary>
- public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
+ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages, IHasEmbeddedImage
{
/// <summary>
/// Artwork repository URL.
@@ -51,6 +52,9 @@ namespace MediaBrowser.Providers.Plugins.StudioImages
public override string ConfigurationFileName => "Jellyfin.Plugin.StudioImages.xml";
/// <inheritdoc/>
+ public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-studioimages.svg";
+
+ /// <inheritdoc/>
public IEnumerable<PluginPageInfo> GetPages()
{
yield return new PluginPageInfo
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/jellyfin-plugin-studioimages.svg b/MediaBrowser.Providers/Plugins/StudioImages/jellyfin-plugin-studioimages.svg
new file mode 100644
index 0000000000..e9da69c571
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/StudioImages/jellyfin-plugin-studioimages.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 1920 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect x="0" y="0" width="1920" height="1080" style="fill:url(#_Radial1);fill-rule:nonzero;"/><g><path d="M487.844,725.411c-10.779,0 -20.007,-3.838 -27.683,-11.514c-7.676,-7.676 -11.514,-16.904 -11.514,-27.683l0,-274.381c0,-10.779 3.838,-20.007 11.514,-27.683c7.676,-7.676 16.904,-11.514 27.683,-11.514l274.381,0c10.779,0 20.007,3.838 27.683,11.514c7.676,7.676 11.514,16.904 11.514,27.683l0,274.381c0,10.779 -3.838,20.007 -11.514,27.683c-7.676,7.676 -16.904,11.514 -27.683,11.514l-274.381,0Zm0,-39.197l274.381,0l0,-274.381l-274.381,0l0,274.381Zm19.599,-39.197l235.184,0l-73.495,-97.993l-58.796,78.395l-44.097,-58.796l-58.796,78.395Zm-19.599,39.197l0,-274.381l0,274.381Z" style="fill:#fff;fill-rule:nonzero;"/><path d="M934.841,493.37c0,6.419 -1.632,12.505 -4.897,18.26c-3.265,5.755 -8.3,10.43 -15.106,14.027c-6.806,3.597 -15.632,5.395 -26.477,5.395c-5.423,0 -10.154,-0.249 -14.193,-0.747c-4.039,-0.498 -7.83,-1.3 -11.371,-2.407c-3.541,-1.107 -7.193,-2.545 -10.956,-4.316l0,-28.552c6.419,3.209 12.893,5.672 19.422,7.387c6.529,1.715 12.45,2.573 17.762,2.573c4.759,0 8.245,-0.83 10.458,-2.49c2.213,-1.66 3.32,-3.763 3.32,-6.308c0,-3.099 -1.632,-5.616 -4.897,-7.553c-3.265,-1.937 -8.77,-4.62 -16.517,-8.051c-5.865,-2.767 -10.956,-5.672 -15.272,-8.715c-4.316,-3.043 -7.636,-6.778 -9.96,-11.205c-2.324,-4.427 -3.486,-10.015 -3.486,-16.766c0,-7.636 1.881,-14.027 5.644,-19.173c3.763,-5.146 8.992,-9.019 15.687,-11.62c6.695,-2.601 14.47,-3.901 23.323,-3.901c7.747,0 14.719,0.858 20.916,2.573c6.197,1.715 11.731,3.68 16.6,5.893l-9.794,24.734c-5.091,-2.324 -10.098,-4.178 -15.023,-5.561c-4.925,-1.383 -9.49,-2.075 -13.695,-2.075c-4.095,0 -7.11,0.719 -9.047,2.158c-1.937,1.439 -2.905,3.265 -2.905,5.478c0,1.881 0.719,3.541 2.158,4.98c1.439,1.439 3.846,3.016 7.221,4.731c3.375,1.715 7.996,3.901 13.861,6.557c5.755,2.545 10.652,5.34 14.691,8.383c4.039,3.043 7.138,6.64 9.296,10.79c2.158,4.15 3.237,9.324 3.237,15.521Zm60.59,12.616c2.877,0 5.506,-0.304 7.885,-0.913c2.379,-0.609 4.842,-1.356 7.387,-2.241l0,23.074c-3.431,1.439 -7.083,2.656 -10.956,3.652c-3.873,0.996 -8.798,1.494 -14.774,1.494c-5.976,0 -11.205,-0.941 -15.687,-2.822c-4.482,-1.881 -7.996,-5.118 -10.541,-9.711c-2.545,-4.593 -3.818,-11.039 -3.818,-19.339l0,-37.848l-11.122,0l0,-12.948l14.11,-9.96l8.134,-19.256l20.75,0l0,18.426l22.576,0l0,23.738l-22.576,0l0,35.69c0,5.976 2.877,8.964 8.632,8.964Zm119.852,-68.392l0,91.798l-23.904,0l-3.984,-11.454l-1.826,0c-2.877,4.648 -6.806,7.996 -11.786,10.043c-4.98,2.047 -10.292,3.071 -15.936,3.071c-5.976,0 -11.399,-1.162 -16.268,-3.486c-4.869,-2.324 -8.715,-5.976 -11.537,-10.956c-2.822,-4.98 -4.233,-11.399 -4.233,-19.256l0,-59.76l31.706,0l0,50.132c0,5.976 0.858,10.513 2.573,13.612c1.715,3.099 4.51,4.648 8.383,4.648c5.976,0 9.988,-2.435 12.035,-7.304c2.047,-4.869 3.071,-11.841 3.071,-20.916l0,-40.172l31.706,0Zm49.634,93.458c-9.407,0 -17.181,-4.039 -23.323,-12.118c-6.142,-8.079 -9.213,-19.865 -9.213,-35.358c0,-15.715 3.126,-27.584 9.379,-35.607c6.253,-8.023 14.359,-12.035 24.319,-12.035c6.197,0 11.15,1.273 14.857,3.818c3.707,2.545 6.778,5.755 9.213,9.628l0.664,0c-0.443,-2.324 -0.802,-5.533 -1.079,-9.628c-0.277,-4.095 -0.415,-8.079 -0.415,-11.952l0,-24.568l31.872,0l0,126.16l-23.904,0l-6.806,-11.62l-1.162,0c-2.213,3.652 -5.257,6.778 -9.13,9.379c-3.873,2.601 -8.964,3.901 -15.272,3.901Zm12.948,-25.066c5.091,0 8.687,-1.605 10.79,-4.814c2.103,-3.209 3.209,-8.134 3.32,-14.774l0,-2.49c0,-7.193 -1.024,-12.727 -3.071,-16.6c-2.047,-3.873 -5.838,-5.81 -11.371,-5.81c-3.763,0 -6.889,1.826 -9.379,5.478c-2.49,3.652 -3.735,9.351 -3.735,17.098c0,7.636 1.245,13.197 3.735,16.683c2.49,3.486 5.727,5.229 9.711,5.229Zm80.51,-105.41c4.537,0 8.494,0.941 11.869,2.822c3.375,1.881 5.063,5.644 5.063,11.288c0,5.423 -1.688,9.102 -5.063,11.039c-3.375,1.937 -7.332,2.905 -11.869,2.905c-4.648,0 -8.604,-0.968 -11.869,-2.905c-3.265,-1.937 -4.897,-5.616 -4.897,-11.039c0,-5.644 1.632,-9.407 4.897,-11.288c3.265,-1.881 7.221,-2.822 11.869,-2.822Zm15.77,37.018l0,91.798l-31.706,0l0,-91.798l31.706,0Zm108.896,45.65c0,15.383 -4.095,27.196 -12.284,35.441c-8.189,8.245 -19.422,12.367 -33.698,12.367c-8.853,0 -16.711,-1.854 -23.572,-5.561c-6.861,-3.707 -12.256,-9.13 -16.185,-16.268c-3.929,-7.138 -5.893,-15.798 -5.893,-25.979c0,-15.161 4.095,-26.837 12.284,-35.026c8.189,-8.189 19.477,-12.284 33.864,-12.284c8.853,0 16.683,1.826 23.489,5.478c6.806,3.652 12.173,8.992 16.102,16.019c3.929,7.027 5.893,15.632 5.893,25.813Zm-59.428,0c0,7.857 1.051,13.861 3.154,18.011c2.103,4.15 5.644,6.225 10.624,6.225c4.869,0 8.328,-2.075 10.375,-6.225c2.047,-4.15 3.071,-10.154 3.071,-18.011c0,-7.857 -1.024,-13.778 -3.071,-17.762c-2.047,-3.984 -5.561,-5.976 -10.541,-5.976c-4.759,0 -8.217,1.992 -10.375,5.976c-2.158,3.984 -3.237,9.905 -3.237,17.762Z" style="fill:#fff;"/><path d="M911.933,687.092l-61.752,0l0,-18.26l14.774,-5.644l0,-70.882l-14.774,-5.478l0,-18.26l61.752,0l0,18.26l-14.774,5.478l0,70.882l14.774,5.644l0,18.26Zm127.82,-93.458c10.513,0 18.537,2.656 24.07,7.968c5.533,5.312 8.3,13.889 8.3,25.73l0,59.76l-31.706,0l0,-49.966c0,-6.972 -0.941,-11.814 -2.822,-14.525c-1.881,-2.711 -4.648,-4.067 -8.3,-4.067c-5.091,0 -8.632,2.241 -10.624,6.723c-1.992,4.482 -2.988,10.818 -2.988,19.007l0,42.828l-31.706,0l0,-49.966c0,-6.64 -0.885,-11.399 -2.656,-14.276c-1.771,-2.877 -4.427,-4.316 -7.968,-4.316c-5.423,0 -9.102,2.435 -11.039,7.304c-1.937,4.869 -2.905,11.897 -2.905,21.082l0,40.172l-31.706,0l0,-91.798l23.904,0l4.648,11.288l0.83,0c2.435,-3.763 5.948,-6.861 10.541,-9.296c4.593,-2.435 10.264,-3.652 17.015,-3.652c6.751,0 12.312,1.134 16.683,3.403c4.371,2.269 7.94,5.395 10.707,9.379l0.996,0c2.877,-4.095 6.612,-7.249 11.205,-9.462c4.593,-2.213 9.766,-3.32 15.521,-3.32Zm97.94,0c11.62,0 20.695,2.877 27.224,8.632c6.529,5.755 9.794,13.944 9.794,24.568l0,60.258l-21.912,0l-6.142,-12.118l-0.664,0c-2.545,3.209 -5.174,5.838 -7.885,7.885c-2.711,2.047 -5.838,3.541 -9.379,4.482c-3.541,0.941 -7.857,1.411 -12.948,1.411c-7.968,0 -14.608,-2.435 -19.92,-7.304c-5.312,-4.869 -7.968,-12.339 -7.968,-22.41c0,-9.849 3.403,-17.153 10.209,-21.912c6.806,-4.759 16.683,-7.415 29.631,-7.968l15.272,-0.498l0,-1.328c0,-4.095 -0.996,-7 -2.988,-8.715c-1.992,-1.715 -4.703,-2.573 -8.134,-2.573c-3.652,0 -7.691,0.636 -12.118,1.909c-4.427,1.273 -8.909,2.905 -13.446,4.897l-9.13,-20.916c5.312,-2.767 11.316,-4.842 18.011,-6.225c6.695,-1.383 14.193,-2.075 22.493,-2.075Zm-1.826,52.788c-5.755,0.221 -9.822,1.245 -12.201,3.071c-2.379,1.826 -3.569,4.399 -3.569,7.719c0,3.099 0.83,5.395 2.49,6.889c1.66,1.494 3.873,2.241 6.64,2.241c3.873,0 7.166,-1.217 9.877,-3.652c2.711,-2.435 4.067,-5.589 4.067,-9.462l0,-7.138l-7.304,0.332Zm88.976,-52.788c6.419,0 11.592,1.245 15.521,3.735c3.929,2.49 7.11,5.561 9.545,9.213l0.664,0l2.324,-11.288l27.39,0l0,91.964c0,12.948 -4.012,22.797 -12.035,29.548c-8.023,6.751 -20.335,10.126 -36.935,10.126c-7.415,0 -13.944,-0.387 -19.588,-1.162c-5.644,-0.775 -11.067,-2.158 -16.268,-4.15l0,-26.394c5.533,2.324 10.79,4.067 15.77,5.229c4.98,1.162 11.067,1.743 18.26,1.743c12.727,0 19.09,-4.537 19.09,-13.612l0,-1.66c0,-3.209 0.332,-7.027 0.996,-11.454l-0.996,0c-2.103,3.652 -5.091,6.778 -8.964,9.379c-3.873,2.601 -8.964,3.901 -15.272,3.901c-9.739,0 -17.651,-4.039 -23.738,-12.118c-6.087,-8.079 -9.13,-19.865 -9.13,-35.358c0,-15.493 3.099,-27.307 9.296,-35.441c6.197,-8.134 14.221,-12.201 24.07,-12.201Zm11.952,24.568c-8.743,0 -13.114,7.857 -13.114,23.572c0,7.968 1.107,13.695 3.32,17.181c2.213,3.486 5.644,5.229 10.292,5.229c5.201,0 8.798,-1.632 10.79,-4.897c1.992,-3.265 2.988,-8.162 2.988,-14.691l0,-3.818c0,-7.193 -0.968,-12.754 -2.905,-16.683c-1.937,-3.929 -5.727,-5.893 -11.371,-5.893Zm105.908,-24.568c13.391,0 23.959,3.486 31.706,10.458c7.747,6.972 11.62,17.485 11.62,31.54l0,14.11l-56.606,0c0.221,4.759 2.02,8.687 5.395,11.786c3.375,3.099 8.272,4.648 14.691,4.648c5.755,0 11.011,-0.553 15.77,-1.66c4.759,-1.107 9.683,-2.877 14.774,-5.312l0,22.742c-4.427,2.324 -9.268,4.039 -14.525,5.146c-5.257,1.107 -11.869,1.66 -19.837,1.66c-9.296,0 -17.568,-1.632 -24.817,-4.897c-7.249,-3.265 -12.976,-8.383 -17.181,-15.355c-4.205,-6.972 -6.308,-15.881 -6.308,-26.726c0,-11.067 1.909,-20.141 5.727,-27.224c3.818,-7.083 9.13,-12.339 15.936,-15.77c6.806,-3.431 14.691,-5.146 23.655,-5.146Zm1.162,21.58c-3.763,0 -6.889,1.162 -9.379,3.486c-2.49,2.324 -3.956,6.031 -4.399,11.122l27.224,0c-0.111,-4.095 -1.3,-7.553 -3.569,-10.375c-2.269,-2.822 -5.561,-4.233 -9.877,-4.233Zm127.488,43.658c0,5.755 -1.3,10.901 -3.901,15.438c-2.601,4.537 -6.751,8.079 -12.45,10.624c-5.699,2.545 -13.197,3.818 -22.493,3.818c-6.529,0 -12.367,-0.36 -17.513,-1.079c-5.146,-0.719 -10.375,-2.075 -15.687,-4.067l0,-25.398c5.865,2.656 11.786,4.565 17.762,5.727c5.976,1.162 10.679,1.743 14.11,1.743c6.861,0 10.292,-1.549 10.292,-4.648c0,-1.328 -0.553,-2.49 -1.66,-3.486c-1.107,-0.996 -3.071,-2.103 -5.893,-3.32c-2.822,-1.217 -6.834,-2.877 -12.035,-4.98c-7.636,-3.209 -13.335,-6.889 -17.098,-11.039c-3.763,-4.15 -5.644,-9.988 -5.644,-17.513c0,-8.964 3.458,-15.715 10.375,-20.252c6.917,-4.537 16.074,-6.806 27.473,-6.806c6.087,0 11.786,0.664 17.098,1.992c5.312,1.328 10.845,3.265 16.6,5.81l-8.632,20.418c-4.537,-2.103 -9.13,-3.763 -13.778,-4.98c-4.648,-1.217 -8.355,-1.826 -11.122,-1.826c-5.201,0 -7.802,1.273 -7.802,3.818c0,1.107 0.47,2.103 1.411,2.988c0.941,0.885 2.739,1.881 5.395,2.988c2.656,1.107 6.529,2.711 11.62,4.814c5.312,2.103 9.711,4.399 13.197,6.889c3.486,2.49 6.087,5.478 7.802,8.964c1.715,3.486 2.573,7.94 2.573,13.363Z" style="fill:#fff;"/></g><defs><radialGradient id="_Radial1" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0,1022.208,-1679.52,0,473.39383,271.872181)"><stop offset="0" style="stop-color:#2f2f2f;stop-opacity:0.98"/><stop offset="1" style="stop-color:#111;stop-opacity:0.98"/></radialGradient></defs></svg> \ No newline at end of file
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
index f11b1d95aa..78405c21fc 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
@@ -77,5 +77,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// Gets or sets a value indicating the still image size to fetch.
/// </summary>
public string? StillSize { get; set; }
+
+ /// <summary>
+ /// Gets or sets the cache duration in days for similar item results. A value of 0 disables caching.
+ /// </summary>
+ public int SimilarItemsCacheDays { get; set; } = 7;
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
index 89d380ec1f..4048fc1655 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
@@ -44,6 +44,13 @@
<span>Hide crew members without profile images.</span>
</label>
</div>
+ <div class="verticalSection">
+ <h2>Similar Items</h2>
+ <div class="inputContainer">
+ <input is="emby-input" type="number" id="similarItemsCacheDays" pattern="[0-9]*" required min="0" max="365" label="Cache duration (days)" />
+ <div class="fieldDescription">Number of days to cache similar item results from TMDb. Set to 0 to disable caching.</div>
+ </div>
+ </div>
<div class="verticalSection verticalSection-extrabottompadding">
<h2>Image Scaling</h2>
<div class="selectContainer">
@@ -161,6 +168,13 @@
cancelable: false
}));
+ var similarItemsCacheDays = document.querySelector('#similarItemsCacheDays');
+ similarItemsCacheDays.value = config.SimilarItemsCacheDays;
+ similarItemsCacheDays.dispatchEvent(new Event('change', {
+ bubbles: true,
+ cancelable: false
+ }));
+
pluginConfig = config;
configureImageScaling();
});
@@ -179,6 +193,7 @@
config.MaxCrewMembers = document.querySelector('#maxCrewMembers').value;
config.HideMissingCastMembers = document.querySelector('#hideMissingCastMembers').checked;
config.HideMissingCrewMembers = document.querySelector('#hideMissingCrewMembers').checked;
+ config.SimilarItemsCacheDays = parseInt(document.querySelector('#similarItemsCacheDays').value, 10);
config.PosterSize = document.querySelector('#selectPosterSize').value;
config.BackdropSize = document.querySelector('#selectBackdropSize').value;
config.LogoSize = document.querySelector('#selectLogoSize').value;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
index 714c57d361..b188f5deb4 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
@@ -95,7 +95,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var posters = movie.Images.Posters;
var backdrops = movie.Images.Backdrops;
var logos = movie.Images.Logos;
- var remoteImages = new List<RemoteImageInfo>(posters?.Count ?? 0 + backdrops?.Count ?? 0 + logos?.Count ?? 0);
+ var remoteImages = new List<RemoteImageInfo>((posters?.Count ?? 0) + (backdrops?.Count ?? 0) + (logos?.Count ?? 0));
if (posters is not null)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieSimilarProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieSimilarProvider.cs
new file mode 100644
index 0000000000..5206de78ce
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieSimilarProvider.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Logging;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Movies;
+
+/// <summary>
+/// TMDb-based similar items provider for movies.
+/// </summary>
+public class TmdbMovieSimilarProvider : IRemoteSimilarItemsProvider<Movie>
+{
+ private readonly TmdbClientManager _tmdbClientManager;
+ private readonly ILogger<TmdbMovieSimilarProvider> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TmdbMovieSimilarProvider"/> class.
+ /// </summary>
+ /// <param name="tmdbClientManager">The TMDb client manager.</param>
+ /// <param name="logger">The logger.</param>
+ public TmdbMovieSimilarProvider(TmdbClientManager tmdbClientManager, ILogger<TmdbMovieSimilarProvider> logger)
+ {
+ _tmdbClientManager = tmdbClientManager;
+ _logger = logger;
+ }
+
+ /// <inheritdoc/>
+ public string Name => TmdbUtils.ProviderName;
+
+ /// <inheritdoc/>
+ public MetadataPluginType Type => MetadataPluginType.SimilarityProvider;
+
+ /// <inheritdoc/>
+ public TimeSpan? CacheDuration
+ {
+ get
+ {
+ var days = Plugin.Instance?.Configuration.SimilarItemsCacheDays ?? 0;
+ return days > 0 ? TimeSpan.FromDays(days) : null;
+ }
+ }
+
+ /// <inheritdoc/>
+ public async IAsyncEnumerable<SimilarItemReference> GetSimilarItemsAsync(
+ Movie item,
+ SimilarItemsQuery query,
+ [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ if (!item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbIdStr) || !int.TryParse(tmdbIdStr, CultureInfo.InvariantCulture, out var tmdbId))
+ {
+ yield break;
+ }
+
+ var providerName = MetadataProvider.Tmdb.ToString();
+ var page = 0;
+ var totalPages = 1;
+
+ while (page <= totalPages && !cancellationToken.IsCancellationRequested)
+ {
+ IReadOnlyList<TMDbLib.Objects.Search.SearchMovie> pageResults;
+ try
+ {
+ (pageResults, totalPages) = await _tmdbClientManager
+ .GetMovieSimilarPageAsync(tmdbId, page, TmdbUtils.GetImageLanguagesParam(string.Empty), cancellationToken)
+ .ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to get similar movies from TMDb for {TmdbId} page {Page}", tmdbId, page);
+ yield break;
+ }
+
+ if (pageResults.Count == 0)
+ {
+ yield break;
+ }
+
+ foreach (var similar in pageResults)
+ {
+ yield return new SimilarItemReference
+ {
+ ProviderName = providerName,
+ ProviderId = similar.Id.ToString(CultureInfo.InvariantCulture)
+ };
+ }
+
+ page++;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs
index 4adde8366a..04f1fd04a3 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
+using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
@@ -12,7 +13,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <summary>
/// Plugin class for the TMDb library.
/// </summary>
- public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
+ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages, IHasEmbeddedImage
{
/// <summary>
/// Initializes a new instance of the <see cref="Plugin"/> class.
@@ -44,6 +45,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <inheritdoc/>
public override string ConfigurationFileName => "Jellyfin.Plugin.Tmdb.xml";
+ /// <inheritdoc/>
+ public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-tmdb.svg";
+
/// <summary>
/// Return the plugin configuration page.
/// </summary>
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesSimilarProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesSimilarProvider.cs
new file mode 100644
index 0000000000..c85718b993
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesSimilarProvider.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.TV;
+
+/// <summary>
+/// TMDb-based similar items provider for TV series.
+/// </summary>
+public class TmdbSeriesSimilarProvider : IRemoteSimilarItemsProvider<Series>
+{
+ private readonly TmdbClientManager _tmdbClientManager;
+ private readonly ILogger<TmdbSeriesSimilarProvider> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TmdbSeriesSimilarProvider"/> class.
+ /// </summary>
+ /// <param name="tmdbClientManager">The TMDb client manager.</param>
+ /// <param name="logger">The logger.</param>
+ public TmdbSeriesSimilarProvider(TmdbClientManager tmdbClientManager, ILogger<TmdbSeriesSimilarProvider> logger)
+ {
+ _tmdbClientManager = tmdbClientManager;
+ _logger = logger;
+ }
+
+ /// <inheritdoc/>
+ public string Name => TmdbUtils.ProviderName;
+
+ /// <inheritdoc/>
+ public MetadataPluginType Type => MetadataPluginType.SimilarityProvider;
+
+ /// <inheritdoc/>
+ public TimeSpan? CacheDuration
+ {
+ get
+ {
+ var days = Plugin.Instance?.Configuration.SimilarItemsCacheDays ?? 0;
+ return days > 0 ? TimeSpan.FromDays(days) : null;
+ }
+ }
+
+ /// <inheritdoc/>
+ public async IAsyncEnumerable<SimilarItemReference> GetSimilarItemsAsync(
+ Series item,
+ SimilarItemsQuery query,
+ [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ if (!item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbIdStr) || !int.TryParse(tmdbIdStr, CultureInfo.InvariantCulture, out var tmdbId))
+ {
+ yield break;
+ }
+
+ var providerName = MetadataProvider.Tmdb.ToString();
+ var page = 1;
+ var totalPages = 1;
+
+ while (page <= totalPages && !cancellationToken.IsCancellationRequested)
+ {
+ IReadOnlyList<TMDbLib.Objects.Search.SearchTv> pageResults;
+ try
+ {
+ (pageResults, totalPages) = await _tmdbClientManager
+ .GetSeriesSimilarPageAsync(tmdbId, page, TmdbUtils.GetImageLanguagesParam(string.Empty), cancellationToken)
+ .ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to get similar TV shows from TMDb for {TmdbId} page {Page}", tmdbId, page);
+ yield break;
+ }
+
+ if (pageResults.Count == 0)
+ {
+ yield break;
+ }
+
+ foreach (var similar in pageResults)
+ {
+ yield return new SimilarItemReference
+ {
+ ProviderName = providerName,
+ ProviderId = similar.Id.ToString(CultureInfo.InvariantCulture)
+ };
+ }
+
+ page++;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
index 274db347ba..174f1546a7 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -505,6 +505,54 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
}
/// <summary>
+ /// Gets a single page of similar movies for a movie from the TMDb API.
+ /// </summary>
+ /// <param name="tmdbId">The TMDb id of the movie.</param>
+ /// <param name="page">The page number to fetch (1-based).</param>
+ /// <param name="language">The language for results.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>A tuple containing the list of similar movies and the total number of pages available.</returns>
+ public async Task<(IReadOnlyList<SearchMovie> Results, int TotalPages)> GetMovieSimilarPageAsync(int tmdbId, int page, string? language, CancellationToken cancellationToken)
+ {
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ var searchResults = await _tmDbClient
+ .GetMovieSimilarAsync(tmdbId, language, page, cancellationToken)
+ .ConfigureAwait(false);
+
+ if (searchResults?.Results is null || searchResults.Results.Count == 0)
+ {
+ return ([], 0);
+ }
+
+ return (searchResults.Results, searchResults.TotalPages);
+ }
+
+ /// <summary>
+ /// Gets a single page of similar TV shows for a series from the TMDb API.
+ /// </summary>
+ /// <param name="tmdbId">The TMDb id of the TV show.</param>
+ /// <param name="page">The page number to fetch (1-based).</param>
+ /// <param name="language">The language for results.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>A tuple containing the list of similar TV shows and the total number of pages available.</returns>
+ public async Task<(IReadOnlyList<SearchTv> Results, int TotalPages)> GetSeriesSimilarPageAsync(int tmdbId, int page, string? language, CancellationToken cancellationToken)
+ {
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ var searchResults = await _tmDbClient
+ .GetTvShowSimilarAsync(tmdbId, language, page, cancellationToken)
+ .ConfigureAwait(false);
+
+ if (searchResults?.Results is null || searchResults.Results.Count == 0)
+ {
+ return ([], 0);
+ }
+
+ return (searchResults.Results, searchResults.TotalPages);
+ }
+
+ /// <summary>
/// Handles bad path checking and builds the absolute url.
/// </summary>
/// <param name="size">The image size to fetch.</param>
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/jellyfin-plugin-tmdb.svg b/MediaBrowser.Providers/Plugins/Tmdb/jellyfin-plugin-tmdb.svg
new file mode 100644
index 0000000000..fbebb32b60
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/jellyfin-plugin-tmdb.svg
@@ -0,0 +1,16 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1920" height="1080" viewBox="0 0 508 285.75">
+ <defs>
+ <linearGradient id="a">
+ <stop style="stop-color:#20464c;stop-opacity:1" offset="0"/>
+ <stop style="stop-color:#0d253f;stop-opacity:.999722" offset="1"/>
+ </linearGradient>
+ <linearGradient id="c" y1="40.76" x2="190.24" y2="40.76" gradientUnits="userSpaceOnUse" gradientTransform="translate(95.25 74.882) scale(1.66894)">
+ <stop offset="0" stop-color="#90cea1"/>
+ <stop offset=".56" stop-color="#3cbec9"/>
+ <stop offset="1" stop-color="#00b3e5"/>
+ </linearGradient>
+ <radialGradient xlink:href="#a" id="b" cx="125.255" cy="16.735" fx="125.255" fy="16.735" r="254" gradientTransform="matrix(0 1.06475 -1.74952 0 154.532 -61.444)" gradientUnits="userSpaceOnUse"/>
+ </defs>
+ <path style="opacity:.98;fill:url(#b);stroke-width:2.64583;fill-opacity:1" d="M0 0h508v285.75H0z"/>
+ <path d="M271.607 135.064H383.26a29.49 29.49 0 0 0 29.49-29.473 29.49 29.49 0 0 0-29.49-29.49H271.607a29.49 29.49 0 0 0-29.49 29.49 29.49 29.49 0 0 0 29.49 29.473zM124.74 210.167h128.342a29.49 29.49 0 0 0 29.49-29.474 29.49 29.49 0 0 0-29.49-29.49H124.74a29.49 29.49 0 0 0-29.49 29.49 29.49 29.49 0 0 0 29.49 29.474zm-12.116-76.17h13.017V86.43h16.857V74.882h-46.73v11.516h16.856zm46.897 0h13.018V88.65h.167l15.02 45.312h10.014l15.52-45.312h.167v45.312h13.018v-59.08h-19.777l-13.686 38.552h-.167l-13.601-38.553H159.52zm190.126 33.795a25.151 25.151 0 0 0-7.543-9.212 30.992 30.992 0 0 0-11.149-5.14 55.976 55.976 0 0 0-13.468-1.67H297.96v59.081h21.279a41.023 41.023 0 0 0 12.6-1.92 32.277 32.277 0 0 0 10.598-5.54 27.154 27.154 0 0 0 7.294-9.18 28.222 28.222 0 0 0 2.72-12.65 30.875 30.875 0 0 0-2.804-13.769zm-12.4 21.58a14.687 14.687 0 0 1-4.406 5.674 17.858 17.858 0 0 1-6.676 3.038 36 36 0 0 1-8.345.918h-6.759v-35.048h7.677a28.372 28.372 0 0 1 7.794 1.051 19.46 19.46 0 0 1 6.476 3.121 15.254 15.254 0 0 1 4.239 5.224 16.472 16.472 0 0 1 1.669 7.544 19.844 19.844 0 0 1-1.67 8.478zm74.485-.217a13.352 13.352 0 0 0-2.637-4.373 13.986 13.986 0 0 0-4.039-3.087 17.207 17.207 0 0 0-5.29-1.67v-.166a15.388 15.388 0 0 0 7.376-4.707 12.4 12.4 0 0 0 2.804-8.344 14.053 14.053 0 0 0-1.92-7.761 13.502 13.502 0 0 0-5.006-4.54 20.962 20.962 0 0 0-6.976-2.17 54.808 54.808 0 0 0-7.71-.55h-22.03v59.08h24.199a37.401 37.401 0 0 0 7.877-.834 22.58 22.58 0 0 0 7.143-2.754 15.721 15.721 0 0 0 5.174-5.006 14.22 14.22 0 0 0 2.003-7.811 15.671 15.671 0 0 0-.918-5.307zm-32.411-26.286h8.845a16.69 16.69 0 0 1 3.088.3 10.314 10.314 0 0 1 2.837.952 5.658 5.658 0 0 1 2.036 1.885 5.374 5.374 0 0 1 .801 3.038 6.058 6.058 0 0 1-.717 3.004 5.674 5.674 0 0 1-1.87 2.003 8.211 8.211 0 0 1-2.636 1.085 12.534 12.534 0 0 1-2.954.333h-9.43zm19.56 33.379a6.509 6.509 0 0 1-2.036 2.17 7.744 7.744 0 0 1-2.804 1.168 13.652 13.652 0 0 1-3.037.333H379.32v-13.351h9.847a25.618 25.618 0 0 1 3.338.25 14.136 14.136 0 0 1 3.421.918 6.676 6.676 0 0 1 2.62 1.97 5.19 5.19 0 0 1 1.052 3.338 6.192 6.192 0 0 1-.718 3.204z" style="fill:url(#c);stroke-width:1.66894"/>
+</svg>
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index 61a31fbfd6..078c396730 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -210,16 +210,19 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
return true;
}
- // Not yet processed
- if (episode.SeasonId.IsEmpty())
+ // Episode has been processed and linked to a season, only needs a virtual season
+ // if it isn't already linked to a known physical season by ID or path
+ if (!episode.SeasonId.IsEmpty())
{
- return false;
+ return !physicalSeasonIds.Contains(episode.SeasonId)
+ && !physicalSeasonPaths.Contains(System.IO.Path.GetDirectoryName(episode.Path) ?? string.Empty);
}
- // Episode has been processed, only needs a virtual season if it isn't
- // already linked to a known physical season by ID or path
- return !physicalSeasonIds.Contains(episode.SeasonId)
- && !physicalSeasonPaths.Contains(System.IO.Path.GetDirectoryName(episode.Path) ?? string.Empty);
+ // Episode not yet linked, check if it's in a physical season folder
+ // If yes then skip it, processing not finished
+ // If no then include it, needs Season Unknown
+ var episodeDirectory = System.IO.Path.GetDirectoryName(episode.Path) ?? string.Empty;
+ return !physicalSeasonPaths.Contains(episodeDirectory);
}
/// <summary>
diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs b/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs
index 52b0583e58..e01b6c78ed 100644
--- a/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs
+++ b/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs
@@ -19,6 +19,6 @@ public class Zap2ItExternalUrlProvider : IExternalUrlProvider
if (item.TryGetProviderId(MetadataProvider.Zap2It, out var externalId))
{
yield return $"http://tvlistings.zap2it.com/overview.html?programSeriesId={externalId}";
- }
+ }
}
}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
index 137abff478..f013863336 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
@@ -96,7 +96,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var artist = reader.ReadNormalizedString();
if (!string.IsNullOrEmpty(artist) && item is MusicVideo artistVideo)
{
- artistVideo.Artists = [..artistVideo.Artists, artist];
+ artistVideo.Artists = [.. artistVideo.Artists, artist];
}
break;
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index ed32e6c76a..78907a5e68 100644
--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
@@ -198,15 +198,23 @@ namespace MediaBrowser.XbmcMetadata.Savers
cancellationToken.ThrowIfCancellationRequested();
- await SaveToFileAsync(memoryStream, path).ConfigureAwait(false);
+ await SaveToFileAsync(memoryStream, path, cancellationToken).ConfigureAwait(false);
}
}
- private async Task SaveToFileAsync(Stream stream, string path)
+ private async Task SaveToFileAsync(Stream stream, string path, CancellationToken cancellationToken)
{
var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
Directory.CreateDirectory(directory);
+ // Compare byte-for-byte before proceeding.
+ if (File.Exists(path) && await stream.IsFileIdenticalAsync(path, cancellationToken).ConfigureAwait(false))
+ {
+ return; // Don't save since .nfo is unchanged.
+ }
+
+ stream.Position = 0;
+
// On Windows, saving the file will fail if the file is hidden or readonly
FileSystem.SetAttributes(path, false, false);
@@ -222,7 +230,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
var filestream = new FileStream(path, fileStreamOptions);
await using (filestream.ConfigureAwait(false))
{
- await stream.CopyToAsync(filestream).ConfigureAwait(false);
+ await stream.CopyToAsync(filestream, cancellationToken).ConfigureAwait(false);
}
if (ConfigurationManager.Configuration.SaveMetadataHidden)
diff --git a/README.md b/README.md
index fbd73edfcf..5e066f3d31 100644
--- a/README.md
+++ b/README.md
@@ -76,7 +76,7 @@ These instructions will help you get set up with a local development environment
### Prerequisites
-Before the project can be built, you must first install the [.NET 9.0 SDK](https://dotnet.microsoft.com/download/dotnet) on your system.
+Before the project can be built, you must first install the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet) on your system.
Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET 6 development will work, but two options are recent versions of [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2022) and [Visual Studio Code](https://code.visualstudio.com/Download).
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs
index 43e6a8bc00..88a2c684ff 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs
@@ -111,7 +111,9 @@ public static class DescendantQueryHelper
private static HashSet<Guid> GetMatchingMediaStreamItemIds(JellyfinDbContext context, HasMediaStreamType criteria)
{
var query = context.MediaStreamInfos
- .Where(ms => ms.StreamType == criteria.StreamType && ms.Language == criteria.Language);
+ .Where(ms => ms.StreamType == criteria.StreamType
+ && (criteria.Language.Contains(ms.Language)
+ || (criteria.Language.Contains("und") && string.IsNullOrEmpty(ms.Language)))); // und = undetermined
if (criteria.IsExternal.HasValue)
{
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
index 6c81fa729c..b10e210e5d 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
@@ -27,6 +27,7 @@ namespace Jellyfin.Database.Implementations.Entities
ArgumentException.ThrowIfNullOrEmpty(passwordResetProviderId);
Username = username;
+ NormalizedUsername = username.ToUpperInvariant();
AuthenticationProviderId = authenticationProviderId;
PasswordResetProviderId = passwordResetProviderId;
@@ -74,6 +75,16 @@ namespace Jellyfin.Database.Implementations.Entities
public string Username { get; set; }
/// <summary>
+ /// Gets or sets the user's normalized name.
+ /// </summary>
+ /// <remarks>
+ /// Required, Max length = 255.
+ /// </remarks>
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string NormalizedUsername { get; set; }
+
+ /// <summary>
/// Gets or sets the user's password, or <c>null</c> if none is set.
/// </summary>
/// <remarks>
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs
index b2bcbf2bb6..34810b9199 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs
@@ -108,5 +108,50 @@ public enum ViewType
/// <summary>
/// Shows upcoming.
/// </summary>
- Upcoming = 20
+ Upcoming = 20,
+
+ /// <summary>
+ /// Shows authors.
+ /// </summary>
+ Authors = 21,
+
+ /// <summary>
+ /// Shows books.
+ /// </summary>
+ Books = 22,
+
+ /// <summary>
+ /// Shows folders.
+ /// </summary>
+ Folders = 23,
+
+ /// <summary>
+ /// Shows mixed media.
+ /// </summary>
+ Mixed = 24,
+
+ /// <summary>
+ /// Shows photos.
+ /// </summary>
+ Photos = 25,
+
+ /// <summary>
+ /// Shows photo albums.
+ /// </summary>
+ PhotoAlbums = 26,
+
+ /// <summary>
+ /// Shows series timers.
+ /// </summary>
+ SeriesTimers = 27,
+
+ /// <summary>
+ /// Shows studios.
+ /// </summary>
+ Studios = 28,
+
+ /// <summary>
+ /// Shows videos.
+ /// </summary>
+ Videos = 29
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs
index f6fce7279a..b0c12bf592 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs
@@ -273,6 +273,11 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
}).ConfigureAwait(false);
return result;
}
+ catch (DbUpdateConcurrencyException)
+ {
+ // a concurrency exception is supposed to be always handled by the invoker of the method, logging it here is only causing log bloat.
+ throw;
+ }
catch (Exception e)
{
logger.LogError(e, "Error trying to save changes.");
@@ -294,6 +299,11 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
});
return result;
}
+ catch (DbUpdateConcurrencyException)
+ {
+ // a concurrency exception is supposed to be always handled by the invoker of the method, logging it here is only causing log bloat.
+ throw;
+ }
catch (Exception e)
{
logger.LogError(e, "Error trying to save changes.");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/MatchCriteria/HasMediaStreamType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/MatchCriteria/HasMediaStreamType.cs
index 68f2ca2786..c1f6ab16a9 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/MatchCriteria/HasMediaStreamType.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/MatchCriteria/HasMediaStreamType.cs
@@ -1,3 +1,6 @@
+#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
+
+using System.Collections.Generic;
using Jellyfin.Database.Implementations.Entities;
namespace Jellyfin.Database.Implementations.MatchCriteria;
@@ -6,9 +9,23 @@ namespace Jellyfin.Database.Implementations.MatchCriteria;
/// Matches folders containing descendants with a specific media stream type and language.
/// </summary>
/// <param name="StreamType">The type of media stream to match (Audio, Subtitle, etc.).</param>
-/// <param name="Language">The language to match.</param>
+/// <param name="Language">List of languages to match.</param>
/// <param name="IsExternal">If not null, filters by internal (false) or external (true) streams. Only applicable to subtitles.</param>
public sealed record HasMediaStreamType(
MediaStreamTypeEntity StreamType,
- string Language,
- bool? IsExternal = null) : FolderMatchCriteria;
+ IReadOnlyCollection<string> Language,
+ bool? IsExternal = null) : FolderMatchCriteria
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HasMediaStreamType"/> class.
+ /// </summary>
+ /// <param name="StreamType">The type of media stream to match (Audio, Subtitle, etc.).</param>
+ /// <param name="Language">The language to match.</param>
+ /// <param name="IsExternal">If not null, filters by internal (false) or external (true) streams. Only applicable to subtitles.</param>
+ public HasMediaStreamType(
+ MediaStreamTypeEntity StreamType,
+ string Language,
+ bool? IsExternal = null) : this(StreamType, [Language], IsExternal)
+ {
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs
index 61b5e06e8a..ed4138680d 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs
@@ -50,6 +50,10 @@ namespace Jellyfin.Database.Implementations.ModelConfiguration
builder
.HasIndex(entity => entity.Username)
.IsUnique();
+
+ builder
+ .HasIndex(entity => entity.NormalizedUsername)
+ .IsUnique();
}
}
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs
index 55b90a54d7..ff10440e0c 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.cs
index 8cc7fb452d..9c03bfed9d 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.cs
index ac78019eda..3fe61f91df 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.cs
index 4558d7c49c..d6b351e2ab 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.cs
index 70e81f3676..a7c9e6fb50 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.cs
index d57ea81b3a..ab8b792a5f 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.cs
index 78611b9e4c..1ed23e7c42 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.cs
index d1b06ceaec..e3a3f3ac64 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.cs
index e82575e418..3d4fd85af2 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.cs
index 2c60dd7a62..1493df35d0 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.cs
index 1e904e833e..713b5c0434 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs
index 71f56a1492..7049ccc214 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs
index c17b35b404..d84940b7e6 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs
index 5766cd3825..63010679e5 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs
index f921856a20..ceb3d32452 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs
index e1220bfcf7..1f6012bbf2 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs
index aa667bafd4..6032969cf3 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250609115616_DetachUserDataInsteadOfDelete.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250609115616_DetachUserDataInsteadOfDelete.cs
index 2935a608d1..a3d8fe2c3a 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250609115616_DetachUserDataInsteadOfDelete.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250609115616_DetachUserDataInsteadOfDelete.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs
index bce6029d5b..44b44dd581 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.cs
index 23cb0c8ba5..e88518d74a 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs
index 38033d07f0..a7f5e369ab 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250925203415_ExtendPeopleMapKey.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250925203415_ExtendPeopleMapKey.cs
index 7c1bcdf445..097504aebb 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250925203415_ExtendPeopleMapKey.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250925203415_ExtendPeopleMapKey.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113102337_AddLinkedChildrenTable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113102337_AddLinkedChildrenTable.cs
index 198bc78cff..1ab6b4240a 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113102337_AddLinkedChildrenTable.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113102337_AddLinkedChildrenTable.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs
index 6334d8b5f1..4927b0e78d 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs
index 0e28abc862..f9cb9aa736 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs
@@ -270,6 +270,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<string>("OfficialRating")
.HasColumnType("TEXT");
+ b.Property<string>("OriginalLanguage")
+ .HasColumnType("TEXT");
+
b.Property<string>("OriginalTitle")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.cs
index c84086d992..39a0805d2a 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
@@ -23,12 +23,40 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
name: "BaseItemEntityId",
table: "BaseItems");
+ migrationBuilder.Sql(
+ """
+ UPDATE BaseItems
+ SET OwnerId = '00000000-0000-0000-0000-000000000001'
+ WHERE OwnerId IS NOT NULL
+ AND OwnerId NOT IN (SELECT Id FROM BaseItems);
+ """);
+
migrationBuilder.AddForeignKey(
name: "FK_BaseItems_BaseItems_OwnerId",
table: "BaseItems",
column: "OwnerId",
principalTable: "BaseItems",
principalColumn: "Id");
+
+ migrationBuilder.AddColumn<bool>(
+ name: "IsOriginal",
+ table: "MediaStreamInfos",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn<string>(
+ name: "OriginalLanguage",
+ table: "BaseItems",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.UpdateData(
+ table: "BaseItems",
+ keyColumn: "Id",
+ keyValue: new Guid("00000000-0000-0000-0000-000000000001"),
+ column: "OriginalLanguage",
+ value: null);
}
/// <inheritdoc />
@@ -62,6 +90,14 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
column: "BaseItemEntityId",
principalTable: "BaseItems",
principalColumn: "Id");
+
+ migrationBuilder.DropColumn(
+ name: "IsOriginal",
+ table: "MediaStreamInfos");
+
+ migrationBuilder.DropColumn(
+ name: "OriginalLanguage",
+ table: "BaseItems");
}
}
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs
index 92ed0cf6bf..29874264af 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs
@@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<string>("OfficialRating")
.HasColumnType("TEXT");
+ b.Property<string>("OriginalLanguage")
+ .HasColumnType("TEXT");
+
b.Property<string>("OriginalTitle")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.cs
index 5387d3351d..6440d0a395 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs
index 89fb3ee815..8282a8a582 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs
@@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<string>("OfficialRating")
.HasColumnType("TEXT");
+ b.Property<string>("OriginalLanguage")
+ .HasColumnType("TEXT");
+
b.Property<string>("OriginalTitle")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.cs
index ba1a131e9b..710ffc35b1 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs
index 83a6a7baf3..5541a0191b 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs
@@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<string>("OfficialRating")
.HasColumnType("TEXT");
+ b.Property<string>("OriginalLanguage")
+ .HasColumnType("TEXT");
+
b.Property<string>("OriginalTitle")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.cs
index 8c8768645b..7e1d619b8a 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs
index 1b396a707c..f6fd1db21e 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs
@@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<string>("OfficialRating")
.HasColumnType("TEXT");
+ b.Property<string>("OriginalLanguage")
+ .HasColumnType("TEXT");
+
b.Property<string>("OriginalTitle")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.cs
index da57c71662..0b540d799b 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs
index ca995decde..5f7131ff65 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs
@@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<string>("OfficialRating")
.HasColumnType("TEXT");
+ b.Property<string>("OriginalLanguage")
+ .HasColumnType("TEXT");
+
b.Property<string>("OriginalTitle")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.cs
index 92836e753f..ef0c039ffe 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs
index 0184154566..0499921fec 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs
@@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<string>("OfficialRating")
.HasColumnType("TEXT");
+ b.Property<string>("OriginalLanguage")
+ .HasColumnType("TEXT");
+
b.Property<string>("OriginalTitle")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs
index 4c9ccc13bf..bf46ad9b39 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs
@@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<string>("OfficialRating")
.HasColumnType("TEXT");
+ b.Property<string>("OriginalLanguage")
+ .HasColumnType("TEXT");
+
b.Property<string>("OriginalTitle")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.cs
index 3932e1c3e4..00d4f24403 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs
index 23ab2a4674..fc5c7afa0e 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs
@@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<string>("OfficialRating")
.HasColumnType("TEXT");
+ b.Property<string>("OriginalLanguage")
+ .HasColumnType("TEXT");
+
b.Property<string>("OriginalTitle")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.cs
index e1f62c12fb..ad51786581 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.cs
@@ -1,4 +1,4 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.cs
deleted file mode 100644
index cda226309a..0000000000
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using System;
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace Jellyfin.Database.Providers.Sqlite.Migrations
-{
- /// <inheritdoc />
- public partial class AddOriginalLanguage : Migration
- {
- /// <inheritdoc />
- protected override void Up(MigrationBuilder migrationBuilder)
- {
- migrationBuilder.AddColumn<bool>(
- name: "IsOriginal",
- table: "MediaStreamInfos",
- type: "INTEGER",
- nullable: false,
- defaultValue: false);
-
- migrationBuilder.AddColumn<string>(
- name: "OriginalLanguage",
- table: "BaseItems",
- type: "TEXT",
- nullable: true);
-
- migrationBuilder.UpdateData(
- table: "BaseItems",
- keyColumn: "Id",
- keyValue: new Guid("00000000-0000-0000-0000-000000000001"),
- column: "OriginalLanguage",
- value: null);
- }
-
- /// <inheritdoc />
- protected override void Down(MigrationBuilder migrationBuilder)
- {
- migrationBuilder.DropColumn(
- name: "IsOriginal",
- table: "MediaStreamInfos");
-
- migrationBuilder.DropColumn(
- name: "OriginalLanguage",
- table: "BaseItems");
- }
- }
-}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260522092303_AddNormalizedUsername.Designer.cs
index e0f5125da1..63f858bc98 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260522092303_AddNormalizedUsername.Designer.cs
@@ -1,4 +1,4 @@
-// <auto-generated />
+// <auto-generated />
using System;
using Jellyfin.Database.Implementations;
using Microsoft.EntityFrameworkCore;
@@ -8,11 +8,11 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
-namespace Jellyfin.Database.Providers.Sqlite.Migrations
+namespace Jellyfin.Server.Implementations.Migrations
{
[DbContext(typeof(JellyfinDbContext))]
- [Migration("20260504180809_AddOriginalLanguage")]
- partial class AddOriginalLanguage
+ [Migration("20260522092303_AddNormalizedUsername")]
+ partial class AddNormalizedUsername
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -961,9 +961,6 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<bool?>("IsInterlaced")
.HasColumnType("INTEGER");
- b.Property<bool>("IsOriginal")
- .HasColumnType("INTEGER");
-
b.Property<string>("KeyFrames")
.HasColumnType("TEXT");
@@ -1351,6 +1348,11 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<bool>("MustUpdatePassword")
.HasColumnType("INTEGER");
+ b.Property<string>("NormalizedUsername")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
b.Property<string>("Password")
.HasMaxLength(65535)
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260522092303_AddNormalizedUsername.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260522092303_AddNormalizedUsername.cs
new file mode 100644
index 0000000000..670f59ba7a
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260522092303_AddNormalizedUsername.cs
@@ -0,0 +1,32 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddNormalizedUsername : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<string>(
+ name: "NormalizedUsername",
+ table: "Users",
+ type: "TEXT",
+ maxLength: 255,
+ nullable: false,
+ defaultValue: string.Empty);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.Sql("ALTER TABLE Users DROP COLUMN NormalizedUsername;");
+
+ migrationBuilder.Sql(
+ @"DELETE FROM __EFMigrationsHistory
+ WHERE MigrationId = '20260522092304_UpdateNormalizedUsername'");
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260524120336_AddUniqueNormalizedUsernameIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260524120336_AddUniqueNormalizedUsernameIndex.Designer.cs
new file mode 100644
index 0000000000..a1f555a59b
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260524120336_AddUniqueNormalizedUsernameIndex.Designer.cs
@@ -0,0 +1,1807 @@
+// <auto-generated />
+using System;
+using Jellyfin.Database.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20260524120336_AddUniqueNormalizedUsernameIndex")]
+ partial class AddUniqueNormalizedUsernameIndex
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Filename")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("MimeType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Index");
+
+ b.ToTable("AttachmentStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Album")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AlbumArtists")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Artists")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Audio")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("ChannelId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanName")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("CommunityRating")
+ .HasColumnType("REAL");
+
+ b.Property<float?>("CriticRating")
+ .HasColumnType("REAL");
+
+ b.Property<string>("CustomRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Data")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastMediaAdded")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastRefreshed")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastSaved")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("EndDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("EpisodeTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalSeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalServiceId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ExtraType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ForcedSortName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Genres")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("InheritedParentalRatingSubValue")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("InheritedParentalRatingValue")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInMixedFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsMovie")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsRepeat")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsSeries")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsVirtualItem")
+ .HasColumnType("INTEGER");
+
+ b.Property<float?>("LUFS")
+ .HasColumnType("REAL");
+
+ b.Property<string>("MediaType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("NormalizationGain")
+ .HasColumnType("REAL");
+
+ b.Property<string>("OfficialRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("OwnerId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ParentIndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataCountryCode")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("PremiereDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.HasIndex("OwnerId");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path");
+
+ b.HasIndex("PresentationUniqueKey");
+
+ b.HasIndex("SeasonId");
+
+ b.HasIndex("SeriesId");
+
+ b.HasIndex("SeriesName");
+
+ b.HasIndex("ExtraType", "OwnerId");
+
+ b.HasIndex("TopParentId", "Id");
+
+ b.HasIndex("Type", "CleanName");
+
+ b.HasIndex("TopParentId", "Type", "IsVirtualItem")
+ .HasFilter("\"PrimaryVersionId\" IS NULL AND (\"OwnerId\" IS NULL OR \"ExtraType\" IS NOT NULL)");
+
+ b.HasIndex("Type", "TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "TopParentId", "SortName");
+
+ b.HasIndex("Type", "TopParentId", "StartDate");
+
+ b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+ b.HasIndex("TopParentId", "IsFolder", "IsVirtualItem", "DateCreated");
+
+ b.HasIndex("TopParentId", "MediaType", "IsVirtualItem", "DateCreated");
+
+ b.HasIndex("TopParentId", "Type", "IsVirtualItem", "DateCreated");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "ParentIndexNumber", "IndexNumber");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+ b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.ToTable("BaseItems");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000001"),
+ IsFolder = false,
+ IsInMixedFolder = false,
+ IsLocked = false,
+ IsMovie = false,
+ IsRepeat = false,
+ IsSeries = false,
+ IsVirtualItem = false,
+ Name = "This is a placeholder item for UserData that has been detached from its original item",
+ Type = "PLACEHOLDER"
+ });
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<byte[]>("Blurhash")
+ .HasColumnType("BLOB");
+
+ b.Property<DateTime?>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ImageType")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemId", "ImageType");
+
+ b.ToTable("BaseItemImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemMetadataFields");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ProviderId");
+
+ b.HasIndex("ProviderId", "ItemId", "ProviderValue");
+
+ b.ToTable("BaseItemProviders");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemTrailerTypes");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ChapterIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("ImageDateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ImagePath")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "ChapterIndex");
+
+ b.ToTable("Chapters");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId");
+
+ b.HasIndex("Type", "CleanValue");
+
+ b.HasIndex("Type", "Value")
+ .IsUnique();
+
+ b.ToTable("ItemValues");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("ItemValuesMap");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("KeyframeTicks")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TotalDuration")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId");
+
+ b.ToTable("KeyframeData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b =>
+ {
+ b.Property<Guid>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ChildId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ChildType")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ParentId", "ChildId");
+
+ b.HasIndex("ChildId", "ChildType");
+
+ b.HasIndex("ParentId", "ChildType");
+
+ b.HasIndex("ParentId", "SortOrder");
+
+ b.ToTable("LinkedChildren", (string)null);
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("Hdr10PlusPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAnamorphic")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAvc")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsExternal")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsForced")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsHearingImpaired")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsInterlaced")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.ToTable("MediaStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId", "Role");
+
+ b.HasIndex("PeopleId");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.ToTable("PeopleBaseItemMap");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingSubScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("NormalizedUsername")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedUsername")
+ .IsUnique();
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<DateTime?>("RetentionDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.HasIndex("UserId", "IsFavorite", "ItemId");
+
+ b.HasIndex("UserId", "ItemId", "LastPlayedDate");
+
+ b.HasIndex("UserId", "Played", "ItemId");
+
+ b.ToTable("UserData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Parents")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("Children")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Owner")
+ .WithMany("Extras")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.NoAction);
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "DirectParent")
+ .WithMany("DirectChildren")
+ .HasForeignKey("ParentId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("DirectParent");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Child")
+ .WithMany("LinkedChildOfEntities")
+ .HasForeignKey("ChildId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Parent")
+ .WithMany("LinkedChildEntities")
+ .HasForeignKey("ParentId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.Navigation("Child");
+
+ b.Navigation("Parent");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("DirectChildren");
+
+ b.Navigation("Extras");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LinkedChildEntities");
+
+ b.Navigation("LinkedChildOfEntities");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("Parents");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260524120336_AddUniqueNormalizedUsernameIndex.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260524120336_AddUniqueNormalizedUsernameIndex.cs
new file mode 100644
index 0000000000..6c17775d16
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260524120336_AddUniqueNormalizedUsernameIndex.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddUniqueNormalizedUsernameIndex : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateIndex(
+ name: "IX_Users_NormalizedUsername",
+ table: "Users",
+ column: "NormalizedUsername",
+ unique: true);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_Users_NormalizedUsername",
+ table: "Users");
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
index 86b838d64e..fd18c035e6 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.12");
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
{
@@ -1348,6 +1348,11 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<bool>("MustUpdatePassword")
.HasColumnType("INTEGER");
+ b.Property<string>("NormalizedUsername")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
b.Property<string>("Password")
.HasMaxLength(65535)
.HasColumnType("TEXT");
@@ -1390,6 +1395,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
+ b.HasIndex("NormalizedUsername")
+ .IsUnique();
+
b.HasIndex("Username")
.IsUnique();
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 3f7ae4d2cd..b6d2914efa 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -234,20 +234,20 @@ public class SkiaEncoder : IImageEncoder
return default;
default:
- {
- var boundsInfo = SKBitmap.DecodeBounds(safePath);
- if (boundsInfo.Width > 0 && boundsInfo.Height > 0)
{
- return new ImageDimensions(boundsInfo.Width, boundsInfo.Height);
+ var boundsInfo = SKBitmap.DecodeBounds(safePath);
+ if (boundsInfo.Width > 0 && boundsInfo.Height > 0)
+ {
+ return new ImageDimensions(boundsInfo.Width, boundsInfo.Height);
+ }
+
+ _logger.LogWarning(
+ "Unable to determine image dimensions for {FilePath}: {SkCodecResult}",
+ path,
+ result);
+
+ return default;
}
-
- _logger.LogWarning(
- "Unable to determine image dimensions for {FilePath}: {SkCodecResult}",
- path,
- result);
-
- return default;
- }
}
}
finally
@@ -398,7 +398,7 @@ public class SkiaEncoder : IImageEncoder
try
{
- // If we have to resize these they often end up distorted
+ // If we have to resize these they often end up distorted
if (resultBitmap.ColorType == SKColorType.Gray8)
{
using (resultBitmap)
diff --git a/src/Jellyfin.Extensions/StreamExtensions.cs b/src/Jellyfin.Extensions/StreamExtensions.cs
index 0cfac384e3..36361c58e8 100644
--- a/src/Jellyfin.Extensions/StreamExtensions.cs
+++ b/src/Jellyfin.Extensions/StreamExtensions.cs
@@ -1,17 +1,22 @@
+using System;
+using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
+using System.Threading.Tasks;
namespace Jellyfin.Extensions
{
/// <summary>
- /// Class BaseExtensions.
+ /// Extension methods for the <see cref="Stream"/> class.
/// </summary>
public static class StreamExtensions
{
+ private const int StreamComparisonBufferSize = 81920;
+
/// <summary>
/// Reads all lines in the <see cref="Stream" />.
/// </summary>
@@ -60,5 +65,172 @@ namespace Jellyfin.Extensions
yield return line;
}
}
+
+ /// <summary>
+ /// Determines whether a stream is identical to a file on disk.
+ /// </summary>
+ /// <param name="stream">The stream to compare.</param>
+ /// <param name="path">The file path to compare against.</param>
+ /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
+ /// <returns>True if the stream and file are identical; otherwise false.</returns>
+ /// <exception cref="ArgumentException"><paramref name="stream"/> does not support seeking.</exception>
+ /// <remarks>
+ /// The entire stream is compared against the file from the beginning (the position is reset to 0 on entry)
+ /// and restored to its original value after the call.
+ /// </remarks>
+ public static async Task<bool> IsFileIdenticalAsync(this Stream stream, string path, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(stream);
+ ArgumentException.ThrowIfNullOrEmpty(path);
+
+ if (!stream.CanSeek)
+ {
+ throw new ArgumentException("Stream must support seeking.", nameof(stream));
+ }
+
+ var originalPosition = stream.Position;
+ try
+ {
+ stream.Position = 0;
+
+ var existingFileStream = new FileStream(
+ path,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.Read,
+ bufferSize: StreamComparisonBufferSize,
+ FileOptions.Asynchronous | FileOptions.SequentialScan);
+ await using (existingFileStream.ConfigureAwait(false))
+ {
+ return await stream.IsStreamIdenticalAsync(existingFileStream, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ finally
+ {
+ stream.Position = originalPosition;
+ }
+ }
+
+ /// <summary>
+ /// Determines whether two streams are identical.
+ /// </summary>
+ /// <param name="a">The first stream to compare.</param>
+ /// <param name="b">The second stream to compare.</param>
+ /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
+ /// <returns>True if the streams are identical; otherwise false.</returns>
+ /// <remarks>
+ /// Seekable streams are compared from the beginning (their position is reset to 0 on entry).
+ /// Non-seekable streams are compared from their current read position. Stream positions are not
+ /// restored after the call.
+ /// </remarks>
+ public static async Task<bool> IsStreamIdenticalAsync(this Stream a, Stream b, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(a);
+ ArgumentNullException.ThrowIfNull(b);
+
+ if (ReferenceEquals(a, b))
+ {
+ return true;
+ }
+
+ if (a.CanSeek is var aCanSeek && aCanSeek)
+ {
+ a.Position = 0;
+ }
+
+ if (b.CanSeek is var bCanSeek && bCanSeek)
+ {
+ b.Position = 0;
+ }
+
+ if (aCanSeek && bCanSeek && b.Length != a.Length)
+ {
+ return false;
+ }
+
+ // MemoryStreams only unlock a fast path if their underlying buffer is exposed via TryGetBuffer.
+ var segmentA = a is MemoryStream streamA && streamA.TryGetBuffer(out var bufA) ? bufA : default;
+ var segmentB = b is MemoryStream streamB && streamB.TryGetBuffer(out var bufB) ? bufB : default;
+
+ // Fast path A: both streams expose buffers, compare segments directly
+ if (segmentA.Array is not null && segmentB.Array is not null)
+ {
+ return segmentA.AsSpan().SequenceEqual(segmentB.AsSpan());
+ }
+
+ if (segmentB.Array is not null) // && segmentA.Array is null guaranteed by previous check
+ {
+ // swap so that segmentA is the non-null one, compared to b we need only one fast path B
+ (segmentA, b) = (segmentB, a);
+ }
+
+ if (segmentA.Array is not null) // either a was non-null, or b was non-null and was swapped there
+ {
+ // Fast path B: only one stream exposed a buffer, compare against the other chunk-by-chunk
+ var bufferB = ArrayPool<byte>.Shared.Rent(StreamComparisonBufferSize);
+ try
+ {
+ var memoryB = bufferB.AsMemory();
+ int offset = 0;
+ int bytesRead;
+ while ((bytesRead = await b.ReadAtLeastAsync(memoryB, memoryB.Length, throwOnEndOfStream: false, cancellationToken).ConfigureAwait(false)) > 0)
+ {
+ if (offset + bytesRead > segmentA.Count || !segmentA.AsSpan(offset, bytesRead).SequenceEqual(memoryB.Span[..bytesRead]))
+ {
+ return false;
+ }
+
+ offset += bytesRead;
+ }
+
+ return offset == segmentA.Count;
+ }
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(bufferB);
+ }
+ }
+ else
+ {
+ var bufferA = ArrayPool<byte>.Shared.Rent(StreamComparisonBufferSize);
+ var bufferB = ArrayPool<byte>.Shared.Rent(StreamComparisonBufferSize);
+ try
+ {
+ var memoryA = bufferA.AsMemory();
+ var memoryB = bufferB.AsMemory();
+ while (true)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var taskA = a.ReadAtLeastAsync(memoryA, memoryA.Length, throwOnEndOfStream: false, cancellationToken).AsTask();
+ var taskB = b.ReadAtLeastAsync(memoryB, memoryB.Length, throwOnEndOfStream: false, cancellationToken).AsTask();
+ await Task.WhenAll(taskA, taskB).ConfigureAwait(false);
+
+ var bytesReadA = await taskA.ConfigureAwait(false);
+ var bytesReadB = await taskB.ConfigureAwait(false);
+
+ if (bytesReadA != bytesReadB)
+ {
+ return false;
+ }
+
+ if (bytesReadA == 0)
+ {
+ return true;
+ }
+
+ if (!memoryA.Span[..bytesReadA].SequenceEqual(memoryB.Span[..bytesReadB]))
+ {
+ return false;
+ }
+ }
+ }
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(bufferA);
+ ArrayPool<byte>.Shared.Return(bufferB);
+ }
+ }
+ }
}
}
diff --git a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
index 2b8e5a0a08..ed02fe6a1d 100644
--- a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
+++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
@@ -1129,7 +1129,7 @@ namespace Jellyfin.LiveTv.Channels
{
if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
{
- item.Tags = [..item.Tags, "livestream"];
+ item.Tags = [.. item.Tags, "livestream"];
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
forceUpdate = true;
}
diff --git a/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs b/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs
index 71e46764ad..bb4238a2ac 100644
--- a/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs
+++ b/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs
@@ -40,10 +40,10 @@ namespace Jellyfin.LiveTv.Channels
}
/// <inheritdoc />
- public string Name => _localization.GetLocalizedString("TasksRefreshChannels");
+ public string Name => _localization.GetLocalizedString("TaskRefreshChannels");
/// <inheritdoc />
- public string Description => _localization.GetLocalizedString("TasksRefreshChannelsDescription");
+ public string Description => _localization.GetLocalizedString("TaskRefreshChannelsDescription");
/// <inheritdoc />
public string Category => _localization.GetLocalizedString("TasksChannelsCategory");
diff --git a/src/Jellyfin.LiveTv/Listings/ListingsManager.cs b/src/Jellyfin.LiveTv/Listings/ListingsManager.cs
index 58683deb30..15e20d6f64 100644
--- a/src/Jellyfin.LiveTv/Listings/ListingsManager.cs
+++ b/src/Jellyfin.LiveTv/Listings/ListingsManager.cs
@@ -67,7 +67,7 @@ public class ListingsManager : IListingsManager
if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
{
info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
- config.ListingProviders = [..list, info];
+ config.ListingProviders = [.. list, info];
}
else
{
@@ -255,7 +255,7 @@ public class ListingsManager : IListingsManager
Name = tunerChannelNumber,
Value = providerChannelNumber
};
- listingsProviderInfo.ChannelMappings = [..listingsProviderInfo.ChannelMappings, newItem];
+ listingsProviderInfo.ChannelMappings = [.. listingsProviderInfo.ChannelMappings, newItem];
}
_config.SaveConfiguration("livetv", config);
diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
index 3aa0f0408b..c1ccb24bf4 100644
--- a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
@@ -684,27 +684,37 @@ namespace Jellyfin.LiveTv.Listings
sdCode?.ToString() ?? "N/A",
responseBody);
- if (sdCode is SdErrorCode.InvalidUser or SdErrorCode.InvalidHash or SdErrorCode.AccountLocked or SdErrorCode.AccountExpired or SdErrorCode.PasswordRequired)
+ if (sdCode is SdErrorCode.AccountExpired or SdErrorCode.InvalidHash or SdErrorCode.InvalidUser or SdErrorCode.AccountLocked or SdErrorCode.AppLocked or SdErrorCode.AccountInactive)
{
// Permanent account errors — disable SD for this server lifetime.
- _logger.LogError("Schedules Direct account error (code {SdCode}). Disabling SD until server restart", sdCode);
+ _logger.LogError("Schedules Direct account error (code {SdCode}). Disabling SD until server restart.", sdCode);
_tokens.Clear();
_accountError = true;
}
- else if (sdCode is SdErrorCode.MaxLoginAttempts or SdErrorCode.TemporaryLockout)
+ else if (sdCode is SdErrorCode.ServiceOffline or SdErrorCode.ServiceBusy or SdErrorCode.AccountTempLock)
{
// Transient login errors — back off for 30 minutes, then allow retry.
+ _logger.LogError("Schedules Direct transient error (code {SdCode}). Backing off for 30 minutes.", sdCode);
_tokens.Clear();
Interlocked.Exchange(ref _lastErrorResponseTicks, DateTime.UtcNow.Ticks);
}
- else if (sdCode is SdErrorCode.MaxImageDownloads)
+ else if (sdCode is SdErrorCode.MaxLoginAttempts or SdErrorCode.MaxIPAttempts)
+ {
+ // 24 hour bans - stop image and metadata requests until SD reset at 00:00 UTC.
+ _logger.LogError("Schedules Direct service limit error (code {SdCode}). Disabling until SD reset.", sdCode);
+ SetImageLimitHit();
+ SetMetadataLimitHit();
+ }
+ else if (sdCode is SdErrorCode.MaxImageDownloads or SdErrorCode.MaxImageDownloadsTrial)
{
// Max image downloads — stop image requests until SD resets at 00:00 UTC.
+ _logger.LogError("Schedules Direct image download limit hit (code {SdCode}). Disabling image acquisition until SD reset.", sdCode);
SetImageLimitHit();
}
else if (sdCode is SdErrorCode.MaxScheduleRequests)
{
// Max schedule/metadata requests — stop metadata requests until SD resets at 00:00 UTC.
+ _logger.LogError("Schedules Direct metadata download limit hit (code {SdCode}). Disabling metadata acquisition until SD reset.", sdCode);
SetMetadataLimitHit();
}
else if (enableRetry
diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/SdErrorCode.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/SdErrorCode.cs
index ec6c6c475b..fffbfb9a58 100644
--- a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/SdErrorCode.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/SdErrorCode.cs
@@ -3,39 +3,59 @@
namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
/// <summary>
-/// Schedules Direct API error codes.
+/// Schedules Direct API error codes. See https://github.com/SchedulesDirect/JSON-Service/wiki/API-20141201#error-response for details.
/// </summary>
public enum SdErrorCode
{
/// <summary>
- /// Invalid user.
+ /// Schedules Direct unavailable/out of service.
/// </summary>
- InvalidUser = 4001,
+ ServiceOffline = 3000,
+
+ /// <summary>
+ /// Schedules Direct busy.
+ /// </summary>
+ ServiceBusy = 3001,
+
+ /// <summary>
+ /// Account expired.
+ /// </summary>
+ AccountExpired = 4001,
/// <summary>
/// Invalid password hash.
/// </summary>
- InvalidHash = 4003,
+ InvalidHash = 4002,
/// <summary>
- /// Account locked or disabled.
+ /// Invalid user or password.
/// </summary>
- AccountLocked = 4004,
+ InvalidUser = 4003,
/// <summary>
- /// Account expired.
+ /// Account temporarily locked due to login failures.
+ /// </summary>
+ AccountTempLock = 4004,
+
+ /// <summary>
+ /// Account permanently locked due to abuse.
/// </summary>
- AccountExpired = 4005,
+ AccountLocked = 4005,
/// <summary>
- /// Token has expired.
+ /// Token has expired. Request a new one.
/// </summary>
TokenExpired = 4006,
/// <summary>
- /// Password is required.
+ /// Application locked out.
/// </summary>
- PasswordRequired = 4008,
+ AppLocked = 4007,
+
+ /// <summary>
+ /// Account not active.
+ /// </summary>
+ AccountInactive = 4008,
/// <summary>
/// Maximum login attempts exceeded.
@@ -43,9 +63,19 @@ public enum SdErrorCode
MaxLoginAttempts = 4009,
/// <summary>
- /// Temporary lockout.
+ /// Maximum unique IP attempts reached.
+ /// </summary>
+ MaxIPAttempts = 4010,
+
+ /// <summary>
+ /// Lineup change maximum reached.
/// </summary>
- TemporaryLockout = 4010,
+ MaxScheduleRequests = 4100,
+
+ /// <summary>
+ /// Requested image not found.
+ /// </summary>
+ ImageNotFound = 5000,
/// <summary>
/// Maximum image downloads reached for the day.
@@ -53,7 +83,12 @@ public enum SdErrorCode
MaxImageDownloads = 5002,
/// <summary>
+ /// Trial specific maximum image downloads reached for the day.
+ /// </summary>
+ MaxImageDownloadsTrial = 5003,
+
+ /// <summary>
/// Maximum schedule/metadata requests reached for the day.
/// </summary>
- MaxScheduleRequests = 5003
+ MaxInvalidImages = 5004
}
diff --git a/src/Jellyfin.LiveTv/LiveTvManager.cs b/src/Jellyfin.LiveTv/LiveTvManager.cs
index 2abc8a8c09..173d3c3e8e 100644
--- a/src/Jellyfin.LiveTv/LiveTvManager.cs
+++ b/src/Jellyfin.LiveTv/LiveTvManager.cs
@@ -178,6 +178,11 @@ namespace Jellyfin.LiveTv
{
var program = _libraryManager.GetItemById(id);
+ if (program is null)
+ {
+ return null;
+ }
+
var dto = _dtoService.GetBaseItemDto(program, new DtoOptions(), user);
var list = new List<(BaseItemDto ItemDto, string ExternalId, string ExternalSeriesId)>
diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs
index 3a2c463695..7e68dbb547 100644
--- a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs
+++ b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs
@@ -288,7 +288,7 @@ public class RecordingsMetadataManager
null,
"dateadded",
null,
- DateTime.Now.ToString(DateAddedFormat, CultureInfo.InvariantCulture)).ConfigureAwait(false);
+ DateTime.UtcNow.ToString(DateAddedFormat, CultureInfo.InvariantCulture)).ConfigureAwait(false);
if (item.ProductionYear.HasValue)
{
diff --git a/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs b/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs
index 6a68b8c25c..74fa1415c6 100644
--- a/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs
+++ b/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs
@@ -116,7 +116,7 @@ namespace Jellyfin.LiveTv.Timers
throw new ArgumentException("item already exists", nameof(item));
}
- _items = [.._items, item];
+ _items = [.. _items, item];
SaveList();
}
@@ -131,7 +131,7 @@ namespace Jellyfin.LiveTv.Timers
int index = Array.FindIndex(_items, i => EqualityComparer(i, item));
if (index == -1)
{
- _items = [.._items, item];
+ _items = [.. _items, item];
}
else
{
diff --git a/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs b/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs
index cfd763b6fd..7c16d2b363 100644
--- a/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs
@@ -83,7 +83,7 @@ public class TunerHostManager : ITunerHostManager
if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
{
info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
- config.TunerHosts = [..list, info];
+ config.TunerHosts = [.. list, info];
}
else
{
diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs
index 6a8a91fa51..4559f68ce8 100644
--- a/src/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -316,7 +316,7 @@ public class NetworkManager : INetworkManager, IDisposable
var subnets = config.LocalNetworkSubnets;
// If no LAN addresses are specified, all private subnets and Loopback are deemed to be the LAN
- if (!NetworkUtils.TryParseToSubnets(subnets, out var lanSubnets, false) || lanSubnets.Count == 0)
+ if (!NetworkUtils.TryParseToSubnets(subnets, out var lanSubnets, false, _logger) || lanSubnets.Count == 0)
{
_logger.LogDebug("Using LAN interface addresses as user provided no LAN details.");
@@ -343,7 +343,7 @@ public class NetworkManager : INetworkManager, IDisposable
_lanSubnets = lanSubnets.Select(x => x.Subnet).ToArray();
}
- _excludedSubnets = NetworkUtils.TryParseToSubnets(subnets, out var excludedSubnets, true)
+ _excludedSubnets = NetworkUtils.TryParseToSubnets(subnets, out var excludedSubnets, true, _logger)
? excludedSubnets.Select(x => x.Subnet).ToArray()
: Array.Empty<IPNetwork>();
}
@@ -356,7 +356,7 @@ public class NetworkManager : INetworkManager, IDisposable
{
lock (_initLock)
{
- _interfaces = FilterBindSettings(config, _interfaces, IsIPv4Enabled, IsIPv6Enabled).ToList();
+ _interfaces = FilterBindSettings(config, _interfaces, IsIPv4Enabled, IsIPv6Enabled).ToList();
}
}
diff --git a/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperAudioBitStreamTests.cs b/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperAudioBitStreamTests.cs
new file mode 100644
index 0000000000..2dcb898051
--- /dev/null
+++ b/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperAudioBitStreamTests.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Globalization;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Configuration;
+using Moq;
+using Xunit;
+using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
+
+namespace Jellyfin.Controller.Tests.MediaEncoding
+{
+ public class EncodingHelperAudioBitStreamTests
+ {
+ private const string BothFilters = " -bsf:a noise=drop='lt(pts*tb\\,63.063)',aac_adtstoasc";
+ private const string NoiseOnly = " -bsf:a noise=drop='lt(pts*tb\\,63.063)'";
+ private const string AdtsOnly = " -bsf:a aac_adtstoasc";
+ private const long DefaultSeekTicks = 630_630_000L;
+ private const string DefaultFfmpegVersion = "5.0";
+
+ private static EncodingHelper CreateHelper(string ffmpegVersion)
+ {
+ var mediaEncoder = new Mock<IMediaEncoder>();
+ mediaEncoder
+ .Setup(e => e.GetTimeParameter(It.IsAny<long>()))
+ .Returns((long ticks) => TimeSpan.FromTicks(ticks).ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture));
+ mediaEncoder
+ .SetupGet(e => e.EncoderVersion)
+ .Returns(Version.Parse(ffmpegVersion));
+
+ return new EncodingHelper(
+ Mock.Of<IApplicationPaths>(),
+ mediaEncoder.Object,
+ Mock.Of<ISubtitleEncoder>(),
+ Mock.Of<IConfiguration>(),
+ Mock.Of<IConfigurationManager>(),
+ Mock.Of<IPathManager>());
+ }
+
+ private static EncodingJobInfo CreateState(
+ TranscodingJobType jobType,
+ string outputVideoCodec,
+ string outputAudioCodec,
+ string audioStreamCodec,
+ string inputContainer,
+ long startTimeTicks)
+ {
+ return new EncodingJobInfo(jobType)
+ {
+ IsVideoRequest = true,
+ OutputVideoCodec = outputVideoCodec,
+ OutputAudioCodec = outputAudioCodec,
+ InputContainer = inputContainer,
+ RunTimeTicks = TimeSpan.FromMinutes(10).Ticks,
+ AudioStream = new MediaStream
+ {
+ Type = MediaStreamType.Audio,
+ Codec = audioStreamCodec
+ },
+ BaseRequest = new BaseEncodingJobOptions
+ {
+ StartTimeTicks = startTimeTicks
+ }
+ };
+ }
+
+ [Theory]
+ [InlineData(TranscodingJobType.Hls, "libx264", "copy", "aac", "ts", DefaultSeekTicks, DefaultFfmpegVersion, "mp4", "ts", BothFilters)]
+ [InlineData(TranscodingJobType.Hls, "libx264", "copy", "aac", "ts", DefaultSeekTicks, DefaultFfmpegVersion, "mp4", "aac", BothFilters)]
+ [InlineData(TranscodingJobType.Hls, "libx264", "copy", "aac", "ts", DefaultSeekTicks, DefaultFfmpegVersion, "mp4", "hls", BothFilters)]
+ [InlineData(TranscodingJobType.Progressive, "libx264", "copy", "aac", "ts", DefaultSeekTicks, DefaultFfmpegVersion, "mp4", "ts", AdtsOnly)]
+ [InlineData(TranscodingJobType.Hls, "copy", "copy", "aac", "ts", DefaultSeekTicks, DefaultFfmpegVersion, "mp4", "ts", AdtsOnly)]
+ [InlineData(TranscodingJobType.Hls, "libx264", "aac", "aac", "ts", DefaultSeekTicks, DefaultFfmpegVersion, "mp4", "ts", AdtsOnly)]
+ [InlineData(TranscodingJobType.Hls, "libx264", "copy", "aac", "wtv", DefaultSeekTicks, DefaultFfmpegVersion, "mp4", "ts", AdtsOnly)]
+ [InlineData(TranscodingJobType.Hls, "libx264", "copy", "aac", "ts", 0L, DefaultFfmpegVersion, "mp4", "ts", AdtsOnly)]
+ [InlineData(TranscodingJobType.Hls, "libx264", "copy", "aac", "ts", DefaultSeekTicks, "4.4.6", "mp4", "ts", AdtsOnly)]
+ [InlineData(TranscodingJobType.Hls, "libx264", "copy", "aac", "ts", DefaultSeekTicks, DefaultFfmpegVersion, "ts", "ts", NoiseOnly)]
+ [InlineData(TranscodingJobType.Hls, "libx264", "copy", "aac", "ts", DefaultSeekTicks, DefaultFfmpegVersion, "mp4", "mkv", NoiseOnly)]
+ [InlineData(TranscodingJobType.Hls, "libx264", "copy", "ac3", "ts", DefaultSeekTicks, DefaultFfmpegVersion, "mp4", "ts", NoiseOnly)]
+ public void AudioBitStreamArguments_AppliesGates(
+ TranscodingJobType jobType,
+ string outputVideoCodec,
+ string outputAudioCodec,
+ string audioStreamCodec,
+ string inputContainer,
+ long startTicks,
+ string ffmpegVersion,
+ string segmentContainer,
+ string mediaSourceContainer,
+ string expected)
+ {
+ var state = CreateState(jobType, outputVideoCodec, outputAudioCodec, audioStreamCodec, inputContainer, startTicks);
+ var result = CreateHelper(ffmpegVersion).GetAudioBitStreamArguments(state, segmentContainer, mediaSourceContainer);
+ Assert.Equal(expected, result);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs b/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs
new file mode 100644
index 0000000000..d7ae6a8a18
--- /dev/null
+++ b/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs
@@ -0,0 +1,258 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Streaming;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using Moq;
+using Xunit;
+
+using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration;
+
+namespace Jellyfin.Controller.Tests.MediaEncoding;
+
+public class EncodingHelperTests
+{
+ [Fact]
+ public void GetMapArgs_NoSubtitle_ExcludesAllSubs()
+ {
+ var state = BuildState(subtitle: null, deliveryMethod: null);
+ var args = CreateHelper().GetMapArgs(state);
+
+ Assert.Contains("-map -0:s", args, StringComparison.Ordinal);
+ Assert.DoesNotContain("-map 1:", args, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void GetMapArgs_InternalSrt_MapsFromPrimaryInput()
+ {
+ var sub = new MediaStream { Index = 2, Type = MediaStreamType.Subtitle, Codec = "srt" };
+ var state = BuildState(sub, SubtitleDeliveryMethod.Embed);
+ var args = CreateHelper().GetMapArgs(state);
+
+ Assert.Contains("-map 0:2", args, StringComparison.Ordinal);
+ Assert.DoesNotContain("-map 1:", args, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void GetMapArgs_InternalSubAtHigherIndex_MapsCorrectIndex()
+ {
+ var sub0 = new MediaStream { Index = 2, Type = MediaStreamType.Subtitle, Codec = "srt" };
+ var sub1 = new MediaStream { Index = 3, Type = MediaStreamType.Subtitle, Codec = "ass" };
+ var state = BuildState(sub1, SubtitleDeliveryMethod.Embed, additionalStreams: [sub0, sub1]);
+ var args = CreateHelper().GetMapArgs(state);
+
+ Assert.Contains("-map 0:3", args, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void GetMapArgs_ExternalSrt_MapsFirstStreamFromInput1()
+ {
+ var sub = new MediaStream
+ {
+ Index = 2,
+ Type = MediaStreamType.Subtitle,
+ Codec = "srt",
+ IsExternal = true,
+ SupportsExternalStream = true,
+ Path = "/media/movie.en.srt"
+ };
+ var state = BuildState(sub, SubtitleDeliveryMethod.Embed);
+ var args = CreateHelper().GetMapArgs(state);
+
+ Assert.Contains("-map 1:0", args, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void GetMapArgs_SecondExternalSrt_StillMaps1Colon0()
+ {
+ // Two separate .srt files — selecting the second one still maps 1:0
+ // because Jellyfin feeds only the selected file as ffmpeg input 1.
+ var ext1 = new MediaStream
+ {
+ Index = 2,
+ Type = MediaStreamType.Subtitle,
+ Codec = "srt",
+ IsExternal = true,
+ SupportsExternalStream = true,
+ Path = "/media/movie.en.srt"
+ };
+ var ext2 = new MediaStream
+ {
+ Index = 3,
+ Type = MediaStreamType.Subtitle,
+ Codec = "srt",
+ IsExternal = true,
+ SupportsExternalStream = true,
+ Path = "/media/movie.fr.srt"
+ };
+ var state = BuildState(ext2, SubtitleDeliveryMethod.Embed, additionalStreams: [ext1, ext2]);
+ var args = CreateHelper().GetMapArgs(state);
+
+ Assert.Contains("-map 1:0", args, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void GetMapArgs_MksFirstTrack_MapsInFileIndex0()
+ {
+ var mks0 = new MediaStream
+ {
+ Index = 2,
+ Type = MediaStreamType.Subtitle,
+ Codec = "subrip",
+ IsExternal = true,
+ SupportsExternalStream = true,
+ Path = "/media/movie.mks"
+ };
+ var mks1 = new MediaStream
+ {
+ Index = 3,
+ Type = MediaStreamType.Subtitle,
+ Codec = "ass",
+ IsExternal = true,
+ SupportsExternalStream = true,
+ Path = "/media/movie.mks"
+ };
+ var state = BuildState(mks0, SubtitleDeliveryMethod.Embed, additionalStreams: [mks0, mks1]);
+ var args = CreateHelper().GetMapArgs(state);
+
+ Assert.Contains("-map 1:0", args, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void GetMapArgs_MksSecondTrack_MapsInFileIndex1()
+ {
+ var mks0 = new MediaStream
+ {
+ Index = 2,
+ Type = MediaStreamType.Subtitle,
+ Codec = "subrip",
+ IsExternal = true,
+ SupportsExternalStream = true,
+ Path = "/media/movie.mks"
+ };
+ var mks1 = new MediaStream
+ {
+ Index = 3,
+ Type = MediaStreamType.Subtitle,
+ Codec = "ass",
+ IsExternal = true,
+ SupportsExternalStream = true,
+ Path = "/media/movie.mks"
+ };
+ var mks2 = new MediaStream
+ {
+ Index = 4,
+ Type = MediaStreamType.Subtitle,
+ Codec = "subrip",
+ IsExternal = true,
+ SupportsExternalStream = true,
+ Path = "/media/movie.mks"
+ };
+ var state = BuildState(mks1, SubtitleDeliveryMethod.Embed, additionalStreams: [mks0, mks1, mks2]);
+ var args = CreateHelper().GetMapArgs(state);
+
+ Assert.Contains("-map 1:1", args, StringComparison.Ordinal);
+ }
+
+ [Theory]
+ [InlineData(SubtitleDeliveryMethod.Embed, true, "movie.idx")]
+ [InlineData(SubtitleDeliveryMethod.Encode, true, "movie.idx")]
+ [InlineData(SubtitleDeliveryMethod.Embed, false, "movie.sub")]
+ [InlineData(SubtitleDeliveryMethod.Encode, false, "movie.sub")]
+ public void GetInputArgument_VobSub_UsesCorrectPath(
+ SubtitleDeliveryMethod deliveryMethod,
+ bool createIdxFile,
+ string expectedFilename)
+ {
+ var tempDir = Directory.CreateTempSubdirectory("jellyfin-test-");
+ try
+ {
+ var subFile = Path.Combine(tempDir.FullName, "movie.sub");
+ File.WriteAllText(subFile, "dummy");
+
+ if (createIdxFile)
+ {
+ File.WriteAllText(Path.Combine(tempDir.FullName, "movie.idx"), "dummy");
+ }
+
+ var sub = new MediaStream
+ {
+ Index = 2,
+ Type = MediaStreamType.Subtitle,
+ Codec = "dvdsub",
+ IsExternal = true,
+ SupportsExternalStream = true,
+ Path = subFile
+ };
+ var state = BuildState(sub, deliveryMethod);
+ var inputArgs = CreateHelper().GetInputArgument(state, new EncodingOptions(), null);
+
+ Assert.Contains(expectedFilename, inputArgs, StringComparison.Ordinal);
+ }
+ finally
+ {
+ tempDir.Delete(true);
+ }
+ }
+
+ private static EncodingJobInfo BuildState(
+ MediaStream? subtitle,
+ SubtitleDeliveryMethod? deliveryMethod,
+ MediaStream[]? additionalStreams = null)
+ {
+ var video = new MediaStream { Index = 0, Type = MediaStreamType.Video, Codec = "h264" };
+ var audio = new MediaStream { Index = 1, Type = MediaStreamType.Audio, Codec = "aac" };
+ var streams = new List<MediaStream> { video, audio };
+
+ if (additionalStreams is not null)
+ {
+ streams.AddRange(additionalStreams);
+ }
+ else if (subtitle is not null)
+ {
+ streams.Add(subtitle);
+ }
+
+ return new EncodingJobInfo(TranscodingJobType.Progressive)
+ {
+ MediaSource = new MediaSourceInfo
+ {
+ Container = "mkv",
+ MediaStreams = streams,
+ },
+ VideoStream = video,
+ AudioStream = audio,
+ SubtitleStream = subtitle,
+ SubtitleDeliveryMethod = deliveryMethod ?? SubtitleDeliveryMethod.Drop,
+ BaseRequest = new VideoRequestDto(),
+ IsVideoRequest = true,
+ IsInputVideo = true,
+ };
+ }
+
+ private static EncodingHelper CreateHelper()
+ {
+ var appPaths = Mock.Of<IApplicationPaths>();
+ var mediaEncoder = new Mock<IMediaEncoder>();
+ var subtitleEncoder = new Mock<ISubtitleEncoder>();
+ var config = new Mock<IConfiguration>();
+ var configurationManager = new Mock<IConfigurationManager>();
+ var pathManager = new Mock<IPathManager>();
+
+ return new EncodingHelper(
+ appPaths,
+ mediaEncoder.Object,
+ subtitleEncoder.Object,
+ config.Object,
+ configurationManager.Object,
+ pathManager.Object);
+ }
+}
diff --git a/tests/Jellyfin.Extensions.Tests/StreamExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/StreamExtensionsTests.cs
new file mode 100644
index 0000000000..cdbf2f8b1d
--- /dev/null
+++ b/tests/Jellyfin.Extensions.Tests/StreamExtensionsTests.cs
@@ -0,0 +1,397 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Jellyfin.Extensions.Tests;
+
+public class StreamExtensionsTests
+{
+ [Fact]
+ public async Task IsStreamIdenticalAsync_SeekableDifferentLengths_ReturnsFalse()
+ {
+ var cancellationToken = TestContext.Current.CancellationToken;
+ await using var a = new MemoryStream(new byte[] { 1, 2, 3 });
+ await using var b = new MemoryStream(new byte[] { 1, 2, 3, 4 });
+
+ var result = await a.IsStreamIdenticalAsync(b, cancellationToken);
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public async Task IsStreamIdenticalAsync_NonSeekableIdenticalStreams_ReturnsTrue()
+ {
+ var cancellationToken = TestContext.Current.CancellationToken;
+ await using var a = new NonSeekableReadStream(new byte[] { 1, 2, 3, 4 });
+ await using var b = new NonSeekableReadStream(new byte[] { 1, 2, 3, 4 });
+
+ var result = await a.IsStreamIdenticalAsync(b, cancellationToken);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public async Task IsStreamIdenticalAsync_NonSeekableDifferentStreams_ReturnsFalse()
+ {
+ var cancellationToken = TestContext.Current.CancellationToken;
+ await using var a = new NonSeekableReadStream(new byte[] { 1, 2, 3, 4 });
+ await using var b = new NonSeekableReadStream(new byte[] { 1, 2, 9, 4 });
+
+ var result = await a.IsStreamIdenticalAsync(b, cancellationToken);
+
+ Assert.False(result);
+ }
+
+ [Fact]
+ public async Task IsFileIdenticalAsync_NonSeekableStream_ThrowsArgumentException()
+ {
+ var cancellationToken = TestContext.Current.CancellationToken;
+ var path = Path.Join(Path.GetTempPath(), Path.GetRandomFileName());
+ await File.WriteAllBytesAsync(path, new byte[] { 1, 2, 3, 4 }, cancellationToken);
+
+ try
+ {
+ await using var stream = new NonSeekableReadStream(new byte[] { 1, 2, 3, 4 });
+
+ await Assert.ThrowsAsync<ArgumentException>(async () =>
+ await stream.IsFileIdenticalAsync(path, cancellationToken));
+ }
+ finally
+ {
+ File.Delete(path);
+ }
+ }
+
+ // Both publiclyVisible values are exercised so the test runs once under the fast path
+ // (TryGetBuffer succeeds) and once under the slow path (TryGetBuffer returns false).
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task IsFileIdenticalAsync_UsesStartOfStreamAndRestoresPosition_OnMatch(bool publiclyVisible)
+ {
+ var cancellationToken = TestContext.Current.CancellationToken;
+ var path = Path.Join(Path.GetTempPath(), Path.GetRandomFileName());
+ var bytes = new byte[] { 10, 20, 30, 40, 50 };
+ await File.WriteAllBytesAsync(path, bytes, cancellationToken);
+
+ try
+ {
+ await using var stream = CreateMemoryStream(bytes, publiclyVisible);
+ stream.Position = 3;
+
+ var result = await stream.IsFileIdenticalAsync(path, cancellationToken);
+
+ Assert.True(result);
+ Assert.Equal(3, stream.Position);
+ }
+ finally
+ {
+ File.Delete(path);
+ }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task IsFileIdenticalAsync_RestoresPosition_OnMismatch(bool publiclyVisible)
+ {
+ var cancellationToken = TestContext.Current.CancellationToken;
+ var path = Path.Join(Path.GetTempPath(), Path.GetRandomFileName());
+ await File.WriteAllBytesAsync(path, new byte[] { 10, 20, 30, 40, 99 }, cancellationToken);
+
+ try
+ {
+ await using var stream = CreateMemoryStream(new byte[] { 10, 20, 30, 40, 50 }, publiclyVisible);
+ stream.Position = 2;
+
+ var result = await stream.IsFileIdenticalAsync(path, cancellationToken);
+
+ Assert.False(result);
+ Assert.Equal(2, stream.Position);
+ }
+ finally
+ {
+ File.Delete(path);
+ }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task IsStreamIdenticalAsync_BothMemoryStreams_NonZeroPositions_SeeksToStart(bool publiclyVisible)
+ {
+ var cancellationToken = TestContext.Current.CancellationToken;
+ await using var a = CreateMemoryStream(new byte[] { 1, 2, 3, 4, 5 }, publiclyVisible);
+ await using var b = CreateMemoryStream(new byte[] { 1, 2, 3, 4, 5 }, publiclyVisible);
+ a.Position = 3;
+ b.Position = 1;
+
+ var result = await a.IsStreamIdenticalAsync(b, cancellationToken);
+
+ Assert.True(result);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task IsStreamIdenticalAsync_MemoryStreamPairedWithSeekableNonMemoryStream_NonZeroPositions_SeeksToStart(bool publiclyVisible)
+ {
+ var cancellationToken = TestContext.Current.CancellationToken;
+ await using var a = CreateMemoryStream(new byte[] { 1, 2, 3, 4 }, publiclyVisible);
+ await using var b = new SeekableNonMemoryStream(new byte[] { 1, 2, 3, 4 });
+ a.Position = 2;
+ b.Position = 3;
+
+ var result = await a.IsStreamIdenticalAsync(b, cancellationToken);
+
+ Assert.True(result);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task IsStreamIdenticalAsync_NonMemoryStreamPairedWithMemoryStream_Swaps_ReturnsTrue(bool publiclyVisible)
+ {
+ var cancellationToken = TestContext.Current.CancellationToken;
+ await using var a = new SeekableNonMemoryStream(new byte[] { 1, 2, 3, 4 });
+ await using var b = CreateMemoryStream(new byte[] { 1, 2, 3, 4 }, publiclyVisible);
+
+ var result = await a.IsStreamIdenticalAsync(b, cancellationToken);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public async Task IsStreamIdenticalAsync_BothSeekableNonMemoryStreams_NonZeroPositions_SeeksToStart()
+ {
+ var cancellationToken = TestContext.Current.CancellationToken;
+ await using var a = new SeekableNonMemoryStream(new byte[] { 1, 2, 3, 4 });
+ await using var b = new SeekableNonMemoryStream(new byte[] { 1, 2, 3, 4 });
+ a.Position = 1;
+ b.Position = 2;
+
+ var result = await a.IsStreamIdenticalAsync(b, cancellationToken);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public async Task IsStreamIdenticalAsync_NonSeekableShortReads_Identical_ReturnsTrue()
+ {
+ var cancellationToken = TestContext.Current.CancellationToken;
+ var data = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
+ await using var a = new ShortReadingNonSeekableStream(data, maxReadSize: 3);
+ await using var b = new ShortReadingNonSeekableStream(data, maxReadSize: 5);
+
+ var result = await a.IsStreamIdenticalAsync(b, cancellationToken);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public async Task IsStreamIdenticalAsync_NonSeekableShortReads_DifferentLengths_ReturnsFalse()
+ {
+ var cancellationToken = TestContext.Current.CancellationToken;
+ await using var a = new ShortReadingNonSeekableStream(new byte[] { 1, 2, 3, 4 }, maxReadSize: 3);
+ await using var b = new ShortReadingNonSeekableStream(new byte[] { 1, 2, 3, 4, 5 }, maxReadSize: 5);
+
+ var result = await a.IsStreamIdenticalAsync(b, cancellationToken);
+
+ Assert.False(result);
+ }
+
+ private static MemoryStream CreateMemoryStream(byte[] data, bool publiclyVisible)
+ => publiclyVisible
+ ? new MemoryStream(data, 0, data.Length, writable: false, publiclyVisible: true)
+ : new MemoryStream(data);
+
+ private sealed class NonSeekableReadStream : Stream
+ {
+ private readonly Stream _inner;
+
+ public NonSeekableReadStream(byte[] data)
+ {
+ _inner = new MemoryStream(data, writable: false);
+ }
+
+ public override bool CanRead => true;
+
+ public override bool CanSeek => false;
+
+ public override bool CanWrite => false;
+
+ public override long Length => throw new NotSupportedException();
+
+ public override long Position
+ {
+ get => throw new NotSupportedException();
+ set => throw new NotSupportedException();
+ }
+
+ public override void Flush()
+ {
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ => _inner.Read(buffer, offset, count);
+
+ public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
+ => _inner.ReadAsync(buffer, cancellationToken);
+
+ public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ => _inner.ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
+
+ public override long Seek(long offset, SeekOrigin origin)
+ => throw new NotSupportedException();
+
+ public override void SetLength(long value)
+ => throw new NotSupportedException();
+
+ public override void Write(byte[] buffer, int offset, int count)
+ => throw new NotSupportedException();
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _inner.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ public override async ValueTask DisposeAsync()
+ {
+ await _inner.DisposeAsync();
+ await base.DisposeAsync();
+ }
+ }
+
+ private sealed class SeekableNonMemoryStream : Stream
+ {
+ private readonly MemoryStream _inner;
+
+ public SeekableNonMemoryStream(byte[] data)
+ {
+ _inner = new MemoryStream(data, writable: false);
+ }
+
+ public override bool CanRead => true;
+
+ public override bool CanSeek => true;
+
+ public override bool CanWrite => false;
+
+ public override long Length => _inner.Length;
+
+ public override long Position
+ {
+ get => _inner.Position;
+ set => _inner.Position = value;
+ }
+
+ public override void Flush()
+ {
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ => _inner.Read(buffer, offset, count);
+
+ public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
+ => _inner.ReadAsync(buffer, cancellationToken);
+
+ public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ => _inner.ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
+
+ public override long Seek(long offset, SeekOrigin origin)
+ => _inner.Seek(offset, origin);
+
+ public override void SetLength(long value)
+ => throw new NotSupportedException();
+
+ public override void Write(byte[] buffer, int offset, int count)
+ => throw new NotSupportedException();
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _inner.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ public override async ValueTask DisposeAsync()
+ {
+ await _inner.DisposeAsync();
+ await base.DisposeAsync();
+ }
+ }
+
+ private sealed class ShortReadingNonSeekableStream : Stream
+ {
+ private readonly Stream _inner;
+ private readonly int _maxReadSize;
+
+ public ShortReadingNonSeekableStream(byte[] data, int maxReadSize)
+ {
+ _inner = new MemoryStream(data, writable: false);
+ _maxReadSize = maxReadSize;
+ }
+
+ public override bool CanRead => true;
+
+ public override bool CanSeek => false;
+
+ public override bool CanWrite => false;
+
+ public override long Length => throw new NotSupportedException();
+
+ public override long Position
+ {
+ get => throw new NotSupportedException();
+ set => throw new NotSupportedException();
+ }
+
+ public override void Flush()
+ {
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ => _inner.Read(buffer, offset, Math.Min(count, _maxReadSize));
+
+ public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
+ => _inner.ReadAsync(buffer[..Math.Min(buffer.Length, _maxReadSize)], cancellationToken);
+
+ public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ => _inner.ReadAsync(buffer.AsMemory(offset, Math.Min(count, _maxReadSize)), cancellationToken).AsTask();
+
+ public override long Seek(long offset, SeekOrigin origin)
+ => throw new NotSupportedException();
+
+ public override void SetLength(long value)
+ => throw new NotSupportedException();
+
+ public override void Write(byte[] buffer, int offset, int count)
+ => throw new NotSupportedException();
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _inner.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ public override async ValueTask DisposeAsync()
+ {
+ await _inner.DisposeAsync();
+ await base.DisposeAsync();
+ }
+ }
+}
diff --git a/tests/Jellyfin.LiveTv.Tests/Recordings/RecordingsMetadataManagerTests.cs b/tests/Jellyfin.LiveTv.Tests/Recordings/RecordingsMetadataManagerTests.cs
new file mode 100644
index 0000000000..14ce470fb4
--- /dev/null
+++ b/tests/Jellyfin.LiveTv.Tests/Recordings/RecordingsMetadataManagerTests.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Threading.Tasks;
+using System.Xml;
+using Jellyfin.Extensions;
+using Jellyfin.LiveTv.Recordings;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.LiveTv;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.LiveTv.Tests.Recordings;
+
+public sealed class RecordingsMetadataManagerTests
+{
+ private readonly string _tempDir =
+ Path.Combine(Path.GetTempPath(), "jellyfin-test-" + Guid.NewGuid());
+
+ [Fact]
+ public async Task SaveRecordingMetadata_DateAddedIsUtc()
+ {
+ Directory.CreateDirectory(_tempDir);
+ var recordingPath = Path.Combine(_tempDir, "test-recording.ts");
+ FileHelper.CreateEmpty(recordingPath);
+
+ var config = new Mock<IConfigurationManager>();
+ config.Setup(c => c.GetConfiguration("livetv"))
+ .Returns(new LiveTvOptions { SaveRecordingNFO = true, SaveRecordingImages = false });
+ config.Setup(c => c.GetConfiguration("xbmcmetadata"))
+ .Returns(new XbmcMetadataOptions());
+
+ var libraryManager = new Mock<ILibraryManager>();
+ libraryManager
+ .Setup(l => l.GetItemList(It.IsAny<InternalItemsQuery>()))
+ .Returns(Array.Empty<BaseItem>());
+
+ var manager = new RecordingsMetadataManager(
+ NullLogger<RecordingsMetadataManager>.Instance,
+ config.Object,
+ libraryManager.Object);
+
+ var timer = new TimerInfo { Name = "Test Recording", ProgramId = null };
+
+ var beforeUtc = DateTime.UtcNow.AddSeconds(-2);
+ await manager.SaveRecordingMetadata(timer, recordingPath, null);
+ var afterUtc = DateTime.UtcNow.AddSeconds(2);
+
+ var doc = new XmlDocument();
+ doc.Load(Path.ChangeExtension(recordingPath, ".nfo"));
+ var dateAddedText = doc.SelectSingleNode("//dateadded")?.InnerText ?? string.Empty;
+ var parsed = DateTime.ParseExact(
+ dateAddedText,
+ "yyyy-MM-dd HH:mm:ss",
+ CultureInfo.InvariantCulture);
+
+ Assert.InRange(parsed, beforeUtc, afterUtc);
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs
deleted file mode 100644
index 5f84e85592..0000000000
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs
+++ /dev/null
@@ -1,282 +0,0 @@
-using System;
-using AutoFixture;
-using AutoFixture.AutoMoq;
-using MediaBrowser.MediaEncoding.Subtitles;
-using MediaBrowser.Model.MediaInfo;
-using Xunit;
-
-namespace Jellyfin.MediaEncoding.Subtitles.Tests
-{
- public class FilterEventsTests
- {
- private readonly SubtitleEncoder _encoder;
-
- public FilterEventsTests()
- {
- var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
- _encoder = fixture.Create<SubtitleEncoder>();
- }
-
- [Fact]
- public void FilterEvents_SubtitleSpanningSegmentBoundary_IsRetained()
- {
- // Subtitle starts at 5s, ends at 15s.
- // Segment requested from 10s to 20s.
- // The subtitle is still on screen at 10s and should NOT be dropped.
- var track = new SubtitleTrackInfo
- {
- TrackEvents = new[]
- {
- new SubtitleTrackEvent("1", "Still on screen")
- {
- StartPositionTicks = TimeSpan.FromSeconds(5).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
- },
- new SubtitleTrackEvent("2", "Next subtitle")
- {
- StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(17).Ticks
- }
- }
- };
-
- _encoder.FilterEvents(
- track,
- startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
- endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
- preserveTimestamps: true);
-
- Assert.Equal(2, track.TrackEvents.Count);
- Assert.Equal("1", track.TrackEvents[0].Id);
- Assert.Equal("2", track.TrackEvents[1].Id);
- }
-
- [Fact]
- public void FilterEvents_SubtitleFullyBeforeSegment_IsDropped()
- {
- // Subtitle starts at 2s, ends at 5s.
- // Segment requested from 10s.
- // The subtitle ended before the segment — should be dropped.
- var track = new SubtitleTrackInfo
- {
- TrackEvents = new[]
- {
- new SubtitleTrackEvent("1", "Already gone")
- {
- StartPositionTicks = TimeSpan.FromSeconds(2).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(5).Ticks
- },
- new SubtitleTrackEvent("2", "Visible")
- {
- StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(17).Ticks
- }
- }
- };
-
- _encoder.FilterEvents(
- track,
- startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
- endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
- preserveTimestamps: true);
-
- Assert.Single(track.TrackEvents);
- Assert.Equal("2", track.TrackEvents[0].Id);
- }
-
- [Fact]
- public void FilterEvents_SubtitleAfterSegment_IsDropped()
- {
- // Segment is 10s-20s, subtitle starts at 25s.
- var track = new SubtitleTrackInfo
- {
- TrackEvents = new[]
- {
- new SubtitleTrackEvent("1", "In range")
- {
- StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
- },
- new SubtitleTrackEvent("2", "After segment")
- {
- StartPositionTicks = TimeSpan.FromSeconds(25).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(30).Ticks
- }
- }
- };
-
- _encoder.FilterEvents(
- track,
- startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
- endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
- preserveTimestamps: true);
-
- Assert.Single(track.TrackEvents);
- Assert.Equal("1", track.TrackEvents[0].Id);
- }
-
- [Fact]
- public void FilterEvents_PreserveTimestampsFalse_AdjustsTimestamps()
- {
- var track = new SubtitleTrackInfo
- {
- TrackEvents = new[]
- {
- new SubtitleTrackEvent("1", "Subtitle")
- {
- StartPositionTicks = TimeSpan.FromSeconds(15).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(20).Ticks
- }
- }
- };
-
- _encoder.FilterEvents(
- track,
- startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
- endTimeTicks: TimeSpan.FromSeconds(30).Ticks,
- preserveTimestamps: false);
-
- Assert.Single(track.TrackEvents);
- // Timestamps should be shifted back by 10s
- Assert.Equal(TimeSpan.FromSeconds(5).Ticks, track.TrackEvents[0].StartPositionTicks);
- Assert.Equal(TimeSpan.FromSeconds(10).Ticks, track.TrackEvents[0].EndPositionTicks);
- }
-
- [Fact]
- public void FilterEvents_PreserveTimestampsTrue_KeepsOriginalTimestamps()
- {
- var startTicks = TimeSpan.FromSeconds(15).Ticks;
- var endTicks = TimeSpan.FromSeconds(20).Ticks;
-
- var track = new SubtitleTrackInfo
- {
- TrackEvents = new[]
- {
- new SubtitleTrackEvent("1", "Subtitle")
- {
- StartPositionTicks = startTicks,
- EndPositionTicks = endTicks
- }
- }
- };
-
- _encoder.FilterEvents(
- track,
- startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
- endTimeTicks: TimeSpan.FromSeconds(30).Ticks,
- preserveTimestamps: true);
-
- Assert.Single(track.TrackEvents);
- Assert.Equal(startTicks, track.TrackEvents[0].StartPositionTicks);
- Assert.Equal(endTicks, track.TrackEvents[0].EndPositionTicks);
- }
-
- [Fact]
- public void FilterEvents_SubtitleEndingExactlyAtSegmentStart_IsRetained()
- {
- // Subtitle ends exactly when the segment begins.
- // EndPositionTicks == startPositionTicks means (end - start) == 0, not < 0,
- // so SkipWhile stops and the subtitle is retained.
- var track = new SubtitleTrackInfo
- {
- TrackEvents = new[]
- {
- new SubtitleTrackEvent("1", "Boundary subtitle")
- {
- StartPositionTicks = TimeSpan.FromSeconds(5).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(10).Ticks
- },
- new SubtitleTrackEvent("2", "In range")
- {
- StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
- }
- }
- };
-
- _encoder.FilterEvents(
- track,
- startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
- endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
- preserveTimestamps: true);
-
- Assert.Equal(2, track.TrackEvents.Count);
- Assert.Equal("1", track.TrackEvents[0].Id);
- }
-
- [Fact]
- public void FilterEvents_SpanningBoundaryWithTimestampAdjustment_DoesNotProduceNegativeTimestamps()
- {
- // Subtitle starts at 5s, ends at 15s.
- // Segment requested from 10s to 20s, preserveTimestamps = false.
- // The subtitle spans the boundary and is retained, but shifting
- // StartPositionTicks by -10s would produce -5s (negative).
- var track = new SubtitleTrackInfo
- {
- TrackEvents = new[]
- {
- new SubtitleTrackEvent("1", "Spans boundary")
- {
- StartPositionTicks = TimeSpan.FromSeconds(5).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
- },
- new SubtitleTrackEvent("2", "Fully in range")
- {
- StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(17).Ticks
- }
- }
- };
-
- _encoder.FilterEvents(
- track,
- startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
- endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
- preserveTimestamps: false);
-
- Assert.Equal(2, track.TrackEvents.Count);
- // Subtitle 1: start should be clamped to 0, not -5s
- Assert.True(track.TrackEvents[0].StartPositionTicks >= 0, "StartPositionTicks must not be negative");
- Assert.Equal(TimeSpan.FromSeconds(5).Ticks, track.TrackEvents[0].EndPositionTicks);
- // Subtitle 2: normal shift (12s - 10s = 2s, 17s - 10s = 7s)
- Assert.Equal(TimeSpan.FromSeconds(2).Ticks, track.TrackEvents[1].StartPositionTicks);
- Assert.Equal(TimeSpan.FromSeconds(7).Ticks, track.TrackEvents[1].EndPositionTicks);
- }
-
- [Fact]
- public void FilterEvents_NoEndTimeTicks_ReturnsAllFromStartPosition()
- {
- var track = new SubtitleTrackInfo
- {
- TrackEvents = new[]
- {
- new SubtitleTrackEvent("1", "Before")
- {
- StartPositionTicks = TimeSpan.FromSeconds(2).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(4).Ticks
- },
- new SubtitleTrackEvent("2", "After")
- {
- StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
- },
- new SubtitleTrackEvent("3", "Much later")
- {
- StartPositionTicks = TimeSpan.FromSeconds(500).Ticks,
- EndPositionTicks = TimeSpan.FromSeconds(505).Ticks
- }
- }
- };
-
- _encoder.FilterEvents(
- track,
- startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
- endTimeTicks: 0,
- preserveTimestamps: true);
-
- Assert.Equal(2, track.TrackEvents.Count);
- Assert.Equal("2", track.TrackEvents[0].Id);
- Assert.Equal("3", track.TrackEvents[1].Id);
- }
- }
-}
diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
index 8269ae58cd..16c586bcda 100644
--- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
+++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
@@ -171,6 +171,9 @@ namespace Jellyfin.Model.Tests
[InlineData("AndroidTVExoPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("AndroidTVExoPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
[InlineData("AndroidTVExoPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow vp9
+ [InlineData("AndroidTVExoPlayer", "mp4-hevc-aac-4000k-r180", PlayMethod.DirectPlay)] // #13712
+ // AndroidTV NoHevcRotation
+ [InlineData("AndroidTVExoPlayer-NoHevcRotation", "mp4-hevc-aac-4000k-r180", PlayMethod.Transcode, TranscodeReason.VideoRotationNotSupported, "Transcode")] // #13712
// Tizen 3 Stereo
[InlineData("Tizen3-stereo", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)]
[InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)]
@@ -672,5 +675,59 @@ namespace Jellyfin.Model.Tests
Assert.Equal(expectedMethod, result.Method);
}
+
+ [Theory]
+ // External text subs embedded into MKV when transcoding (#16403)
+ [InlineData("srt", true, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)]
+ [InlineData("ass", true, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)]
+ // External graphical subs embedded into MKV when transcoding
+ [InlineData("pgssub", true, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)]
+ [InlineData("dvdsub", true, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)]
+ // External subs remain external when transcoding to non-MKV containers
+ [InlineData("srt", true, PlayMethod.Transcode, "mp4", MediaStreamProtocol.hls, SubtitleDeliveryMethod.External)]
+ [InlineData("srt", true, PlayMethod.Transcode, "ts", MediaStreamProtocol.hls, SubtitleDeliveryMethod.External)]
+ // External subs remain external during DirectPlay even with MKV
+ [InlineData("srt", true, PlayMethod.DirectPlay, "mkv", null, SubtitleDeliveryMethod.External)]
+ // Internal subs still embedded into MKV when transcoding (existing behavior)
+ [InlineData("srt", false, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)]
+ [InlineData("pgssub", false, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)]
+ public void GetSubtitleProfile_ReturnsExpectedDeliveryMethod(
+ string codec,
+ bool isExternal,
+ PlayMethod playMethod,
+ string outputContainer,
+ MediaStreamProtocol? transcodingSubProtocol,
+ SubtitleDeliveryMethod expectedMethod)
+ {
+ var mediaSource = new MediaSourceInfo();
+ var subtitleStream = new MediaStream
+ {
+ Codec = codec,
+ Language = "eng",
+ IsExternal = isExternal,
+ Type = MediaStreamType.Subtitle,
+ SupportsExternalStream = true
+ };
+
+ var subtitleProfiles = new[]
+ {
+ new SubtitleProfile { Format = codec, Method = SubtitleDeliveryMethod.Embed },
+ new SubtitleProfile { Format = codec, Method = SubtitleDeliveryMethod.External }
+ };
+
+ var transcoderSupport = new Mock<ITranscoderSupport>();
+ transcoderSupport.Setup(x => x.CanExtractSubtitles(It.IsAny<string>())).Returns(true);
+
+ var result = StreamBuilder.GetSubtitleProfile(
+ mediaSource,
+ subtitleStream,
+ subtitleProfiles,
+ playMethod,
+ transcoderSupport.Object,
+ outputContainer,
+ transcodingSubProtocol);
+
+ Assert.Equal(expectedMethod, result.Method);
+ }
}
}
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer-NoHevcRotation.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer-NoHevcRotation.json
new file mode 100644
index 0000000000..341638bc52
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer-NoHevcRotation.json
@@ -0,0 +1,162 @@
+{
+ "Name": "Jellyfin AndroidTV-ExoPlayer",
+ "EnableAlbumArtInDidl": false,
+ "EnableSingleAlbumArtLimit": false,
+ "EnableSingleSubtitleLimit": false,
+ "SupportedMediaTypes": "Audio,Photo,Video",
+ "MaxAlbumArtWidth": 0,
+ "MaxAlbumArtHeight": 0,
+ "MaxStreamingBitrate": 120000000,
+ "MaxStaticBitrate": 100000000,
+ "MusicStreamingTranscodingBitrate": 192000,
+ "TimelineOffsetSeconds": 0,
+ "RequiresPlainVideoItems": false,
+ "RequiresPlainFolders": false,
+ "EnableMSMediaReceiverRegistrar": false,
+ "IgnoreTranscodeByteRangeRequests": false,
+ "DirectPlayProfiles": [
+ {
+ "Container": "m4v,mov,xvid,vob,mkv,wmv,asf,ogm,ogv,mp4,webm",
+ "AudioCodec": "aac,mp3,mp2,aac_latm,alac,ac3,eac3,dca,dts,mlp,truehd,pcm_alaw,pcm_mulaw",
+ "VideoCodec": "h264,hevc,vp8,vp9,mpeg,mpeg2video",
+ "Type": "Video",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "aac,mp3,mp2,aac_latm,alac,ac3,eac3,dca,dts,mlp,truehd,pcm_alaw,pcm_mulaw,,pa,flac,wav,wma,ogg,oga,webma,ape,opus",
+ "Type": "Audio",
+ "$type": "DirectPlayProfile"
+ },
+ {
+ "Container": "jpg,jpeg,png,gif,web",
+ "Type": "Photo",
+ "$type": "DirectPlayProfile"
+ }
+ ],
+ "CodecProfiles": [
+ {
+ "Type": "Video",
+ "Conditions": [
+ {
+ "Condition": "EqualsAny",
+ "Property": "VideoProfile",
+ "Value": "main|main 10",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "LessThanEqual",
+ "Property": "VideoLevel",
+ "Value": "51",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ },
+ {
+ "Condition": "Equals",
+ "Property": "VideoRotation",
+ "Value": "0",
+ "IsRequired": false,
+ "$type": "ProfileCondition"
+ }
+ ],
+ "Codec": "hevc",
+ "$type": "CodecProfile"
+ }
+ ],
+ "TranscodingProfiles": [
+ {
+ "Container": "ts",
+ "Type": "Video",
+ "VideoCodec": "h264",
+ "AudioCodec": "aac,mp3",
+ "Protocol": "hls",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "$type": "TranscodingProfile"
+ },
+ {
+ "Container": "mp3",
+ "Type": "Audio",
+ "AudioCodec": "mp3",
+ "Protocol": "http",
+ "EstimateContentLength": false,
+ "EnableMpegtsM2TsMode": false,
+ "TranscodeSeekInfo": "Auto",
+ "CopyTimestamps": false,
+ "Context": "Streaming",
+ "EnableSubtitlesInManifest": false,
+ "MinSegments": 0,
+ "SegmentLength": 0,
+ "$type": "TranscodingProfile"
+ }
+ ],
+ "SubtitleProfiles": [
+ {
+ "Format": "srt",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "srt",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "subrip",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "subrip",
+ "Method": "External",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ass",
+ "Method": "Encode",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "ssa",
+ "Method": "Encode",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "pgs",
+ "Method": "Encode",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "pgssub",
+ "Method": "Encode",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "dvdsub",
+ "Method": "Encode",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "vtt",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "sub",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ },
+ {
+ "Format": "idx",
+ "Method": "Embed",
+ "$type": "SubtitleProfile"
+ }
+ ],
+ "$type": "DeviceProfile"
+}
diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-4000k-r180.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-4000k-r180.json
new file mode 100644
index 0000000000..393b10171d
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-4000k-r180.json
@@ -0,0 +1,56 @@
+{
+ "Id": "b7a9e2d4c815f36b0d9241a7e58c3f42",
+ "Path": "/Media/MyVideo-1080p.mp4",
+ "Container": "mov,mp4,m4a,3gp,3g2,mj2",
+ "Size": 1421636271,
+ "Name": "MyVideo-1080p",
+ "ETag": "d8e2a1b5c4f907e8a1d2b3c4e5f6a7b8",
+ "RunTimeTicks": 25801230336,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "hevc",
+ "CodecTag": "hvc1",
+ "Language": "eng",
+ "TimeBase": "1/11988",
+ "VideoRange": "SDR",
+ "DisplayTitle": "1080p HEVC SDR",
+ "NalLengthSize": "0",
+ "BitRate": 4014613,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": true,
+ "Height": 1080,
+ "Width": 1920,
+ "AverageFrameRate": 23.976,
+ "RealFrameRate": 23.976,
+ "Profile": "Main",
+ "Type": 1,
+ "AspectRatio": "16:9",
+ "PixelFormat": "yuv420p",
+ "Level": 50,
+ "Rotation": 180
+ },
+ {
+ "Codec": "aac",
+ "CodecTag": "mp4a",
+ "Language": "eng",
+ "TimeBase": "1/48000",
+ "DisplayTitle": "En - AAC - Stereo - Default",
+ "ChannelLayout": "stereo",
+ "BitRate": 125427,
+ "Channels": 2,
+ "SampleRate": 48000,
+ "IsDefault": true,
+ "Profile": "LC",
+ "Index": 1,
+ "Score": 203
+ }
+ ],
+ "Bitrate": 4331578,
+ "DefaultAudioStreamIndex": 1,
+ "DefaultSubtitleStreamIndex": 2
+}
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs
index 4dbe769bf4..2035140f00 100644
--- a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs
@@ -83,4 +83,26 @@ public class SeasonPathParserTests
Assert.Equal(seasonNumber, result.SeasonNumber);
Assert.Equal(isSeasonDirectory, result.IsSeasonFolder);
}
+
+ [Theory]
+ [InlineData("/Drive/300 Collection/300 (2006)", "/Drive/300 Collection", null, false)]
+ [InlineData("/Drive/300 Collection/300 Rise of an Empire", "/Drive/300 Collection", null, false)]
+ [InlineData("/Drive/300 Collection/1", "/Drive/300 Collection", null, false)]
+ [InlineData("/Drive/300 Collection/300 Disc 1", "/Drive/300 Collection", null, false)]
+ [InlineData("/Drive/28 Years Later Collection/28 Days Later", "/Drive/28 Years Later Collection", null, false)]
+ [InlineData("/Drive/28 Years Later Collection/28 Weeks Later (2007)", "/Drive/28 Years Later Collection", null, false)]
+ [InlineData("/Drive/28 Years Later Collection/28 Years Later 2025", "/Drive/28 Years Later Collection", null, false)]
+ [InlineData("/Drive/300 Collection/Season 1", "/Drive/300 Collection", 1, true)]
+ [InlineData("/Drive/28 Years Later Collection/Season 01", "/Drive/28 Years Later Collection", 1, true)]
+ [InlineData("/Drive/300 Collection/S01", "/Drive/300 Collection", 1, true)]
+ [InlineData("/Drive/300 Collection/S1", "/Drive/300 Collection", 1, true)]
+
+ public void GetSeasonNumberFromPathMixedLibraryTest(string path, string? parentPath, int? seasonNumber, bool isSeasonDirectory)
+ {
+ var result = SeasonPathParser.Parse(path, parentPath, false, false);
+
+ Assert.Equal(result.SeasonNumber is not null, result.Success);
+ Assert.Equal(seasonNumber, result.SeasonNumber);
+ Assert.Equal(isSeasonDirectory, result.IsSeasonFolder);
+ }
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
index 2fb45600b1..b29c64f50d 100644
--- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Model.Entities;
using Xunit;
namespace Jellyfin.Naming.Tests.Video
@@ -10,6 +12,12 @@ namespace Jellyfin.Naming.Tests.Video
public class MultiVersionTests
{
private readonly NamingOptions _namingOptions = new NamingOptions();
+ private readonly VideoListResolver _videoListResolver;
+
+ public MultiVersionTests()
+ {
+ _videoListResolver = new VideoListResolver(_namingOptions);
+ }
[Fact]
public void TestMultiEdition1()
@@ -22,9 +30,8 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Single(result, v => v.ExtraType is null);
Assert.Single(result, v => v.ExtraType is not null);
@@ -41,9 +48,8 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Single(result, v => v.ExtraType is null);
Assert.Single(result, v => v.ExtraType is not null);
@@ -59,9 +65,8 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Single(result);
Assert.Single(result[0].AlternateVersions);
@@ -81,9 +86,8 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/M/Movie 7.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(7, result.Count);
Assert.Empty(result[0].AlternateVersions);
@@ -104,9 +108,8 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Movie/Movie-8.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Single(result);
Assert.Equal(7, result[0].AlternateVersions.Count);
@@ -128,9 +131,8 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Mo/Movie 9.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(9, result.Count);
Assert.Empty(result[0].AlternateVersions);
@@ -148,9 +150,8 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Movie/Movie 5.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].AlternateVersions);
@@ -170,9 +171,8 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Iron Man/Iron Man (2011).mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].AlternateVersions);
@@ -192,19 +192,18 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Iron Man/Iron Man[test].mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Single(result);
Assert.Equal("/movies/Iron Man/Iron Man.mkv", result[0].Files[0].Path);
Assert.Equal(6, result[0].AlternateVersions.Count);
- Assert.Equal("/movies/Iron Man/Iron Man-720p.mkv", result[0].AlternateVersions[0].Path);
- Assert.Equal("/movies/Iron Man/Iron Man-3d.mkv", result[0].AlternateVersions[1].Path);
- Assert.Equal("/movies/Iron Man/Iron Man-3d-hsbs.mkv", result[0].AlternateVersions[2].Path);
- Assert.Equal("/movies/Iron Man/Iron Man-bluray.mkv", result[0].AlternateVersions[3].Path);
- Assert.Equal("/movies/Iron Man/Iron Man-test.mkv", result[0].AlternateVersions[4].Path);
- Assert.Equal("/movies/Iron Man/Iron Man[test].mkv", result[0].AlternateVersions[5].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man-720p.mkv", result[0].AlternateVersions[0].Files[0].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man-3d.mkv", result[0].AlternateVersions[1].Files[0].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man-3d-hsbs.mkv", result[0].AlternateVersions[2].Files[0].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man-bluray.mkv", result[0].AlternateVersions[3].Files[0].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man-test.mkv", result[0].AlternateVersions[4].Files[0].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man[test].mkv", result[0].AlternateVersions[5].Files[0].Path);
}
[Fact]
@@ -221,19 +220,18 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Iron Man/Iron Man [test].mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Single(result);
Assert.Equal("/movies/Iron Man/Iron Man.mkv", result[0].Files[0].Path);
Assert.Equal(6, result[0].AlternateVersions.Count);
- Assert.Equal("/movies/Iron Man/Iron Man - 720p.mkv", result[0].AlternateVersions[0].Path);
- Assert.Equal("/movies/Iron Man/Iron Man - 3d.mkv", result[0].AlternateVersions[1].Path);
- Assert.Equal("/movies/Iron Man/Iron Man - 3d-hsbs.mkv", result[0].AlternateVersions[2].Path);
- Assert.Equal("/movies/Iron Man/Iron Man - bluray.mkv", result[0].AlternateVersions[3].Path);
- Assert.Equal("/movies/Iron Man/Iron Man - test.mkv", result[0].AlternateVersions[4].Path);
- Assert.Equal("/movies/Iron Man/Iron Man [test].mkv", result[0].AlternateVersions[5].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man - 720p.mkv", result[0].AlternateVersions[0].Files[0].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man - 3d.mkv", result[0].AlternateVersions[1].Files[0].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man - 3d-hsbs.mkv", result[0].AlternateVersions[2].Files[0].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man - bluray.mkv", result[0].AlternateVersions[3].Files[0].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man - test.mkv", result[0].AlternateVersions[4].Files[0].Path);
+ Assert.Equal("/movies/Iron Man/Iron Man [test].mkv", result[0].AlternateVersions[5].Files[0].Path);
}
[Fact]
@@ -245,9 +243,8 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Iron Man/Iron Man - C (2007).mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(2, result.Count);
}
@@ -266,17 +263,16 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Iron Man/Iron Man_3d.hsbs.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Single(result);
Assert.Equal(6, result[0].AlternateVersions.Count);
// Verify 3D recognition is preserved on alternate versions
- var hsbs = result[0].AlternateVersions.First(v => v.Path.Contains("3d-hsbs", StringComparison.Ordinal));
- Assert.True(hsbs.Is3D);
- Assert.Equal("hsbs", hsbs.Format3D);
+ var hsbs = result[0].AlternateVersions.First(v => v.Files[0].Path.Contains("3d-hsbs", StringComparison.Ordinal));
+ Assert.True(hsbs.Files[0].Is3D);
+ Assert.Equal("hsbs", hsbs.Files[0].Format3D);
}
[Fact]
@@ -293,9 +289,8 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Iron Man/Iron Man (2011).mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(5, result.Count);
Assert.Empty(result[0].AlternateVersions);
@@ -310,9 +305,8 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Single(result);
Assert.Single(result[0].AlternateVersions);
@@ -327,9 +321,8 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Single(result);
Assert.Single(result[0].AlternateVersions);
@@ -348,18 +341,17 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv",
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Single(result);
Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", result[0].Files[0].Path);
Assert.Equal(5, result[0].AlternateVersions.Count);
- Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv", result[0].AlternateVersions[0].Path);
- Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", result[0].AlternateVersions[1].Path);
- Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv", result[0].AlternateVersions[2].Path);
- Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv", result[0].AlternateVersions[3].Path);
- Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv", result[0].AlternateVersions[4].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv", result[0].AlternateVersions[0].Files[0].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", result[0].AlternateVersions[1].Files[0].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv", result[0].AlternateVersions[2].Files[0].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv", result[0].AlternateVersions[3].Files[0].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv", result[0].AlternateVersions[4].Files[0].Path);
}
[Fact]
@@ -381,24 +373,23 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv",
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Single(result);
Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", result[0].Files[0].Path);
Assert.Equal(11, result[0].AlternateVersions.Count);
- Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv", result[0].AlternateVersions[0].Path);
- Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p Remux.mkv", result[0].AlternateVersions[1].Path);
- Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", result[0].AlternateVersions[2].Path);
- Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Directors Cut.mkv", result[0].AlternateVersions[3].Path);
- Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p High Bitrate.mkv", result[0].AlternateVersions[4].Path);
- Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Remux.mkv", result[0].AlternateVersions[5].Path);
- Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Theatrical Release.mkv", result[0].AlternateVersions[6].Path);
- Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv", result[0].AlternateVersions[7].Path);
- Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p Directors Cut.mkv", result[0].AlternateVersions[8].Path);
- Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv", result[0].AlternateVersions[9].Path);
- Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv", result[0].AlternateVersions[10].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p.mkv", result[0].AlternateVersions[0].Files[0].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 2160p Remux.mkv", result[0].AlternateVersions[1].Files[0].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", result[0].AlternateVersions[2].Files[0].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Directors Cut.mkv", result[0].AlternateVersions[3].Files[0].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p High Bitrate.mkv", result[0].AlternateVersions[4].Files[0].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Remux.mkv", result[0].AlternateVersions[5].Files[0].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p Theatrical Release.mkv", result[0].AlternateVersions[6].Files[0].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p.mkv", result[0].AlternateVersions[7].Files[0].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 720p Directors Cut.mkv", result[0].AlternateVersions[8].Files[0].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Directors Cut.mkv", result[0].AlternateVersions[9].Files[0].Path);
+ Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - Theatrical Release.mkv", result[0].AlternateVersions[10].Files[0].Path);
}
[Fact]
@@ -410,9 +401,8 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Single(result);
Assert.Single(result[0].AlternateVersions);
@@ -427,9 +417,8 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 2.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(2, result.Count);
}
@@ -437,7 +426,7 @@ namespace Jellyfin.Naming.Tests.Video
[Fact]
public void TestEmptyList()
{
- var result = VideoListResolver.Resolve(new List<VideoFileInfo>(), _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(new List<VideoFileInfo>()).ToList();
Assert.Empty(result);
}
@@ -451,9 +440,8 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Movie (2020)/Movie (2020)_1080p.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Single(result);
Assert.Single(result[0].AlternateVersions);
@@ -468,11 +456,678 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Movie (2020)/Movie (2020).1080p.mkv"
};
- var result = VideoListResolver.Resolve(
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
+
+ Assert.Single(result);
+ Assert.Single(result[0].AlternateVersions);
+ }
+
+ // Episode multi-version tests
+
+ [Fact]
+ public void TestMultiVersionEpisodeInOwnFolder()
+ {
+ // Two versions of S01E01 in their own subfolder should merge
+ var files = new[]
+ {
+ "/TV/Dexter/Dexter - S01E01/Dexter - S01E01 - 1080p.mkv",
+ "/TV/Dexter/Dexter - S01E01/Dexter - S01E01 - 720p.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Single(result);
+ Assert.Single(result[0].AlternateVersions);
+ // 1080p should be primary (higher resolution)
+ Assert.Contains("1080p", result[0].Files[0].Path, StringComparison.Ordinal);
+ Assert.Contains("720p", result[0].AlternateVersions[0].Files[0].Path, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeMixedSeasonFolder()
+ {
+ // Multiple episodes in season folder, some with versions
+ var files = new[]
+ {
+ "/TV/Dexter/Season 1/Dexter - S01E01 - 1080p.mkv",
+ "/TV/Dexter/Season 1/Dexter - S01E01 - 720p.mkv",
+ "/TV/Dexter/Season 1/Dexter - S01E02.mkv",
+ "/TV/Dexter/Season 1/Dexter - S01E03 - 1080p.mkv",
+ "/TV/Dexter/Season 1/Dexter - S01E03 - 720p.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Equal(3, result.Count);
+
+ // S01E01 - should have one alternate version
+ var e01 = result.FirstOrDefault(r => r.Files[0].Path.Contains("S01E01", StringComparison.Ordinal));
+ Assert.NotNull(e01);
+ Assert.Single(e01!.AlternateVersions);
+ Assert.Contains("1080p", e01.Files[0].Path, StringComparison.Ordinal);
+
+ // S01E02 - standalone, no alternates
+ var e02 = result.FirstOrDefault(r => r.Files[0].Path.Contains("S01E02", StringComparison.Ordinal));
+ Assert.NotNull(e02);
+ Assert.Empty(e02!.AlternateVersions);
+
+ // S01E03 - should have one alternate version
+ var e03 = result.FirstOrDefault(r => r.Files[0].Path.Contains("S01E03", StringComparison.Ordinal));
+ Assert.NotNull(e03);
+ Assert.Single(e03!.AlternateVersions);
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeDontCollapse()
+ {
+ // Different episodes should NOT collapse into versions
+ var files = new[]
+ {
+ "/TV/Dexter/Season 1/Dexter - S01E01.mkv",
+ "/TV/Dexter/Season 1/Dexter - S01E02.mkv",
+ "/TV/Dexter/Season 1/Dexter - S01E03.mkv",
+ "/TV/Dexter/Season 1/Dexter - S01E04.mkv",
+ "/TV/Dexter/Season 1/Dexter - S01E05.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Equal(5, result.Count);
+ Assert.All(result, r => Assert.Empty(r.AlternateVersions));
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeWithVersionSuffix()
+ {
+ // Episodes with named versions (like Aired/Uncensored)
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01 - Aired.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - Uncensored.mkv",
+ "/TV/Show/Season 1/Show - S01E02 - Aired.mkv",
+ "/TV/Show/Season 1/Show - S01E02 - Uncensored.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Equal(2, result.Count);
+ Assert.All(result, r => Assert.Single(r.AlternateVersions));
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeFourVersions()
+ {
+ // Four versions of the same episode
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01 - VersionA.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - VersionB.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - VersionC.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - VersionD.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Single(result);
+ Assert.Equal(3, result[0].AlternateVersions.Count);
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeWithResolutions()
+ {
+ // Resolution sorting should work for episodes too
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01 - 720p.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - 2160p.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - 1080p.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Single(result);
+ Assert.Equal(2, result[0].AlternateVersions.Count);
+ // Primary should be 2160p (highest resolution)
+ Assert.Contains("2160p", result[0].Files[0].Path, StringComparison.Ordinal);
+ // Next should be 1080p, then 720p
+ Assert.Contains("1080p", result[0].AlternateVersions[0].Files[0].Path, StringComparison.Ordinal);
+ Assert.Contains("720p", result[0].AlternateVersions[1].Files[0].Path, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeDifferentSeasons()
+ {
+ // Same episode number but different seasons should NOT group
+ var files = new[]
+ {
+ "/TV/Show/Show - S01E01.mkv",
+ "/TV/Show/Show - S02E01.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Equal(2, result.Count);
+ Assert.All(result, r => Assert.Empty(r.AlternateVersions));
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeDisabledByDefault()
+ {
+ // Without collectionType: CollectionType.tvshows, episodes should NOT group
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01 - 1080p.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - 720p.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
+
+ // Without the tvshows collection type, these fall through the movie path
+ // (folder-name eligibility fails) and are treated as separate items.
+ Assert.Equal(2, result.Count);
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeSameNumberDifferentTitle()
+ {
+ // Two files parse to the same S01E01 but carry distinct episode titles.
+ // Current behavior: they are grouped as alternate versions because
+ // grouping keys only on season + episode number, not on episode title.
+ // This documents the trade-off: users with mis-numbered episodes will
+ // see one of the files collapsed into AlternateVersions of the other.
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01 - Pilot.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - Completely Different Title.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Single(result);
+ Assert.Single(result[0].AlternateVersions);
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeWithTitle()
+ {
+ // Episodes with an episode title AND a version suffix should group
+ var files = new[]
+ {
+ "/TV/Show/Show - S01E01/Show - S01E01 - Episode Title - 1080p.mkv",
+ "/TV/Show/Show - S01E01/Show - S01E01 - Episode Title - 720p.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Single(result);
+ Assert.Single(result[0].AlternateVersions);
+ Assert.Contains("1080p", result[0].Files[0].Path, StringComparison.Ordinal);
+ Assert.Contains("720p", result[0].AlternateVersions[0].Files[0].Path, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeWithTitleMixedFolder()
+ {
+ // Multiple different episodes with titles and resolution variants in a season folder
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01 - Pilot - 1080p.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - Pilot - 720p.mkv",
+ "/TV/Show/Season 1/Show - S01E02 - Second Episode - 1080p.mkv",
+ "/TV/Show/Season 1/Show - S01E02 - Second Episode - 720p.mkv",
+ "/TV/Show/Season 1/Show - S01E03 - Third Episode.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Equal(3, result.Count);
+
+ var e01 = result.FirstOrDefault(r => r.Files[0].Path.Contains("S01E01", StringComparison.Ordinal));
+ Assert.NotNull(e01);
+ Assert.Single(e01!.AlternateVersions);
+
+ var e02 = result.FirstOrDefault(r => r.Files[0].Path.Contains("S01E02", StringComparison.Ordinal));
+ Assert.NotNull(e02);
+ Assert.Single(e02!.AlternateVersions);
+
+ var e03 = result.FirstOrDefault(r => r.Files[0].Path.Contains("S01E03", StringComparison.Ordinal));
+ Assert.NotNull(e03);
+ Assert.Empty(e03!.AlternateVersions);
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeInSeasonSubfolder()
+ {
+ // Two versions of S01E01 in their own subfolder under a season folder
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01/Show - S01E01 - 1080p.mkv",
+ "/TV/Show/Season 1/Show - S01E01/Show - S01E01 - 720p.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Single(result);
+ Assert.Single(result[0].AlternateVersions);
+ Assert.Contains("1080p", result[0].Files[0].Path, StringComparison.Ordinal);
+ Assert.Contains("720p", result[0].AlternateVersions[0].Files[0].Path, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeWithTitleAndVersionSuffix()
+ {
+ // Episodes with episode title AND a named version suffix
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01 - Pilot - Aired.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - Pilot - Uncensored.mkv",
+ "/TV/Show/Season 1/Show - S01E02 - The Getaway - Aired.mkv",
+ "/TV/Show/Season 1/Show - S01E02 - The Getaway - Uncensored.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Equal(2, result.Count);
+ Assert.All(result, r => Assert.Single(r.AlternateVersions));
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeWithAdditionalPartsCd()
+ {
+ // Stacked episode (cd1/cd2) with higher resolution alongside a single-file lower-res version
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01 - 1080p cd1.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - 1080p cd2.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - 720p.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Single(result);
+ Assert.Equal(2, result[0].Files.Count);
+ Assert.Single(result[0].AlternateVersions);
+ Assert.Contains("720p", result[0].AlternateVersions[0].Files[0].Path, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeWithAdditionalPartsDashPart()
+ {
+ // Stacked episode using "- part1" / "- part2" separator
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01 - 1080p - part1.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - 1080p - part2.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - 720p.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Single(result);
+ Assert.Equal(2, result[0].Files.Count);
+ Assert.Single(result[0].AlternateVersions);
+ Assert.Contains("720p", result[0].AlternateVersions[0].Files[0].Path, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeWithAdditionalPartsPt()
+ {
+ // Stacked episode using "pt1" / "pt2" short form
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01 - 1080p.pt1.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - 1080p.pt2.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - 720p.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Single(result);
+ Assert.Equal(2, result[0].Files.Count);
+ Assert.Single(result[0].AlternateVersions);
+ Assert.Contains("720p", result[0].AlternateVersions[0].Files[0].Path, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeWithAdditionalPartsAndTitle()
+ {
+ // Stacked episode with episode title in filename
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01 - Pilot - 1080p part1.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - Pilot - 1080p part2.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - Pilot - 720p.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Single(result);
+ // Primary should be the stacked 1080p version with 2 files
+ Assert.Equal(2, result[0].Files.Count);
+ Assert.Single(result[0].AlternateVersions);
+ Assert.Contains("720p", result[0].AlternateVersions[0].Files[0].Path, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeWithAdditionalPartsAndTitleDashSeparator()
+ {
+ // Stacked episode with episode title using "- part1" separator
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01 - Pilot - 1080p - part1.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - Pilot - 1080p - part2.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - Pilot - 720p.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Single(result);
+ // Primary should be the stacked 1080p version with 2 files
+ Assert.Equal(2, result[0].Files.Count);
+ Assert.Single(result[0].AlternateVersions);
+ Assert.Contains("720p", result[0].AlternateVersions[0].Files[0].Path, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeWithAdditionalPartsAndMultipleEpisodes()
+ {
+ // Stacked episode alongside single-file version, plus a different episode
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01 - 1080p cd1.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - 1080p cd2.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - 720p.mkv",
+ "/TV/Show/Season 1/Show - S01E02 - Other.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Equal(2, result.Count);
+
+ // S01E01: stacked (cd1+cd2) primary with 720p alternate
+ var e01 = result.FirstOrDefault(r => r.Files[0].Path.Contains("S01E01", StringComparison.Ordinal));
+ Assert.NotNull(e01);
+ Assert.Equal(2, e01!.Files.Count);
+ Assert.Single(e01.AlternateVersions);
+
+ // S01E02: standalone
+ var e02 = result.FirstOrDefault(r => r.Files[0].Path.Contains("S01E02", StringComparison.Ordinal));
+ Assert.NotNull(e02);
+ Assert.Empty(e02!.AlternateVersions);
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodePartStackAlongsideSingleFileResolutions()
+ {
+ // A part-stacked episode (3 parts, no resolution suffix) alongside single-file 720p and 1080p versions.
+ // The multi-part stack is preferred as primary.
+ var files = new[]
+ {
+ "/TV/Show/Season 1/S01E01 - 720p.mkv",
+ "/TV/Show/Season 1/S01E01 - 1080p.mkv",
+ "/TV/Show/Season 1/S01E01 - Part 1.mkv",
+ "/TV/Show/Season 1/S01E01 - Part 2.mkv",
+ "/TV/Show/Season 1/S01E01 - Part 3.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Single(result);
+ Assert.Equal(3, result[0].Files.Count);
+ Assert.All(result[0].Files, f => Assert.Contains("Part", f.Path, StringComparison.Ordinal));
+ Assert.Equal(2, result[0].AlternateVersions.Count);
+ Assert.Contains(result[0].AlternateVersions, f => f.Files[0].Path.Contains("1080p", StringComparison.Ordinal));
+ Assert.Contains(result[0].AlternateVersions, f => f.Files[0].Path.Contains("720p", StringComparison.Ordinal));
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodeTwoPartStacks()
+ {
+ // Two part-suffixed stacks of the same episode at different resolutions.
+ // The 1080p stack is primary, the 720p stack is preserved as a multi-file alternate.
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01 - 1080p - part1.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - 1080p - part2.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - 720p - part1.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - 720p - part2.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Single(result);
+ Assert.Equal(2, result[0].Files.Count);
+ Assert.Contains("1080p", result[0].Files[0].Path, StringComparison.Ordinal);
+
+ Assert.Single(result[0].AlternateVersions);
+ var alt = result[0].AlternateVersions[0];
+ Assert.Equal(2, alt.Files.Count);
+ Assert.All(alt.Files, f => Assert.Contains("720p", f.Path, StringComparison.Ordinal));
+ }
+
+ [Fact]
+ public void TestMultiVersionEpisodePartStackWithTrailer()
+ {
+ // A part-stacked multi-version episode alongside a trailer must not pull the trailer into the version group
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01 - 1080p part1.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - 1080p part2.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - 720p.mkv",
+ "/TV/Show/Season 1/Show - S01E01-trailer.mp4"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Equal(2, result.Count);
+
+ var episode = result.FirstOrDefault(r => r.ExtraType is null);
+ Assert.NotNull(episode);
+ Assert.Equal(2, episode!.Files.Count);
+ Assert.Single(episode.AlternateVersions);
+ Assert.Contains("720p", episode.AlternateVersions[0].Files[0].Path, StringComparison.Ordinal);
+
+ var trailer = result.FirstOrDefault(r => r.ExtraType is not null);
+ Assert.NotNull(trailer);
+ Assert.Equal(ExtraType.Trailer, trailer!.ExtraType);
+ }
+
+ [Fact]
+ public void TestMovieStackingWithPartNaming()
+ {
+ // Movie stacking with "part1"/"part2" naming
+ var files = new[]
+ {
+ "/movies/Movie/Movie part1.mkv",
+ "/movies/Movie/Movie part2.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
+
+ Assert.Single(result);
+ Assert.Equal(2, result[0].Files.Count);
+ }
+
+ [Fact]
+ public void TestMovieStackingWithDashPartNaming()
+ {
+ // Movie stacking with "- part1" / "- part2" dash separator
+ var files = new[]
+ {
+ "/movies/Movie/Movie - part1.mkv",
+ "/movies/Movie/Movie - part2.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
+
+ Assert.Single(result);
+ Assert.Equal(2, result[0].Files.Count);
+ }
+
+ [Fact]
+ public void TestMovieStackingWithPtNaming()
+ {
+ // Movie stacking with "pt1"/"pt2" short form
+ var files = new[]
+ {
+ "/movies/Movie/Movie.pt1.mkv",
+ "/movies/Movie/Movie.pt2.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
+
+ Assert.Single(result);
+ Assert.Equal(2, result[0].Files.Count);
+ }
+
+ [Fact]
+ public void TestMovieStackingWithHyphenNoSpaces()
+ {
+ // Movie stacking with hyphen directly adjacent to "part" (no spaces)
+ var files = new[]
+ {
+ "/movies/Movie/Movie-part1.mkv",
+ "/movies/Movie/Movie-part2.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
+
+ Assert.Single(result);
+ Assert.Equal(2, result[0].Files.Count);
+ }
+
+ [Fact]
+ public void TestMovieStackingWithHyphenNoSpacesAndVersion()
+ {
+ // Movie stacking with hyphen-no-space separators plus a version alternate
+ var files = new[]
+ {
+ "/movies/Movie/Movie-1080p-part1.mkv",
+ "/movies/Movie/Movie-1080p-part2.mkv",
+ "/movies/Movie/Movie-720p.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
+
+ Assert.Single(result);
+ // Stacked 1080p (2 files) should be primary, 720p is alternate
+ Assert.Equal(2, result[0].Files.Count);
+ Assert.Single(result[0].AlternateVersions);
+ }
+
+ [Fact]
+ public void TestMovieMultiVersionWithStackedAlternate()
+ {
+ // Movie folder where the folder-named file is the primary (single file via primaryOverride)
+ // and an alternate version is itself a stack. The stacked alternate must keep all its files.
+ var files = new[]
+ {
+ "/movies/Inception (2010)/Inception (2010).mkv",
+ "/movies/Inception (2010)/Inception (2010) - 4k part1.mkv",
+ "/movies/Inception (2010)/Inception (2010) - 4k part2.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
+
+ Assert.Single(result);
+ Assert.Single(result[0].Files);
+ Assert.Equal("/movies/Inception (2010)/Inception (2010).mkv", result[0].Files[0].Path);
+
+ Assert.Single(result[0].AlternateVersions);
+ var stackedAlternate = result[0].AlternateVersions[0];
+ Assert.Equal(2, stackedAlternate.Files.Count);
+ Assert.All(stackedAlternate.Files, f => Assert.Contains("4k part", f.Path, StringComparison.Ordinal));
+ }
+
+ [Fact]
+ public void TestEpisodeStackingWithHyphenNoSpaces()
+ {
+ // Episode stacking with hyphen-no-space separators plus version alternate
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01-1080p-cd1.mkv",
+ "/TV/Show/Season 1/Show - S01E01-1080p-cd2.mkv",
+ "/TV/Show/Season 1/Show - S01E01-720p.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ collectionType: CollectionType.tvshows).ToList();
+
+ Assert.Single(result);
+ // Stacked 1080p (2 files) should be primary, 720p is alternate
+ Assert.Equal(2, result[0].Files.Count);
+ Assert.Single(result[0].AlternateVersions);
+ }
+
+ [Fact]
+ public void TestEpisodeStackingWithHyphenNoSpacesAndTitle()
+ {
+ // Episode stacking with title and hyphen-no-space separators
+ var files = new[]
+ {
+ "/TV/Show/Season 1/Show - S01E01 - Pilot-1080p-part1.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - Pilot-1080p-part2.mkv",
+ "/TV/Show/Season 1/Show - S01E01 - Pilot-720p.mkv"
+ };
+
+ var result = _videoListResolver.Resolve(
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ collectionType: CollectionType.tvshows).ToList();
Assert.Single(result);
+ // Stacked 1080p (2 files) should be primary, 720p is alternate
+ Assert.Equal(2, result[0].Files.Count);
Assert.Single(result[0].AlternateVersions);
}
}
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
index d3164ba9c9..53f16b92d6 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
@@ -10,6 +10,12 @@ namespace Jellyfin.Naming.Tests.Video
public class VideoListResolverTests
{
private readonly NamingOptions _namingOptions = new NamingOptions();
+ private readonly VideoListResolver _videoListResolver;
+
+ public VideoListResolverTests()
+ {
+ _videoListResolver = new VideoListResolver(_namingOptions);
+ }
[Fact]
public void TestStackAndExtras()
@@ -40,9 +46,8 @@ namespace Jellyfin.Naming.Tests.Video
"WillyWonka-trailer.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(11, result.Count);
var batman = result.FirstOrDefault(x => string.Equals(x.Name, "Batman", StringComparison.Ordinal));
@@ -74,9 +79,8 @@ namespace Jellyfin.Naming.Tests.Video
"300.nfo"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Single(result);
}
@@ -90,9 +94,8 @@ namespace Jellyfin.Naming.Tests.Video
"300 - trailer.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(2, result.Count);
Assert.False(result[0].ExtraType.HasValue);
@@ -108,9 +111,8 @@ namespace Jellyfin.Naming.Tests.Video
"X-Men Days of Future Past-trailer.mp4"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(2, result.Count);
Assert.False(result[0].ExtraType.HasValue);
@@ -127,9 +129,8 @@ namespace Jellyfin.Naming.Tests.Video
"X-Men Days of Future Past-trailer2.mp4"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(3, result.Count);
Assert.False(result[0].ExtraType.HasValue);
@@ -147,9 +148,8 @@ namespace Jellyfin.Naming.Tests.Video
"Looper.2012.bluray.720p.x264.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(3, result.Count);
Assert.False(result[0].ExtraType.HasValue);
@@ -166,9 +166,8 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Looper (2012)/Looper.bluray.720p.x264.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(2, result.Count);
Assert.False(result[0].ExtraType.HasValue);
@@ -188,9 +187,8 @@ namespace Jellyfin.Naming.Tests.Video
"My video 5.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(5, result.Count);
}
@@ -204,9 +202,8 @@ namespace Jellyfin.Naming.Tests.Video
"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, true, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, true, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Single(result);
}
@@ -221,9 +218,8 @@ namespace Jellyfin.Naming.Tests.Video
"My movie #2.mp4"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, true, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, true, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(2, result.Count);
}
@@ -239,9 +235,8 @@ namespace Jellyfin.Naming.Tests.Video
"No (2012)-trailer.mp4"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(3, result.Count);
Assert.False(result[0].ExtraType.HasValue);
@@ -260,9 +255,8 @@ namespace Jellyfin.Naming.Tests.Video
"/Movies/trailer.mp4"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(4, result.Count);
Assert.False(result[0].ExtraType.HasValue);
@@ -282,9 +276,8 @@ namespace Jellyfin.Naming.Tests.Video
"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(2, result.Count);
}
@@ -297,9 +290,8 @@ namespace Jellyfin.Naming.Tests.Video
"/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Single(result);
}
@@ -312,9 +304,8 @@ namespace Jellyfin.Naming.Tests.Video
"The Colony.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Single(result);
}
@@ -328,9 +319,8 @@ namespace Jellyfin.Naming.Tests.Video
"Four Sisters and a Wedding - B.avi"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
// The result should contain two individual movies
// Version grouping should not work here, because the files are not in a directory with the name 'Four Sisters and a Wedding'
@@ -346,9 +336,8 @@ namespace Jellyfin.Naming.Tests.Video
"Four Rooms - A.mp4"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(2, result.Count);
}
@@ -362,9 +351,8 @@ namespace Jellyfin.Naming.Tests.Video
"/Server/Despicable Me/trailer.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(2, result.Count);
Assert.False(result[0].ExtraType.HasValue);
@@ -380,9 +368,8 @@ namespace Jellyfin.Naming.Tests.Video
"/Server/Despicable Me/trailers/some title.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(2, result.Count);
Assert.False(result[0].ExtraType.HasValue);
@@ -398,9 +385,8 @@ namespace Jellyfin.Naming.Tests.Video
"/Movies/Despicable Me/trailers/trailer.mkv"
};
- var result = VideoListResolver.Resolve(
- files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
- _namingOptions).ToList();
+ var result = _videoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList()).ToList();
Assert.Equal(2, result.Count);
Assert.False(result[0].ExtraType.HasValue);
diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
index b63009d6a5..1f523f7f21 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
@@ -7,6 +7,7 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
@@ -94,10 +95,106 @@ namespace Jellyfin.Networking.Tests
[InlineData("256.128.0.0.0.1")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")]
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")]
+ [InlineData("fd23:184f:2029:0100/56")]
public static void TryParseInvalidIPStringsFalse(string address)
=> Assert.False(NetworkUtils.TryParseToSubnet(address, out _));
/// <summary>
+ /// Verifies that <see cref="NetworkUtils.TryParseToSubnets"/> emits a targeted warning
+ /// for IPv6 prefix-only notation and a generic warning for other malformed entries.
+ /// </summary>
+ [Fact]
+ public static void TryParseToSubnets_InvalidEntries_LogsWarnings()
+ {
+ var logger = new Mock<ILogger>();
+
+ var values = new[] { "10.0.0.0/8", "fd23:184f:2029:0100/56", "not-an-address" };
+ Assert.True(NetworkUtils.TryParseToSubnets(values, out var result, false, logger.Object));
+ Assert.NotNull(result);
+ Assert.Single(result);
+
+ // IPv6 prefix-only notation should produce a specific, actionable warning.
+ logger.Verify(
+ l => l.Log(
+ LogLevel.Warning,
+ It.IsAny<EventId>(),
+ It.Is<It.IsAnyType>((state, _) => state.ToString()!.Contains("IPv6 prefix-only", StringComparison.Ordinal)
+ && state.ToString()!.Contains("fd23:184f:2029:0100/56", StringComparison.Ordinal)),
+ It.IsAny<Exception>(),
+ It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
+ Times.Once);
+
+ // Other malformed entries should still produce a generic warning.
+ logger.Verify(
+ l => l.Log(
+ LogLevel.Warning,
+ It.IsAny<EventId>(),
+ It.Is<It.IsAnyType>((state, _) => state.ToString()!.Contains("not-an-address", StringComparison.Ordinal)),
+ It.IsAny<Exception>(),
+ It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
+ Times.Once);
+ }
+
+ /// <summary>
+ /// Verifies that IPv4 entries whose '!' polarity doesn't match the requested pass are skipped silently,
+ /// not logged as invalid. Callers parse the same list twice (LAN and excluded) so the off-polarity
+ /// entries are expected, not erroneous.
+ /// </summary>
+ [Fact]
+ public static void TryParseToSubnets_PolarityMismatchIPv4_DoesNotWarn()
+ {
+ var logger = new Mock<ILogger>();
+ var values = new[] { "127.0.0.0/8", "192.168.178.0/24", "!10.0.0.0/8" };
+
+ // Non-negated pass picks up the two non-'!' entries and ignores '!10.0.0.0/8' silently.
+ Assert.True(NetworkUtils.TryParseToSubnets(values, out var lanResult, false, logger.Object));
+ Assert.NotNull(lanResult);
+ Assert.Equal(2, lanResult.Count);
+
+ // Negated pass picks up the single '!' entry and ignores the others silently.
+ Assert.True(NetworkUtils.TryParseToSubnets(values, out var excludedResult, true, logger.Object));
+ Assert.NotNull(excludedResult);
+ Assert.Single(excludedResult);
+
+ logger.Verify(
+ l => l.Log(
+ LogLevel.Warning,
+ It.IsAny<EventId>(),
+ It.IsAny<It.IsAnyType>(),
+ It.IsAny<Exception>(),
+ It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
+ Times.Never);
+ }
+
+ /// <summary>
+ /// Same as the IPv4 case but for IPv6 entries — makes sure the polarity pre-check works
+ /// for IPv6 CIDR notation (with '::') as well.
+ /// </summary>
+ [Fact]
+ public static void TryParseToSubnets_PolarityMismatchIPv6_DoesNotWarn()
+ {
+ var logger = new Mock<ILogger>();
+ var values = new[] { "fd00::/8", "fe80::/10", "!fd12:3456:789a::/48" };
+
+ Assert.True(NetworkUtils.TryParseToSubnets(values, out var lanResult, false, logger.Object));
+ Assert.NotNull(lanResult);
+ Assert.Equal(2, lanResult.Count);
+
+ Assert.True(NetworkUtils.TryParseToSubnets(values, out var excludedResult, true, logger.Object));
+ Assert.NotNull(excludedResult);
+ Assert.Single(excludedResult);
+
+ logger.Verify(
+ l => l.Log(
+ LogLevel.Warning,
+ It.IsAny<EventId>(),
+ It.IsAny<It.IsAnyType>(),
+ It.IsAny<Exception>(),
+ It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
+ Times.Never);
+ }
+
+ /// <summary>
/// Checks if IPv4 address is within a defined subnet.
/// </summary>
/// <param name="netMask">Network mask.</param>
diff --git a/tests/Jellyfin.Providers.Tests/ExternalId/ComicVineExternalUrlProviderTests.cs b/tests/Jellyfin.Providers.Tests/ExternalId/ComicVineExternalUrlProviderTests.cs
index 99604e0933..aaa500b762 100644
--- a/tests/Jellyfin.Providers.Tests/ExternalId/ComicVineExternalUrlProviderTests.cs
+++ b/tests/Jellyfin.Providers.Tests/ExternalId/ComicVineExternalUrlProviderTests.cs
@@ -1,7 +1,7 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Providers.Plugins.ComicVine;
+using MediaBrowser.Providers.Books.ComicVine;
using Xunit;
namespace Jellyfin.Providers.Tests.ExternalId
diff --git a/tests/Jellyfin.Providers.Tests/ExternalId/GoogleBooksExternalUrlProviderTests.cs b/tests/Jellyfin.Providers.Tests/ExternalId/GoogleBooksExternalUrlProviderTests.cs
index eec64ac53f..b9ce895dbc 100644
--- a/tests/Jellyfin.Providers.Tests/ExternalId/GoogleBooksExternalUrlProviderTests.cs
+++ b/tests/Jellyfin.Providers.Tests/ExternalId/GoogleBooksExternalUrlProviderTests.cs
@@ -1,7 +1,7 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Providers.Plugins.GoogleBooks;
+using MediaBrowser.Providers.Books.GoogleBooks;
using Xunit;
namespace Jellyfin.Providers.Tests.ExternalId
diff --git a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
index 87e7a4b564..5749944fcd 100644
--- a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs
@@ -576,7 +576,8 @@ namespace Jellyfin.Providers.Tests.Manager
baseItemManager!,
Mock.Of<ILyricManager>(),
Mock.Of<IMemoryCache>(),
- Mock.Of<IMediaSegmentManager>());
+ Mock.Of<IMediaSegmentManager>(),
+ Mock.Of<ISimilarItemsManager>());
return providerManager;
}
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs
index a7491f42e9..2438ef06d1 100644
--- a/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs
@@ -37,9 +37,9 @@ public class FFProbeVideoInfoTests
{
Assert.Throws<ArgumentException>(
() => _fFProbeVideoInfo.CreateDummyChapters(new Video()
- {
- RunTimeTicks = runtime
- }));
+ {
+ RunTimeTicks = runtime
+ }));
}
[Theory]
@@ -53,9 +53,9 @@ public class FFProbeVideoInfoTests
public void CreateDummyChapters_ValidRuntime_CorrectChaptersCount(long? runtime, int chaptersCount)
{
var chapters = _fFProbeVideoInfo.CreateDummyChapters(new Video()
- {
- RunTimeTicks = runtime
- });
+ {
+ RunTimeTicks = runtime
+ });
Assert.Equal(chaptersCount, chapters.Length);
}
@@ -69,9 +69,9 @@ public class FFProbeVideoInfoTests
public void CreateDummyChapters_PositiveRuntime_NoChapterBeyondRuntime(long runtime)
{
var chapters = _fFProbeVideoInfo.CreateDummyChapters(new Video()
- {
- RunTimeTicks = runtime
- });
+ {
+ RunTimeTicks = runtime
+ });
Assert.All(chapters, chapter => Assert.True(chapter.StartPositionTicks < runtime));
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceTests.cs
new file mode 100644
index 0000000000..a5de0a4416
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceTests.cs
@@ -0,0 +1,131 @@
+using System;
+using Emby.Server.Implementations.Dto;
+using MediaBrowser.Common;
+using MediaBrowser.Controller.Chapters;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Dto;
+
+public class DtoServiceTests
+{
+ private readonly Mock<ILibraryManager> _libraryManagerMock;
+ private readonly DtoService _dtoService;
+
+ public DtoServiceTests()
+ {
+ _libraryManagerMock = new Mock<ILibraryManager>();
+
+ var imageProcessor = new Mock<IImageProcessor>();
+ // Deterministic tag derived from the image so each item gets a distinct, assertable tag.
+ imageProcessor
+ .Setup(x => x.GetImageCacheTag(It.IsAny<BaseItem>(), It.IsAny<ItemImageInfo>()))
+ .Returns((BaseItem _, ItemImageInfo image) => "tag:" + image.Path);
+
+ var appHost = new Mock<IApplicationHost>();
+ appHost.Setup(x => x.SystemId).Returns("test-server");
+
+ // Video.SourceType probes the active-recording manager; provide one so it doesn't NRE.
+ Video.RecordingsManager = new Mock<IRecordingsManager>().Object;
+
+ _dtoService = new DtoService(
+ NullLogger<DtoService>.Instance,
+ _libraryManagerMock.Object,
+ new Mock<IUserDataManager>().Object,
+ imageProcessor.Object,
+ new Mock<IProviderManager>().Object,
+ new Mock<IRecordingsManager>().Object,
+ appHost.Object,
+ new Mock<IMediaSourceManager>().Object,
+ new Lazy<ILiveTvManager>(() => new Mock<ILiveTvManager>().Object),
+ new Mock<ITrickplayManager>().Object,
+ new Mock<IChapterManager>().Object);
+
+ // Episode.Series / Episode.Season resolve through the static BaseItem.LibraryManager.
+ BaseItem.LibraryManager = _libraryManagerMock.Object;
+ }
+
+ [Fact]
+ public void GetBaseItemDto_PreferEpisodeParentPoster_PrefersSeasonPosterOverEpisodeAndSeries()
+ {
+ var (episode, season, series) = BuildEpisode(seasonHasPoster: true);
+ var options = new DtoOptions(false) { PreferEpisodeParentPoster = true };
+
+ var dto = _dtoService.GetBaseItemDto(episode, options);
+
+ // The episode's own 16:9 primary is dropped in favor of the season's portrait poster.
+ Assert.False(dto.ImageTags is not null && dto.ImageTags.ContainsKey(ImageType.Primary));
+ Assert.Null(dto.SeriesPrimaryImageTag);
+ Assert.Equal(season.Id, dto.ParentPrimaryImageItemId);
+ Assert.Equal("tag:" + season.GetImageInfo(ImageType.Primary, 0)!.Path, dto.ParentPrimaryImageTag);
+ // Aspect ratio follows the (portrait) poster, not the episode's 16:9 image.
+ Assert.Equal(season.GetDefaultPrimaryImageAspectRatio(), dto.PrimaryImageAspectRatio);
+ }
+
+ [Fact]
+ public void GetBaseItemDto_PreferEpisodeParentPoster_FallsBackToSeriesWhenSeasonHasNoPoster()
+ {
+ var (episode, _, series) = BuildEpisode(seasonHasPoster: false);
+ var options = new DtoOptions(false) { PreferEpisodeParentPoster = true };
+
+ var dto = _dtoService.GetBaseItemDto(episode, options);
+
+ Assert.False(dto.ImageTags is not null && dto.ImageTags.ContainsKey(ImageType.Primary));
+ Assert.Null(dto.SeriesPrimaryImageTag);
+ Assert.Equal(series.Id, dto.ParentPrimaryImageItemId);
+ Assert.Equal("tag:" + series.GetImageInfo(ImageType.Primary, 0)!.Path, dto.ParentPrimaryImageTag);
+ }
+
+ [Fact]
+ public void GetBaseItemDto_WithoutPreferEpisodeParentPoster_KeepsEpisodePrimary()
+ {
+ var (episode, _, _) = BuildEpisode(seasonHasPoster: true);
+ var options = new DtoOptions(false);
+
+ var dto = _dtoService.GetBaseItemDto(episode, options);
+
+ // Default behavior: the episode keeps its own primary and exposes the series poster as a tag.
+ Assert.NotNull(dto.ImageTags);
+ Assert.True(dto.ImageTags.ContainsKey(ImageType.Primary));
+ Assert.NotNull(dto.SeriesPrimaryImageTag);
+ Assert.Null(dto.ParentPrimaryImageItemId);
+ }
+
+ private (Episode Episode, Season Season, Series Series) BuildEpisode(bool seasonHasPoster)
+ {
+ // Non-local (http) paths keep aspect-ratio resolution off the image processor and on the
+ // item's default ratio, which is portrait (2/3) for Season/Series and 16:9 for Episode.
+ var series = new Series { Id = Guid.NewGuid(), Name = "Series" };
+ series.SetImage(new ItemImageInfo { Type = ImageType.Primary, Path = "http://test/series.jpg" }, 0);
+
+ var season = new Season { Id = Guid.NewGuid(), Name = "Season", SeriesId = series.Id };
+ if (seasonHasPoster)
+ {
+ season.SetImage(new ItemImageInfo { Type = ImageType.Primary, Path = "http://test/season.jpg" }, 0);
+ }
+
+ var episode = new Episode
+ {
+ Id = Guid.NewGuid(),
+ Name = "Episode",
+ SeasonId = season.Id,
+ SeriesId = series.Id
+ };
+ episode.SetImage(new ItemImageInfo { Type = ImageType.Primary, Path = "http://test/episode.jpg" }, 0);
+
+ _libraryManagerMock.Setup(x => x.GetItemById(season.Id)).Returns(season);
+ _libraryManagerMock.Setup(x => x.GetItemById(series.Id)).Returns(series);
+
+ return (episode, season, series);
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs
index aed584355c..e1346a8436 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs
@@ -1,7 +1,13 @@
+using System.Collections.Generic;
using Emby.Naming.Common;
+using Emby.Naming.Video;
using Emby.Server.Implementations.Library.Resolvers.Movies;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
@@ -14,11 +20,12 @@ namespace Jellyfin.Server.Implementations.Tests.Library;
public class MovieResolverTests
{
private static readonly NamingOptions _namingOptions = new();
+ private static readonly VideoListResolver _videoListResolver = new(_namingOptions);
[Fact]
public void Resolve_GivenLocalAlternateVersion_ResolvesToVideo()
{
- var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>());
+ var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>(), _videoListResolver);
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
null)
@@ -32,4 +39,54 @@ public class MovieResolverTests
Assert.NotNull(movieResolver.Resolve(itemResolveArgs));
}
+
+ [Fact]
+ public void ResolveMultiple_GivenTvShowsCollection_CreatesEpisodeItems()
+ {
+ // For a tvshows collection, the multi-version grouping must still produce
+ // Episode BaseItems (not generic Video) so downstream metadata fetching
+ // and series-aware logic apply.
+ var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>(), _videoListResolver);
+
+ var parent = new Folder { Path = "/TV/Show/Season 1" };
+ var files = new List<FileSystemMetadata>
+ {
+ new() { FullName = "/TV/Show/Season 1/Show - S01E01 - 1080p.mkv", Name = "Show - S01E01 - 1080p.mkv", IsDirectory = false },
+ new() { FullName = "/TV/Show/Season 1/Show - S01E01 - 720p.mkv", Name = "Show - S01E01 - 720p.mkv", IsDirectory = false },
+ new() { FullName = "/TV/Show/Season 1/Show - S01E02.mkv", Name = "Show - S01E02.mkv", IsDirectory = false }
+ };
+
+ var result = movieResolver.ResolveMultiple(parent, files, CollectionType.tvshows, Mock.Of<IDirectoryService>());
+
+ Assert.NotNull(result);
+ Assert.Equal(2, result.Items.Count);
+ Assert.All(result.Items, item => Assert.IsType<Episode>(item));
+
+ // The S01E01 item should have one alternate version
+ var s01e01 = result.Items.Find(i => i.Path.Contains("S01E01", System.StringComparison.Ordinal));
+ Assert.NotNull(s01e01);
+ Assert.Single(((Video)s01e01).LocalAlternateVersions);
+ }
+
+ [Fact]
+ public void ResolveMultiple_GivenMoviesCollection_CreatesMovieItems()
+ {
+ // For a movies collection, the multi-version grouping must produce Movie
+ // BaseItems (not generic Video) so downstream movie-specific logic applies.
+ var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>(), _videoListResolver);
+
+ var parent = new Folder { Path = "/movies/Inception (2010)" };
+ var files = new List<FileSystemMetadata>
+ {
+ new() { FullName = "/movies/Inception (2010)/Inception (2010) - 1080p.mkv", Name = "Inception (2010) - 1080p.mkv", IsDirectory = false },
+ new() { FullName = "/movies/Inception (2010)/Inception (2010) - 720p.mkv", Name = "Inception (2010) - 720p.mkv", IsDirectory = false }
+ };
+
+ var result = movieResolver.ResolveMultiple(parent, files, CollectionType.movies, Mock.Of<IDirectoryService>());
+
+ Assert.NotNull(result);
+ Assert.Single(result.Items);
+ Assert.All(result.Items, item => Assert.IsType<Movie>(item));
+ Assert.Single(((Video)result.Items[0]).LocalAlternateVersions);
+ }
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
index acabaf3acb..3b8fe5ca60 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BitFaster.Caching;
@@ -305,6 +306,98 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
Assert.Equal(key, translated);
}
+ [Fact]
+ public void GetLocalizedString_WithCulture_ReturnsTranslation()
+ {
+ var localizationManager = Setup(new ServerConfiguration
+ {
+ UICulture = "en-US"
+ });
+
+ var translated = localizationManager.GetLocalizedString("Artists", "de");
+ Assert.Equal("Interpreten", translated);
+ }
+
+ [Fact]
+ public void GetLocalizedString_WithCulture_FallsBackToEnUs()
+ {
+ var localizationManager = Setup(new ServerConfiguration
+ {
+ UICulture = "en-US"
+ });
+
+ // A culture with no translation file should fall back to en-US
+ var translated = localizationManager.GetLocalizedString("Artists", "zz");
+ Assert.Equal("Artists", translated);
+ }
+
+ [Fact]
+ public void GetLocalizedString_WithBcp47Normalization_ReturnsTranslation()
+ {
+ var localizationManager = Setup(new ServerConfiguration
+ {
+ UICulture = "en-US"
+ });
+
+ // es-419 is stored as es_419 in Jellyfin
+ var translated = localizationManager.GetLocalizedString("Default", "es-419");
+ Assert.NotEqual("Default", translated);
+ }
+
+ [Fact]
+ public void GetServerLocalizedString_UsesServerCulture()
+ {
+ var localizationManager = Setup(new ServerConfiguration
+ {
+ UICulture = "de"
+ });
+
+ // Even if CurrentUICulture is fr, GetServerLocalizedString should use the server's "de"
+ var previousCulture = CultureInfo.CurrentUICulture;
+ try
+ {
+ CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("fr");
+ var translated = localizationManager.GetServerLocalizedString("Artists");
+ Assert.Equal("Interpreten", translated);
+ }
+ finally
+ {
+ CultureInfo.CurrentUICulture = previousCulture;
+ }
+ }
+
+ [Fact]
+ public void GetLocalizedString_UsesCurrentUICulture()
+ {
+ var localizationManager = Setup(new ServerConfiguration
+ {
+ UICulture = "en-US"
+ });
+
+ var previousCulture = CultureInfo.CurrentUICulture;
+ try
+ {
+ CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("de");
+ var translated = localizationManager.GetLocalizedString("Artists");
+ Assert.Equal("Interpreten", translated);
+ }
+ finally
+ {
+ CultureInfo.CurrentUICulture = previousCulture;
+ }
+ }
+
+ [Fact]
+ public void GetSupportedUICultures_IncludesCommonCultures()
+ {
+ var supported = LocalizationManager.GetSupportedUICultures();
+ Assert.Contains(supported, c => c.Name.Equals("de", StringComparison.OrdinalIgnoreCase));
+ Assert.Contains(supported, c => c.Name.Equals("en-US", StringComparison.OrdinalIgnoreCase));
+ Assert.Contains(supported, c => c.Name.Equals("fr", StringComparison.OrdinalIgnoreCase));
+ // Underscore variants get normalized to BCP-47 hyphen form for CultureInfo compatibility.
+ Assert.Contains(supported, c => c.Name.Equals("es-419", StringComparison.OrdinalIgnoreCase));
+ }
+
private LocalizationManager Setup(ServerConfiguration config)
{
var mockConfiguration = new Mock<IServerConfigurationManager>();
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerNormalizedUsernameTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerNormalizedUsernameTests.cs
new file mode 100644
index 0000000000..596bf58fb1
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerNormalizedUsernameTests.cs
@@ -0,0 +1,240 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Locking;
+using Jellyfin.Database.Providers.Sqlite;
+using Jellyfin.Server.Implementations.Users;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Cryptography;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Users
+{
+ public sealed class UserManagerNormalizedUsernameTests : IDisposable
+ {
+ private readonly SqliteConnection _connection;
+ private readonly DbContextOptions<JellyfinDbContext> _dbOptions;
+ private readonly UserManager _userManager;
+
+ public UserManagerNormalizedUsernameTests()
+ {
+ _connection = new SqliteConnection("Data Source=:memory:");
+ _connection.Open();
+
+ _dbOptions = new DbContextOptionsBuilder<JellyfinDbContext>()
+ .UseSqlite(_connection)
+ .Options;
+
+ // Create the schema
+ using var ctx = CreateDbContext();
+ ctx.Database.EnsureCreated();
+
+ var factory = new Mock<IDbContextFactory<JellyfinDbContext>>();
+ factory.Setup(f => f.CreateDbContext()).Returns(CreateDbContext);
+ factory.Setup(f => f.CreateDbContextAsync(It.IsAny<CancellationToken>()))
+ .ReturnsAsync(CreateDbContext);
+
+ var cryptoProvider = new Mock<ICryptoProvider>();
+ var configManager = new Mock<IServerConfigurationManager>();
+ var appPaths = new Mock<IServerApplicationPaths>();
+ appPaths.Setup(x => x.ProgramDataPath).Returns(Path.GetTempPath());
+ configManager.Setup(x => x.ApplicationPaths).Returns(appPaths.Object);
+
+ var appHost = new Mock<IApplicationHost>();
+
+ var defaultAuthProvider = new DefaultAuthenticationProvider(
+ NullLogger<DefaultAuthenticationProvider>.Instance,
+ cryptoProvider.Object);
+ var invalidAuthProvider = new InvalidAuthProvider();
+ var defaultPasswordResetProvider = new DefaultPasswordResetProvider(
+ configManager.Object,
+ appHost.Object);
+
+ _userManager = new UserManager(
+ factory.Object,
+ new NoopEventManager(),
+ new Mock<INetworkManager>().Object,
+ appHost.Object,
+ new Mock<IImageProcessor>().Object,
+ NullLogger<UserManager>.Instance,
+ configManager.Object,
+ new IPasswordResetProvider[] { defaultPasswordResetProvider },
+ new IAuthenticationProvider[] { defaultAuthProvider, invalidAuthProvider });
+ }
+
+ public void Dispose()
+ {
+ _userManager.Dispose();
+ _connection.Dispose();
+ }
+
+ private JellyfinDbContext CreateDbContext()
+ {
+ return new JellyfinDbContext(
+ _dbOptions,
+ NullLogger<JellyfinDbContext>.Instance,
+ new SqliteDatabaseProvider(null!, NullLogger<SqliteDatabaseProvider>.Instance),
+ new NoLockBehavior(NullLogger<NoLockBehavior>.Instance));
+ }
+
+ // ----- GetUserByName tests -----
+
+ [Theory]
+ // German umlauts
+ [InlineData("münchen", "MÜNCHEN")]
+ // Spanish tilde-n
+ [InlineData("Ñoño", "ÑOÑO")]
+ // ASCII, invariant uppercase lookup
+ [InlineData("jellyfin", "JELLYFIN")]
+ // Turkish cedilla: invariant 'i' uppercases to 'I' (U+0049), not Turkish 'İ' (U+0130)
+ [InlineData("Çelebi", "ÇELEBI")]
+ public async Task GetUserByName_WithNonAsciiUsername_FindsUserByNormalizedName(
+ string username, string normalizedLookup)
+ {
+ await _userManager.CreateUserAsync(username);
+
+ var found = _userManager.GetUserByName(normalizedLookup);
+
+ Assert.NotNull(found);
+ Assert.Equal(username, found.Username);
+ }
+
+ [Theory]
+ // German umlaut, look up by both upper and lower case
+ [InlineData("münchen")]
+ // Spanish tilde-n
+ [InlineData("Ñoño")]
+ // lowercase 'i' — invariant ToUpperInvariant gives 'I', not Turkish 'İ'
+ [InlineData("ali")]
+ // mixed ASCII + umlaut
+ [InlineData("testüser")]
+ public async Task GetUserByName_WithVariousCase_FindsUserCaseInsensitively(string username)
+ {
+ await _userManager.CreateUserAsync(username);
+
+ var upperFound = _userManager.GetUserByName(username.ToUpperInvariant());
+ var lowerFound = _userManager.GetUserByName(username.ToLowerInvariant());
+ var exactFound = _userManager.GetUserByName(username);
+
+ Assert.NotNull(upperFound);
+ Assert.NotNull(lowerFound);
+ Assert.NotNull(exactFound);
+ }
+
+ [Theory]
+ [InlineData("nonexistent")]
+ // No user with NormalizedUsername = "MÜNCHEN" has been created
+ [InlineData("MÜNCHEN")]
+ public void GetUserByName_WhenUserDoesNotExist_ReturnsNull(string lookupName)
+ {
+ var result = _userManager.GetUserByName(lookupName);
+
+ Assert.Null(result);
+ }
+
+ // ----- CreateUserAsync duplicate detection tests -----
+
+ [Theory]
+ // German umlaut, case-swapped duplicate
+ [InlineData("münchen", "MÜNCHEN")]
+ // Spanish tilde-n, lowercase duplicate
+ [InlineData("Ñoño", "ñoño")]
+ // ASCII, uppercase duplicate
+ [InlineData("alice", "ALICE")]
+ // Turkish cedilla: "çelebi".ToUpperInvariant() == "ÇELEBI" == "ÇELEBI".ToUpperInvariant()
+ [InlineData("çelebi", "ÇELEBI")]
+ public async Task CreateUserAsync_WhenNormalizedNameAlreadyExists_ThrowsArgumentException(
+ string existingUsername, string duplicateUsername)
+ {
+ await _userManager.CreateUserAsync(existingUsername);
+
+ await Assert.ThrowsAsync<ArgumentException>(
+ () => _userManager.CreateUserAsync(duplicateUsername));
+ }
+
+ [Theory]
+ // Different non-ASCII names that do not collide after normalization
+ [InlineData("münchen", "münchen2")]
+ [InlineData("ali", "ali2")]
+ // Visually similar but different Unicode code points: ñ (U+00F1) vs n (U+006E)
+ [InlineData("noño", "nono")]
+ public async Task CreateUserAsync_WithDistinctNonAsciiUsernames_CreatesBothUsers(
+ string firstUsername, string secondUsername)
+ {
+ var first = await _userManager.CreateUserAsync(firstUsername);
+ var second = await _userManager.CreateUserAsync(secondUsername);
+
+ Assert.NotNull(first);
+ Assert.NotNull(second);
+ Assert.NotEqual(first.Id, second.Id);
+ }
+
+ // ----- RenameUser tests -----
+
+ [Theory]
+ // Rename to non-ASCII name
+ [InlineData("alice", "münchen")]
+ // Rename between similar non-ASCII and ASCII
+ [InlineData("müller", "mueller")]
+ // Contains 'i': invariant uppercase is always 'I', never Turkish 'İ'
+ [InlineData("ali", "ALI2")]
+ // Rename to Spanish tilde-n name
+ [InlineData("testuser", "Ñoño")]
+ public async Task RenameUser_SetsNormalizedUsernameToUpperInvariant(
+ string originalName, string newName)
+ {
+ var user = await _userManager.CreateUserAsync(originalName);
+
+ await _userManager.RenameUser(user.Id, originalName, newName);
+
+ var renamed = _userManager.GetUserById(user.Id);
+ Assert.NotNull(renamed);
+ Assert.Equal(newName, renamed.Username);
+ Assert.Equal(newName.ToUpperInvariant(), renamed.NormalizedUsername);
+ }
+
+ [Theory]
+ // Same name different case: NormalizedUsername already taken
+ [InlineData("münchen", "MÜNCHEN")]
+ // Spanish, lowercase conflicts with existing uppercase-normalised entry
+ [InlineData("Ñoño", "ñoño")]
+ // ASCII, capitalised conflict
+ [InlineData("alice", "Alice")]
+ // Mixed ASCII + umlaut
+ [InlineData("testüser", "TESTÜSER")]
+ public async Task RenameUser_WhenNormalizedNameConflictsWithExistingUser_ThrowsArgumentException(
+ string existingUsername, string conflictingNewName)
+ {
+ var targetUser = await _userManager.CreateUserAsync("renametarget");
+ await _userManager.CreateUserAsync(existingUsername);
+
+ await Assert.ThrowsAsync<ArgumentException>(
+ () => _userManager.RenameUser(targetUser.Id, "renametarget", conflictingNewName));
+ }
+
+ private sealed class NoopEventManager : IEventManager
+ {
+ public void Publish<T>(T eventArgs)
+ where T : EventArgs
+ {
+ }
+
+ public Task PublishAsync<T>(T eventArgs)
+ where T : EventArgs
+ => Task.CompletedTask;
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs
index edbb46b34c..b9b2862c65 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs
@@ -23,6 +23,7 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa
[InlineData("Items/{0}/ThemeMedia")]
[InlineData("Items/{0}/Ancestors")]
[InlineData("Items/{0}/Download")]
+ [InlineData("Items/{0}/Collections")]
[InlineData("Artists/{0}/Similar")]
[InlineData("Items/{0}/Similar")]
[InlineData("Albums/{0}/Similar")]