aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs602
-rw-r--r--Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs6
-rw-r--r--Emby.Server.Implementations/AppBase/ConfigurationHelper.cs16
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs469
-rw-r--r--Emby.Server.Implementations/Browser/BrowserLauncher.cs49
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs66
-rw-r--r--Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs2
-rw-r--r--Emby.Server.Implementations/Collections/CollectionManager.cs112
-rw-r--r--Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs61
-rw-r--r--Emby.Server.Implementations/ConfigurationOptions.cs7
-rw-r--r--Emby.Server.Implementations/Cryptography/CryptographyProvider.cs4
-rw-r--r--Emby.Server.Implementations/Data/BaseSqliteRepository.cs25
-rw-r--r--Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs3
-rw-r--r--Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs225
-rw-r--r--Emby.Server.Implementations/Data/SqliteExtensions.cs4
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs595
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs21
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserRepository.cs240
-rw-r--r--Emby.Server.Implementations/Devices/DeviceId.cs4
-rw-r--r--Emby.Server.Implementations/Devices/DeviceManager.cs63
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs203
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj25
-rw-r--r--Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs4
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs110
-rw-r--r--Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs25
-rw-r--r--Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs77
-rw-r--r--Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs229
-rw-r--r--Emby.Server.Implementations/EntryPoints/StartupWizard.cs83
-rw-r--r--Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs19
-rw-r--r--Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs3
-rw-r--r--Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs335
-rw-r--r--Emby.Server.Implementations/HttpServer/FileWriter.cs252
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpListenerHost.cs763
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpResultFactory.cs713
-rw-r--r--Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs225
-rw-r--r--Emby.Server.Implementations/HttpServer/ResponseFilter.cs114
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs216
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs127
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/SessionContext.cs22
-rw-r--r--Emby.Server.Implementations/HttpServer/StreamWriter.cs120
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs31
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketManager.cs95
-rw-r--r--Emby.Server.Implementations/IO/FileRefresher.cs10
-rw-r--r--Emby.Server.Implementations/IO/LibraryMonitor.cs33
-rw-r--r--Emby.Server.Implementations/IO/LibraryMonitorStartup.cs35
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs48
-rw-r--r--Emby.Server.Implementations/IO/StreamHelper.cs32
-rw-r--r--Emby.Server.Implementations/IStartupOptions.cs12
-rw-r--r--Emby.Server.Implementations/Images/ArtistImageProvider.cs54
-rw-r--r--Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs26
-rw-r--r--Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs (renamed from Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs)7
-rw-r--r--Emby.Server.Implementations/Images/DynamicImageProvider.cs (renamed from Emby.Server.Implementations/UserViews/DynamicImageProvider.cs)9
-rw-r--r--Emby.Server.Implementations/Images/FolderImageProvider.cs (renamed from Emby.Server.Implementations/UserViews/FolderImageProvider.cs)16
-rw-r--r--Emby.Server.Implementations/Images/GenreImageProvider.cs (renamed from Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs)80
-rw-r--r--Emby.Server.Implementations/Images/PlaylistImageProvider.cs66
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs16
-rw-r--r--Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs177
-rw-r--r--Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs140
-rw-r--r--Emby.Server.Implementations/Library/ExclusiveLiveStream.cs24
-rw-r--r--Emby.Server.Implementations/Library/IgnorePatterns.cs73
-rw-r--r--Emby.Server.Implementations/Library/InvalidAuthProvider.cs54
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs485
-rw-r--r--Emby.Server.Implementations/Library/LiveStreamHelper.cs31
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs164
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs4
-rw-r--r--Emby.Server.Implementations/Library/MusicManager.cs20
-rw-r--r--Emby.Server.Implementations/Library/ResolverHelper.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs10
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs71
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs14
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs7
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs5
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs1
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs12
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs8
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs25
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs12
-rw-r--r--Emby.Server.Implementations/Library/UserManager.cs1107
-rw-r--r--Emby.Server.Implementations/Library/UserViewManager.cs30
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs4
-rw-r--r--Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs5
-rw-r--r--Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs4
-rw-r--r--Emby.Server.Implementations/Library/Validators/GenresValidator.cs4
-rw-r--r--Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs4
-rw-r--r--Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs4
-rw-r--r--Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs4
-rw-r--r--Emby.Server.Implementations/Library/Validators/StudiosValidator.cs5
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs51
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs66
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs57
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs1
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs530
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs39
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs270
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs11
-rw-r--r--Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs6
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs42
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs182
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs11
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs14
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs5
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs22
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs44
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs67
-rw-r--r--Emby.Server.Implementations/Localization/Core/af.json43
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/bg-BG.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/bn.json63
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json25
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json11
-rw-r--r--Emby.Server.Implementations/Localization/Core/el.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-GB.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/en-US.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-MX.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_419.json116
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_DO.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/fil.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr-CA.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/gl.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/gsw.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json57
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json51
-rw-r--r--Emby.Server.Implementations/Localization/Core/hu.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json53
-rw-r--r--Emby.Server.Implementations/Localization/Core/is.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/kk.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/ko.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/lt-LT.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/lv.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/mk.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/mr.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json65
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json24
-rw-r--r--Emby.Server.Implementations/Localization/Core/ne.json85
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/nn.json64
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-BR.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json17
-rw-r--r--Emby.Server.Implementations/Localization/Core/ro.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/sk.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/sq.json116
-rw-r--r--Emby.Server.Implementations/Localization/Core/sr.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/ta.json116
-rw-r--r--Emby.Server.Implementations/Localization/Core/th.json165
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json108
-rw-r--r--Emby.Server.Implementations/Localization/Core/ur_PK.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/vi.json116
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-CN.json1
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json57
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs11
-rw-r--r--Emby.Server.Implementations/MediaEncoder/EncodingManager.cs4
-rw-r--r--Emby.Server.Implementations/Net/SocketFactory.cs9
-rw-r--r--Emby.Server.Implementations/Net/UdpSocket.cs47
-rw-r--r--Emby.Server.Implementations/Networking/NetworkManager.cs204
-rw-r--r--Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs5
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs91
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManifest.cs60
-rw-r--r--Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs285
-rw-r--r--Emby.Server.Implementations/ResourceFileManager.cs4
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs187
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/TaskManager.cs37
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs17
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs64
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs61
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs13
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs10
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs50
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs17
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs23
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs22
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs25
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs25
-rw-r--r--Emby.Server.Implementations/Security/AuthenticationRepository.cs33
-rw-r--r--Emby.Server.Implementations/Serialization/JsonSerializer.cs5
-rw-r--r--Emby.Server.Implementations/Serialization/MyXmlSerializer.cs10
-rw-r--r--Emby.Server.Implementations/Services/HttpResult.cs62
-rw-r--r--Emby.Server.Implementations/Services/RequestHelper.cs52
-rw-r--r--Emby.Server.Implementations/Services/ResponseHelper.cs138
-rw-r--r--Emby.Server.Implementations/Services/ServiceController.cs188
-rw-r--r--Emby.Server.Implementations/Services/ServiceExec.cs214
-rw-r--r--Emby.Server.Implementations/Services/ServiceHandler.cs204
-rw-r--r--Emby.Server.Implementations/Services/ServiceMethod.cs17
-rw-r--r--Emby.Server.Implementations/Services/ServicePath.cs538
-rw-r--r--Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs114
-rw-r--r--Emby.Server.Implementations/Services/SwaggerService.cs251
-rw-r--r--Emby.Server.Implementations/Services/UrlExtensions.cs25
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs270
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs54
-rw-r--r--Emby.Server.Implementations/Session/WebSocketController.cs5
-rw-r--r--Emby.Server.Implementations/SocketSharp/HttpFile.cs18
-rw-r--r--Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs198
-rw-r--r--Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs246
-rw-r--r--Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs8
-rw-r--r--Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/AlbumComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs18
-rw-r--r--Emby.Server.Implementations/Sorting/CriticRatingComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/DateCreatedComparer.cs6
-rw-r--r--Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs19
-rw-r--r--Emby.Server.Implementations/Sorting/DatePlayedComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs39
-rw-r--r--Emby.Server.Implementations/Sorting/IsFolderComparer.cs14
-rw-r--r--Emby.Server.Implementations/Sorting/IsPlayedComparer.cs39
-rw-r--r--Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs39
-rw-r--r--Emby.Server.Implementations/Sorting/NameComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs14
-rw-r--r--Emby.Server.Implementations/Sorting/PlayCountComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/PremiereDateComparer.cs3
-rw-r--r--Emby.Server.Implementations/Sorting/ProductionYearComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/RandomComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/RuntimeComparer.cs6
-rw-r--r--Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs14
-rw-r--r--Emby.Server.Implementations/Sorting/SortNameComparer.cs6
-rw-r--r--Emby.Server.Implementations/Sorting/StartDateComparer.cs19
-rw-r--r--Emby.Server.Implementations/Sorting/StudioComparer.cs2
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayController.cs111
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs110
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs52
-rw-r--r--Emby.Server.Implementations/Udp/UdpServer.cs19
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs315
243 files changed, 5376 insertions, 11626 deletions
diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
deleted file mode 100644
index 3983824a3..000000000
--- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
+++ /dev/null
@@ -1,602 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
-using MediaBrowser.Common.Plugins;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Controller.Subtitles;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Notifications;
-using MediaBrowser.Model.Tasks;
-using MediaBrowser.Model.Updates;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Activity
-{
- /// <summary>
- /// Entry point for the activity logger.
- /// </summary>
- public sealed class ActivityLogEntryPoint : IServerEntryPoint
- {
- private readonly ILogger<ActivityLogEntryPoint> _logger;
- private readonly IInstallationManager _installationManager;
- private readonly ISessionManager _sessionManager;
- private readonly ITaskManager _taskManager;
- private readonly IActivityManager _activityManager;
- private readonly ILocalizationManager _localization;
- private readonly ISubtitleManager _subManager;
- private readonly IUserManager _userManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class.
- /// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="sessionManager">The session manager.</param>
- /// <param name="taskManager">The task manager.</param>
- /// <param name="activityManager">The activity manager.</param>
- /// <param name="localization">The localization manager.</param>
- /// <param name="installationManager">The installation manager.</param>
- /// <param name="subManager">The subtitle manager.</param>
- /// <param name="userManager">The user manager.</param>
- public ActivityLogEntryPoint(
- ILogger<ActivityLogEntryPoint> logger,
- ISessionManager sessionManager,
- ITaskManager taskManager,
- IActivityManager activityManager,
- ILocalizationManager localization,
- IInstallationManager installationManager,
- ISubtitleManager subManager,
- IUserManager userManager)
- {
- _logger = logger;
- _sessionManager = sessionManager;
- _taskManager = taskManager;
- _activityManager = activityManager;
- _localization = localization;
- _installationManager = installationManager;
- _subManager = subManager;
- _userManager = userManager;
- }
-
- /// <inheritdoc />
- public Task RunAsync()
- {
- _taskManager.TaskCompleted += OnTaskCompleted;
-
- _installationManager.PluginInstalled += OnPluginInstalled;
- _installationManager.PluginUninstalled += OnPluginUninstalled;
- _installationManager.PluginUpdated += OnPluginUpdated;
- _installationManager.PackageInstallationFailed += OnPackageInstallationFailed;
-
- _sessionManager.SessionStarted += OnSessionStarted;
- _sessionManager.AuthenticationFailed += OnAuthenticationFailed;
- _sessionManager.AuthenticationSucceeded += OnAuthenticationSucceeded;
- _sessionManager.SessionEnded += OnSessionEnded;
- _sessionManager.PlaybackStart += OnPlaybackStart;
- _sessionManager.PlaybackStopped += OnPlaybackStopped;
-
- _subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure;
-
- _userManager.UserCreated += OnUserCreated;
- _userManager.UserPasswordChanged += OnUserPasswordChanged;
- _userManager.UserDeleted += OnUserDeleted;
- _userManager.UserPolicyUpdated += OnUserPolicyUpdated;
- _userManager.UserLockedOut += OnUserLockedOut;
-
- return Task.CompletedTask;
- }
-
- private async void OnUserLockedOut(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserLockedOutWithName"),
- e.Argument.Name),
- NotificationType.UserLockedOut.ToString(),
- e.Argument.Id))
- .ConfigureAwait(false);
- }
-
- private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
- e.Provider,
- Notifications.NotificationEntryPoint.GetItemName(e.Item)),
- "SubtitleDownloadFailure",
- Guid.Empty)
- {
- ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
- ShortOverview = e.Exception.Message
- }).ConfigureAwait(false);
- }
-
- private async void OnPlaybackStopped(object sender, PlaybackStopEventArgs e)
- {
- var item = e.MediaInfo;
-
- if (item == null)
- {
- _logger.LogWarning("PlaybackStopped reported with null media info.");
- return;
- }
-
- if (e.Item != null && e.Item.IsThemeMedia)
- {
- // Don't report theme song or local trailer playback
- return;
- }
-
- if (e.Users.Count == 0)
- {
- return;
- }
-
- var user = e.Users[0];
-
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
- user.Name,
- GetItemName(item),
- e.DeviceName),
- GetPlaybackStoppedNotificationType(item.MediaType),
- user.Id))
- .ConfigureAwait(false);
- }
-
- private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
- {
- var item = e.MediaInfo;
-
- if (item == null)
- {
- _logger.LogWarning("PlaybackStart reported with null media info.");
- return;
- }
-
- if (e.Item != null && e.Item.IsThemeMedia)
- {
- // Don't report theme song or local trailer playback
- return;
- }
-
- if (e.Users.Count == 0)
- {
- return;
- }
-
- var user = e.Users.First();
-
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserStartedPlayingItemWithValues"),
- user.Name,
- GetItemName(item),
- e.DeviceName),
- GetPlaybackNotificationType(item.MediaType),
- user.Id))
- .ConfigureAwait(false);
- }
-
- private static string GetItemName(BaseItemDto item)
- {
- var name = item.Name;
-
- if (!string.IsNullOrEmpty(item.SeriesName))
- {
- name = item.SeriesName + " - " + name;
- }
-
- if (item.Artists != null && item.Artists.Count > 0)
- {
- name = item.Artists[0] + " - " + name;
- }
-
- return name;
- }
-
- private static string GetPlaybackNotificationType(string mediaType)
- {
- if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
- {
- return NotificationType.AudioPlayback.ToString();
- }
-
- if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
- {
- return NotificationType.VideoPlayback.ToString();
- }
-
- return null;
- }
-
- private static string GetPlaybackStoppedNotificationType(string mediaType)
- {
- if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
- {
- return NotificationType.AudioPlaybackStopped.ToString();
- }
-
- if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
- {
- return NotificationType.VideoPlaybackStopped.ToString();
- }
-
- return null;
- }
-
- private async void OnSessionEnded(object sender, SessionEventArgs e)
- {
- var session = e.SessionInfo;
-
- if (string.IsNullOrEmpty(session.UserName))
- {
- return;
- }
-
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserOfflineFromDevice"),
- session.UserName,
- session.DeviceName),
- "SessionEnded",
- session.UserId)
- {
- ShortOverview = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("LabelIpAddressValue"),
- session.RemoteEndPoint),
- }).ConfigureAwait(false);
- }
-
- private async void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
- {
- var user = e.Argument.User;
-
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("AuthenticationSucceededWithUserName"),
- user.Name),
- "AuthenticationSucceeded",
- user.Id)
- {
- ShortOverview = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("LabelIpAddressValue"),
- e.Argument.SessionInfo.RemoteEndPoint),
- }).ConfigureAwait(false);
- }
-
- private async void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("FailedLoginAttemptWithUserName"),
- e.Argument.Username),
- "AuthenticationFailed",
- Guid.Empty)
- {
- LogSeverity = LogLevel.Error,
- ShortOverview = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("LabelIpAddressValue"),
- e.Argument.RemoteEndPoint),
- }).ConfigureAwait(false);
- }
-
- private async void OnUserPolicyUpdated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserPolicyUpdatedWithName"),
- e.Argument.Name),
- "UserPolicyUpdated",
- e.Argument.Id))
- .ConfigureAwait(false);
- }
-
- private async void OnUserDeleted(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserDeletedWithName"),
- e.Argument.Name),
- "UserDeleted",
- Guid.Empty))
- .ConfigureAwait(false);
- }
-
- private async void OnUserPasswordChanged(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserPasswordChangedWithName"),
- e.Argument.Name),
- "UserPasswordChanged",
- e.Argument.Id))
- .ConfigureAwait(false);
- }
-
- private async void OnUserCreated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserCreatedWithName"),
- e.Argument.Name),
- "UserCreated",
- e.Argument.Id))
- .ConfigureAwait(false);
- }
-
- private async void OnSessionStarted(object sender, SessionEventArgs e)
- {
- var session = e.SessionInfo;
-
- if (string.IsNullOrEmpty(session.UserName))
- {
- return;
- }
-
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserOnlineFromDevice"),
- session.UserName,
- session.DeviceName),
- "SessionStarted",
- session.UserId)
- {
- ShortOverview = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("LabelIpAddressValue"),
- session.RemoteEndPoint)
- }).ConfigureAwait(false);
- }
-
- private async void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("PluginUpdatedWithName"),
- e.Argument.Item1.Name),
- NotificationType.PluginUpdateInstalled.ToString(),
- Guid.Empty)
- {
- ShortOverview = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("VersionNumber"),
- e.Argument.Item2.version),
- Overview = e.Argument.Item2.changelog
- }).ConfigureAwait(false);
- }
-
- private async void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("PluginUninstalledWithName"),
- e.Argument.Name),
- NotificationType.PluginUninstalled.ToString(),
- Guid.Empty))
- .ConfigureAwait(false);
- }
-
- private async void OnPluginInstalled(object sender, GenericEventArgs<VersionInfo> e)
- {
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("PluginInstalledWithName"),
- e.Argument.name),
- NotificationType.PluginInstalled.ToString(),
- Guid.Empty)
- {
- ShortOverview = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("VersionNumber"),
- e.Argument.version)
- }).ConfigureAwait(false);
- }
-
- private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
- {
- var installationInfo = e.InstallationInfo;
-
- await CreateLogEntry(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("NameInstallFailed"),
- installationInfo.Name),
- NotificationType.InstallationFailed.ToString(),
- Guid.Empty)
- {
- ShortOverview = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("VersionNumber"),
- installationInfo.Version),
- Overview = e.Exception.Message
- }).ConfigureAwait(false);
- }
-
- private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
- {
- var result = e.Result;
- var task = e.Task;
-
- if (task.ScheduledTask is IConfigurableScheduledTask activityTask
- && !activityTask.IsLogged)
- {
- return;
- }
-
- var time = result.EndTimeUtc - result.StartTimeUtc;
- var runningTime = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("LabelRunningTimeValue"),
- ToUserFriendlyString(time));
-
- if (result.Status == TaskCompletionStatus.Failed)
- {
- var vals = new List<string>();
-
- if (!string.IsNullOrEmpty(e.Result.ErrorMessage))
- {
- vals.Add(e.Result.ErrorMessage);
- }
-
- if (!string.IsNullOrEmpty(e.Result.LongErrorMessage))
- {
- vals.Add(e.Result.LongErrorMessage);
- }
-
- await CreateLogEntry(new ActivityLog(
- string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
- NotificationType.TaskFailed.ToString(),
- Guid.Empty)
- {
- LogSeverity = LogLevel.Error,
- Overview = string.Join(Environment.NewLine, vals),
- ShortOverview = runningTime
- }).ConfigureAwait(false);
- }
- }
-
- private async Task CreateLogEntry(ActivityLog entry)
- => await _activityManager.CreateAsync(entry).ConfigureAwait(false);
-
- /// <inheritdoc />
- public void Dispose()
- {
- _taskManager.TaskCompleted -= OnTaskCompleted;
-
- _installationManager.PluginInstalled -= OnPluginInstalled;
- _installationManager.PluginUninstalled -= OnPluginUninstalled;
- _installationManager.PluginUpdated -= OnPluginUpdated;
- _installationManager.PackageInstallationFailed -= OnPackageInstallationFailed;
-
- _sessionManager.SessionStarted -= OnSessionStarted;
- _sessionManager.AuthenticationFailed -= OnAuthenticationFailed;
- _sessionManager.AuthenticationSucceeded -= OnAuthenticationSucceeded;
- _sessionManager.SessionEnded -= OnSessionEnded;
-
- _sessionManager.PlaybackStart -= OnPlaybackStart;
- _sessionManager.PlaybackStopped -= OnPlaybackStopped;
-
- _subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure;
-
- _userManager.UserCreated -= OnUserCreated;
- _userManager.UserPasswordChanged -= OnUserPasswordChanged;
- _userManager.UserDeleted -= OnUserDeleted;
- _userManager.UserPolicyUpdated -= OnUserPolicyUpdated;
- _userManager.UserLockedOut -= OnUserLockedOut;
- }
-
- /// <summary>
- /// Constructs a user-friendly string for this TimeSpan instance.
- /// </summary>
- private static string ToUserFriendlyString(TimeSpan span)
- {
- const int DaysInYear = 365;
- const int DaysInMonth = 30;
-
- // Get each non-zero value from TimeSpan component
- var values = new List<string>();
-
- // Number of years
- int days = span.Days;
- if (days >= DaysInYear)
- {
- int years = days / DaysInYear;
- values.Add(CreateValueString(years, "year"));
- days %= DaysInYear;
- }
-
- // Number of months
- if (days >= DaysInMonth)
- {
- int months = days / DaysInMonth;
- values.Add(CreateValueString(months, "month"));
- days = days % DaysInMonth;
- }
-
- // Number of days
- if (days >= 1)
- {
- values.Add(CreateValueString(days, "day"));
- }
-
- // Number of hours
- if (span.Hours >= 1)
- {
- values.Add(CreateValueString(span.Hours, "hour"));
- }
-
- // Number of minutes
- if (span.Minutes >= 1)
- {
- values.Add(CreateValueString(span.Minutes, "minute"));
- }
-
- // Number of seconds (include when 0 if no other components included)
- if (span.Seconds >= 1 || values.Count == 0)
- {
- values.Add(CreateValueString(span.Seconds, "second"));
- }
-
- // Combine values into string
- var builder = new StringBuilder();
- for (int i = 0; i < values.Count; i++)
- {
- if (builder.Length > 0)
- {
- builder.Append(i == values.Count - 1 ? " and " : ", ");
- }
-
- builder.Append(values[i]);
- }
-
- // Return result
- return builder.ToString();
- }
-
- /// <summary>
- /// Constructs a string description of a time-span value.
- /// </summary>
- /// <param name="value">The value of this item.</param>
- /// <param name="description">The name of this item (singular form).</param>
- private static string CreateValueString(int value, string description)
- {
- return string.Format(
- CultureInfo.InvariantCulture,
- "{0:#,##0} {1}",
- value,
- value == 1 ? description : string.Format(CultureInfo.InvariantCulture, "{0}s", description));
- }
- }
-}
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index 080cfbbd1..4ab0a2a3f 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.AppBase
CommonApplicationPaths = applicationPaths;
XmlSerializer = xmlSerializer;
_fileSystem = fileSystem;
- Logger = loggerFactory.CreateLogger(GetType().Name);
+ Logger = loggerFactory.CreateLogger<BaseConfigurationManager>();
UpdateCachePath();
}
@@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.AppBase
/// Gets the logger.
/// </summary>
/// <value>The logger.</value>
- protected ILogger Logger { get; private set; }
+ protected ILogger<BaseConfigurationManager> Logger { get; private set; }
/// <summary>
/// Gets the XML serializer.
@@ -308,7 +308,7 @@ namespace Emby.Server.Implementations.AppBase
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error loading configuration file: {path}", path);
+ Logger.LogError(ex, "Error loading configuration file: {Path}", path);
return Activator.CreateInstance(configurationType);
}
diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
index 0b681fddf..4c9ab33a7 100644
--- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
+++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
using System;
using System.IO;
using System.Linq;
@@ -22,7 +24,7 @@ namespace Emby.Server.Implementations.AppBase
{
object configuration;
- byte[] buffer = null;
+ byte[]? buffer = null;
// Use try/catch to avoid the extra file system lookup using File.Exists
try
@@ -36,19 +38,23 @@ namespace Emby.Server.Implementations.AppBase
configuration = Activator.CreateInstance(type);
}
- using var stream = new MemoryStream();
+ using var stream = new MemoryStream(buffer?.Length ?? 0);
xmlSerializer.SerializeToStream(configuration, stream);
// Take the object we just got and serialize it back to bytes
- var newBytes = stream.ToArray();
+ byte[] newBytes = stream.GetBuffer();
+ int newBytesLen = (int)stream.Length;
// If the file didn't exist before, or if something has changed, re-save
- if (buffer == null || !buffer.SequenceEqual(newBytes))
+ if (buffer == null || !newBytes.AsSpan(0, newBytesLen).SequenceEqual(buffer))
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
// Save it after load in case we got new items
- File.WriteAllBytes(path, newBytes);
+ using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
+ {
+ fs.Write(newBytes, 0, newBytesLen);
+ }
}
return configuration;
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index be4e05a64..31ca73829 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -38,23 +38,24 @@ using Emby.Server.Implementations.LiveTv;
using Emby.Server.Implementations.Localization;
using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Playlists;
+using Emby.Server.Implementations.Plugins;
+using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization;
-using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.Session;
+using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
-using Emby.Server.Implementations.SyncPlay;
-using MediaBrowser.Api;
+using Jellyfin.Api.Helpers;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller;
-using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Collections;
@@ -73,13 +74,14 @@ using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Subtitles;
-using MediaBrowser.Controller.TV;
using MediaBrowser.Controller.SyncPlay;
+using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.Model.Configuration;
@@ -90,20 +92,20 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb;
+using MediaBrowser.Providers.Plugins.Tmdb;
using MediaBrowser.Providers.Subtitles;
-using MediaBrowser.WebDashboard.Api;
using MediaBrowser.XbmcMetadata.Providers;
-using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
+using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
namespace Emby.Server.Implementations
{
@@ -120,18 +122,22 @@ namespace Emby.Server.Implementations
private readonly IFileSystem _fileSystemManager;
private readonly INetworkManager _networkManager;
private readonly IXmlSerializer _xmlSerializer;
+ private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions;
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
- private IHttpServer _httpServer;
- private IHttpClient _httpClient;
+ private IHttpClientFactory _httpClientFactory;
+
+ private string[] _urlPrefixes;
/// <summary>
/// Gets a value indicating whether this instance can self restart.
/// </summary>
public bool CanSelfRestart => _startupOptions.RestartPath != null;
+ public bool CoreStartupHasCompleted { get; private set; }
+
public virtual bool CanLaunchWebBrowser
{
get
@@ -173,7 +179,9 @@ namespace Emby.Server.Implementations
/// <summary>
/// Gets the logger.
/// </summary>
- protected ILogger Logger { get; }
+ protected ILogger<ApplicationHost> Logger { get; }
+
+ protected IServiceCollection ServiceCollection { get; }
private IPlugin[] _plugins;
@@ -192,7 +200,7 @@ namespace Emby.Server.Implementations
/// Gets or sets the application paths.
/// </summary>
/// <value>The application paths.</value>
- protected ServerApplicationPaths ApplicationPaths { get; set; }
+ protected IServerApplicationPaths ApplicationPaths { get; set; }
/// <summary>
/// Gets or sets all concrete types.
@@ -233,16 +241,26 @@ namespace Emby.Server.Implementations
public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
/// <summary>
- /// Initializes a new instance of the <see cref="ApplicationHost" /> class.
+ /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
/// </summary>
+ /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
+ /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
+ /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+ /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
public ApplicationHost(
- ServerApplicationPaths applicationPaths,
+ IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IFileSystem fileSystem,
- INetworkManager networkManager)
+ INetworkManager networkManager,
+ IServiceCollection serviceCollection)
{
_xmlSerializer = new MyXmlSerializer();
+ _jsonSerializer = new JsonSerializer();
+
+ ServiceCollection = serviceCollection;
_networkManager = networkManager;
networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets;
@@ -273,6 +291,10 @@ namespace Emby.Server.Implementations
Password = ServerConfigurationManager.Configuration.CertificatePassword
};
Certificate = GetCertificate(CertificateInfo);
+
+ ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
+ ApplicationVersionString = ApplicationVersion.ToString(3);
+ ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
}
public string ExpandVirtualPath(string path)
@@ -302,16 +324,16 @@ namespace Emby.Server.Implementations
}
/// <inheritdoc />
- public Version ApplicationVersion { get; } = typeof(ApplicationHost).Assembly.GetName().Version;
+ public Version ApplicationVersion { get; }
/// <inheritdoc />
- public string ApplicationVersionString { get; } = typeof(ApplicationHost).Assembly.GetName().Version.ToString(3);
+ public string ApplicationVersionString { get; }
/// <summary>
/// Gets the current application user agent.
/// </summary>
/// <value>The application user agent.</value>
- public string ApplicationUserAgent => Name.Replace(' ', '-') + "/" + ApplicationVersionString;
+ public string ApplicationUserAgent { get; }
/// <summary>
/// Gets the email address for use within a comment section of a user agent field.
@@ -442,8 +464,7 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
Logger.LogInformation("Core startup complete");
- _httpServer.GlobalResponse = null;
-
+ CoreStartupHasCompleted = true;
stopWatch.Restart();
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
@@ -466,7 +487,7 @@ namespace Emby.Server.Implementations
}
/// <inheritdoc/>
- public void Init(IServiceCollection serviceCollection)
+ public void Init()
{
HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber;
HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber;
@@ -484,12 +505,10 @@ namespace Emby.Server.Implementations
foreach (var plugin in Plugins)
{
- pluginBuilder.AppendLine(
- string.Format(
- CultureInfo.InvariantCulture,
- "{0} {1}",
- plugin.Name,
- plugin.Version));
+ pluginBuilder.Append(plugin.Name)
+ .Append(' ')
+ .Append(plugin.Version)
+ .AppendLine();
}
Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
@@ -497,150 +516,142 @@ namespace Emby.Server.Implementations
DiscoverTypes();
- RegisterServices(serviceCollection);
+ RegisterServices();
}
- public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
- => _httpServer.RequestHandler(context);
-
/// <summary>
/// Registers services/resources with the service collection that will be available via DI.
/// </summary>
- protected virtual void RegisterServices(IServiceCollection serviceCollection)
+ protected virtual void RegisterServices()
{
- serviceCollection.AddSingleton(_startupOptions);
-
- serviceCollection.AddMemoryCache();
-
- serviceCollection.AddSingleton(ConfigurationManager);
- serviceCollection.AddSingleton<IApplicationHost>(this);
-
- serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
-
- serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
+ ServiceCollection.AddSingleton(_startupOptions);
- serviceCollection.AddSingleton(_fileSystemManager);
- serviceCollection.AddSingleton<TvdbClientManager>();
+ ServiceCollection.AddMemoryCache();
- serviceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
+ ServiceCollection.AddSingleton(ConfigurationManager);
+ ServiceCollection.AddSingleton<IApplicationHost>(this);
- serviceCollection.AddSingleton(_networkManager);
+ ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
- serviceCollection.AddSingleton<IIsoManager, IsoManager>();
+ ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
- serviceCollection.AddSingleton<ITaskManager, TaskManager>();
+ ServiceCollection.AddSingleton(_fileSystemManager);
+ ServiceCollection.AddSingleton<TvdbClientManager>();
+ ServiceCollection.AddSingleton<TmdbClientManager>();
- serviceCollection.AddSingleton(_xmlSerializer);
+ ServiceCollection.AddSingleton(_networkManager);
- serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
+ ServiceCollection.AddSingleton<IIsoManager, IsoManager>();
- serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
+ ServiceCollection.AddSingleton<ITaskManager, TaskManager>();
- serviceCollection.AddSingleton<ISocketFactory, SocketFactory>();
+ ServiceCollection.AddSingleton(_xmlSerializer);
- serviceCollection.AddSingleton<IInstallationManager, InstallationManager>();
+ ServiceCollection.AddSingleton<IStreamHelper, StreamHelper>();
- serviceCollection.AddSingleton<IZipClient, ZipClient>();
+ ServiceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
- serviceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>();
+ ServiceCollection.AddSingleton<ISocketFactory, SocketFactory>();
- serviceCollection.AddSingleton<IServerApplicationHost>(this);
- serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
+ ServiceCollection.AddSingleton<IInstallationManager, InstallationManager>();
- serviceCollection.AddSingleton(ServerConfigurationManager);
+ ServiceCollection.AddSingleton<IZipClient, ZipClient>();
- serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
+ ServiceCollection.AddSingleton<IServerApplicationHost>(this);
+ ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
- serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
+ ServiceCollection.AddSingleton(ServerConfigurationManager);
- serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
- serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
+ ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
- serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>();
+ ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
- serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
+ ServiceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
+ ServiceCollection.AddSingleton<IUserDataManager, UserDataManager>();
- serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
+ ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
- serviceCollection.AddSingleton<IUserRepository, SqliteUserRepository>();
+ ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
- serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
- serviceCollection.AddSingleton<IUserManager, UserManager>();
+ ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
- // TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation
- serviceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
- serviceCollection.AddSingleton<IMediaEncoder>(provider =>
- ActivatorUtilities.CreateInstance<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(provider, _startupOptions.FFmpegPath ?? string.Empty));
+ ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
+ ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
- serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
- serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
- serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
- serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
+ ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
+ ServiceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
+ ServiceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
+ ServiceCollection.AddSingleton<ILibraryManager, LibraryManager>();
- serviceCollection.AddSingleton<IMusicManager, MusicManager>();
+ ServiceCollection.AddSingleton<IMusicManager, MusicManager>();
- serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
+ ServiceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
- serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
+ ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
- serviceCollection.AddSingleton<ServiceController>();
- serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
+ ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
- serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
+ ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
- serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
+ ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
- serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
+ ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
- serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
+ ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
- serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
+ ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
- serviceCollection.AddSingleton<IProviderManager, ProviderManager>();
+ ServiceCollection.AddSingleton<IProviderManager, ProviderManager>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
- serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
- serviceCollection.AddSingleton<IDtoService, DtoService>();
+ ServiceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
+ ServiceCollection.AddSingleton<IDtoService, DtoService>();
- serviceCollection.AddSingleton<IChannelManager, ChannelManager>();
+ ServiceCollection.AddSingleton<IChannelManager, ChannelManager>();
- serviceCollection.AddSingleton<ISessionManager, SessionManager>();
+ ServiceCollection.AddSingleton<ISessionManager, SessionManager>();
- serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
+ ServiceCollection.AddSingleton<IDlnaManager, DlnaManager>();
- serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
+ ServiceCollection.AddSingleton<ICollectionManager, CollectionManager>();
- serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
+ ServiceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
- serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
+ ServiceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
- serviceCollection.AddSingleton<LiveTvDtoService>();
- serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
+ ServiceCollection.AddSingleton<LiveTvDtoService>();
+ ServiceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
- serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
+ ServiceCollection.AddSingleton<IUserViewManager, UserViewManager>();
- serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
+ ServiceCollection.AddSingleton<INotificationManager, NotificationManager>();
- serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
+ ServiceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
- serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
+ ServiceCollection.AddSingleton<IChapterManager, ChapterManager>();
- serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
+ ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
- serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
- serviceCollection.AddSingleton<ISessionContext, SessionContext>();
+ ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
+ ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
- serviceCollection.AddSingleton<IAuthService, AuthService>();
+ ServiceCollection.AddSingleton<IAuthService, AuthService>();
+ ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
- serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
+ ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
- serviceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
- serviceCollection.AddSingleton<EncodingHelper>();
+ ServiceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
+ ServiceCollection.AddSingleton<EncodingHelper>();
- serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
+ ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
+
+ ServiceCollection.AddSingleton<TranscodingJobHelper>();
+ ServiceCollection.AddScoped<MediaInfoHelper>();
+ ServiceCollection.AddScoped<AudioHelper>();
+ ServiceCollection.AddScoped<DynamicHlsHelper>();
}
/// <summary>
@@ -654,20 +665,14 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>();
- _httpServer = Resolve<IHttpServer>();
- _httpClient = Resolve<IHttpClient>();
+ _httpClientFactory = Resolve<IHttpClientFactory>();
- ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
- ((SqliteUserRepository)Resolve<IUserRepository>()).Initialize();
SetStaticProperties();
- var userManager = (UserManager)Resolve<IUserManager>();
- userManager.Initialize();
-
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
- ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, userManager);
+ ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, Resolve<IUserManager>());
FindParts();
}
@@ -750,7 +755,6 @@ namespace Emby.Server.Implementations
BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.ItemRepository = Resolve<IItemRepository>();
- User.UserManager = Resolve<IUserManager>();
BaseItem.FileSystem = _fileSystemManager;
BaseItem.UserDataManager = Resolve<IUserDataManager>();
BaseItem.ChannelManager = Resolve<IChannelManager>();
@@ -762,7 +766,6 @@ namespace Emby.Server.Implementations
CollectionFolder.XmlSerializer = _xmlSerializer;
CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
CollectionFolder.ApplicationHost = this;
- AuthenticatedAttribute.AuthService = Resolve<IAuthService>();
}
/// <summary>
@@ -782,7 +785,7 @@ namespace Emby.Server.Implementations
.Where(i => i != null)
.ToArray();
- _httpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes());
+ _urlPrefixes = GetUrlPrefixes().ToArray();
Resolve<ILibraryManager>().AddParts(
GetExports<IResolverIgnoreRule>(),
@@ -807,7 +810,6 @@ namespace Emby.Server.Implementations
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
- Resolve<IUserManager>().AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
}
@@ -816,37 +818,7 @@ namespace Emby.Server.Implementations
{
try
{
- if (plugin is IPluginAssembly assemblyPlugin)
- {
- var assembly = plugin.GetType().Assembly;
- var assemblyName = assembly.GetName();
- var assemblyFilePath = assembly.Location;
-
- var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
-
- assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
-
- try
- {
- var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
- if (idAttributes.Length > 0)
- {
- var attribute = (GuidAttribute)idAttributes[0];
- var assemblyId = new Guid(attribute.Value);
-
- assemblyPlugin.SetId(assemblyId);
- }
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error getting plugin Id from {PluginName}.", plugin.GetType().FullName);
- }
- }
-
- if (plugin is IHasPluginConfiguration hasPluginConfiguration)
- {
- hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
- }
+ plugin.RegisterServices(ServiceCollection);
}
catch (Exception ex)
{
@@ -881,6 +853,11 @@ namespace Emby.Server.Implementations
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
continue;
}
+ catch (TypeLoadException ex)
+ {
+ Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
+ continue;
+ }
foreach (Type type in exportedTypes)
{
@@ -942,7 +919,7 @@ namespace Emby.Server.Implementations
}
}
- if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
+ if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
{
requiresRestart = true;
}
@@ -964,7 +941,7 @@ namespace Emby.Server.Implementations
}
/// <summary>
- /// Notifies that the kernel that a change has been made that requires a restart
+ /// Notifies that the kernel that a change has been made that requires a restart.
/// </summary>
public void NotifyPendingRestart()
{
@@ -1017,6 +994,119 @@ namespace Emby.Server.Implementations
protected abstract void RestartInternal();
/// <summary>
+ /// Comparison function used in <see cref="GetPlugins" />.
+ /// </summary>
+ /// <param name="a">Item to compare.</param>
+ /// <param name="b">Item to compare with.</param>
+ /// <returns>Boolean result of the operation.</returns>
+ private static int VersionCompare(
+ (Version PluginVersion, string Name, string Path) a,
+ (Version PluginVersion, string Name, string Path) b)
+ {
+ int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
+
+ if (compare == 0)
+ {
+ return a.PluginVersion.CompareTo(b.PluginVersion);
+ }
+
+ return compare;
+ }
+
+ /// <summary>
+ /// Returns a list of plugins to install.
+ /// </summary>
+ /// <param name="path">Path to check.</param>
+ /// <param name="cleanup">True if an attempt should be made to delete old plugs.</param>
+ /// <returns>Enumerable list of dlls to load.</returns>
+ private IEnumerable<string> GetPlugins(string path, bool cleanup = true)
+ {
+ var dllList = new List<string>();
+ var versions = new List<(Version PluginVersion, string Name, string Path)>();
+ var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
+ string metafile;
+
+ foreach (var dir in directories)
+ {
+ try
+ {
+ metafile = Path.Combine(dir, "meta.json");
+ if (File.Exists(metafile))
+ {
+ var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
+
+ if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
+ {
+ targetAbi = new Version(0, 0, 0, 1);
+ }
+
+ if (!Version.TryParse(manifest.Version, out var version))
+ {
+ version = new Version(0, 0, 0, 1);
+ }
+
+ if (ApplicationVersion >= targetAbi)
+ {
+ // Only load Plugins if the plugin is built for this version or below.
+ versions.Add((version, manifest.Name, dir));
+ }
+ }
+ else
+ {
+ // No metafile, so lets see if the folder is versioned.
+ metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
+
+ int versionIndex = dir.LastIndexOf('_');
+ if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver))
+ {
+ // Versioned folder.
+ versions.Add((ver, metafile, dir));
+ }
+ else
+ {
+ // Un-versioned folder - Add it under the path name and version 0.0.0.1.
+ versions.Add((new Version(0, 0, 0, 1), metafile, dir));
+ }
+ }
+ }
+ catch
+ {
+ continue;
+ }
+ }
+
+ string lastName = string.Empty;
+ versions.Sort(VersionCompare);
+ // Traverse backwards through the list.
+ // The first item will be the latest version.
+ for (int x = versions.Count - 1; x >= 0; x--)
+ {
+ if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
+ {
+ dllList.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
+ lastName = versions[x].Name;
+ continue;
+ }
+
+ if (!string.IsNullOrEmpty(lastName) && cleanup)
+ {
+ // Attempt a cleanup of old folders.
+ try
+ {
+ Logger.LogDebug("Deleting {Path}", versions[x].Path);
+ Directory.Delete(versions[x].Path, true);
+ }
+ catch (Exception e)
+ {
+ Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
+ }
+ }
+ }
+
+ return dllList;
+ }
+
+ /// <summary>
/// Gets the composable part assemblies.
/// </summary>
/// <returns>IEnumerable{Assembly}.</returns>
@@ -1024,7 +1114,7 @@ namespace Emby.Server.Implementations
{
if (Directory.Exists(ApplicationPaths.PluginsPath))
{
- foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.AllDirectories))
+ foreach (var file in GetPlugins(ApplicationPaths.PluginsPath))
{
Assembly plugAss;
try
@@ -1042,12 +1132,6 @@ namespace Emby.Server.Implementations
}
}
- // Include composable parts in the Api assembly
- yield return typeof(ApiEntryPoint).Assembly;
-
- // Include composable parts in the Dashboard assembly
- yield return typeof(DashboardService).Assembly;
-
// Include composable parts in the Model assembly
yield return typeof(SystemInfo).Assembly;
@@ -1144,7 +1228,8 @@ namespace Emby.Server.Implementations
Id = SystemId,
OperatingSystem = OperatingSystem.Id.ToString(),
ServerName = FriendlyName,
- LocalAddress = localAddress
+ LocalAddress = localAddress,
+ StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
};
}
@@ -1163,7 +1248,7 @@ namespace Emby.Server.Implementations
return null;
}
- return GetLocalApiUrl(addresses.First());
+ return GetLocalApiUrl(addresses[0]);
}
catch (Exception ex)
{
@@ -1236,13 +1321,13 @@ namespace Emby.Server.Implementations
var addresses = ServerConfigurationManager
.Configuration
.LocalNetworkAddresses
- .Select(NormalizeConfiguredLocalAddress)
+ .Select(x => NormalizeConfiguredLocalAddress(x))
.Where(i => i != null)
.ToList();
if (addresses.Count == 0)
{
- addresses.AddRange(_networkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces));
+ addresses.AddRange(_networkManager.GetLocalIpAddresses());
}
var resultList = new List<IPAddress>();
@@ -1257,8 +1342,7 @@ namespace Emby.Server.Implementations
}
}
- var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false);
- if (valid)
+ if (await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false))
{
resultList.Add(address);
@@ -1272,13 +1356,12 @@ namespace Emby.Server.Implementations
return resultList;
}
- public IPAddress NormalizeConfiguredLocalAddress(string address)
+ public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan<char> address)
{
var index = address.Trim('/').IndexOf('/');
-
if (index != -1)
{
- address = address.Substring(index + 1);
+ address = address.Slice(index + 1);
}
if (IPAddress.TryParse(address.Trim('/'), out IPAddress result))
@@ -1308,25 +1391,17 @@ namespace Emby.Server.Implementations
try
{
- using (var response = await _httpClient.SendAsync(
- new HttpRequestOptions
- {
- Url = apiUrl,
- LogErrorResponseBody = false,
- BufferContent = false,
- CancellationToken = cancellationToken
- }, HttpMethod.Post).ConfigureAwait(false))
- {
- using (var reader = new StreamReader(response.Content))
- {
- var result = await reader.ReadToEndAsync().ConfigureAwait(false);
- var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
+ using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl);
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid);
- Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid);
- return valid;
- }
- }
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var result = await System.Text.Json.JsonSerializer.DeserializeAsync<string>(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false);
+ var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
+
+ _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid);
+ Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid);
+ return valid;
}
catch (OperationCanceledException)
{
@@ -1404,6 +1479,20 @@ namespace Emby.Server.Implementations
_plugins = list.ToArray();
}
+ public IEnumerable<Assembly> GetApiPluginAssemblies()
+ {
+ var assemblies = _allConcreteTypes
+ .Where(i => typeof(ControllerBase).IsAssignableFrom(i))
+ .Select(i => i.Assembly)
+ .Distinct();
+
+ foreach (var assembly in assemblies)
+ {
+ Logger.LogDebug("Found API endpoints in plugin {Name}", assembly.FullName);
+ yield return assembly;
+ }
+ }
+
public virtual void LaunchUrl(string url)
{
if (!CanLaunchWebBrowser)
@@ -1434,10 +1523,6 @@ namespace Emby.Server.Implementations
}
}
- public virtual void EnableLoopback(string appName)
- {
- }
-
private bool _disposed = false;
/// <summary>
diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs
deleted file mode 100644
index 7f7c6a0be..000000000
--- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using System;
-using MediaBrowser.Controller;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Browser
-{
- /// <summary>
- /// Assists in opening application URLs in an external browser.
- /// </summary>
- public static class BrowserLauncher
- {
- /// <summary>
- /// Opens the home page of the web client.
- /// </summary>
- /// <param name="appHost">The app host.</param>
- public static void OpenWebApp(IServerApplicationHost appHost)
- {
- TryOpenUrl(appHost, "/web/index.html");
- }
-
- /// <summary>
- /// Opens the swagger API page.
- /// </summary>
- /// <param name="appHost">The app host.</param>
- public static void OpenSwaggerPage(IServerApplicationHost appHost)
- {
- TryOpenUrl(appHost, "/swagger/index.html");
- }
-
- /// <summary>
- /// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored.
- /// </summary>
- /// <param name="appHost">The application host.</param>
- /// <param name="relativeUrl">The URL to open, relative to the server base URL.</param>
- private static void TryOpenUrl(IServerApplicationHost appHost, string relativeUrl)
- {
- try
- {
- string baseUrl = appHost.GetLocalApiUrl("localhost");
- appHost.LaunchUrl(baseUrl + relativeUrl);
- }
- catch (Exception ex)
- {
- var logger = appHost.Resolve<ILogger>();
- logger?.LogError(ex, "Failed to open browser window with URL {URL}", relativeUrl);
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 138832fb8..fb1bb65a0 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -1,11 +1,12 @@
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
@@ -13,8 +14,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Channels;
@@ -23,7 +22,13 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Season = MediaBrowser.Controller.Entities.TV.Season;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace Emby.Server.Implementations.Channels
{
@@ -36,29 +41,27 @@ namespace Emby.Server.Implementations.Channels
private readonly IUserDataManager _userDataManager;
private readonly IDtoService _dtoService;
private readonly ILibraryManager _libraryManager;
- private readonly ILogger _logger;
+ private readonly ILogger<ChannelManager> _logger;
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
private readonly IJsonSerializer _jsonSerializer;
private readonly IProviderManager _providerManager;
-
- private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo =
- new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
-
+ private readonly IMemoryCache _memoryCache;
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
-
+
/// <summary>
/// Initializes a new instance of the <see cref="ChannelManager"/> class.
/// </summary>
/// <param name="userManager">The user manager.</param>
/// <param name="dtoService">The dto service.</param>
/// <param name="libraryManager">The library manager.</param>
- /// <param name="loggerFactory">The logger factory.</param>
+ /// <param name="logger">The logger.</param>
/// <param name="config">The server configuration manager.</param>
/// <param name="fileSystem">The filesystem.</param>
/// <param name="userDataManager">The user data manager.</param>
/// <param name="jsonSerializer">The JSON serializer.</param>
/// <param name="providerManager">The provider manager.</param>
+ /// <param name="memoryCache">The memory cache.</param>
public ChannelManager(
IUserManager userManager,
IDtoService dtoService,
@@ -68,7 +71,8 @@ namespace Emby.Server.Implementations.Channels
IFileSystem fileSystem,
IUserDataManager userDataManager,
IJsonSerializer jsonSerializer,
- IProviderManager providerManager)
+ IProviderManager providerManager,
+ IMemoryCache memoryCache)
{
_userManager = userManager;
_dtoService = dtoService;
@@ -79,6 +83,7 @@ namespace Emby.Server.Implementations.Channels
_userDataManager = userDataManager;
_jsonSerializer = jsonSerializer;
_providerManager = providerManager;
+ _memoryCache = memoryCache;
}
internal IChannel[] Channels { get; private set; }
@@ -413,20 +418,15 @@ namespace Emby.Server.Implementations.Channels
private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
{
- if (_channelItemMediaInfo.TryGetValue(id, out Tuple<DateTime, List<MediaSourceInfo>> cachedInfo))
+ if (_memoryCache.TryGetValue(id, out List<MediaSourceInfo> cachedInfo))
{
- if ((DateTime.UtcNow - cachedInfo.Item1).TotalMinutes < 5)
- {
- return cachedInfo.Item2;
- }
+ return cachedInfo;
}
var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
.ConfigureAwait(false);
var list = mediaInfo.ToList();
-
- var item2 = new Tuple<DateTime, List<MediaSourceInfo>>(DateTime.UtcNow, list);
- _channelItemMediaInfo.AddOrUpdate(id, item2, (key, oldValue) => item2);
+ _memoryCache.Set(id, list, DateTimeOffset.UtcNow.AddMinutes(5));
return list;
}
@@ -746,12 +746,21 @@ namespace Emby.Server.Implementations.Channels
// null if came from cache
if (itemsResult != null)
{
- var internalItems = itemsResult.Items
- .Select(i => GetChannelItemEntity(i, channelProvider, channel.Id, parentItem, cancellationToken))
- .ToArray();
+ var items = itemsResult.Items;
+ var itemsLen = items.Count;
+ var internalItems = new Guid[itemsLen];
+ for (int i = 0; i < itemsLen; i++)
+ {
+ internalItems[i] = (await GetChannelItemEntityAsync(
+ items[i],
+ channelProvider,
+ channel.Id,
+ parentItem,
+ cancellationToken).ConfigureAwait(false)).Id;
+ }
var existingIds = _libraryManager.GetItemIds(query);
- var deadIds = existingIds.Except(internalItems.Select(i => i.Id))
+ var deadIds = existingIds.Except(internalItems)
.ToArray();
foreach (var deadId in deadIds)
@@ -791,7 +800,8 @@ namespace Emby.Server.Implementations.Channels
return result;
}
- private async Task<ChannelItemResult> GetChannelItems(IChannel channel,
+ private async Task<ChannelItemResult> GetChannelItems(
+ IChannel channel,
User user,
string externalFolderId,
ChannelItemSortField? sortField,
@@ -880,7 +890,7 @@ namespace Emby.Server.Implementations.Channels
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error writing to channel cache file: {path}", path);
+ _logger.LogError(ex, "Error writing to channel cache file: {Path}", path);
}
}
@@ -962,7 +972,7 @@ namespace Emby.Server.Implementations.Channels
return item;
}
- private BaseItem GetChannelItemEntity(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, BaseItem parentFolder, CancellationToken cancellationToken)
+ private async Task<BaseItem> GetChannelItemEntityAsync(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, BaseItem parentFolder, CancellationToken cancellationToken)
{
var parentFolderId = parentFolder.Id;
@@ -1067,7 +1077,7 @@ namespace Emby.Server.Implementations.Channels
}
// was used for status
- //if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
+ // if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
//{
// item.ExternalEtag = info.Etag;
// forceUpdate = true;
@@ -1164,7 +1174,7 @@ namespace Emby.Server.Implementations.Channels
}
else if (forceUpdate)
{
- item.UpdateToRepository(ItemUpdateType.None, cancellationToken);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.None, cancellationToken).ConfigureAwait(false);
}
if ((isNew || forceUpdate) && info.Type == ChannelItemType.Media)
diff --git a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
index 54b621e25..e5dde48d8 100644
--- a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
+++ b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Channels
public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask
{
private readonly IChannelManager _channelManager;
- private readonly ILogger _logger;
+ private readonly ILogger<RefreshChannelsScheduledTask> _logger;
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization;
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index 7c518d483..3011a37e3 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
@@ -29,7 +30,7 @@ namespace Emby.Server.Implementations.Collections
private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _fileSystem;
private readonly ILibraryMonitor _iLibraryMonitor;
- private readonly ILogger _logger;
+ private readonly ILogger<CollectionManager> _logger;
private readonly IProviderManager _providerManager;
private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths;
@@ -56,7 +57,7 @@ namespace Emby.Server.Implementations.Collections
_libraryManager = libraryManager;
_fileSystem = fileSystem;
_iLibraryMonitor = iLibraryMonitor;
- _logger = loggerFactory.CreateLogger(nameof(CollectionManager));
+ _logger = loggerFactory.CreateLogger<CollectionManager>();
_providerManager = providerManager;
_localizationManager = localizationManager;
_appPaths = appPaths;
@@ -131,7 +132,7 @@ namespace Emby.Server.Implementations.Collections
}
/// <inheritdoc />
- public BoxSet CreateCollection(CollectionCreationOptions options)
+ public async Task<BoxSet> CreateCollectionAsync(CollectionCreationOptions options)
{
var name = options.Name;
@@ -140,7 +141,7 @@ namespace Emby.Server.Implementations.Collections
// This could cause it to get re-resolved as a plain folder
var folderName = _fileSystem.GetValidFilename(name) + " [boxset]";
- var parentFolder = GetCollectionsFolder(true).GetAwaiter().GetResult();
+ var parentFolder = await GetCollectionsFolder(true).ConfigureAwait(false);
if (parentFolder == null)
{
@@ -168,12 +169,16 @@ namespace Emby.Server.Implementations.Collections
if (options.ItemIdList.Length > 0)
{
- AddToCollection(collection.Id, options.ItemIdList, false, new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- // The initial adding of items is going to create a local metadata file
- // This will cause internet metadata to be skipped as a result
- MetadataRefreshMode = MetadataRefreshMode.FullRefresh
- });
+ await AddToCollectionAsync(
+ collection.Id,
+ options.ItemIdList.Select(x => new Guid(x)),
+ false,
+ new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ // The initial adding of items is going to create a local metadata file
+ // This will cause internet metadata to be skipped as a result
+ MetadataRefreshMode = MetadataRefreshMode.FullRefresh
+ }).ConfigureAwait(false);
}
else
{
@@ -196,18 +201,10 @@ namespace Emby.Server.Implementations.Collections
}
/// <inheritdoc />
- public void AddToCollection(Guid collectionId, IEnumerable<string> ids)
- {
- AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
- }
+ public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids)
+ => AddToCollectionAsync(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
- /// <inheritdoc />
- public void AddToCollection(Guid collectionId, IEnumerable<Guid> ids)
- {
- AddToCollection(collectionId, ids.Select(i => i.ToString("N", CultureInfo.InvariantCulture)), true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
- }
-
- private void AddToCollection(Guid collectionId, IEnumerable<string> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
+ private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
{
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
if (collection == null)
@@ -223,15 +220,14 @@ namespace Emby.Server.Implementations.Collections
foreach (var id in ids)
{
- var guidId = new Guid(id);
- var item = _libraryManager.GetItemById(guidId);
+ var item = _libraryManager.GetItemById(id);
if (item == null)
{
throw new ArgumentException("No item exists with the supplied Id");
}
- if (!currentLinkedChildrenIds.Contains(guidId))
+ if (!currentLinkedChildrenIds.Contains(id))
{
itemList.Add(item);
@@ -248,7 +244,7 @@ namespace Emby.Server.Implementations.Collections
collection.UpdateRatingToItems(linkedChildrenList);
- collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+ await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
refreshOptions.ForceSave = true;
_providerManager.QueueRefresh(collection.Id, refreshOptions, RefreshPriority.High);
@@ -265,13 +261,7 @@ namespace Emby.Server.Implementations.Collections
}
/// <inheritdoc />
- public void RemoveFromCollection(Guid collectionId, IEnumerable<string> itemIds)
- {
- RemoveFromCollection(collectionId, itemIds.Select(i => new Guid(i)));
- }
-
- /// <inheritdoc />
- public void RemoveFromCollection(Guid collectionId, IEnumerable<Guid> itemIds)
+ public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
{
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
@@ -308,7 +298,7 @@ namespace Emby.Server.Implementations.Collections
collection.LinkedChildren = collection.LinkedChildren.Except(list).ToArray();
}
- collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+ await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
_providerManager.QueueRefresh(
collection.Id,
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
@@ -362,60 +352,4 @@ namespace Emby.Server.Implementations.Collections
return results.Values;
}
}
-
- /// <summary>
- /// The collection manager entry point.
- /// </summary>
- public sealed class CollectionManagerEntryPoint : IServerEntryPoint
- {
- private readonly CollectionManager _collectionManager;
- private readonly IServerConfigurationManager _config;
- private readonly ILogger _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="CollectionManagerEntryPoint"/> class.
- /// </summary>
- /// <param name="collectionManager">The collection manager.</param>
- /// <param name="config">The server configuration manager.</param>
- /// <param name="logger">The logger.</param>
- public CollectionManagerEntryPoint(
- ICollectionManager collectionManager,
- IServerConfigurationManager config,
- ILogger<CollectionManagerEntryPoint> logger)
- {
- _collectionManager = (CollectionManager)collectionManager;
- _config = config;
- _logger = logger;
- }
-
- /// <inheritdoc />
- public async Task RunAsync()
- {
- if (!_config.Configuration.CollectionsUpgraded && _config.Configuration.IsStartupWizardCompleted)
- {
- var path = _collectionManager.GetCollectionsFolderPath();
-
- if (Directory.Exists(path))
- {
- try
- {
- await _collectionManager.EnsureLibraryFolder(path, true).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error creating camera uploads library");
- }
-
- _config.Configuration.CollectionsUpgraded = true;
- _config.SaveConfiguration();
- }
- }
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- // Nothing to dispose
- }
- }
}
diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
index 305e67e8c..f05a30a89 100644
--- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
+++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
@@ -2,11 +2,11 @@ using System;
using System.Globalization;
using System.IO;
using Emby.Server.Implementations.AppBase;
+using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
@@ -90,7 +90,7 @@ namespace Emby.Server.Implementations.Configuration
ValidateMetadataPath(newConfig);
ValidateSslCertificate(newConfig);
- ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration> { Argument = newConfig });
+ ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig));
base.ReplaceConfiguration(newConfiguration);
}
@@ -109,7 +109,6 @@ namespace Emby.Server.Implementations.Configuration
if (!string.IsNullOrWhiteSpace(newPath)
&& !string.Equals(Configuration.CertificatePath, newPath, StringComparison.Ordinal))
{
- // Validate
if (!File.Exists(newPath))
{
throw new FileNotFoundException(
@@ -133,7 +132,6 @@ namespace Emby.Server.Implementations.Configuration
if (!string.IsNullOrWhiteSpace(newPath)
&& !string.Equals(Configuration.MetadataPath, newPath, StringComparison.Ordinal))
{
- // Validate
if (!Directory.Exists(newPath))
{
throw new DirectoryNotFoundException(
@@ -146,60 +144,5 @@ namespace Emby.Server.Implementations.Configuration
EnsureWriteAccess(newPath);
}
}
-
- /// <summary>
- /// Sets all configuration values to their optimal values.
- /// </summary>
- /// <returns>If the configuration changed.</returns>
- public bool SetOptimalValues()
- {
- var config = Configuration;
-
- var changed = false;
-
- if (!config.EnableCaseSensitiveItemIds)
- {
- config.EnableCaseSensitiveItemIds = true;
- changed = true;
- }
-
- if (!config.SkipDeserializationForBasicTypes)
- {
- config.SkipDeserializationForBasicTypes = true;
- changed = true;
- }
-
- if (!config.EnableSimpleArtistDetection)
- {
- config.EnableSimpleArtistDetection = true;
- changed = true;
- }
-
- if (!config.EnableNormalizedItemByNameIds)
- {
- config.EnableNormalizedItemByNameIds = true;
- changed = true;
- }
-
- if (!config.DisableLiveTvChannelUserDataName)
- {
- config.DisableLiveTvChannelUserDataName = true;
- changed = true;
- }
-
- if (!config.EnableNewOmdbSupport)
- {
- config.EnableNewOmdbSupport = true;
- changed = true;
- }
-
- if (!config.CollectionsUpgraded)
- {
- config.CollectionsUpgraded = true;
- changed = true;
- }
-
- return changed;
- }
}
}
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index dea9b6682..cd9dbb1bd 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -1,6 +1,5 @@
using System.Collections.Generic;
using Emby.Server.Implementations.HttpServer;
-using Emby.Server.Implementations.Updates;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Emby.Server.Implementations
@@ -16,11 +15,11 @@ namespace Emby.Server.Implementations
public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string>
{
{ HostWebClientKey, bool.TrueString },
- { HttpListenerHost.DefaultRedirectKey, "web/index.html" },
- { InstallationManager.PluginManifestUrlKey, "https://repo.jellyfin.org/releases/plugin/manifest-stable.json" },
+ { DefaultRedirectKey, "web/index.html" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
- { PlaylistsAllowDuplicatesKey, bool.TrueString }
+ { PlaylistsAllowDuplicatesKey, bool.FalseString },
+ { BindToUnixSocketKey, bool.FalseString }
};
}
}
diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
index a037415a9..fd302d136 100644
--- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
+++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
@@ -129,8 +131,6 @@ namespace Emby.Server.Implementations.Cryptography
_randomNumberGenerator.Dispose();
}
- _randomNumberGenerator = null;
-
_disposed = true;
}
}
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index 0654132f4..8c756a7f4 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Data
/// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
- protected BaseSqliteRepository(ILogger logger)
+ protected BaseSqliteRepository(ILogger<BaseSqliteRepository> logger)
{
Logger = logger;
}
@@ -32,7 +32,7 @@ namespace Emby.Server.Implementations.Data
/// Gets the logger.
/// </summary>
/// <value>The logger.</value>
- protected ILogger Logger { get; }
+ protected ILogger<BaseSqliteRepository> Logger { get; }
/// <summary>
/// Gets the default connection flags.
@@ -143,12 +143,22 @@ namespace Emby.Server.Implementations.Data
public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
=> connection.PrepareStatement(sql);
- public IEnumerable<IStatement> PrepareAll(IDatabaseConnection connection, IEnumerable<string> sql)
- => sql.Select(connection.PrepareStatement);
+ public IStatement[] PrepareAll(IDatabaseConnection connection, IReadOnlyList<string> sql)
+ {
+ int len = sql.Count;
+ IStatement[] statements = new IStatement[len];
+ for (int i = 0; i < len; i++)
+ {
+ statements[i] = connection.PrepareStatement(sql[i]);
+ }
+
+ return statements;
+ }
protected bool TableExists(ManagedConnection connection, string name)
{
- return connection.RunInTransaction(db =>
+ return connection.RunInTransaction(
+ db =>
{
using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
{
@@ -162,7 +172,6 @@ namespace Emby.Server.Implementations.Data
}
return false;
-
}, ReadTransactionMode);
}
@@ -248,12 +257,12 @@ namespace Emby.Server.Implementations.Data
public enum SynchronousMode
{
/// <summary>
- /// SQLite continues without syncing as soon as it has handed data off to the operating system
+ /// SQLite continues without syncing as soon as it has handed data off to the operating system.
/// </summary>
Off = 0,
/// <summary>
- /// SQLite database engine will still sync at the most critical moments
+ /// SQLite database engine will still sync at the most critical moments.
/// </summary>
Normal = 1,
diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
index 37c678a5d..3de9d6371 100644
--- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
+++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
@@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Data
public class CleanDatabaseScheduledTask : ILibraryPostScanTask
{
private readonly ILibraryManager _libraryManager;
- private readonly ILogger _logger;
+ private readonly ILogger<CleanDatabaseScheduledTask> _logger;
public CleanDatabaseScheduledTask(ILibraryManager libraryManager, ILogger<CleanDatabaseScheduledTask> logger)
{
@@ -51,7 +51,6 @@ namespace Emby.Server.Implementations.Data
_libraryManager.DeleteItem(item, new DeleteOptions
{
DeleteFileLocation = false
-
});
}
diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
deleted file mode 100644
index d474f1c6b..000000000
--- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
+++ /dev/null
@@ -1,225 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Text.Json;
-using System.Threading;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Data
-{
- /// <summary>
- /// Class SQLiteDisplayPreferencesRepository.
- /// </summary>
- public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository
- {
- private readonly IFileSystem _fileSystem;
-
- private readonly JsonSerializerOptions _jsonOptions;
-
- public SqliteDisplayPreferencesRepository(ILogger<SqliteDisplayPreferencesRepository> logger, IApplicationPaths appPaths, IFileSystem fileSystem)
- : base(logger)
- {
- _fileSystem = fileSystem;
-
- _jsonOptions = JsonDefaults.GetOptions();
-
- DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db");
- }
-
- /// <summary>
- /// Gets the name of the repository.
- /// </summary>
- /// <value>The name.</value>
- public string Name => "SQLite";
-
- public void Initialize()
- {
- try
- {
- InitializeInternal();
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error loading database file. Will reset and retry.");
-
- _fileSystem.DeleteFile(DbFilePath);
-
- InitializeInternal();
- }
- }
-
- /// <summary>
- /// Opens the connection to the database
- /// </summary>
- /// <returns>Task.</returns>
- private void InitializeInternal()
- {
- string[] queries =
- {
- "create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)",
- "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)"
- };
-
- using (var connection = GetConnection())
- {
- connection.RunQueries(queries);
- }
- }
-
- /// <summary>
- /// Save the display preferences associated with an item in the repo
- /// </summary>
- /// <param name="displayPreferences">The display preferences.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="client">The client.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <exception cref="ArgumentNullException">item</exception>
- public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken)
- {
- if (displayPreferences == null)
- {
- throw new ArgumentNullException(nameof(displayPreferences));
- }
-
- if (string.IsNullOrEmpty(displayPreferences.Id))
- {
- throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences));
- }
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db => SaveDisplayPreferences(displayPreferences, userId, client, db),
- TransactionMode);
- }
- }
-
- private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection)
- {
- var serialized = JsonSerializer.SerializeToUtf8Bytes(displayPreferences, _jsonOptions);
-
- using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)"))
- {
- statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray());
- statement.TryBind("@userId", userId.ToByteArray());
- statement.TryBind("@client", client);
- statement.TryBind("@data", serialized);
-
- statement.MoveNext();
- }
- }
-
- /// <summary>
- /// Save all display preferences associated with a user in the repo
- /// </summary>
- /// <param name="displayPreferences">The display preferences.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <exception cref="ArgumentNullException">item</exception>
- public void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, CancellationToken cancellationToken)
- {
- if (displayPreferences == null)
- {
- throw new ArgumentNullException(nameof(displayPreferences));
- }
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- foreach (var displayPreference in displayPreferences)
- {
- SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db);
- }
- },
- TransactionMode);
- }
- }
-
- /// <summary>
- /// Gets the display preferences.
- /// </summary>
- /// <param name="displayPreferencesId">The display preferences id.</param>
- /// <param name="userId">The user id.</param>
- /// <param name="client">The client.</param>
- /// <returns>Task{DisplayPreferences}.</returns>
- /// <exception cref="ArgumentNullException">item</exception>
- public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client)
- {
- if (string.IsNullOrEmpty(displayPreferencesId))
- {
- throw new ArgumentNullException(nameof(displayPreferencesId));
- }
-
- var guidId = displayPreferencesId.GetMD5();
-
- using (var connection = GetConnection(true))
- {
- using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"))
- {
- statement.TryBind("@id", guidId.ToByteArray());
- statement.TryBind("@userId", userId.ToByteArray());
- statement.TryBind("@client", client);
-
- foreach (var row in statement.ExecuteQuery())
- {
- return Get(row);
- }
- }
- }
-
- return new DisplayPreferences
- {
- Id = guidId.ToString("N", CultureInfo.InvariantCulture)
- };
- }
-
- /// <summary>
- /// Gets all display preferences for the given user.
- /// </summary>
- /// <param name="userId">The user id.</param>
- /// <returns>Task{DisplayPreferences}.</returns>
- /// <exception cref="ArgumentNullException">item</exception>
- public IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId)
- {
- var list = new List<DisplayPreferences>();
-
- using (var connection = GetConnection(true))
- using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId"))
- {
- statement.TryBind("@userId", userId.ToByteArray());
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(Get(row));
- }
- }
-
- return list;
- }
-
- private DisplayPreferences Get(IReadOnlyList<IResultSetValue> row)
- => JsonSerializer.Deserialize<DisplayPreferences>(row[0].ToBlob(), _jsonOptions);
-
- public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken)
- => SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken);
-
- public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client)
- => GetDisplayPreferences(displayPreferencesId, new Guid(userId), client);
- }
-}
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index 716e5071d..70a6df977 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -234,7 +234,9 @@ namespace Emby.Server.Implementations.Data
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
- bindParam.Bind(value.ToByteArray());
+ Span<byte> byteValue = stackalloc byte[16];
+ value.TryWriteBytes(byteValue);
+ bindParam.Bind(byteValue);
}
else
{
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index ca5cd6fdd..1c2aeda70 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -7,6 +9,7 @@ using System.Text;
using System.Text.Json;
using System.Threading;
using Emby.Server.Implementations.Playlists;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
@@ -33,7 +36,7 @@ using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
/// <summary>
- /// Class SQLiteItemRepository
+ /// Class SQLiteItemRepository.
/// </summary>
public class SqliteItemRepository : BaseSqliteRepository, IItemRepository
{
@@ -100,7 +103,7 @@ namespace Emby.Server.Implementations.Data
protected override TempStoreMode TempStore => TempStoreMode.Memory;
/// <summary>
- /// Opens the connection to the database
+ /// Opens the connection to the database.
/// </summary>
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
{
@@ -135,7 +138,6 @@ namespace Emby.Server.Implementations.Data
"pragma shrink_memory"
};
-
string[] postQueries =
{
// obsolete
@@ -217,7 +219,8 @@ namespace Emby.Server.Implementations.Data
{
connection.RunQueries(queries);
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var existingColumnNames = GetColumnNames(db, "AncestorIds");
AddColumn(db, "AncestorIds", "AncestorIdText", "Text", existingColumnNames);
@@ -319,7 +322,6 @@ namespace Emby.Server.Implementations.Data
AddColumn(db, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames);
AddColumn(db, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames);
AddColumn(db, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames);
-
}, TransactionMode);
connection.RunQueries(postQueries);
@@ -399,6 +401,8 @@ namespace Emby.Server.Implementations.Data
"OwnerId"
};
+ private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
+
private static readonly string[] _mediaStreamSaveColumns =
{
"ItemId",
@@ -438,6 +442,12 @@ namespace Emby.Server.Implementations.Data
"ColorTransfer"
};
+ private static readonly string _mediaStreamSaveColumnsInsertQuery =
+ $"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
+
+ private static readonly string _mediaStreamSaveColumnsSelectQuery =
+ $"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
+
private static readonly string[] _mediaAttachmentSaveColumns =
{
"ItemId",
@@ -449,105 +459,18 @@ namespace Emby.Server.Implementations.Data
"MIMEType"
};
- private static readonly string _mediaAttachmentInsertPrefix;
-
- private static string GetSaveItemCommandText()
- {
- var saveColumns = new[]
- {
- "guid",
- "type",
- "data",
- "Path",
- "StartDate",
- "EndDate",
- "ChannelId",
- "IsMovie",
- "IsSeries",
- "EpisodeTitle",
- "IsRepeat",
- "CommunityRating",
- "CustomRating",
- "IndexNumber",
- "IsLocked",
- "Name",
- "OfficialRating",
- "MediaType",
- "Overview",
- "ParentIndexNumber",
- "PremiereDate",
- "ProductionYear",
- "ParentId",
- "Genres",
- "InheritedParentalRatingValue",
- "SortName",
- "ForcedSortName",
- "RunTimeTicks",
- "Size",
- "DateCreated",
- "DateModified",
- "PreferredMetadataLanguage",
- "PreferredMetadataCountryCode",
- "Width",
- "Height",
- "DateLastRefreshed",
- "DateLastSaved",
- "IsInMixedFolder",
- "LockedFields",
- "Studios",
- "Audio",
- "ExternalServiceId",
- "Tags",
- "IsFolder",
- "UnratedType",
- "TopParentId",
- "TrailerTypes",
- "CriticRating",
- "CleanName",
- "PresentationUniqueKey",
- "OriginalTitle",
- "PrimaryVersionId",
- "DateLastMediaAdded",
- "Album",
- "IsVirtualItem",
- "SeriesName",
- "UserDataKey",
- "SeasonName",
- "SeasonId",
- "SeriesId",
- "ExternalSeriesId",
- "Tagline",
- "ProviderIds",
- "Images",
- "ProductionLocations",
- "ExtraIds",
- "TotalBitrate",
- "ExtraType",
- "Artists",
- "AlbumArtists",
- "ExternalId",
- "SeriesPresentationUniqueKey",
- "ShowId",
- "OwnerId"
- };
-
- var saveItemCommandCommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns) + ") values (";
-
- for (var i = 0; i < saveColumns.Length; i++)
- {
- if (i != 0)
- {
- saveItemCommandCommandText += ",";
- }
+ private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
+ $"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
- saveItemCommandCommandText += "@" + saveColumns[i];
- }
+ private static readonly string _mediaAttachmentInsertPrefix;
- return saveItemCommandCommandText + ")";
- }
+ private const string SaveItemCommandText =
+ @"replace into TypedBaseItems
+ (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
+ values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
/// <summary>
- /// Save a standard item in the repo
+ /// Save a standard item in the repo.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
@@ -573,7 +496,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
{
@@ -624,7 +548,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
SaveItemsInTranscation(db, tuples);
}, TransactionMode);
@@ -635,9 +560,9 @@ namespace Emby.Server.Implementations.Data
{
var statements = PrepareAll(db, new string[]
{
- GetSaveItemCommandText(),
+ SaveItemCommandText,
"delete from AncestorIds where ItemId=@ItemId"
- }).ToList();
+ });
using (var saveItemStatement = statements[0])
using (var deleteAncestorsStatement = statements[1])
@@ -792,6 +717,7 @@ namespace Emby.Server.Implementations.Data
{
saveItemStatement.TryBindNull("@Width");
}
+
if (item.Height > 0)
{
saveItemStatement.TryBind("@Height", item.Height);
@@ -931,6 +857,7 @@ namespace Emby.Server.Implementations.Data
{
saveItemStatement.TryBindNull("@SeriesName");
}
+
if (string.IsNullOrWhiteSpace(userDataKey))
{
saveItemStatement.TryBindNull("@UserDataKey");
@@ -1006,6 +933,7 @@ namespace Emby.Server.Implementations.Data
{
artists = string.Join("|", hasArtists.Artists);
}
+
saveItemStatement.TryBind("@Artists", artists);
string albumArtists = null;
@@ -1052,7 +980,10 @@ namespace Emby.Server.Implementations.Data
continue;
}
- str.Append($"{i.Key}={i.Value}|");
+ str.Append(i.Key)
+ .Append('=')
+ .Append(i.Value)
+ .Append('|');
}
if (str.Length == 0)
@@ -1105,7 +1036,9 @@ namespace Emby.Server.Implementations.Data
{
continue;
}
- str.Append(ToValueString(i) + "|");
+
+ AppendItemImageInfo(str, i);
+ str.Append('|');
}
str.Length -= 1; // Remove last |
@@ -1139,26 +1072,26 @@ namespace Emby.Server.Implementations.Data
item.ImageInfos = list.ToArray();
}
- public string ToValueString(ItemImageInfo image)
+ public void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
{
- var delimeter = "*";
-
- var path = image.Path;
-
- if (path == null)
- {
- path = string.Empty;
- }
-
- return GetPathToSave(path) +
- delimeter +
- image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) +
- delimeter +
- image.Type +
- delimeter +
- image.Width.ToString(CultureInfo.InvariantCulture) +
- delimeter +
- image.Height.ToString(CultureInfo.InvariantCulture);
+ const char Delimiter = '*';
+
+ var path = image.Path ?? string.Empty;
+ var hash = image.BlurHash ?? string.Empty;
+
+ bldr.Append(GetPathToSave(path))
+ .Append(Delimiter)
+ .Append(image.DateModified.Ticks)
+ .Append(Delimiter)
+ .Append(image.Type)
+ .Append(Delimiter)
+ .Append(image.Width)
+ .Append(Delimiter)
+ .Append(image.Height)
+ .Append(Delimiter)
+ // Replace delimiters with other characters.
+ // This can be removed when we migrate to a proper DB.
+ .Append(hash.Replace('*', '/').Replace('|', '\\'));
}
public ItemImageInfo ItemImageInfoFromValueString(string value)
@@ -1192,13 +1125,18 @@ namespace Emby.Server.Implementations.Data
image.Width = width;
image.Height = height;
}
+
+ if (parts.Length >= 6)
+ {
+ image.BlurHash = parts[5].Replace('/', '*').Replace('\\', '|');
+ }
}
return image;
}
/// <summary>
- /// Internal retrieve from items or users table
+ /// Internal retrieve from items or users table.
/// </summary>
/// <param name="id">The id.</param>
/// <returns>BaseItem.</returns>
@@ -1215,7 +1153,7 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection(true))
{
- using (var statement = PrepareStatement(connection, "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems where guid = @guid"))
+ using (var statement = PrepareStatement(connection, _retriveItemColumnsSelectQuery))
{
statement.TryBind("@guid", id);
@@ -1360,6 +1298,7 @@ namespace Emby.Server.Implementations.Data
hasStartDate.StartDate = reader[index].ReadDateTime();
}
}
+
index++;
}
@@ -1367,12 +1306,14 @@ namespace Emby.Server.Implementations.Data
{
item.EndDate = reader[index].TryReadDateTime();
}
+
index++;
if (!reader.IsDBNull(index))
{
item.ChannelId = new Guid(reader.GetString(index));
}
+
index++;
if (enableProgramAttributes)
@@ -1383,24 +1324,28 @@ namespace Emby.Server.Implementations.Data
{
hasProgramAttributes.IsMovie = reader.GetBoolean(index);
}
+
index++;
if (!reader.IsDBNull(index))
{
hasProgramAttributes.IsSeries = reader.GetBoolean(index);
}
+
index++;
if (!reader.IsDBNull(index))
{
hasProgramAttributes.EpisodeTitle = reader.GetString(index);
}
+
index++;
if (!reader.IsDBNull(index))
{
hasProgramAttributes.IsRepeat = reader.GetBoolean(index);
}
+
index++;
}
else
@@ -1413,6 +1358,7 @@ namespace Emby.Server.Implementations.Data
{
item.CommunityRating = reader.GetFloat(index);
}
+
index++;
if (HasField(query, ItemFields.CustomRating))
@@ -1421,6 +1367,7 @@ namespace Emby.Server.Implementations.Data
{
item.CustomRating = reader.GetString(index);
}
+
index++;
}
@@ -1428,6 +1375,7 @@ namespace Emby.Server.Implementations.Data
{
item.IndexNumber = reader.GetInt32(index);
}
+
index++;
if (HasField(query, ItemFields.Settings))
@@ -1436,18 +1384,21 @@ namespace Emby.Server.Implementations.Data
{
item.IsLocked = reader.GetBoolean(index);
}
+
index++;
if (!reader.IsDBNull(index))
{
item.PreferredMetadataLanguage = reader.GetString(index);
}
+
index++;
if (!reader.IsDBNull(index))
{
item.PreferredMetadataCountryCode = reader.GetString(index);
}
+
index++;
}
@@ -1457,6 +1408,7 @@ namespace Emby.Server.Implementations.Data
{
item.Width = reader.GetInt32(index);
}
+
index++;
}
@@ -1466,6 +1418,7 @@ namespace Emby.Server.Implementations.Data
{
item.Height = reader.GetInt32(index);
}
+
index++;
}
@@ -1475,6 +1428,7 @@ namespace Emby.Server.Implementations.Data
{
item.DateLastRefreshed = reader[index].ReadDateTime();
}
+
index++;
}
@@ -1482,18 +1436,21 @@ namespace Emby.Server.Implementations.Data
{
item.Name = reader.GetString(index);
}
+
index++;
if (!reader.IsDBNull(index))
{
item.Path = RestorePath(reader.GetString(index));
}
+
index++;
if (!reader.IsDBNull(index))
{
item.PremiereDate = reader[index].TryReadDateTime();
}
+
index++;
if (HasField(query, ItemFields.Overview))
@@ -1502,6 +1459,7 @@ namespace Emby.Server.Implementations.Data
{
item.Overview = reader.GetString(index);
}
+
index++;
}
@@ -1509,18 +1467,21 @@ namespace Emby.Server.Implementations.Data
{
item.ParentIndexNumber = reader.GetInt32(index);
}
+
index++;
if (!reader.IsDBNull(index))
{
item.ProductionYear = reader.GetInt32(index);
}
+
index++;
if (!reader.IsDBNull(index))
{
item.OfficialRating = reader.GetString(index);
}
+
index++;
if (HasField(query, ItemFields.SortName))
@@ -1529,6 +1490,7 @@ namespace Emby.Server.Implementations.Data
{
item.ForcedSortName = reader.GetString(index);
}
+
index++;
}
@@ -1536,12 +1498,14 @@ namespace Emby.Server.Implementations.Data
{
item.RunTimeTicks = reader.GetInt64(index);
}
+
index++;
if (!reader.IsDBNull(index))
{
item.Size = reader.GetInt64(index);
}
+
index++;
if (HasField(query, ItemFields.DateCreated))
@@ -1550,6 +1514,7 @@ namespace Emby.Server.Implementations.Data
{
item.DateCreated = reader[index].ReadDateTime();
}
+
index++;
}
@@ -1557,6 +1522,7 @@ namespace Emby.Server.Implementations.Data
{
item.DateModified = reader[index].ReadDateTime();
}
+
index++;
item.Id = reader.GetGuid(index);
@@ -1568,6 +1534,7 @@ namespace Emby.Server.Implementations.Data
{
item.Genres = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
}
+
index++;
}
@@ -1575,6 +1542,7 @@ namespace Emby.Server.Implementations.Data
{
item.ParentId = reader.GetGuid(index);
}
+
index++;
if (!reader.IsDBNull(index))
@@ -1584,6 +1552,7 @@ namespace Emby.Server.Implementations.Data
item.Audio = audio;
}
}
+
index++;
// TODO: Even if not needed by apps, the server needs it internally
@@ -1597,6 +1566,7 @@ namespace Emby.Server.Implementations.Data
liveTvChannel.ServiceName = reader.GetString(index);
}
}
+
index++;
}
@@ -1604,6 +1574,7 @@ namespace Emby.Server.Implementations.Data
{
item.IsInMixedFolder = reader.GetBoolean(index);
}
+
index++;
if (HasField(query, ItemFields.DateLastSaved))
@@ -1612,6 +1583,7 @@ namespace Emby.Server.Implementations.Data
{
item.DateLastSaved = reader[index].ReadDateTime();
}
+
index++;
}
@@ -1619,18 +1591,20 @@ namespace Emby.Server.Implementations.Data
{
if (!reader.IsDBNull(index))
{
- IEnumerable<MetadataFields> GetLockedFields(string s)
+ IEnumerable<MetadataField> GetLockedFields(string s)
{
foreach (var i in s.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries))
{
- if (Enum.TryParse(i, true, out MetadataFields parsedValue))
+ if (Enum.TryParse(i, true, out MetadataField parsedValue))
{
yield return parsedValue;
}
}
}
+
item.LockedFields = GetLockedFields(reader.GetString(index)).ToArray();
}
+
index++;
}
@@ -1640,6 +1614,7 @@ namespace Emby.Server.Implementations.Data
{
item.Studios = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
}
+
index++;
}
@@ -1649,6 +1624,7 @@ namespace Emby.Server.Implementations.Data
{
item.Tags = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
}
+
index++;
}
@@ -1668,9 +1644,11 @@ namespace Emby.Server.Implementations.Data
}
}
}
+
trailer.TrailerTypes = GetTrailerTypes(reader.GetString(index)).ToArray();
}
}
+
index++;
}
@@ -1680,6 +1658,7 @@ namespace Emby.Server.Implementations.Data
{
item.OriginalTitle = reader.GetString(index);
}
+
index++;
}
@@ -1690,6 +1669,7 @@ namespace Emby.Server.Implementations.Data
video.PrimaryVersionId = reader.GetString(index);
}
}
+
index++;
if (HasField(query, ItemFields.DateLastMediaAdded))
@@ -1698,6 +1678,7 @@ namespace Emby.Server.Implementations.Data
{
folder.DateLastMediaAdded = reader[index].TryReadDateTime();
}
+
index++;
}
@@ -1705,18 +1686,21 @@ namespace Emby.Server.Implementations.Data
{
item.Album = reader.GetString(index);
}
+
index++;
if (!reader.IsDBNull(index))
{
item.CriticRating = reader.GetFloat(index);
}
+
index++;
if (!reader.IsDBNull(index))
{
item.IsVirtualItem = reader.GetBoolean(index);
}
+
index++;
if (item is IHasSeries hasSeriesName)
@@ -1726,6 +1710,7 @@ namespace Emby.Server.Implementations.Data
hasSeriesName.SeriesName = reader.GetString(index);
}
}
+
index++;
if (hasEpisodeAttributes)
@@ -1736,6 +1721,7 @@ namespace Emby.Server.Implementations.Data
{
episode.SeasonName = reader.GetString(index);
}
+
index++;
if (!reader.IsDBNull(index))
{
@@ -1746,6 +1732,7 @@ namespace Emby.Server.Implementations.Data
{
index++;
}
+
index++;
}
@@ -1759,6 +1746,7 @@ namespace Emby.Server.Implementations.Data
hasSeries.SeriesId = reader.GetGuid(index);
}
}
+
index++;
}
@@ -1768,6 +1756,7 @@ namespace Emby.Server.Implementations.Data
{
item.PresentationUniqueKey = reader.GetString(index);
}
+
index++;
}
@@ -1777,6 +1766,7 @@ namespace Emby.Server.Implementations.Data
{
item.InheritedParentalRatingValue = reader.GetInt32(index);
}
+
index++;
}
@@ -1786,6 +1776,7 @@ namespace Emby.Server.Implementations.Data
{
item.ExternalSeriesId = reader.GetString(index);
}
+
index++;
}
@@ -1795,6 +1786,7 @@ namespace Emby.Server.Implementations.Data
{
item.Tagline = reader.GetString(index);
}
+
index++;
}
@@ -1802,6 +1794,7 @@ namespace Emby.Server.Implementations.Data
{
DeserializeProviderIds(reader.GetString(index), item);
}
+
index++;
if (query.DtoOptions.EnableImages)
@@ -1810,6 +1803,7 @@ namespace Emby.Server.Implementations.Data
{
DeserializeImages(reader.GetString(index), item);
}
+
index++;
}
@@ -1819,6 +1813,7 @@ namespace Emby.Server.Implementations.Data
{
item.ProductionLocations = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).ToArray();
}
+
index++;
}
@@ -1828,6 +1823,7 @@ namespace Emby.Server.Implementations.Data
{
item.ExtraIds = SplitToGuids(reader.GetString(index));
}
+
index++;
}
@@ -1835,6 +1831,7 @@ namespace Emby.Server.Implementations.Data
{
item.TotalBitrate = reader.GetInt32(index);
}
+
index++;
if (!reader.IsDBNull(index))
@@ -1844,6 +1841,7 @@ namespace Emby.Server.Implementations.Data
item.ExtraType = extraType;
}
}
+
index++;
if (hasArtistFields)
@@ -1852,12 +1850,14 @@ namespace Emby.Server.Implementations.Data
{
hasArtists.Artists = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
}
+
index++;
if (item is IHasAlbumArtist hasAlbumArtists && !reader.IsDBNull(index))
{
hasAlbumArtists.AlbumArtists = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
}
+
index++;
}
@@ -1865,6 +1865,7 @@ namespace Emby.Server.Implementations.Data
{
item.ExternalId = reader.GetString(index);
}
+
index++;
if (HasField(query, ItemFields.SeriesPresentationUniqueKey))
@@ -1876,6 +1877,7 @@ namespace Emby.Server.Implementations.Data
hasSeries.SeriesPresentationUniqueKey = reader.GetString(index);
}
}
+
index++;
}
@@ -1885,6 +1887,7 @@ namespace Emby.Server.Implementations.Data
{
program.ShowId = reader.GetString(index);
}
+
index++;
}
@@ -1892,6 +1895,7 @@ namespace Emby.Server.Implementations.Data
{
item.OwnerId = reader.GetGuid(index);
}
+
index++;
return item;
@@ -1912,7 +1916,7 @@ namespace Emby.Server.Implementations.Data
}
/// <summary>
- /// Gets chapters for an item
+ /// Gets chapters for an item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>IEnumerable{ChapterInfo}.</returns>
@@ -1940,7 +1944,7 @@ namespace Emby.Server.Implementations.Data
}
/// <summary>
- /// Gets a single chapter for an item
+ /// Gets a single chapter for an item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="index">The index.</param>
@@ -1971,6 +1975,7 @@ namespace Emby.Server.Implementations.Data
/// Gets the chapter.
/// </summary>
/// <param name="reader">The reader.</param>
+ /// <param name="item">The item.</param>
/// <returns>ChapterInfo.</returns>
private ChapterInfo GetChapter(IReadOnlyList<IResultSetValue> reader, BaseItem item)
{
@@ -2030,13 +2035,13 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
// First delete chapters
db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob);
InsertChapters(idBlob, chapters, db);
-
}, TransactionMode);
}
}
@@ -2262,7 +2267,6 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Contains("Trailer", StringComparer.OrdinalIgnoreCase);
}
-
private static readonly HashSet<string> _artistExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Series",
@@ -2395,7 +2399,7 @@ namespace Emby.Server.Implementations.Data
var item = query.SimilarTo;
var builder = new StringBuilder();
- builder.Append("(");
+ builder.Append('(');
if (string.IsNullOrEmpty(item.OfficialRating))
{
@@ -2433,7 +2437,7 @@ namespace Emby.Server.Implementations.Data
if (!string.IsNullOrEmpty(query.SearchTerm))
{
var builder = new StringBuilder();
- builder.Append("(");
+ builder.Append('(');
builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)");
@@ -2467,6 +2471,7 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind("@SearchTermStartsWith", searchTerm + "%");
}
+
if (commandText.IndexOf("@SearchTermContains", StringComparison.OrdinalIgnoreCase) != -1)
{
statement.TryBind("@SearchTermContains", "%" + searchTerm + "%");
@@ -2698,22 +2703,85 @@ namespace Emby.Server.Implementations.Data
private string FixUnicodeChars(string buffer)
{
- if (buffer.IndexOf('\u2013') > -1) buffer = buffer.Replace('\u2013', '-'); // en dash
- if (buffer.IndexOf('\u2014') > -1) buffer = buffer.Replace('\u2014', '-'); // em dash
- if (buffer.IndexOf('\u2015') > -1) buffer = buffer.Replace('\u2015', '-'); // horizontal bar
- if (buffer.IndexOf('\u2017') > -1) buffer = buffer.Replace('\u2017', '_'); // double low line
- if (buffer.IndexOf('\u2018') > -1) buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
- if (buffer.IndexOf('\u2019') > -1) buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
- if (buffer.IndexOf('\u201a') > -1) buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
- if (buffer.IndexOf('\u201b') > -1) buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
- if (buffer.IndexOf('\u201c') > -1) buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
- if (buffer.IndexOf('\u201d') > -1) buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
- if (buffer.IndexOf('\u201e') > -1) buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
- if (buffer.IndexOf('\u2026') > -1) buffer = buffer.Replace("\u2026", "..."); // horizontal ellipsis
- if (buffer.IndexOf('\u2032') > -1) buffer = buffer.Replace('\u2032', '\''); // prime
- if (buffer.IndexOf('\u2033') > -1) buffer = buffer.Replace('\u2033', '\"'); // double prime
- if (buffer.IndexOf('\u0060') > -1) buffer = buffer.Replace('\u0060', '\''); // grave accent
- if (buffer.IndexOf('\u00B4') > -1) buffer = buffer.Replace('\u00B4', '\''); // acute accent
+ if (buffer.IndexOf('\u2013', StringComparison.Ordinal) > -1)
+ {
+ buffer = buffer.Replace('\u2013', '-'); // en dash
+ }
+
+ if (buffer.IndexOf('\u2014', StringComparison.Ordinal) > -1)
+ {
+ buffer = buffer.Replace('\u2014', '-'); // em dash
+ }
+
+ if (buffer.IndexOf('\u2015', StringComparison.Ordinal) > -1)
+ {
+ buffer = buffer.Replace('\u2015', '-'); // horizontal bar
+ }
+
+ if (buffer.IndexOf('\u2017', StringComparison.Ordinal) > -1)
+ {
+ buffer = buffer.Replace('\u2017', '_'); // double low line
+ }
+
+ if (buffer.IndexOf('\u2018', StringComparison.Ordinal) > -1)
+ {
+ buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
+ }
+
+ if (buffer.IndexOf('\u2019', StringComparison.Ordinal) > -1)
+ {
+ buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
+ }
+
+ if (buffer.IndexOf('\u201a', StringComparison.Ordinal) > -1)
+ {
+ buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
+ }
+
+ if (buffer.IndexOf('\u201b', StringComparison.Ordinal) > -1)
+ {
+ buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
+ }
+
+ if (buffer.IndexOf('\u201c', StringComparison.Ordinal) > -1)
+ {
+ buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
+ }
+
+ if (buffer.IndexOf('\u201d', StringComparison.Ordinal) > -1)
+ {
+ buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
+ }
+
+ if (buffer.IndexOf('\u201e', StringComparison.Ordinal) > -1)
+ {
+ buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
+ }
+
+ if (buffer.IndexOf('\u2026', StringComparison.Ordinal) > -1)
+ {
+ buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis
+ }
+
+ if (buffer.IndexOf('\u2032', StringComparison.Ordinal) > -1)
+ {
+ buffer = buffer.Replace('\u2032', '\''); // prime
+ }
+
+ if (buffer.IndexOf('\u2033', StringComparison.Ordinal) > -1)
+ {
+ buffer = buffer.Replace('\u2033', '\"'); // double prime
+ }
+
+ if (buffer.IndexOf('\u0060', StringComparison.Ordinal) > -1)
+ {
+ buffer = buffer.Replace('\u0060', '\''); // grave accent
+ }
+
+ if (buffer.IndexOf('\u00B4', StringComparison.Ordinal) > -1)
+ {
+ buffer = buffer.Replace('\u00B4', '\''); // acute accent
+ }
return buffer;
}
@@ -2726,7 +2794,7 @@ namespace Emby.Server.Implementations.Data
foreach (var providerId in newItem.ProviderIds)
{
- if (providerId.Key == MetadataProviders.TmdbCollection.ToString())
+ if (providerId.Key == MetadataProvider.TmdbCollection.ToString())
{
continue;
}
@@ -2737,6 +2805,7 @@ namespace Emby.Server.Implementations.Data
{
items[i] = newItem;
}
+
return;
}
}
@@ -2829,6 +2898,7 @@ namespace Emby.Server.Implementations.Data
{
statementTexts.Add(commandText);
}
+
if (query.EnableTotalRecordCount)
{
commandText = string.Empty;
@@ -2855,10 +2925,10 @@ namespace Emby.Server.Implementations.Data
var result = new QueryResult<BaseItem>();
using (var connection = GetConnection(true))
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
-
- var statements = PrepareAll(db, statementTexts).ToList();
+ var statements = PrepareAll(db, statementTexts);
if (!isReturningZeroItems)
{
@@ -2896,7 +2966,7 @@ namespace Emby.Server.Implementations.Data
if (query.EnableTotalRecordCount)
{
- using (var statement = statements[statements.Count - 1])
+ using (var statement = statements[statements.Length - 1])
{
if (EnableJoinUserData(query))
{
@@ -3225,7 +3295,6 @@ namespace Emby.Server.Implementations.Data
}
}
-
var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
var statementTexts = new List<string>();
@@ -3233,6 +3302,7 @@ namespace Emby.Server.Implementations.Data
{
statementTexts.Add(commandText);
}
+
if (query.EnableTotalRecordCount)
{
commandText = string.Empty;
@@ -3259,9 +3329,10 @@ namespace Emby.Server.Implementations.Data
var result = new QueryResult<Guid>();
using (var connection = GetConnection(true))
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
- var statements = PrepareAll(db, statementTexts).ToList();
+ var statements = PrepareAll(db, statementTexts);
if (!isReturningZeroItems)
{
@@ -3287,7 +3358,7 @@ namespace Emby.Server.Implementations.Data
if (query.EnableTotalRecordCount)
{
- using (var statement = statements[statements.Count - 1])
+ using (var statement = statements[statements.Length - 1])
{
if (EnableJoinUserData(query))
{
@@ -3586,11 +3657,13 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add("IndexNumber=@IndexNumber");
statement?.TryBind("@IndexNumber", query.IndexNumber.Value);
}
+
if (query.ParentIndexNumber.HasValue)
{
whereClauses.Add("ParentIndexNumber=@ParentIndexNumber");
statement?.TryBind("@ParentIndexNumber", query.ParentIndexNumber.Value);
}
+
if (query.ParentIndexNumberNotEquals.HasValue)
{
whereClauses.Add("(ParentIndexNumber<>@ParentIndexNumberNotEquals or ParentIndexNumber is null)");
@@ -3648,26 +3721,31 @@ namespace Emby.Server.Implementations.Data
statement?.TryBind("@MaxPremiereDate", query.MaxPremiereDate.Value);
}
+ StringBuilder clauseBuilder = new StringBuilder();
+ const string Or = " OR ";
+
var trailerTypes = query.TrailerTypes;
int trailerTypesLen = trailerTypes.Length;
if (trailerTypesLen > 0)
{
- const string Or = " OR ";
- StringBuilder clause = new StringBuilder("(", trailerTypesLen * 32);
+ clauseBuilder.Append('(');
+
for (int i = 0; i < trailerTypesLen; i++)
{
var paramName = "@TrailerTypes" + i;
- clause.Append("TrailerTypes like ")
+ clauseBuilder.Append("TrailerTypes like ")
.Append(paramName)
.Append(Or);
statement?.TryBind(paramName, "%" + trailerTypes[i] + "%");
}
// Remove last " OR "
- clause.Length -= Or.Length;
- clause.Append(')');
+ clauseBuilder.Length -= Or.Length;
+ clauseBuilder.Append(')');
- whereClauses.Add(clause.ToString());
+ whereClauses.Add(clauseBuilder.ToString());
+
+ clauseBuilder.Length = 0;
}
if (query.IsAiring.HasValue)
@@ -3687,23 +3765,35 @@ namespace Emby.Server.Implementations.Data
}
}
- if (query.PersonIds.Length > 0)
+ int personIdsLen = query.PersonIds.Length;
+ if (personIdsLen > 0)
{
// TODO: Should this query with CleanName ?
- var clauses = new List<string>();
- var index = 0;
- foreach (var personId in query.PersonIds)
+ clauseBuilder.Append('(');
+
+ Span<byte> idBytes = stackalloc byte[16];
+ for (int i = 0; i < personIdsLen; i++)
{
- var paramName = "@PersonId" + index;
+ string paramName = "@PersonId" + i;
+ clauseBuilder.Append("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=")
+ .Append(paramName)
+ .Append("))) OR ");
- clauses.Add("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=" + paramName + ")))");
- statement?.TryBind(paramName, personId.ToByteArray());
- index++;
+ if (statement != null)
+ {
+ query.PersonIds[i].TryWriteBytes(idBytes);
+ statement.TryBind(paramName, idBytes);
+ }
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ // Remove last " OR "
+ clauseBuilder.Length -= Or.Length;
+ clauseBuilder.Append(')');
+
+ whereClauses.Add(clauseBuilder.ToString());
+
+ clauseBuilder.Length = 0;
}
if (!string.IsNullOrWhiteSpace(query.Person))
@@ -3876,6 +3966,7 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind(paramName, artistId.ToByteArray());
}
+
index++;
}
@@ -3896,6 +3987,7 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind(paramName, artistId.ToByteArray());
}
+
index++;
}
@@ -3916,8 +4008,10 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind(paramName, artistId.ToByteArray());
}
+
index++;
}
+
var clause = "(" + string.Join(" OR ", clauses) + ")";
whereClauses.Add(clause);
}
@@ -3935,8 +4029,10 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind(paramName, albumId.ToByteArray());
}
+
index++;
}
+
var clause = "(" + string.Join(" OR ", clauses) + ")";
whereClauses.Add(clause);
}
@@ -3954,8 +4050,10 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind(paramName, artistId.ToByteArray());
}
+
index++;
}
+
var clause = "(" + string.Join(" OR ", clauses) + ")";
whereClauses.Add(clause);
}
@@ -3973,8 +4071,10 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind(paramName, genreId.ToByteArray());
}
+
index++;
}
+
var clause = "(" + string.Join(" OR ", clauses) + ")";
whereClauses.Add(clause);
}
@@ -3990,8 +4090,10 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind("@Genre" + index, GetCleanValue(item));
}
+
index++;
}
+
var clause = "(" + string.Join(" OR ", clauses) + ")";
whereClauses.Add(clause);
}
@@ -4007,8 +4109,10 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind("@Tag" + index, GetCleanValue(item));
}
+
index++;
}
+
var clause = "(" + string.Join(" OR ", clauses) + ")";
whereClauses.Add(clause);
}
@@ -4024,8 +4128,10 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind("@ExcludeTag" + index, GetCleanValue(item));
}
+
index++;
}
+
var clause = "(" + string.Join(" OR ", clauses) + ")";
whereClauses.Add(clause);
}
@@ -4044,8 +4150,10 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind(paramName, studioId.ToByteArray());
}
+
index++;
}
+
var clause = "(" + string.Join(" OR ", clauses) + ")";
whereClauses.Add(clause);
}
@@ -4061,8 +4169,10 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind("@OfficialRating" + index, item);
}
+
index++;
}
+
var clause = "(" + string.Join(" OR ", clauses) + ")";
whereClauses.Add(clause);
}
@@ -4218,7 +4328,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add("ProductionYear=@Years");
if (statement != null)
{
- statement.TryBind("@Years", query.Years[0].ToString());
+ statement.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture));
}
}
else if (query.Years.Length > 1)
@@ -4237,6 +4347,7 @@ namespace Emby.Server.Implementations.Data
statement.TryBind("@IsVirtualItem", isVirtualItem.Value);
}
}
+
if (query.IsSpecialSeason.HasValue)
{
if (query.IsSpecialSeason.Value)
@@ -4248,6 +4359,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add("IndexNumber <> 0");
}
}
+
if (query.IsUnaired.HasValue)
{
if (query.IsUnaired.Value)
@@ -4259,6 +4371,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add("PremiereDate < DATETIME('now')");
}
}
+
var queryMediaTypes = query.MediaTypes.Where(IsValidMediaType).ToArray();
if (queryMediaTypes.Length == 1)
{
@@ -4274,6 +4387,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add("MediaType in (" + val + ")");
}
+
if (query.ItemIds.Length > 0)
{
var includeIds = new List<string>();
@@ -4286,11 +4400,13 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind("@IncludeId" + index, id);
}
+
index++;
}
whereClauses.Add("(" + string.Join(" OR ", includeIds) + ")");
}
+
if (query.ExcludeItemIds.Length > 0)
{
var excludeIds = new List<string>();
@@ -4303,6 +4419,7 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind("@ExcludeId" + index, id);
}
+
index++;
}
@@ -4316,7 +4433,7 @@ namespace Emby.Server.Implementations.Data
var index = 0;
foreach (var pair in query.ExcludeProviderIds)
{
- if (string.Equals(pair.Key, MetadataProviders.TmdbCollection.ToString(), StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(pair.Key, MetadataProvider.TmdbCollection.ToString(), StringComparison.OrdinalIgnoreCase))
{
continue;
}
@@ -4327,6 +4444,7 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
}
+
index++;
break;
@@ -4345,14 +4463,14 @@ namespace Emby.Server.Implementations.Data
var index = 0;
foreach (var pair in query.HasAnyProviderId)
{
- if (string.Equals(pair.Key, MetadataProviders.TmdbCollection.ToString(), StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(pair.Key, MetadataProvider.TmdbCollection.ToString(), StringComparison.OrdinalIgnoreCase))
{
continue;
}
// TODO this seems to be an idea for a better schema where ProviderIds are their own table
// buut this is not implemented
- //hasProviderIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")");
+ // hasProviderIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")");
// TODO this is a really BAD way to do it since the pair:
// Tmdb, 1234 matches Tmdb=1234 but also Tmdb=1234567
@@ -4369,6 +4487,7 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%");
}
+
index++;
break;
@@ -4419,6 +4538,7 @@ namespace Emby.Server.Implementations.Data
{
whereClauses.Add("(TopParentId=@TopParentId)");
}
+
if (statement != null)
{
statement.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture));
@@ -4456,15 +4576,17 @@ namespace Emby.Server.Implementations.Data
statement.TryBind("@AncestorId", query.AncestorIds[0]);
}
}
+
if (query.AncestorIds.Length > 1)
{
var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
- whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
+ whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
}
+
if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey))
{
var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey";
- whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
+ whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
if (statement != null)
{
statement.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
@@ -4489,10 +4611,15 @@ namespace Emby.Server.Implementations.Data
statement.TryBind("@UnratedType", query.BlockUnratedItems[0].ToString());
}
}
+
if (query.BlockUnratedItems.Length > 1)
{
- var inClause = string.Join(",", query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
- whereClauses.Add(string.Format("(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", inClause));
+ var inClause = string.Join(',', query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
+ whereClauses.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))",
+ inClause));
}
if (query.ExcludeInheritedTags.Length > 0)
@@ -4501,7 +4628,7 @@ namespace Emby.Server.Implementations.Data
if (statement == null)
{
int index = 0;
- string excludedTags = string.Join(",", query.ExcludeInheritedTags.Select(t => paramName + index++));
+ string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(t => paramName + index++));
whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
}
else
@@ -4778,10 +4905,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
connection.ExecuteAll(sql);
-
}, TransactionMode);
}
}
@@ -4830,7 +4957,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var idBlob = id.ToByteArray();
@@ -4964,6 +5092,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
statement.TryBind("@ItemId", query.ItemId.ToByteArray());
}
}
+
if (!query.AppearsInItemId.Equals(Guid.Empty))
{
whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)");
@@ -4972,6 +5101,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
}
}
+
var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList();
if (queryPersonTypes.Count == 1)
@@ -4988,6 +5118,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
whereClauses.Add("PersonType in (" + val + ")");
}
+
var queryExcludePersonTypes = query.ExcludePersonTypes.Where(IsValidPersonType).ToList();
if (queryExcludePersonTypes.Count == 1)
@@ -5004,6 +5135,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
whereClauses.Add("PersonType not in (" + val + ")");
}
+
if (query.MaxListOrder.HasValue)
{
whereClauses.Add("ListOrder<=@MaxListOrder");
@@ -5012,6 +5144,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
statement.TryBind("@MaxListOrder", query.MaxListOrder.Value);
}
}
+
if (!string.IsNullOrWhiteSpace(query.NameContains))
{
whereClauses.Add("Name like @NameContains");
@@ -5038,7 +5171,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
CheckDisposed();
- var itemIdBlob = itemId.ToByteArray();
+ Span<byte> itemIdBlob = stackalloc byte[16];
+ itemId.TryWriteBytes(itemIdBlob);
// First delete
deleteAncestorsStatement.Reset();
@@ -5054,14 +5188,15 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
for (var i = 0; i < ancestorIds.Count; i++)
{
- if (i > 0)
- {
- insertText.Append(",");
- }
-
- insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture));
+ insertText.AppendFormat(
+ CultureInfo.InvariantCulture,
+ "(@ItemId, @AncestorId{0}, @AncestorIdText{0}),",
+ i.ToString(CultureInfo.InvariantCulture));
}
+ // Remove last ,
+ insertText.Length--;
+
using (var statement = PrepareStatement(db, insertText.ToString()))
{
statement.TryBind("@ItemId", itemIdBlob);
@@ -5071,8 +5206,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
var index = i.ToString(CultureInfo.InvariantCulture);
var ancestorId = ancestorIds[i];
+ ancestorId.TryWriteBytes(itemIdBlob);
- statement.TryBind("@AncestorId" + index, ancestorId.ToByteArray());
+ statement.TryBind("@AncestorId" + index, itemIdBlob);
statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture));
}
@@ -5151,6 +5287,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
var typeString = string.Join(",", withItemTypes.Select(i => "'" + i + "'"));
commandText += " AND ItemId In (select guid from typedbaseitems where type in (" + typeString + "))";
}
+
if (excludeItemTypes.Count > 0)
{
var typeString = string.Join(",", excludeItemTypes.Select(i => "'" + i + "'"));
@@ -5172,7 +5309,6 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
}
}
-
}
LogQueryTime("GetItemValueNames", commandText, now);
@@ -5229,7 +5365,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
itemCountColumns = new Dictionary<string, string>()
{
- { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes"}
+ { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes" }
};
}
@@ -5352,7 +5488,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
connection.RunInTransaction(
db =>
{
- var statements = PrepareAll(db, statementTexts).ToList();
+ var statements = PrepareAll(db, statementTexts);
if (!isReturningZeroItems)
{
@@ -5403,7 +5539,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
+ GetJoinUserDataText(query)
+ whereText;
- using (var statement = statements[statements.Count - 1])
+ using (var statement = statements[statements.Length - 1])
{
statement.TryBind("@SelectType", returnType);
if (EnableJoinUserData(query))
@@ -5551,10 +5687,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
const int Limit = 100;
var startIndex = 0;
+ const string StartInsertText = "insert into ItemValues (ItemId, Type, Value, CleanValue) values ";
+ var insertText = new StringBuilder(StartInsertText);
while (startIndex < values.Count)
{
- var insertText = new StringBuilder("insert into ItemValues (ItemId, Type, Value, CleanValue) values ");
-
var endIndex = Math.Min(values.Count, startIndex + Limit);
for (var i = startIndex; i < endIndex; i++)
@@ -5596,6 +5732,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
startIndex += Limit;
+ insertText.Length = StartInsertText.Length;
}
}
@@ -5615,7 +5752,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var itemIdBlob = itemId.ToByteArray();
@@ -5623,7 +5761,6 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
db.Execute("delete from People where ItemId=@ItemId", itemIdBlob);
InsertPeople(itemIdBlob, people, db);
-
}, TransactionMode);
}
}
@@ -5634,10 +5771,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
var startIndex = 0;
var listIndex = 0;
+ const string StartInsertText = "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ";
+ var insertText = new StringBuilder(StartInsertText);
while (startIndex < people.Count)
{
- var insertText = new StringBuilder("insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ");
-
var endIndex = Math.Min(people.Count, startIndex + Limit);
for (var i = startIndex; i < endIndex; i++)
{
@@ -5671,6 +5808,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
startIndex += Limit;
+ insertText.Length = StartInsertText.Length;
}
}
@@ -5709,10 +5847,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
throw new ArgumentNullException(nameof(query));
}
- var cmdText = "select "
- + string.Join(",", _mediaStreamSaveColumns)
- + " from mediastreams where"
- + " ItemId=@ItemId";
+ var cmdText = _mediaStreamSaveColumnsSelectQuery;
if (query.Type.HasValue)
{
@@ -5772,7 +5907,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var itemIdBlob = id.ToByteArray();
@@ -5780,7 +5916,6 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob);
InsertMediaStreams(itemIdBlob, streams, db);
-
}, TransactionMode);
}
}
@@ -5790,18 +5925,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
const int Limit = 10;
var startIndex = 0;
+ var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
while (startIndex < streams.Count)
{
- var insertText = new StringBuilder("insert into mediastreams (");
- foreach (var column in _mediaStreamSaveColumns)
- {
- insertText.Append(column).Append(',');
- }
-
- // Remove last comma
- insertText.Length--;
- insertText.Append(") values ");
-
var endIndex = Math.Min(streams.Count, startIndex + Limit);
for (var i = startIndex; i < endIndex; i++)
@@ -5884,10 +6010,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
startIndex += Limit;
+ insertText.Length = _mediaStreamSaveColumnsInsertQuery.Length;
}
}
-
/// <summary>
/// Gets the chapter.
/// </summary>
@@ -6067,10 +6193,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
throw new ArgumentNullException(nameof(query));
}
- var cmdText = "select "
- + string.Join(",", _mediaAttachmentSaveColumns)
- + " from mediaattachments where"
- + " ItemId=@ItemId";
+ var cmdText = _mediaAttachmentSaveColumnsSelectQuery;
if (query.Index.HasValue)
{
@@ -6119,14 +6242,14 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var itemIdBlob = id.ToByteArray();
db.Execute("delete from mediaattachments where ItemId=@ItemId", itemIdBlob);
InsertMediaAttachments(itemIdBlob, attachments, db, cancellationToken);
-
}, TransactionMode);
}
}
@@ -6139,10 +6262,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
{
const int InsertAtOnce = 10;
+ var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce)
{
- var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
-
var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce);
for (var i = startIndex; i < endIndex; i++)
@@ -6152,7 +6274,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
{
- insertText.Append("@" + column + index + ",");
+ insertText.Append('@')
+ .Append(column)
+ .Append(index)
+ .Append(',');
}
insertText.Length -= 1;
@@ -6185,6 +6310,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
statement.Reset();
statement.MoveNext();
}
+
+ insertText.Length = _mediaAttachmentInsertPrefix.Length;
}
}
@@ -6192,7 +6319,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
/// Gets the attachment.
/// </summary>
/// <param name="reader">The reader.</param>
- /// <returns>MediaAttachment</returns>
+ /// <returns>MediaAttachment.</returns>
private MediaAttachment GetMediaAttachment(IReadOnlyList<IResultSetValue> reader)
{
var item = new MediaAttachment
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 6ee6230fc..2c4e8e0fc 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
+using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -43,7 +44,8 @@ namespace Emby.Server.Implementations.Data
var users = userDatasTableExists ? null : userManager.Users;
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
db.ExecuteAll(string.Join(";", new[] {
@@ -134,10 +136,12 @@ namespace Emby.Server.Implementations.Data
{
throw new ArgumentNullException(nameof(userData));
}
+
if (internalUserId <= 0)
{
throw new ArgumentNullException(nameof(internalUserId));
}
+
if (string.IsNullOrEmpty(key))
{
throw new ArgumentNullException(nameof(key));
@@ -152,6 +156,7 @@ namespace Emby.Server.Implementations.Data
{
throw new ArgumentNullException(nameof(userData));
}
+
if (internalUserId <= 0)
{
throw new ArgumentNullException(nameof(internalUserId));
@@ -174,7 +179,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
SaveUserData(db, internalUserId, key, userData);
}, TransactionMode);
@@ -234,7 +240,7 @@ namespace Emby.Server.Implementations.Data
}
/// <summary>
- /// Persist all user data for the specified user
+ /// Persist all user data for the specified user.
/// </summary>
private void PersistAllUserData(long internalUserId, UserItemData[] userDataList, CancellationToken cancellationToken)
{
@@ -242,7 +248,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
foreach (var userItemData in userDataList)
{
@@ -308,7 +315,7 @@ namespace Emby.Server.Implementations.Data
}
/// <summary>
- /// Return all user-data associated with the given user
+ /// Return all user-data associated with the given user.
/// </summary>
/// <param name="internalUserId"></param>
/// <returns></returns>
@@ -338,7 +345,7 @@ namespace Emby.Server.Implementations.Data
}
/// <summary>
- /// Read a row from the specified reader into the provided userData object
+ /// Read a row from the specified reader into the provided userData object.
/// </summary>
/// <param name="reader"></param>
private UserItemData ReadRow(IReadOnlyList<IResultSetValue> reader)
@@ -346,7 +353,7 @@ namespace Emby.Server.Implementations.Data
var userData = new UserItemData();
userData.Key = reader[0].ToString();
- //userData.UserId = reader[1].ReadGuidFromBlob();
+ // userData.UserId = reader[1].ReadGuidFromBlob();
if (reader[2].SQLiteType != SQLiteType.Null)
{
diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs
deleted file mode 100644
index 0c3f26974..000000000
--- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs
+++ /dev/null
@@ -1,240 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text.Json;
-using MediaBrowser.Common.Json;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Persistence;
-using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Data
-{
- /// <summary>
- /// Class SQLiteUserRepository
- /// </summary>
- public class SqliteUserRepository : BaseSqliteRepository, IUserRepository
- {
- private readonly JsonSerializerOptions _jsonOptions;
-
- public SqliteUserRepository(
- ILogger<SqliteUserRepository> logger,
- IServerApplicationPaths appPaths)
- : base(logger)
- {
- _jsonOptions = JsonDefaults.GetOptions();
-
- DbFilePath = Path.Combine(appPaths.DataPath, "users.db");
- }
-
- /// <summary>
- /// Gets the name of the repository
- /// </summary>
- /// <value>The name.</value>
- public string Name => "SQLite";
-
- /// <summary>
- /// Opens the connection to the database.
- /// </summary>
- public void Initialize()
- {
- using (var connection = GetConnection())
- {
- var localUsersTableExists = TableExists(connection, "LocalUsersv2");
-
- connection.RunQueries(new[] {
- "create table if not exists LocalUsersv2 (Id INTEGER PRIMARY KEY, guid GUID NOT NULL, data BLOB NOT NULL)",
- "drop index if exists idx_users"
- });
-
- if (!localUsersTableExists && TableExists(connection, "Users"))
- {
- TryMigrateToLocalUsersTable(connection);
- }
-
- RemoveEmptyPasswordHashes(connection);
- }
- }
-
- private void TryMigrateToLocalUsersTable(ManagedConnection connection)
- {
- try
- {
- connection.RunQueries(new[]
- {
- "INSERT INTO LocalUsersv2 (guid, data) SELECT guid,data from users"
- });
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error migrating users database");
- }
- }
-
- private void RemoveEmptyPasswordHashes(ManagedConnection connection)
- {
- foreach (var user in RetrieveAllUsers(connection))
- {
- // If the user password is the sha1 hash of the empty string, remove it
- if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
- && !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
- {
- continue;
- }
-
- user.Password = null;
- var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions);
-
- connection.RunInTransaction(db =>
- {
- using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
- {
- statement.TryBind("@InternalId", user.InternalId);
- statement.TryBind("@data", serialized);
- statement.MoveNext();
- }
- }, TransactionMode);
- }
- }
-
- /// <summary>
- /// Save a user in the repo
- /// </summary>
- public void CreateUser(User user)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions);
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(db =>
- {
- using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)"))
- {
- statement.TryBind("@guid", user.Id.ToByteArray());
- statement.TryBind("@data", serialized);
-
- statement.MoveNext();
- }
-
- var createdUser = GetUser(user.Id, connection);
-
- if (createdUser == null)
- {
- throw new ApplicationException("created user should never be null");
- }
-
- user.InternalId = createdUser.InternalId;
-
- }, TransactionMode);
- }
- }
-
- public void UpdateUser(User user)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions);
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(db =>
- {
- using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
- {
- statement.TryBind("@InternalId", user.InternalId);
- statement.TryBind("@data", serialized);
- statement.MoveNext();
- }
-
- }, TransactionMode);
- }
- }
-
- private User GetUser(Guid guid, ManagedConnection connection)
- {
- using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid"))
- {
- statement.TryBind("@guid", guid);
-
- foreach (var row in statement.ExecuteQuery())
- {
- return GetUser(row);
- }
- }
-
- return null;
- }
-
- private User GetUser(IReadOnlyList<IResultSetValue> row)
- {
- var id = row[0].ToInt64();
- var guid = row[1].ReadGuidFromBlob();
-
- var user = JsonSerializer.Deserialize<User>(row[2].ToBlob(), _jsonOptions);
- user.InternalId = id;
- user.Id = guid;
- return user;
- }
-
- /// <summary>
- /// Retrieve all users from the database
- /// </summary>
- /// <returns>IEnumerable{User}.</returns>
- public List<User> RetrieveAllUsers()
- {
- using (var connection = GetConnection(true))
- {
- return new List<User>(RetrieveAllUsers(connection));
- }
- }
-
- /// <summary>
- /// Retrieve all users from the database
- /// </summary>
- /// <returns>IEnumerable{User}.</returns>
- private IEnumerable<User> RetrieveAllUsers(ManagedConnection connection)
- {
- foreach (var row in connection.Query("select id,guid,data from LocalUsersv2"))
- {
- yield return GetUser(row);
- }
- }
-
- /// <summary>
- /// Deletes the user.
- /// </summary>
- /// <param name="user">The user.</param>
- /// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">user</exception>
- public void DeleteUser(User user)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(db =>
- {
- using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id"))
- {
- statement.TryBind("@id", user.InternalId);
- statement.MoveNext();
- }
- }, TransactionMode);
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Devices/DeviceId.cs b/Emby.Server.Implementations/Devices/DeviceId.cs
index f0d43e665..fa6ac95fd 100644
--- a/Emby.Server.Implementations/Devices/DeviceId.cs
+++ b/Emby.Server.Implementations/Devices/DeviceId.cs
@@ -12,7 +12,7 @@ namespace Emby.Server.Implementations.Devices
public class DeviceId
{
private readonly IApplicationPaths _appPaths;
- private readonly ILogger _logger;
+ private readonly ILogger<DeviceId> _logger;
private readonly object _syncLock = new object();
@@ -90,7 +90,7 @@ namespace Emby.Server.Implementations.Devices
public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
{
_appPaths = appPaths;
- _logger = loggerFactory.CreateLogger("SystemId");
+ _logger = loggerFactory.CreateLogger<DeviceId>();
}
public string Value => _id ?? (_id = GetDeviceId());
diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs
index 2283f2433..f98c694c4 100644
--- a/Emby.Server.Implementations/Devices/DeviceManager.cs
+++ b/Emby.Server.Implementations/Devices/DeviceManager.cs
@@ -5,44 +5,45 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Devices;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session;
-using MediaBrowser.Model.Users;
+using Microsoft.Extensions.Caching.Memory;
namespace Emby.Server.Implementations.Devices
{
public class DeviceManager : IDeviceManager
{
+ private readonly IMemoryCache _memoryCache;
private readonly IJsonSerializer _json;
private readonly IUserManager _userManager;
private readonly IServerConfigurationManager _config;
private readonly IAuthenticationRepository _authRepo;
- private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
+ private readonly object _capabilitiesSyncLock = new object();
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
- private readonly object _capabilitiesSyncLock = new object();
-
public DeviceManager(
IAuthenticationRepository authRepo,
IJsonSerializer json,
IUserManager userManager,
- IServerConfigurationManager config)
+ IServerConfigurationManager config,
+ IMemoryCache memoryCache)
{
_json = json;
_userManager = userManager;
_config = config;
+ _memoryCache = memoryCache;
_authRepo = authRepo;
- _capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
}
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
@@ -52,8 +53,7 @@ namespace Emby.Server.Implementations.Devices
lock (_capabilitiesSyncLock)
{
- _capabilitiesCache[deviceId] = capabilities;
-
+ _memoryCache.Set(deviceId, capabilities);
_json.SerializeToFile(capabilities, path);
}
}
@@ -62,13 +62,7 @@ namespace Emby.Server.Implementations.Devices
{
_authRepo.UpdateDeviceOptions(deviceId, options);
- if (DeviceOptionsUpdated != null)
- {
- DeviceOptionsUpdated(this, new GenericEventArgs<Tuple<string, DeviceOptions>>()
- {
- Argument = new Tuple<string, DeviceOptions>(deviceId, options)
- });
- }
+ DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, options)));
}
public DeviceOptions GetDeviceOptions(string deviceId)
@@ -78,13 +72,13 @@ namespace Emby.Server.Implementations.Devices
public ClientCapabilities GetCapabilities(string id)
{
- lock (_capabilitiesSyncLock)
+ if (_memoryCache.TryGetValue(id, out ClientCapabilities result))
{
- if (_capabilitiesCache.TryGetValue(id, out var result))
- {
- return result;
- }
+ return result;
+ }
+ lock (_capabilitiesSyncLock)
+ {
var path = Path.Combine(GetDevicePath(id), "capabilities.json");
try
{
@@ -119,7 +113,7 @@ namespace Emby.Server.Implementations.Devices
{
IEnumerable<AuthenticationInfo> sessions = _authRepo.Get(new AuthenticationInfoQuery
{
- //UserId = query.UserId
+ // UserId = query.UserId
HasUser = true
}).Items;
@@ -176,12 +170,18 @@ namespace Emby.Server.Implementations.Devices
{
throw new ArgumentException("user not found");
}
+
if (string.IsNullOrEmpty(deviceId))
{
throw new ArgumentNullException(nameof(deviceId));
}
- if (!CanAccessDevice(user.Policy, deviceId))
+ if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator))
+ {
+ return true;
+ }
+
+ if (!user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase))
{
var capabilities = GetCapabilities(deviceId);
@@ -193,20 +193,5 @@ namespace Emby.Server.Implementations.Devices
return true;
}
-
- private static bool CanAccessDevice(UserPolicy policy, string id)
- {
- if (policy.EnableAllDevices)
- {
- return true;
- }
-
- if (policy.IsAdministrator)
- {
- return true;
- }
-
- return policy.EnabledDevices.Contains(id, StringComparer.OrdinalIgnoreCase);
- }
}
}
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index c4b65d265..edb8753fd 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -6,14 +6,14 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
@@ -24,12 +24,20 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
+using Book = MediaBrowser.Controller.Entities.Book;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
+using Person = MediaBrowser.Controller.Entities.Person;
+using Photo = MediaBrowser.Controller.Entities.Photo;
+using Season = MediaBrowser.Controller.Entities.TV.Season;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace Emby.Server.Implementations.Dto
{
public class DtoService : IDtoService
{
- private readonly ILogger _logger;
+ private readonly ILogger<DtoService> _logger;
private readonly ILibraryManager _libraryManager;
private readonly IUserDataManager _userDataRepository;
private readonly IItemRepository _itemRepo;
@@ -65,25 +73,6 @@ namespace Emby.Server.Implementations.Dto
_livetvManagerFactory = livetvManagerFactory;
}
- /// <summary>
- /// Converts a BaseItem to a DTOBaseItem
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="fields">The fields.</param>
- /// <param name="user">The user.</param>
- /// <param name="owner">The owner.</param>
- /// <returns>Task{DtoBaseItem}.</returns>
- /// <exception cref="ArgumentNullException">item</exception>
- public BaseItemDto GetBaseItemDto(BaseItem item, ItemFields[] fields, User user = null, BaseItem owner = null)
- {
- var options = new DtoOptions
- {
- Fields = fields
- };
-
- return GetBaseItemDto(item, options, user, owner);
- }
-
/// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
{
@@ -208,7 +197,7 @@ namespace Emby.Server.Implementations.Dto
catch (Exception ex)
{
// Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
- _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {itemName}", item.Name);
+ _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {ItemName}", item.Name);
}
}
@@ -269,6 +258,7 @@ namespace Emby.Server.Implementations.Dto
dto.EpisodeTitle = dto.Name;
dto.Name = dto.SeriesName;
}
+
liveTvManager.AddInfoToRecordingDto(item, dto, activeRecording, user);
}
@@ -284,6 +274,7 @@ namespace Emby.Server.Implementations.Dto
{
continue;
}
+
var containers = container.Split(new[] { ',' });
if (containers.Length < 2)
{
@@ -384,7 +375,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.ChildCount))
{
- dto.ChildCount = dto.ChildCount ?? GetChildCount(folder, user);
+ dto.ChildCount ??= GetChildCount(folder, user);
}
}
@@ -398,7 +389,6 @@ namespace Emby.Server.Implementations.Dto
dto.DateLastMediaAdded = folder.DateLastMediaAdded;
}
}
-
else
{
if (options.EnableUserData)
@@ -414,7 +404,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.BasicSyncInfo))
{
- var userCanSync = user != null && user.Policy.EnableContentDownloading;
+ var userCanSync = user != null && user.HasPermission(PermissionKind.EnableContentDownloading);
if (userCanSync && item.SupportsExternalTransfer)
{
dto.SupportsSync = true;
@@ -434,21 +424,11 @@ namespace Emby.Server.Implementations.Dto
return folder.GetChildCount(user);
}
- /// <summary>
- /// Gets client-side Id of a server-side BaseItem
- /// </summary>
- /// <param name="item">The item.</param>
- /// <returns>System.String.</returns>
- /// <exception cref="ArgumentNullException">item</exception>
- public string GetDtoId(BaseItem item)
- {
- return item.Id.ToString("N", CultureInfo.InvariantCulture);
- }
-
private static void SetBookProperties(BaseItemDto dto, Book item)
{
dto.SeriesName = item.SeriesName;
}
+
private static void SetPhotoProperties(BaseItemDto dto, Photo item)
{
dto.CameraMake = item.CameraMake;
@@ -474,6 +454,11 @@ namespace Emby.Server.Implementations.Dto
}
}
+ private string GetDtoId(BaseItem item)
+ {
+ return item.Id.ToString("N", CultureInfo.InvariantCulture);
+ }
+
private void SetMusicVideoProperties(BaseItemDto dto, MusicVideo item)
{
if (!string.IsNullOrEmpty(item.Album))
@@ -483,7 +468,6 @@ namespace Emby.Server.Implementations.Dto
IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
Name = item.Album,
Limit = 1
-
});
if (parentAlbumIds.Count > 0)
@@ -503,19 +487,6 @@ namespace Emby.Server.Implementations.Dto
.ToArray();
}
- private string GetImageCacheTag(BaseItem item, ImageType type)
- {
- try
- {
- return _imageProcessor.GetImageCacheTag(item, type);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error getting {type} image info", type);
- return null;
- }
- }
-
private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
{
try
@@ -530,7 +501,7 @@ namespace Emby.Server.Implementations.Dto
}
/// <summary>
- /// Attaches People DTO's to a DTOBaseItem
+ /// Attaches People DTO's to a DTOBaseItem.
/// </summary>
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
@@ -547,22 +518,27 @@ namespace Emby.Server.Implementations.Dto
{
return 0;
}
+
if (i.IsType(PersonType.GuestStar))
{
return 1;
}
+
if (i.IsType(PersonType.Director))
{
return 2;
}
+
if (i.IsType(PersonType.Writer))
{
return 3;
}
+
if (i.IsType(PersonType.Producer))
{
return 4;
}
+
if (i.IsType(PersonType.Composer))
{
return 4;
@@ -586,7 +562,6 @@ namespace Emby.Server.Implementations.Dto
_logger.LogError(ex, "Error getting person {Name}", c);
return null;
}
-
}).Where(i => i != null)
.GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.Select(x => x.First())
@@ -605,8 +580,9 @@ namespace Emby.Server.Implementations.Dto
if (dictionary.TryGetValue(person.Name, out Person entity))
{
- baseItemPerson.PrimaryImageTag = GetImageCacheTag(entity, ImageType.Primary);
+ baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture);
+ baseItemPerson.ImageBlurHashes = dto.ImageBlurHashes;
list.Add(baseItemPerson);
}
}
@@ -654,8 +630,72 @@ namespace Emby.Server.Implementations.Dto
return _libraryManager.GetGenreId(name);
}
+ private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0)
+ {
+ var image = item.GetImageInfo(imageType, imageIndex);
+ if (image != null)
+ {
+ return GetTagAndFillBlurhash(dto, item, image);
+ }
+
+ return null;
+ }
+
+ private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ItemImageInfo image)
+ {
+ var tag = GetImageCacheTag(item, image);
+ if (!string.IsNullOrEmpty(image.BlurHash))
+ {
+ if (dto.ImageBlurHashes == null)
+ {
+ dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+ }
+
+ if (!dto.ImageBlurHashes.ContainsKey(image.Type))
+ {
+ dto.ImageBlurHashes[image.Type] = new Dictionary<string, string>();
+ }
+
+ dto.ImageBlurHashes[image.Type][tag] = image.BlurHash;
+ }
+
+ return tag;
+ }
+
+ private string[] GetTagsAndFillBlurhashes(BaseItemDto dto, BaseItem item, ImageType imageType, int limit)
+ {
+ return GetTagsAndFillBlurhashes(dto, item, imageType, item.GetImages(imageType).Take(limit).ToList());
+ }
+
+ private string[] GetTagsAndFillBlurhashes(BaseItemDto dto, BaseItem item, ImageType imageType, List<ItemImageInfo> images)
+ {
+ var tags = GetImageTags(item, images);
+ var hashes = new Dictionary<string, string>();
+ for (int i = 0; i < images.Count; i++)
+ {
+ var img = images[i];
+ if (!string.IsNullOrEmpty(img.BlurHash))
+ {
+ var tag = tags[i];
+ hashes[tag] = img.BlurHash;
+ }
+ }
+
+ if (hashes.Count > 0)
+ {
+ if (dto.ImageBlurHashes == null)
+ {
+ dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+ }
+
+ dto.ImageBlurHashes[imageType] = hashes;
+ }
+
+ return tags;
+ }
+
/// <summary>
- /// Sets simple property values on a DTOBaseItem
+ /// Sets simple property values on a DTOBaseItem.
/// </summary>
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
@@ -674,8 +714,8 @@ namespace Emby.Server.Implementations.Dto
dto.LockData = item.IsLocked;
dto.ForcedSortName = item.ForcedSortName;
}
- dto.Container = item.Container;
+ dto.Container = item.Container;
dto.EndDate = item.EndDate;
if (options.ContainsField(ItemFields.ExternalUrls))
@@ -694,10 +734,12 @@ namespace Emby.Server.Implementations.Dto
dto.AspectRatio = hasAspectRatio.AspectRatio;
}
+ dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+
var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
if (backdropLimit > 0)
{
- dto.BackdropImageTags = GetImageTags(item, item.GetImages(ImageType.Backdrop).Take(backdropLimit).ToList());
+ dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit);
}
if (options.ContainsField(ItemFields.ScreenshotImageTags))
@@ -705,7 +747,7 @@ namespace Emby.Server.Implementations.Dto
var screenshotLimit = options.GetImageLimit(ImageType.Screenshot);
if (screenshotLimit > 0)
{
- dto.ScreenshotImageTags = GetImageTags(item, item.GetImages(ImageType.Screenshot).Take(screenshotLimit).ToList());
+ dto.ScreenshotImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Screenshot, screenshotLimit);
}
}
@@ -721,12 +763,11 @@ namespace Emby.Server.Implementations.Dto
// Prevent implicitly captured closure
var currentItem = item;
- foreach (var image in currentItem.ImageInfos.Where(i => !currentItem.AllowsMultipleImages(i.Type))
- .ToList())
+ foreach (var image in currentItem.ImageInfos.Where(i => !currentItem.AllowsMultipleImages(i.Type)))
{
if (options.GetImageLimit(image.Type) > 0)
{
- var tag = GetImageCacheTag(item, image);
+ var tag = GetTagAndFillBlurhash(dto, item, image);
if (tag != null)
{
@@ -871,11 +912,10 @@ namespace Emby.Server.Implementations.Dto
if (albumParent != null)
{
dto.AlbumId = albumParent.Id;
-
- dto.AlbumPrimaryImageTag = GetImageCacheTag(albumParent, ImageType.Primary);
+ dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary);
}
- //if (options.ContainsField(ItemFields.MediaSourceCount))
+ // if (options.ContainsField(ItemFields.MediaSourceCount))
//{
// Songs always have one
//}
@@ -885,13 +925,13 @@ namespace Emby.Server.Implementations.Dto
{
dto.Artists = hasArtist.Artists;
- //var artistItems = _libraryManager.GetArtists(new InternalItemsQuery
+ // var artistItems = _libraryManager.GetArtists(new InternalItemsQuery
//{
// EnableTotalRecordCount = false,
// ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
//});
- //dto.ArtistItems = artistItems.Items
+ // dto.ArtistItems = artistItems.Items
// .Select(i =>
// {
// var artist = i.Item1;
@@ -904,7 +944,7 @@ namespace Emby.Server.Implementations.Dto
// .ToList();
// Include artists that are not in the database yet, e.g., just added via metadata editor
- //var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
+ // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
dto.ArtistItems = hasArtist.Artists
//.Except(foundArtists, new DistinctNameComparer())
.Select(i =>
@@ -929,7 +969,6 @@ namespace Emby.Server.Implementations.Dto
}
return null;
-
}).Where(i => i != null).ToArray();
}
@@ -938,13 +977,13 @@ namespace Emby.Server.Implementations.Dto
{
dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
- //var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery
+ // var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery
//{
// EnableTotalRecordCount = false,
// ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
//});
- //dto.AlbumArtists = artistItems.Items
+ // dto.AlbumArtists = artistItems.Items
// .Select(i =>
// {
// var artist = i.Item1;
@@ -980,7 +1019,6 @@ namespace Emby.Server.Implementations.Dto
}
return null;
-
}).Where(i => i != null).ToArray();
}
@@ -1094,12 +1132,13 @@ namespace Emby.Server.Implementations.Dto
// this block will add the series poster for episodes without a poster
// TODO maybe remove the if statement entirely
- //if (options.ContainsField(ItemFields.SeriesPrimaryImage))
+ // if (options.ContainsField(ItemFields.SeriesPrimaryImage))
{
episodeSeries = episodeSeries ?? episode.Series;
if (episodeSeries != null)
{
- dto.SeriesPrimaryImageTag = GetImageCacheTag(episodeSeries, ImageType.Primary);
+ dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
+ AttachPrimaryImageAspectRatio(dto, episodeSeries);
}
}
@@ -1140,12 +1179,13 @@ namespace Emby.Server.Implementations.Dto
// this block will add the series poster for seasons without a poster
// TODO maybe remove the if statement entirely
- //if (options.ContainsField(ItemFields.SeriesPrimaryImage))
+ // if (options.ContainsField(ItemFields.SeriesPrimaryImage))
{
series = series ?? season.Series;
if (series != null)
{
- dto.SeriesPrimaryImageTag = GetImageCacheTag(series, ImageType.Primary);
+ dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
+ AttachPrimaryImageAspectRatio(dto, series);
}
}
}
@@ -1275,9 +1315,10 @@ namespace Emby.Server.Implementations.Dto
if (image != null)
{
dto.ParentLogoItemId = GetDtoId(parent);
- dto.ParentLogoImageTag = GetImageCacheTag(parent, image);
+ dto.ParentLogoImageTag = GetTagAndFillBlurhash(dto, parent, image);
}
}
+
if (artLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtItemId == null)
{
var image = allImages.FirstOrDefault(i => i.Type == ImageType.Art);
@@ -1285,9 +1326,10 @@ namespace Emby.Server.Implementations.Dto
if (image != null)
{
dto.ParentArtItemId = GetDtoId(parent);
- dto.ParentArtImageTag = GetImageCacheTag(parent, image);
+ dto.ParentArtImageTag = GetTagAndFillBlurhash(dto, parent, image);
}
}
+
if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && !(parent is ICollectionFolder) && !(parent is UserView))
{
var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
@@ -1295,9 +1337,10 @@ namespace Emby.Server.Implementations.Dto
if (image != null)
{
dto.ParentThumbItemId = GetDtoId(parent);
- dto.ParentThumbImageTag = GetImageCacheTag(parent, image);
+ dto.ParentThumbImageTag = GetTagAndFillBlurhash(dto, parent, image);
}
}
+
if (backdropLimit > 0 && !((dto.BackdropImageTags != null && dto.BackdropImageTags.Length > 0) || (dto.ParentBackdropImageTags != null && dto.ParentBackdropImageTags.Length > 0)))
{
var images = allImages.Where(i => i.Type == ImageType.Backdrop).Take(backdropLimit).ToList();
@@ -1305,7 +1348,7 @@ namespace Emby.Server.Implementations.Dto
if (images.Count > 0)
{
dto.ParentBackdropItemId = GetDtoId(parent);
- dto.ParentBackdropImageTags = GetImageTags(parent, images);
+ dto.ParentBackdropImageTags = GetTagsAndFillBlurhashes(dto, parent, ImageType.Backdrop, images);
}
}
@@ -1389,7 +1432,7 @@ namespace Emby.Server.Implementations.Dto
return null;
}
- return width / height;
+ return (double)width / height;
}
}
}
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index b69a126b3..c762aa0b8 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -13,10 +13,8 @@
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
<ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" />
- <ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" />
<ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" />
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
- <ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
<ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" />
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
<ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
@@ -24,8 +22,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="IPNetwork2" Version="2.4.0.126" />
- <PackageReference Include="Jellyfin.XmlTv" Version="10.4.3" />
+ <PackageReference Include="IPNetwork2" Version="2.5.226" />
+ <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
@@ -34,16 +32,16 @@
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.4" />
- <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.4" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.4" />
- <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.4" />
- <PackageReference Include="Mono.Nat" Version="2.0.1" />
- <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
- <PackageReference Include="ServiceStack.Text.Core" Version="5.8.0" />
- <PackageReference Include="sharpcompress" Version="0.25.0" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" />
+ <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.9" />
+ <PackageReference Include="Mono.Nat" Version="3.0.0" />
+ <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" />
+ <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
+ <PackageReference Include="sharpcompress" Version="0.26.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
- <PackageReference Include="DotNet.Glob" Version="3.0.9" />
+ <PackageReference Include="DotNet.Glob" Version="3.1.0" />
</ItemGroup>
<ItemGroup>
@@ -54,6 +52,7 @@
<TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
</PropertyGroup>
<!-- Code Analyzers-->
diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
index 878cee23c..2e8cc76d2 100644
--- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
@@ -7,11 +7,11 @@ using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Events;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Events;
using Microsoft.Extensions.Logging;
using Mono.Nat;
@@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.EntryPoints
public class ExternalPortForwarding : IServerEntryPoint
{
private readonly IServerApplicationHost _appHost;
- private readonly ILogger _logger;
+ private readonly ILogger<ExternalPortForwarding> _logger;
private readonly IServerConfigurationManager _config;
private readonly IDeviceDiscovery _deviceDiscovery;
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index 8e3236407..ff64e217a 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
@@ -6,6 +6,8 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Events;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -14,7 +16,7 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
@@ -22,13 +24,15 @@ namespace Emby.Server.Implementations.EntryPoints
public class LibraryChangedNotifier : IServerEntryPoint
{
/// <summary>
- /// The library manager.
+ /// The library update duration.
/// </summary>
- private readonly ILibraryManager _libraryManager;
+ private const int LibraryUpdateDuration = 30000;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IProviderManager _providerManager;
private readonly ISessionManager _sessionManager;
private readonly IUserManager _userManager;
- private readonly ILogger _logger;
+ private readonly ILogger<LibraryChangedNotifier> _logger;
/// <summary>
/// The library changed sync lock.
@@ -37,23 +41,10 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly List<Folder> _foldersAddedTo = new List<Folder>();
private readonly List<Folder> _foldersRemovedFrom = new List<Folder>();
-
private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
private readonly List<BaseItem> _itemsRemoved = new List<BaseItem>();
private readonly List<BaseItem> _itemsUpdated = new List<BaseItem>();
-
- /// <summary>
- /// Gets or sets the library update timer.
- /// </summary>
- /// <value>The library update timer.</value>
- private Timer LibraryUpdateTimer { get; set; }
-
- /// <summary>
- /// The library update duration.
- /// </summary>
- private const int LibraryUpdateDuration = 30000;
-
- private readonly IProviderManager _providerManager;
+ private readonly Dictionary<Guid, DateTime> _lastProgressMessageTimes = new Dictionary<Guid, DateTime>();
public LibraryChangedNotifier(
ILibraryManager libraryManager,
@@ -69,22 +60,26 @@ namespace Emby.Server.Implementations.EntryPoints
_providerManager = providerManager;
}
+ /// <summary>
+ /// Gets or sets the library update timer.
+ /// </summary>
+ /// <value>The library update timer.</value>
+ private Timer LibraryUpdateTimer { get; set; }
+
public Task RunAsync()
{
- _libraryManager.ItemAdded += libraryManager_ItemAdded;
- _libraryManager.ItemUpdated += libraryManager_ItemUpdated;
- _libraryManager.ItemRemoved += libraryManager_ItemRemoved;
+ _libraryManager.ItemAdded += OnLibraryItemAdded;
+ _libraryManager.ItemUpdated += OnLibraryItemUpdated;
+ _libraryManager.ItemRemoved += OnLibraryItemRemoved;
- _providerManager.RefreshCompleted += _providerManager_RefreshCompleted;
- _providerManager.RefreshStarted += _providerManager_RefreshStarted;
- _providerManager.RefreshProgress += _providerManager_RefreshProgress;
+ _providerManager.RefreshCompleted += OnProviderRefreshCompleted;
+ _providerManager.RefreshStarted += OnProviderRefreshStarted;
+ _providerManager.RefreshProgress += OnProviderRefreshProgress;
return Task.CompletedTask;
}
- private Dictionary<Guid, DateTime> _lastProgressMessageTimes = new Dictionary<Guid, DateTime>();
-
- private void _providerManager_RefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
+ private void OnProviderRefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
{
var item = e.Argument.Item1;
@@ -111,7 +106,7 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
- _sessionManager.SendMessageToAdminSessions("RefreshProgress", dict, CancellationToken.None);
+ _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None);
}
catch
{
@@ -121,36 +116,35 @@ namespace Emby.Server.Implementations.EntryPoints
foreach (var collectionFolder in collectionFolders)
{
- var collectionFolderDict = new Dictionary<string, string>();
- collectionFolderDict["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture);
- collectionFolderDict["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture);
+ var collectionFolderDict = new Dictionary<string, string>
+ {
+ ["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
+ ["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture)
+ };
try
{
- _sessionManager.SendMessageToAdminSessions("RefreshProgress", collectionFolderDict, CancellationToken.None);
+ _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None);
}
catch
{
-
}
}
}
- private void _providerManager_RefreshStarted(object sender, GenericEventArgs<BaseItem> e)
+ private void OnProviderRefreshStarted(object sender, GenericEventArgs<BaseItem> e)
{
- _providerManager_RefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
+ OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
}
- private void _providerManager_RefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
+ private void OnProviderRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
{
- _providerManager_RefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
+ OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
}
private static bool EnableRefreshMessage(BaseItem item)
{
- var folder = item as Folder;
-
- if (folder == null)
+ if (!(item is Folder folder))
{
return false;
}
@@ -183,7 +177,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
- void libraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
+ private void OnLibraryItemAdded(object sender, ItemChangeEventArgs e)
{
if (!FilterItem(e.Item))
{
@@ -205,8 +199,7 @@ namespace Emby.Server.Implementations.EntryPoints
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
}
- var parent = e.Item.GetParent() as Folder;
- if (parent != null)
+ if (e.Item.GetParent() is Folder parent)
{
_foldersAddedTo.Add(parent);
}
@@ -220,7 +213,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
- void libraryManager_ItemUpdated(object sender, ItemChangeEventArgs e)
+ private void OnLibraryItemUpdated(object sender, ItemChangeEventArgs e)
{
if (!FilterItem(e.Item))
{
@@ -231,8 +224,7 @@ namespace Emby.Server.Implementations.EntryPoints
{
if (LibraryUpdateTimer == null)
{
- LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration,
- Timeout.Infinite);
+ LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
}
else
{
@@ -248,7 +240,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
- void libraryManager_ItemRemoved(object sender, ItemChangeEventArgs e)
+ private void OnLibraryItemRemoved(object sender, ItemChangeEventArgs e)
{
if (!FilterItem(e.Item))
{
@@ -259,16 +251,14 @@ namespace Emby.Server.Implementations.EntryPoints
{
if (LibraryUpdateTimer == null)
{
- LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration,
- Timeout.Infinite);
+ LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
}
else
{
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
}
- var parent = e.Parent as Folder;
- if (parent != null)
+ if (e.Parent is Folder parent)
{
_foldersRemovedFrom.Add(parent);
}
@@ -302,7 +292,7 @@ namespace Emby.Server.Implementations.EntryPoints
.Select(x => x.First())
.ToList();
- SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None);
+ SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult();
if (LibraryUpdateTimer != null)
{
@@ -327,7 +317,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// <param name="foldersAddedTo">The folders added to.</param>
/// <param name="foldersRemovedFrom">The folders removed from.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- private async void SendChangeNotifications(List<BaseItem> itemsAdded, List<BaseItem> itemsUpdated, List<BaseItem> itemsRemoved, List<Folder> foldersAddedTo, List<Folder> foldersRemovedFrom, CancellationToken cancellationToken)
+ private async Task SendChangeNotifications(List<BaseItem> itemsAdded, List<BaseItem> itemsUpdated, List<BaseItem> itemsRemoved, List<Folder> foldersAddedTo, List<Folder> foldersRemovedFrom, CancellationToken cancellationToken)
{
var userIds = _sessionManager.Sessions
.Select(i => i.UserId)
@@ -356,7 +346,7 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
- await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "LibraryChanged", info, cancellationToken).ConfigureAwait(false);
+ await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.LibraryChanged, info, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -486,13 +476,13 @@ namespace Emby.Server.Implementations.EntryPoints
LibraryUpdateTimer = null;
}
- _libraryManager.ItemAdded -= libraryManager_ItemAdded;
- _libraryManager.ItemUpdated -= libraryManager_ItemUpdated;
- _libraryManager.ItemRemoved -= libraryManager_ItemRemoved;
+ _libraryManager.ItemAdded -= OnLibraryItemAdded;
+ _libraryManager.ItemUpdated -= OnLibraryItemUpdated;
+ _libraryManager.ItemRemoved -= OnLibraryItemRemoved;
- _providerManager.RefreshCompleted -= _providerManager_RefreshCompleted;
- _providerManager.RefreshStarted -= _providerManager_RefreshStarted;
- _providerManager.RefreshProgress -= _providerManager_RefreshProgress;
+ _providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
+ _providerManager.RefreshStarted -= OnProviderRefreshStarted;
+ _providerManager.RefreshProgress -= OnProviderRefreshProgress;
}
}
}
diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
index 41c0c5115..824bb85f4 100644
--- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
@@ -4,10 +4,13 @@ using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
@@ -17,7 +20,7 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly ILiveTvManager _liveTvManager;
private readonly ISessionManager _sessionManager;
private readonly IUserManager _userManager;
- private readonly ILogger _logger;
+ private readonly ILogger<RecordingNotifier> _logger;
public RecordingNotifier(
ISessionManager sessionManager,
@@ -42,29 +45,29 @@ namespace Emby.Server.Implementations.EntryPoints
return Task.CompletedTask;
}
- private void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+ private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
{
- SendMessage("SeriesTimerCreated", e.Argument);
+ await SendMessage(SessionMessageType.SeriesTimerCreated, e.Argument).ConfigureAwait(false);
}
- private void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+ private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
{
- SendMessage("TimerCreated", e.Argument);
+ await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false);
}
- private void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+ private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
{
- SendMessage("SeriesTimerCancelled", e.Argument);
+ await SendMessage(SessionMessageType.SeriesTimerCancelled, e.Argument).ConfigureAwait(false);
}
- private void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
+ private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
{
- SendMessage("TimerCancelled", e.Argument);
+ await SendMessage(SessionMessageType.TimerCancelled, e.Argument).ConfigureAwait(false);
}
- private async void SendMessage(string name, TimerEventInfo info)
+ private async Task SendMessage(SessionMessageType name, TimerEventInfo info)
{
- var users = _userManager.Users.Where(i => i.Policy.EnableLiveTvAccess).Select(i => i.Id).ToList();
+ var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList();
try
{
diff --git a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs
deleted file mode 100644
index 54f4b67e6..000000000
--- a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Tasks;
-
-namespace Emby.Server.Implementations.EntryPoints
-{
- /// <summary>
- /// Class RefreshUsersMetadata.
- /// </summary>
- public class RefreshUsersMetadata : IScheduledTask, IConfigurableScheduledTask
- {
- /// <summary>
- /// The user manager.
- /// </summary>
- private readonly IUserManager _userManager;
- private readonly IFileSystem _fileSystem;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="RefreshUsersMetadata" /> class.
- /// </summary>
- public RefreshUsersMetadata(IUserManager userManager, IFileSystem fileSystem)
- {
- _userManager = userManager;
- _fileSystem = fileSystem;
- }
-
- /// <inheritdoc />
- public string Name => "Refresh Users";
-
- /// <inheritdoc />
- public string Key => "RefreshUsers";
-
- /// <inheritdoc />
- public string Description => "Refresh user infos";
-
- /// <inheritdoc />
- public string Category => "Library";
-
- /// <inheritdoc />
- public bool IsHidden => true;
-
- /// <inheritdoc />
- public bool IsEnabled => true;
-
- /// <inheritdoc />
- public bool IsLogged => true;
-
- /// <inheritdoc />
- public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
- {
- foreach (var user in _userManager.Users)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false);
- }
- }
-
- /// <inheritdoc />
- public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
- {
- return new[]
- {
- new TaskTriggerInfo
- {
- IntervalTicks = TimeSpan.FromDays(1).Ticks,
- Type = TaskTriggerInfo.TriggerInterval
- }
- };
- }
- }
-}
diff --git a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs b/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs
deleted file mode 100644
index e1dbb663b..000000000
--- a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs
+++ /dev/null
@@ -1,229 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Plugins;
-using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Tasks;
-
-namespace Emby.Server.Implementations.EntryPoints
-{
- /// <summary>
- /// Class WebSocketEvents.
- /// </summary>
- public class ServerEventNotifier : IServerEntryPoint
- {
- /// <summary>
- /// The user manager.
- /// </summary>
- private readonly IUserManager _userManager;
-
- /// <summary>
- /// The installation manager.
- /// </summary>
- private readonly IInstallationManager _installationManager;
-
- /// <summary>
- /// The kernel.
- /// </summary>
- private readonly IServerApplicationHost _appHost;
-
- /// <summary>
- /// The task manager.
- /// </summary>
- private readonly ITaskManager _taskManager;
-
- private readonly ISessionManager _sessionManager;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ServerEventNotifier"/> class.
- /// </summary>
- /// <param name="appHost">The application host.</param>
- /// <param name="userManager">The user manager.</param>
- /// <param name="installationManager">The installation manager.</param>
- /// <param name="taskManager">The task manager.</param>
- /// <param name="sessionManager">The session manager.</param>
- public ServerEventNotifier(
- IServerApplicationHost appHost,
- IUserManager userManager,
- IInstallationManager installationManager,
- ITaskManager taskManager,
- ISessionManager sessionManager)
- {
- _userManager = userManager;
- _installationManager = installationManager;
- _appHost = appHost;
- _taskManager = taskManager;
- _sessionManager = sessionManager;
- }
-
- /// <inheritdoc />
- public Task RunAsync()
- {
- _userManager.UserDeleted += OnUserDeleted;
- _userManager.UserUpdated += OnUserUpdated;
- _userManager.UserPolicyUpdated += OnUserPolicyUpdated;
- _userManager.UserConfigurationUpdated += OnUserConfigurationUpdated;
-
- _appHost.HasPendingRestartChanged += OnHasPendingRestartChanged;
-
- _installationManager.PluginUninstalled += OnPluginUninstalled;
- _installationManager.PackageInstalling += OnPackageInstalling;
- _installationManager.PackageInstallationCancelled += OnPackageInstallationCancelled;
- _installationManager.PackageInstallationCompleted += OnPackageInstallationCompleted;
- _installationManager.PackageInstallationFailed += OnPackageInstallationFailed;
-
- _taskManager.TaskCompleted += OnTaskCompleted;
-
- return Task.CompletedTask;
- }
-
- private void OnPackageInstalling(object sender, InstallationEventArgs e)
- {
- SendMessageToAdminSessions("PackageInstalling", e.InstallationInfo);
- }
-
- private void OnPackageInstallationCancelled(object sender, InstallationEventArgs e)
- {
- SendMessageToAdminSessions("PackageInstallationCancelled", e.InstallationInfo);
- }
-
- private void OnPackageInstallationCompleted(object sender, InstallationEventArgs e)
- {
- SendMessageToAdminSessions("PackageInstallationCompleted", e.InstallationInfo);
- }
-
- private void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
- {
- SendMessageToAdminSessions("PackageInstallationFailed", e.InstallationInfo);
- }
-
- private void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
- {
- SendMessageToAdminSessions("ScheduledTaskEnded", e.Result);
- }
-
- /// <summary>
- /// Installations the manager_ plugin uninstalled.
- /// </summary>
- /// <param name="sender">The sender.</param>
- /// <param name="e">The e.</param>
- private void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e)
- {
- SendMessageToAdminSessions("PluginUninstalled", e.Argument.GetPluginInfo());
- }
-
- /// <summary>
- /// Handles the HasPendingRestartChanged event of the kernel control.
- /// </summary>
- /// <param name="sender">The source of the event.</param>
- /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
- private void OnHasPendingRestartChanged(object sender, EventArgs e)
- {
- _sessionManager.SendRestartRequiredNotification(CancellationToken.None);
- }
-
- /// <summary>
- /// Users the manager_ user updated.
- /// </summary>
- /// <param name="sender">The sender.</param>
- /// <param name="e">The e.</param>
- private void OnUserUpdated(object sender, GenericEventArgs<User> e)
- {
- var dto = _userManager.GetUserDto(e.Argument);
-
- SendMessageToUserSession(e.Argument, "UserUpdated", dto);
- }
-
- /// <summary>
- /// Users the manager_ user deleted.
- /// </summary>
- /// <param name="sender">The sender.</param>
- /// <param name="e">The e.</param>
- private void OnUserDeleted(object sender, GenericEventArgs<User> e)
- {
- SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture));
- }
-
- private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e)
- {
- var dto = _userManager.GetUserDto(e.Argument);
-
- SendMessageToUserSession(e.Argument, "UserPolicyUpdated", dto);
- }
-
- private void OnUserConfigurationUpdated(object sender, GenericEventArgs<User> e)
- {
- var dto = _userManager.GetUserDto(e.Argument);
-
- SendMessageToUserSession(e.Argument, "UserConfigurationUpdated", dto);
- }
-
- private async void SendMessageToAdminSessions<T>(string name, T data)
- {
- try
- {
- await _sessionManager.SendMessageToAdminSessions(name, data, CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception)
- {
-
- }
- }
-
- private async void SendMessageToUserSession<T>(User user, string name, T data)
- {
- try
- {
- await _sessionManager.SendMessageToUserSessions(
- new List<Guid> { user.Id },
- name,
- data,
- CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception)
- {
-
- }
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Releases unmanaged and - optionally - managed resources.
- /// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool dispose)
- {
- if (dispose)
- {
- _userManager.UserDeleted -= OnUserDeleted;
- _userManager.UserUpdated -= OnUserUpdated;
- _userManager.UserPolicyUpdated -= OnUserPolicyUpdated;
- _userManager.UserConfigurationUpdated -= OnUserConfigurationUpdated;
-
- _installationManager.PluginUninstalled -= OnPluginUninstalled;
- _installationManager.PackageInstalling -= OnPackageInstalling;
- _installationManager.PackageInstallationCancelled -= OnPackageInstallationCancelled;
- _installationManager.PackageInstallationCompleted -= OnPackageInstallationCompleted;
- _installationManager.PackageInstallationFailed -= OnPackageInstallationFailed;
-
- _appHost.HasPendingRestartChanged -= OnHasPendingRestartChanged;
-
- _taskManager.TaskCompleted -= OnTaskCompleted;
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs
deleted file mode 100644
index 2e738deeb..000000000
--- a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Browser;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Extensions;
-using MediaBrowser.Controller.Plugins;
-using Microsoft.Extensions.Configuration;
-
-namespace Emby.Server.Implementations.EntryPoints
-{
- /// <summary>
- /// Class StartupWizard.
- /// </summary>
- public sealed class StartupWizard : IServerEntryPoint
- {
- private readonly IServerApplicationHost _appHost;
- private readonly IConfiguration _appConfig;
- private readonly IServerConfigurationManager _config;
- private readonly IStartupOptions _startupOptions;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="StartupWizard"/> class.
- /// </summary>
- /// <param name="appHost">The application host.</param>
- /// <param name="appConfig">The application configuration.</param>
- /// <param name="config">The configuration manager.</param>
- /// <param name="startupOptions">The application startup options.</param>
- public StartupWizard(
- IServerApplicationHost appHost,
- IConfiguration appConfig,
- IServerConfigurationManager config,
- IStartupOptions startupOptions)
- {
- _appHost = appHost;
- _appConfig = appConfig;
- _config = config;
- _startupOptions = startupOptions;
- }
-
- /// <inheritdoc />
- public Task RunAsync()
- {
- Run();
- return Task.CompletedTask;
- }
-
- private void Run()
- {
- if (!_appHost.CanLaunchWebBrowser)
- {
- return;
- }
-
- // Always launch the startup wizard if possible when it has not been completed
- if (!_config.Configuration.IsStartupWizardCompleted && _appConfig.HostWebClient())
- {
- BrowserLauncher.OpenWebApp(_appHost);
- return;
- }
-
- // Do nothing if the web app is configured to not run automatically
- if (!_config.Configuration.AutoRunWebApp || _startupOptions.NoAutoRunWebApp)
- {
- return;
- }
-
- // Launch the swagger page if the web client is not hosted, otherwise open the web client
- if (_appConfig.HostWebClient())
- {
- BrowserLauncher.OpenWebApp(_appHost);
- }
- else
- {
- BrowserLauncher.OpenSwaggerPage(_appHost);
- }
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- }
- }
-}
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index 6929c81f9..9486874d5 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -1,3 +1,4 @@
+using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Udp;
@@ -21,7 +22,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// <summary>
/// The logger.
/// </summary>
- private readonly ILogger _logger;
+ private readonly ILogger<UdpServerEntryPoint> _logger;
private readonly IServerApplicationHost _appHost;
private readonly IConfiguration _config;
@@ -43,14 +44,22 @@ namespace Emby.Server.Implementations.EntryPoints
_logger = logger;
_appHost = appHost;
_config = configuration;
-
}
/// <inheritdoc />
- public async Task RunAsync()
+ public Task RunAsync()
{
- _udpServer = new UdpServer(_logger, _appHost, _config);
- _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
+ try
+ {
+ _udpServer = new UdpServer(_logger, _appHost, _config);
+ _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
+ }
+ catch (SocketException ex)
+ {
+ _logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber);
+ }
+
+ return Task.CompletedTask;
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index 3618b88c5..1989e9ed2 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -28,7 +28,6 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly object _syncLock = new object();
private Timer _updateTimer;
-
public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
{
_userDataManager = userDataManager;
@@ -116,7 +115,7 @@ namespace Emby.Server.Implementations.EntryPoints
private Task SendNotifications(Guid userId, List<BaseItem> changedItems, CancellationToken cancellationToken)
{
- return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "UserDataChanged", () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
+ return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.UserDataChanged, () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
}
private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List<BaseItem> changedItems)
diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
deleted file mode 100644
index d66bb7638..000000000
--- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
+++ /dev/null
@@ -1,335 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpClientManager
-{
- /// <summary>
- /// Class HttpClientManager.
- /// </summary>
- public class HttpClientManager : IHttpClient
- {
- private readonly ILogger _logger;
- private readonly IApplicationPaths _appPaths;
- private readonly IFileSystem _fileSystem;
- private readonly IApplicationHost _appHost;
-
- /// <summary>
- /// Holds a dictionary of http clients by host. Use GetHttpClient(host) to retrieve or create a client for web requests.
- /// DON'T dispose it after use.
- /// </summary>
- /// <value>The HTTP clients.</value>
- private readonly ConcurrentDictionary<string, HttpClient> _httpClients = new ConcurrentDictionary<string, HttpClient>();
-
- /// <summary>
- /// Initializes a new instance of the <see cref="HttpClientManager" /> class.
- /// </summary>
- public HttpClientManager(
- IApplicationPaths appPaths,
- ILogger<HttpClientManager> logger,
- IFileSystem fileSystem,
- IApplicationHost appHost)
- {
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- _fileSystem = fileSystem;
- _appPaths = appPaths ?? throw new ArgumentNullException(nameof(appPaths));
- _appHost = appHost;
- }
-
- /// <summary>
- /// Gets the correct http client for the given url.
- /// </summary>
- /// <param name="url">The url.</param>
- /// <returns>HttpClient.</returns>
- private HttpClient GetHttpClient(string url)
- {
- var key = GetHostFromUrl(url);
-
- if (!_httpClients.TryGetValue(key, out var client))
- {
- client = new HttpClient()
- {
- BaseAddress = new Uri(url)
- };
-
- _httpClients.TryAdd(key, client);
- }
-
- return client;
- }
-
- private HttpRequestMessage GetRequestMessage(HttpRequestOptions options, HttpMethod method)
- {
- string url = options.Url;
- var uriAddress = new Uri(url);
- string userInfo = uriAddress.UserInfo;
- if (!string.IsNullOrWhiteSpace(userInfo))
- {
- _logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url);
- url = url.Replace(userInfo + '@', string.Empty, StringComparison.Ordinal);
- }
-
- var request = new HttpRequestMessage(method, url);
-
- foreach (var header in options.RequestHeaders)
- {
- request.Headers.TryAddWithoutValidation(header.Key, header.Value);
- }
-
- if (options.EnableDefaultUserAgent
- && !request.Headers.TryGetValues(HeaderNames.UserAgent, out _))
- {
- request.Headers.Add(HeaderNames.UserAgent, _appHost.ApplicationUserAgent);
- }
-
- switch (options.DecompressionMethod)
- {
- case CompressionMethods.Deflate | CompressionMethods.Gzip:
- request.Headers.Add(HeaderNames.AcceptEncoding, new[] { "gzip", "deflate" });
- break;
- case CompressionMethods.Deflate:
- request.Headers.Add(HeaderNames.AcceptEncoding, "deflate");
- break;
- case CompressionMethods.Gzip:
- request.Headers.Add(HeaderNames.AcceptEncoding, "gzip");
- break;
- default:
- break;
- }
-
- if (options.EnableKeepAlive)
- {
- request.Headers.Add(HeaderNames.Connection, "Keep-Alive");
- }
-
- // request.Headers.Add(HeaderNames.CacheControl, "no-cache");
-
- /*
- if (!string.IsNullOrWhiteSpace(userInfo))
- {
- var parts = userInfo.Split(':');
- if (parts.Length == 2)
- {
- request.Headers.Add(HeaderNames., GetCredential(url, parts[0], parts[1]);
- }
- }
- */
-
- return request;
- }
-
- /// <summary>
- /// Gets the response internal.
- /// </summary>
- /// <param name="options">The options.</param>
- /// <returns>Task{HttpResponseInfo}.</returns>
- public Task<HttpResponseInfo> GetResponse(HttpRequestOptions options)
- => SendAsync(options, HttpMethod.Get);
-
- /// <summary>
- /// Performs a GET request and returns the resulting stream
- /// </summary>
- /// <param name="options">The options.</param>
- /// <returns>Task{Stream}.</returns>
- public async Task<Stream> Get(HttpRequestOptions options)
- {
- var response = await GetResponse(options).ConfigureAwait(false);
- return response.Content;
- }
-
- /// <summary>
- /// send as an asynchronous operation.
- /// </summary>
- /// <param name="options">The options.</param>
- /// <param name="httpMethod">The HTTP method.</param>
- /// <returns>Task{HttpResponseInfo}.</returns>
- public Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod)
- => SendAsync(options, new HttpMethod(httpMethod));
-
- /// <summary>
- /// send as an asynchronous operation.
- /// </summary>
- /// <param name="options">The options.</param>
- /// <param name="httpMethod">The HTTP method.</param>
- /// <returns>Task{HttpResponseInfo}.</returns>
- public async Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, HttpMethod httpMethod)
- {
- if (options.CacheMode == CacheMode.None)
- {
- return await SendAsyncInternal(options, httpMethod).ConfigureAwait(false);
- }
-
- var url = options.Url;
- var urlHash = url.ToUpperInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
-
- var responseCachePath = Path.Combine(_appPaths.CachePath, "httpclient", urlHash);
-
- var response = GetCachedResponse(responseCachePath, options.CacheLength, url);
- if (response != null)
- {
- return response;
- }
-
- response = await SendAsyncInternal(options, httpMethod).ConfigureAwait(false);
-
- if (response.StatusCode == HttpStatusCode.OK)
- {
- await CacheResponse(response, responseCachePath).ConfigureAwait(false);
- }
-
- return response;
- }
-
- private HttpResponseInfo GetCachedResponse(string responseCachePath, TimeSpan cacheLength, string url)
- {
- if (File.Exists(responseCachePath)
- && _fileSystem.GetLastWriteTimeUtc(responseCachePath).Add(cacheLength) > DateTime.UtcNow)
- {
- var stream = new FileStream(responseCachePath, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true);
-
- return new HttpResponseInfo
- {
- ResponseUrl = url,
- Content = stream,
- StatusCode = HttpStatusCode.OK,
- ContentLength = stream.Length
- };
- }
-
- return null;
- }
-
- private async Task CacheResponse(HttpResponseInfo response, string responseCachePath)
- {
- Directory.CreateDirectory(Path.GetDirectoryName(responseCachePath));
-
- using (var fileStream = new FileStream(
- responseCachePath,
- FileMode.Create,
- FileAccess.Write,
- FileShare.None,
- IODefaults.FileStreamBufferSize,
- true))
- {
- await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
-
- response.Content.Position = 0;
- }
- }
-
- private async Task<HttpResponseInfo> SendAsyncInternal(HttpRequestOptions options, HttpMethod httpMethod)
- {
- ValidateParams(options);
-
- options.CancellationToken.ThrowIfCancellationRequested();
-
- var client = GetHttpClient(options.Url);
-
- var httpWebRequest = GetRequestMessage(options, httpMethod);
-
- if (!string.IsNullOrEmpty(options.RequestContent)
- || httpMethod == HttpMethod.Post)
- {
- if (options.RequestContent != null)
- {
- httpWebRequest.Content = new StringContent(
- options.RequestContent,
- null,
- options.RequestContentType);
- }
- else
- {
- httpWebRequest.Content = new ByteArrayContent(Array.Empty<byte>());
- }
- }
-
- options.CancellationToken.ThrowIfCancellationRequested();
-
- var response = await client.SendAsync(
- httpWebRequest,
- options.BufferContent || options.CacheMode == CacheMode.Unconditional ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead,
- options.CancellationToken).ConfigureAwait(false);
-
- await EnsureSuccessStatusCode(response, options).ConfigureAwait(false);
-
- options.CancellationToken.ThrowIfCancellationRequested();
-
- var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- return new HttpResponseInfo(response.Headers, response.Content.Headers)
- {
- Content = stream,
- StatusCode = response.StatusCode,
- ContentType = response.Content.Headers.ContentType?.MediaType,
- ContentLength = response.Content.Headers.ContentLength,
- ResponseUrl = response.Content.Headers.ContentLocation?.ToString()
- };
- }
-
- /// <inheritdoc />
- public Task<HttpResponseInfo> Post(HttpRequestOptions options)
- => SendAsync(options, HttpMethod.Post);
-
- private void ValidateParams(HttpRequestOptions options)
- {
- if (string.IsNullOrEmpty(options.Url))
- {
- throw new ArgumentNullException(nameof(options));
- }
- }
-
- /// <summary>
- /// Gets the host from URL.
- /// </summary>
- /// <param name="url">The URL.</param>
- /// <returns>System.String.</returns>
- private static string GetHostFromUrl(string url)
- {
- var index = url.IndexOf("://", StringComparison.OrdinalIgnoreCase);
-
- if (index != -1)
- {
- url = url.Substring(index + 3);
- var host = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
-
- if (!string.IsNullOrWhiteSpace(host))
- {
- return host;
- }
- }
-
- return url;
- }
-
- private async Task EnsureSuccessStatusCode(HttpResponseMessage response, HttpRequestOptions options)
- {
- if (response.IsSuccessStatusCode)
- {
- return;
- }
-
- if (options.LogErrorResponseBody)
- {
- string msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
- _logger.LogError("HTTP request failed with message: {Message}", msg);
- }
-
- throw new HttpException(response.ReasonPhrase)
- {
- StatusCode = response.StatusCode
- };
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs
deleted file mode 100644
index 0b61e40b0..000000000
--- a/Emby.Server.Implementations/HttpServer/FileWriter.cs
+++ /dev/null
@@ -1,252 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Runtime.InteropServices;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
- public class FileWriter : IHttpResult
- {
- private static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
-
- private static readonly string[] _skipLogExtensions = {
- ".js",
- ".html",
- ".css"
- };
-
- private readonly IStreamHelper _streamHelper;
- private readonly ILogger _logger;
- private readonly IFileSystem _fileSystem;
-
- /// <summary>
- /// The _options
- /// </summary>
- private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
-
- /// <summary>
- /// The _requested ranges
- /// </summary>
- private List<KeyValuePair<long, long?>> _requestedRanges;
-
- public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem, IStreamHelper streamHelper)
- {
- if (string.IsNullOrEmpty(contentType))
- {
- throw new ArgumentNullException(nameof(contentType));
- }
-
- _streamHelper = streamHelper;
- _fileSystem = fileSystem;
-
- Path = path;
- _logger = logger;
- RangeHeader = rangeHeader;
-
- Headers[HeaderNames.ContentType] = contentType;
-
- TotalContentLength = fileSystem.GetFileInfo(path).Length;
- Headers[HeaderNames.AcceptRanges] = "bytes";
-
- if (string.IsNullOrWhiteSpace(rangeHeader))
- {
- Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture);
- StatusCode = HttpStatusCode.OK;
- }
- else
- {
- StatusCode = HttpStatusCode.PartialContent;
- SetRangeValues();
- }
-
- FileShare = FileShare.Read;
- Cookies = new List<Cookie>();
- }
-
- private string RangeHeader { get; set; }
-
- private bool IsHeadRequest { get; set; }
-
- private long RangeStart { get; set; }
-
- private long RangeEnd { get; set; }
-
- private long RangeLength { get; set; }
-
- public long TotalContentLength { get; set; }
-
- public Action OnComplete { get; set; }
-
- public Action OnError { get; set; }
-
- public List<Cookie> Cookies { get; private set; }
-
- public FileShare FileShare { get; set; }
-
- /// <summary>
- /// Gets the options.
- /// </summary>
- /// <value>The options.</value>
- public IDictionary<string, string> Headers => _options;
-
- public string Path { get; set; }
-
- /// <summary>
- /// Gets the requested ranges.
- /// </summary>
- /// <value>The requested ranges.</value>
- protected List<KeyValuePair<long, long?>> RequestedRanges
- {
- get
- {
- if (_requestedRanges == null)
- {
- _requestedRanges = new List<KeyValuePair<long, long?>>();
-
- // Example: bytes=0-,32-63
- var ranges = RangeHeader.Split('=')[1].Split(',');
-
- foreach (var range in ranges)
- {
- var vals = range.Split('-');
-
- long start = 0;
- long? end = null;
-
- if (!string.IsNullOrEmpty(vals[0]))
- {
- start = long.Parse(vals[0], UsCulture);
- }
-
- if (!string.IsNullOrEmpty(vals[1]))
- {
- end = long.Parse(vals[1], UsCulture);
- }
-
- _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
- }
- }
-
- return _requestedRanges;
- }
- }
-
- public string ContentType { get; set; }
-
- public IRequest RequestContext { get; set; }
-
- public object Response { get; set; }
-
- public int Status { get; set; }
-
- public HttpStatusCode StatusCode
- {
- get => (HttpStatusCode)Status;
- set => Status = (int)value;
- }
-
- /// <summary>
- /// Sets the range values.
- /// </summary>
- private void SetRangeValues()
- {
- var requestedRange = RequestedRanges[0];
-
- // If the requested range is "0-", we can optimize by just doing a stream copy
- if (!requestedRange.Value.HasValue)
- {
- RangeEnd = TotalContentLength - 1;
- }
- else
- {
- RangeEnd = requestedRange.Value.Value;
- }
-
- RangeStart = requestedRange.Key;
- RangeLength = 1 + RangeEnd - RangeStart;
-
- // Content-Length is the length of what we're serving, not the original content
- var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture);
- Headers[HeaderNames.ContentLength] = lengthString;
- var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
- Headers[HeaderNames.ContentRange] = rangeString;
-
- _logger.LogDebug("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString);
- }
-
- public async Task WriteToAsync(HttpResponse response, CancellationToken cancellationToken)
- {
- try
- {
- // Headers only
- if (IsHeadRequest)
- {
- return;
- }
-
- var path = Path;
- var offset = RangeStart;
- var count = RangeLength;
-
- if (string.IsNullOrWhiteSpace(RangeHeader) || RangeStart <= 0 && RangeEnd >= TotalContentLength - 1)
- {
- var extension = System.IO.Path.GetExtension(path);
-
- if (extension == null || !_skipLogExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
- {
- _logger.LogDebug("Transmit file {0}", path);
- }
-
- offset = 0;
- count = 0;
- }
-
- await TransmitFile(response.Body, path, offset, count, FileShare, cancellationToken).ConfigureAwait(false);
- }
- finally
- {
- OnComplete?.Invoke();
- }
- }
-
- public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShare fileShare, CancellationToken cancellationToken)
- {
- var fileOptions = FileOptions.SequentialScan;
-
- // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- fileOptions |= FileOptions.Asynchronous;
- }
-
- using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, fileOptions))
- {
- if (offset > 0)
- {
- fs.Position = offset;
- }
-
- if (count > 0)
- {
- await _streamHelper.CopyToAsync(fs, stream, count, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- await fs.CopyToAsync(stream, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false);
- }
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
deleted file mode 100644
index 7de4f168c..000000000
--- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ /dev/null
@@ -1,763 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Net.Sockets;
-using System.Net.WebSockets;
-using System.Reflection;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Services;
-using Emby.Server.Implementations.SocketSharp;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.AspNetCore.WebUtilities;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Primitives;
-using ServiceStack.Text.Jsv;
-
-namespace Emby.Server.Implementations.HttpServer
-{
- public class HttpListenerHost : IHttpServer
- {
- /// <summary>
- /// The key for a setting that specifies the default redirect path
- /// to use for requests where the URL base prefix is invalid or missing.
- /// </summary>
- public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
-
- private readonly ILogger _logger;
- private readonly ILoggerFactory _loggerFactory;
- private readonly IServerConfigurationManager _config;
- private readonly INetworkManager _networkManager;
- private readonly IServerApplicationHost _appHost;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IXmlSerializer _xmlSerializer;
- private readonly Func<Type, Func<string, object>> _funcParseFn;
- private readonly string _defaultRedirectPath;
- private readonly string _baseUrlPrefix;
-
- private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
- private readonly IHostEnvironment _hostEnvironment;
-
- private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
- private bool _disposed = false;
-
- public HttpListenerHost(
- IServerApplicationHost applicationHost,
- ILogger<HttpListenerHost> logger,
- IServerConfigurationManager config,
- IConfiguration configuration,
- INetworkManager networkManager,
- IJsonSerializer jsonSerializer,
- IXmlSerializer xmlSerializer,
- ILocalizationManager localizationManager,
- ServiceController serviceController,
- IHostEnvironment hostEnvironment,
- ILoggerFactory loggerFactory)
- {
- _appHost = applicationHost;
- _logger = logger;
- _config = config;
- _defaultRedirectPath = configuration[DefaultRedirectKey];
- _baseUrlPrefix = _config.Configuration.BaseUrl;
- _networkManager = networkManager;
- _jsonSerializer = jsonSerializer;
- _xmlSerializer = xmlSerializer;
- ServiceController = serviceController;
- _hostEnvironment = hostEnvironment;
- _loggerFactory = loggerFactory;
-
- _funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
-
- Instance = this;
- ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>();
- GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
- }
-
- public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
-
- public Action<IRequest, HttpResponse, object>[] ResponseFilters { get; set; }
-
- public static HttpListenerHost Instance { get; protected set; }
-
- public string[] UrlPrefixes { get; private set; }
-
- public string GlobalResponse { get; set; }
-
- public ServiceController ServiceController { get; }
-
- public object CreateInstance(Type type)
- {
- return _appHost.CreateInstance(type);
- }
-
- private static string NormalizeUrlPath(string path)
- {
- if (path.Length > 0 && path[0] == '/')
- {
- // If the path begins with a leading slash, just return it as-is
- return path;
- }
- else
- {
- // If the path does not begin with a leading slash, append one for consistency
- return "/" + path;
- }
- }
-
- /// <summary>
- /// Applies the request filters. Returns whether or not the request has been handled
- /// and no more processing should be done.
- /// </summary>
- /// <returns></returns>
- public void ApplyRequestFilters(IRequest req, HttpResponse res, object requestDto)
- {
- // Exec all RequestFilter attributes with Priority < 0
- var attributes = GetRequestFilterAttributes(requestDto.GetType());
-
- int count = attributes.Count;
- int i = 0;
- for (; i < count && attributes[i].Priority < 0; i++)
- {
- var attribute = attributes[i];
- attribute.RequestFilter(req, res, requestDto);
- }
-
- // Exec remaining RequestFilter attributes with Priority >= 0
- for (; i < count && attributes[i].Priority >= 0; i++)
- {
- var attribute = attributes[i];
- attribute.RequestFilter(req, res, requestDto);
- }
- }
-
- public Type GetServiceTypeByRequest(Type requestType)
- {
- _serviceOperationsMap.TryGetValue(requestType, out var serviceType);
- return serviceType;
- }
-
- public void AddServiceInfo(Type serviceType, Type requestType)
- {
- _serviceOperationsMap[requestType] = serviceType;
- }
-
- private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
- {
- var attributes = requestDtoType.GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList();
-
- var serviceType = GetServiceTypeByRequest(requestDtoType);
- if (serviceType != null)
- {
- attributes.AddRange(serviceType.GetCustomAttributes(true).OfType<IHasRequestFilter>());
- }
-
- attributes.Sort((x, y) => x.Priority - y.Priority);
-
- return attributes;
- }
-
- private static Exception GetActualException(Exception ex)
- {
- if (ex is AggregateException agg)
- {
- var inner = agg.InnerException;
- if (inner != null)
- {
- return GetActualException(inner);
- }
- else
- {
- var inners = agg.InnerExceptions;
- if (inners.Count > 0)
- {
- return GetActualException(inners[0]);
- }
- }
- }
-
- return ex;
- }
-
- private int GetStatusCode(Exception ex)
- {
- switch (ex)
- {
- case ArgumentException _: return 400;
- case AuthenticationException _: return 401;
- case SecurityException _: return 403;
- case DirectoryNotFoundException _:
- case FileNotFoundException _:
- case ResourceNotFoundException _: return 404;
- case MethodNotAllowedException _: return 405;
- default: return 500;
- }
- }
-
- private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace)
- {
- if (ignoreStackTrace)
- {
- _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog);
- }
- else
- {
- _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog);
- }
-
- var httpRes = httpReq.Response;
-
- if (httpRes.HasStarted)
- {
- return;
- }
-
- httpRes.StatusCode = statusCode;
-
- var errContent = NormalizeExceptionMessage(ex) ?? string.Empty;
- httpRes.ContentType = "text/plain";
- httpRes.ContentLength = errContent.Length;
- await httpRes.WriteAsync(errContent).ConfigureAwait(false);
- }
-
- private string NormalizeExceptionMessage(Exception ex)
- {
- // Do not expose the exception message for AuthenticationException
- if (ex is AuthenticationException)
- {
- return null;
- }
-
- // Strip any information we don't want to reveal
- return ex.Message
- ?.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase)
- .Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
- }
-
- public static string RemoveQueryStringByKey(string url, string key)
- {
- var uri = new Uri(url);
-
- // this gets all the query string key value pairs as a collection
- var newQueryString = QueryHelpers.ParseQuery(uri.Query);
-
- var originalCount = newQueryString.Count;
-
- if (originalCount == 0)
- {
- return url;
- }
-
- // this removes the key if exists
- newQueryString.Remove(key);
-
- if (originalCount == newQueryString.Count)
- {
- return url;
- }
-
- // this gets the page path from root without QueryString
- string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0];
-
- return newQueryString.Count > 0
- ? QueryHelpers.AddQueryString(pagePathWithoutQueryString, newQueryString.ToDictionary(kv => kv.Key, kv => kv.Value.ToString()))
- : pagePathWithoutQueryString;
- }
-
- private static string GetUrlToLog(string url)
- {
- url = RemoveQueryStringByKey(url, "api_key");
-
- return url;
- }
-
- private static string NormalizeConfiguredLocalAddress(string address)
- {
- var add = address.AsSpan().Trim('/');
- int index = add.IndexOf('/');
- if (index != -1)
- {
- add = add.Slice(index + 1);
- }
-
- return add.TrimStart('/').ToString();
- }
-
- private bool ValidateHost(string host)
- {
- var hosts = _config
- .Configuration
- .LocalNetworkAddresses
- .Select(NormalizeConfiguredLocalAddress)
- .ToList();
-
- if (hosts.Count == 0)
- {
- return true;
- }
-
- host ??= string.Empty;
-
- if (_networkManager.IsInPrivateAddressSpace(host))
- {
- hosts.Add("localhost");
- hosts.Add("127.0.0.1");
-
- return hosts.Any(i => host.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1);
- }
-
- return true;
- }
-
- private bool ValidateRequest(string remoteIp, bool isLocal)
- {
- if (isLocal)
- {
- return true;
- }
-
- if (_config.Configuration.EnableRemoteAccess)
- {
- var addressFilter = _config.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
-
- if (addressFilter.Length > 0 && !_networkManager.IsInLocalNetwork(remoteIp))
- {
- if (_config.Configuration.IsRemoteIPFilterBlacklist)
- {
- return !_networkManager.IsAddressInSubnets(remoteIp, addressFilter);
- }
- else
- {
- return _networkManager.IsAddressInSubnets(remoteIp, addressFilter);
- }
- }
- }
- else
- {
- if (!_networkManager.IsInLocalNetwork(remoteIp))
- {
- return false;
- }
- }
-
- return true;
- }
-
- /// <summary>
- /// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required.
- /// </summary>
- /// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns>
- private bool ValidateSsl(string remoteIp, string urlString)
- {
- if (_config.Configuration.RequireHttps
- && _appHost.ListenWithHttps
- && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase))
- {
- // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
- if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
- || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
- {
- return true;
- }
-
- if (!_networkManager.IsInLocalNetwork(remoteIp))
- {
- return false;
- }
- }
-
- return true;
- }
-
- /// <inheritdoc />
- public Task RequestHandler(HttpContext context)
- {
- if (context.WebSockets.IsWebSocketRequest)
- {
- return WebSocketRequestHandler(context);
- }
-
- var request = context.Request;
- var response = context.Response;
- var localPath = context.Request.Path.ToString();
-
- var req = new WebSocketSharpRequest(request, response, request.Path, _logger);
- return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted);
- }
-
- /// <summary>
- /// Overridable method that can be used to implement a custom handler.
- /// </summary>
- private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
- {
- var stopWatch = new Stopwatch();
- stopWatch.Start();
- var httpRes = httpReq.Response;
- string urlToLog = GetUrlToLog(urlString);
- string remoteIp = httpReq.RemoteIp;
-
- try
- {
- if (_disposed)
- {
- httpRes.StatusCode = 503;
- httpRes.ContentType = "text/plain";
- await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false);
- return;
- }
-
- if (!ValidateHost(host))
- {
- httpRes.StatusCode = 400;
- httpRes.ContentType = "text/plain";
- await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false);
- return;
- }
-
- if (!ValidateRequest(remoteIp, httpReq.IsLocal))
- {
- httpRes.StatusCode = 403;
- httpRes.ContentType = "text/plain";
- await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false);
- return;
- }
-
- if (!ValidateSsl(httpReq.RemoteIp, urlString))
- {
- RedirectToSecureUrl(httpReq, httpRes, urlString);
- return;
- }
-
- if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
- {
- httpRes.StatusCode = 200;
- foreach(var (key, value) in GetDefaultCorsHeaders(httpReq))
- {
- httpRes.Headers.Add(key, value);
- }
- httpRes.ContentType = "text/plain";
- await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false);
- return;
- }
-
- if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
- || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
- || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
- || string.IsNullOrEmpty(localPath)
- || !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
- {
- // Always redirect back to the default path if the base prefix is invalid or missing
- _logger.LogDebug("Normalizing a URL at {0}", localPath);
- httpRes.Redirect(_baseUrlPrefix + "/" + _defaultRedirectPath);
- return;
- }
-
- if (!string.IsNullOrEmpty(GlobalResponse))
- {
- // We don't want the address pings in ApplicationHost to fail
- if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1)
- {
- httpRes.StatusCode = 503;
- httpRes.ContentType = "text/html";
- await httpRes.WriteAsync(GlobalResponse, cancellationToken).ConfigureAwait(false);
- return;
- }
- }
-
- var handler = GetServiceHandler(httpReq);
- if (handler != null)
- {
- await handler.ProcessRequestAsync(this, httpReq, httpRes, _logger, cancellationToken).ConfigureAwait(false);
- }
- else
- {
- throw new FileNotFoundException();
- }
- }
- catch (Exception requestEx)
- {
- try
- {
- var requestInnerEx = GetActualException(requestEx);
- var statusCode = GetStatusCode(requestInnerEx);
-
- foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
- {
- if (!httpRes.Headers.ContainsKey(key))
- {
- httpRes.Headers.Add(key, value);
- }
- }
-
- bool ignoreStackTrace =
- requestInnerEx is SocketException
- || requestInnerEx is IOException
- || requestInnerEx is OperationCanceledException
- || requestInnerEx is SecurityException
- || requestInnerEx is AuthenticationException
- || requestInnerEx is FileNotFoundException;
-
- // Do not handle 500 server exceptions manually when in development mode.
- // Instead, re-throw the exception so it can be handled by the DeveloperExceptionPageMiddleware.
- // However, do not use the DeveloperExceptionPageMiddleware when the stack trace should be ignored,
- // because it will log the stack trace when it handles the exception.
- if (statusCode == 500 && !ignoreStackTrace && _hostEnvironment.IsDevelopment())
- {
- throw;
- }
-
- await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false);
- }
- catch (Exception handlerException)
- {
- var aggregateEx = new AggregateException("Error while handling request exception", requestEx, handlerException);
- _logger.LogError(aggregateEx, "Error while handling exception in response to {Url}", urlToLog);
-
- if (_hostEnvironment.IsDevelopment())
- {
- throw aggregateEx;
- }
- }
- }
- finally
- {
- if (httpRes.StatusCode >= 500)
- {
- _logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog);
- }
-
- stopWatch.Stop();
- var elapsed = stopWatch.Elapsed;
- if (elapsed.TotalMilliseconds > 500)
- {
- _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog);
- }
- }
- }
-
- private async Task WebSocketRequestHandler(HttpContext context)
- {
- if (_disposed)
- {
- return;
- }
-
- try
- {
- _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
-
- WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
-
- var connection = new WebSocketConnection(
- _loggerFactory.CreateLogger<WebSocketConnection>(),
- webSocket,
- context.Connection.RemoteIpAddress,
- context.Request.Query)
- {
- OnReceive = ProcessWebSocketMessageReceived
- };
-
- WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
-
- await connection.ProcessAsync().ConfigureAwait(false);
- _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
- }
- catch (Exception ex) // Otherwise ASP.Net will ignore the exception
- {
- _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress);
- if (!context.Response.HasStarted)
- {
- context.Response.StatusCode = 500;
- }
- }
- }
-
- /// <summary>
- /// Get the default CORS headers
- /// </summary>
- /// <param name="req"></param>
- /// <returns></returns>
- public IDictionary<string, string> GetDefaultCorsHeaders(IRequest req)
- {
- var origin = req.Headers["Origin"];
- if (origin == StringValues.Empty)
- {
- origin = req.Headers["Host"];
- if (origin == StringValues.Empty)
- {
- origin = "*";
- }
- }
-
- var headers = new Dictionary<string, string>();
- headers.Add("Access-Control-Allow-Origin", origin);
- headers.Add("Access-Control-Allow-Credentials", "true");
- headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
- headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie");
- return headers;
- }
-
- // Entry point for HttpListener
- public ServiceHandler GetServiceHandler(IHttpRequest httpReq)
- {
- var pathInfo = httpReq.PathInfo;
-
- pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType);
- var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo);
- if (restPath != null)
- {
- return new ServiceHandler(restPath, contentType);
- }
-
- _logger.LogError("Could not find handler for {PathInfo}", pathInfo);
- return null;
- }
-
- private void RedirectToSecureUrl(IHttpRequest httpReq, HttpResponse httpRes, string url)
- {
- if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
- {
- var builder = new UriBuilder(uri)
- {
- Port = _config.Configuration.PublicHttpsPort,
- Scheme = "https"
- };
- url = builder.Uri.ToString();
- }
-
- httpRes.Redirect(url);
- }
-
- /// <summary>
- /// Adds the rest handlers.
- /// </summary>
- /// <param name="serviceTypes">The service types to register with the <see cref="ServiceController"/>.</param>
- /// <param name="listeners">The web socket listeners.</param>
- /// <param name="urlPrefixes">The URL prefixes. See <see cref="UrlPrefixes"/>.</param>
- public void Init(IEnumerable<Type> serviceTypes, IEnumerable<IWebSocketListener> listeners, IEnumerable<string> urlPrefixes)
- {
- _webSocketListeners = listeners.ToArray();
- UrlPrefixes = urlPrefixes.ToArray();
-
- ServiceController.Init(this, serviceTypes);
-
- ResponseFilters = new Action<IRequest, HttpResponse, object>[]
- {
- new ResponseFilter(this, _logger).FilterResponse
- };
- }
-
- public RouteAttribute[] GetRouteAttributes(Type requestType)
- {
- var routes = requestType.GetTypeInfo().GetCustomAttributes<RouteAttribute>(true).ToList();
- var clone = routes.ToList();
-
- foreach (var route in clone)
- {
- routes.Add(new RouteAttribute(NormalizeCustomRoutePath(route.Path), route.Verbs)
- {
- Notes = route.Notes,
- Priority = route.Priority,
- Summary = route.Summary
- });
-
- routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs)
- {
- Notes = route.Notes,
- Priority = route.Priority,
- Summary = route.Summary
- });
-
- routes.Add(new RouteAttribute(NormalizeMediaBrowserRoutePath(route.Path), route.Verbs)
- {
- Notes = route.Notes,
- Priority = route.Priority,
- Summary = route.Summary
- });
- }
-
- return routes.ToArray();
- }
-
- public Func<string, object> GetParseFn(Type propertyType)
- {
- return _funcParseFn(propertyType);
- }
-
- public void SerializeToJson(object o, Stream stream)
- {
- _jsonSerializer.SerializeToStream(o, stream);
- }
-
- public void SerializeToXml(object o, Stream stream)
- {
- _xmlSerializer.SerializeToStream(o, stream);
- }
-
- public Task<object> DeserializeXml(Type type, Stream stream)
- {
- return Task.FromResult(_xmlSerializer.DeserializeFromStream(type, stream));
- }
-
- public Task<object> DeserializeJson(Type type, Stream stream)
- {
- return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
- }
-
- private string NormalizeEmbyRoutePath(string path)
- {
- _logger.LogDebug("Normalizing /emby route");
- return _baseUrlPrefix + "/emby" + NormalizeUrlPath(path);
- }
-
- private string NormalizeMediaBrowserRoutePath(string path)
- {
- _logger.LogDebug("Normalizing /mediabrowser route");
- return _baseUrlPrefix + "/mediabrowser" + NormalizeUrlPath(path);
- }
-
- private string NormalizeCustomRoutePath(string path)
- {
- _logger.LogDebug("Normalizing custom route {0}", path);
- return _baseUrlPrefix + NormalizeUrlPath(path);
- }
-
- /// <summary>
- /// Processes the web socket message received.
- /// </summary>
- /// <param name="result">The result.</param>
- private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
- {
- if (_disposed)
- {
- return Task.CompletedTask;
- }
-
- IEnumerable<Task> GetTasks()
- {
- foreach (var x in _webSocketListeners)
- {
- yield return x.ProcessMessageAsync(result);
- }
- }
-
- return Task.WhenAll(GetTasks());
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
deleted file mode 100644
index 2e9ecc4ae..000000000
--- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
+++ /dev/null
@@ -1,713 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.IO.Compression;
-using System.Net;
-using System.Runtime.Serialization;
-using System.Text;
-using System.Threading.Tasks;
-using System.Xml;
-using Emby.Server.Implementations.Services;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Primitives;
-using Microsoft.Net.Http.Headers;
-using IRequest = MediaBrowser.Model.Services.IRequest;
-using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
-
-namespace Emby.Server.Implementations.HttpServer
-{
- /// <summary>
- /// Class HttpResultFactory.
- /// </summary>
- public class HttpResultFactory : IHttpResultFactory
- {
- // Last-Modified and If-Modified-Since must follow strict date format,
- // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
- private const string HttpDateFormat = "ddd, dd MMM yyyy HH:mm:ss \"GMT\"";
- // We specifically use en-US culture because both day of week and month names require it
- private static readonly CultureInfo _enUSculture = new CultureInfo("en-US", false);
-
- /// <summary>
- /// The logger.
- /// </summary>
- private readonly ILogger _logger;
- private readonly IFileSystem _fileSystem;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IStreamHelper _streamHelper;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="HttpResultFactory" /> class.
- /// </summary>
- public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IStreamHelper streamHelper)
- {
- _fileSystem = fileSystem;
- _jsonSerializer = jsonSerializer;
- _streamHelper = streamHelper;
- _logger = loggerfactory.CreateLogger("HttpResultFactory");
- }
-
- /// <summary>
- /// Gets the result.
- /// </summary>
- /// <param name="content">The content.</param>
- /// <param name="contentType">Type of the content.</param>
- /// <param name="responseHeaders">The response headers.</param>
- /// <returns>System.Object.</returns>
- public object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null)
- {
- return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
- }
-
- public object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null)
- {
- return GetHttpResult(null, content, contentType, true, responseHeaders);
- }
-
- public object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null)
- {
- return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
- }
-
- public object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null)
- {
- return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
- }
-
- public object GetRedirectResult(string url)
- {
- var responseHeaders = new Dictionary<string, string>();
- responseHeaders[HeaderNames.Location] = url;
-
- var result = new HttpResult(Array.Empty<byte>(), "text/plain", HttpStatusCode.Redirect);
-
- AddResponseHeaders(result, responseHeaders);
-
- return result;
- }
-
- /// <summary>
- /// Gets the HTTP result.
- /// </summary>
- private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
- {
- var result = new StreamWriter(content, contentType);
-
- if (responseHeaders == null)
- {
- responseHeaders = new Dictionary<string, string>();
- }
-
- if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string expires))
- {
- responseHeaders[HeaderNames.Expires] = "0";
- }
-
- AddResponseHeaders(result, responseHeaders);
-
- return result;
- }
-
- /// <summary>
- /// Gets the HTTP result.
- /// </summary>
- private IHasHeaders GetHttpResult(IRequest requestContext, byte[] content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
- {
- string compressionType = null;
- bool isHeadRequest = false;
-
- if (requestContext != null)
- {
- compressionType = GetCompressionType(requestContext, content, contentType);
- isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
- }
-
- IHasHeaders result;
- if (string.IsNullOrEmpty(compressionType))
- {
- var contentLength = content.Length;
-
- if (isHeadRequest)
- {
- content = Array.Empty<byte>();
- }
-
- result = new StreamWriter(content, contentType, contentLength);
- }
- else
- {
- result = GetCompressedResult(content, compressionType, responseHeaders, isHeadRequest, contentType);
- }
-
- if (responseHeaders == null)
- {
- responseHeaders = new Dictionary<string, string>();
- }
-
- if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
- {
- responseHeaders[HeaderNames.Expires] = "0";
- }
-
- AddResponseHeaders(result, responseHeaders);
-
- return result;
- }
-
- /// <summary>
- /// Gets the HTTP result.
- /// </summary>
- private IHasHeaders GetHttpResult(IRequest requestContext, string content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
- {
- IHasHeaders result;
-
- var bytes = Encoding.UTF8.GetBytes(content);
-
- var compressionType = requestContext == null ? null : GetCompressionType(requestContext, bytes, contentType);
-
- var isHeadRequest = requestContext == null ? false : string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
-
- if (string.IsNullOrEmpty(compressionType))
- {
- var contentLength = bytes.Length;
-
- if (isHeadRequest)
- {
- bytes = Array.Empty<byte>();
- }
-
- result = new StreamWriter(bytes, contentType, contentLength);
- }
- else
- {
- result = GetCompressedResult(bytes, compressionType, responseHeaders, isHeadRequest, contentType);
- }
-
- if (responseHeaders == null)
- {
- responseHeaders = new Dictionary<string, string>();
- }
-
- if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
- {
- responseHeaders[HeaderNames.Expires] = "0";
- }
-
- AddResponseHeaders(result, responseHeaders);
-
- return result;
- }
-
- /// <summary>
- /// Gets the optimized result.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- public object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null)
- where T : class
- {
- if (result == null)
- {
- throw new ArgumentNullException(nameof(result));
- }
-
- if (responseHeaders == null)
- {
- responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- }
-
- responseHeaders[HeaderNames.Expires] = "0";
-
- return ToOptimizedResultInternal(requestContext, result, responseHeaders);
- }
-
- private string GetCompressionType(IRequest request, byte[] content, string responseContentType)
- {
- if (responseContentType == null)
- {
- return null;
- }
-
- // Per apple docs, hls manifests must be compressed
- if (!responseContentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) &&
- responseContentType.IndexOf("json", StringComparison.OrdinalIgnoreCase) == -1 &&
- responseContentType.IndexOf("javascript", StringComparison.OrdinalIgnoreCase) == -1 &&
- responseContentType.IndexOf("xml", StringComparison.OrdinalIgnoreCase) == -1 &&
- responseContentType.IndexOf("application/x-mpegURL", StringComparison.OrdinalIgnoreCase) == -1)
- {
- return null;
- }
-
- if (content.Length < 1024)
- {
- return null;
- }
-
- return GetCompressionType(request);
- }
-
- private static string GetCompressionType(IRequest request)
- {
- var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString();
-
- if (string.IsNullOrEmpty(acceptEncoding))
- {
- //if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
- // return "br";
-
- if (acceptEncoding.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1)
- return "deflate";
-
- if (acceptEncoding.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1)
- return "gzip";
- }
-
- return null;
- }
-
- /// <summary>
- /// Returns the optimized result for the IRequestContext.
- /// Does not use or store results in any cache.
- /// </summary>
- /// <param name="request"></param>
- /// <param name="dto"></param>
- /// <returns></returns>
- public object ToOptimizedResult<T>(IRequest request, T dto)
- {
- return ToOptimizedResultInternal(request, dto);
- }
-
- private object ToOptimizedResultInternal<T>(IRequest request, T dto, IDictionary<string, string> responseHeaders = null)
- {
- // TODO: @bond use Span and .Equals
- var contentType = request.ResponseContentType?.Split(';')[0].Trim().ToLowerInvariant();
-
- switch (contentType)
- {
- case "application/xml":
- case "text/xml":
- case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
- return GetHttpResult(request, SerializeToXmlString(dto), contentType, false, responseHeaders);
-
- case "application/json":
- case "text/json":
- return GetHttpResult(request, _jsonSerializer.SerializeToString(dto), contentType, false, responseHeaders);
- default:
- break;
- }
-
- var isHeadRequest = string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase);
-
- var ms = new MemoryStream();
- var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
-
- writerFn(dto, ms);
-
- ms.Position = 0;
-
- if (isHeadRequest)
- {
- using (ms)
- {
- return GetHttpResult(request, Array.Empty<byte>(), contentType, true, responseHeaders);
- }
- }
-
- return GetHttpResult(request, ms, contentType, true, responseHeaders);
- }
-
- private IHasHeaders GetCompressedResult(byte[] content,
- string requestedCompressionType,
- IDictionary<string, string> responseHeaders,
- bool isHeadRequest,
- string contentType)
- {
- if (responseHeaders == null)
- {
- responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- }
-
- content = Compress(content, requestedCompressionType);
- responseHeaders[HeaderNames.ContentEncoding] = requestedCompressionType;
-
- responseHeaders[HeaderNames.Vary] = HeaderNames.AcceptEncoding;
-
- var contentLength = content.Length;
-
- if (isHeadRequest)
- {
- var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
- AddResponseHeaders(result, responseHeaders);
- return result;
- }
- else
- {
- var result = new StreamWriter(content, contentType, contentLength);
- AddResponseHeaders(result, responseHeaders);
- return result;
- }
- }
-
- private byte[] Compress(byte[] bytes, string compressionType)
- {
- if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase))
- {
- return Deflate(bytes);
- }
-
- if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase))
- {
- return GZip(bytes);
- }
-
- throw new NotSupportedException(compressionType);
- }
-
- private static byte[] Deflate(byte[] bytes)
- {
- // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream
- // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream
- using (var ms = new MemoryStream())
- using (var zipStream = new DeflateStream(ms, CompressionMode.Compress))
- {
- zipStream.Write(bytes, 0, bytes.Length);
- zipStream.Dispose();
-
- return ms.ToArray();
- }
- }
-
- private static byte[] GZip(byte[] buffer)
- {
- using (var ms = new MemoryStream())
- using (var zipStream = new GZipStream(ms, CompressionMode.Compress))
- {
- zipStream.Write(buffer, 0, buffer.Length);
- zipStream.Dispose();
-
- return ms.ToArray();
- }
- }
-
- private static string SerializeToXmlString(object from)
- {
- using (var ms = new MemoryStream())
- {
- var xwSettings = new XmlWriterSettings();
- xwSettings.Encoding = new UTF8Encoding(false);
- xwSettings.OmitXmlDeclaration = false;
-
- using (var xw = XmlWriter.Create(ms, xwSettings))
- {
- var serializer = new DataContractSerializer(from.GetType());
- serializer.WriteObject(xw, from);
- xw.Flush();
- ms.Seek(0, SeekOrigin.Begin);
- using (var reader = new StreamReader(ms))
- {
- return reader.ReadToEnd();
- }
- }
- }
- }
-
- /// <summary>
- /// Pres the process optimized result.
- /// </summary>
- private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, StaticResultOptions options)
- {
- bool noCache = (requestContext.Headers[HeaderNames.CacheControl].ToString()).IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
- AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified);
-
- if (!noCache)
- {
- if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ifModifiedSinceHeader))
- {
- _logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]);
- return null;
- }
-
- if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified))
- {
- AddAgeHeader(responseHeaders, options.DateLastModified);
-
- var result = new HttpResult(Array.Empty<byte>(), options.ContentType ?? "text/html", HttpStatusCode.NotModified);
-
- AddResponseHeaders(result, responseHeaders);
-
- return result;
- }
- }
-
- return null;
- }
-
- public Task<object> GetStaticFileResult(IRequest requestContext,
- string path,
- FileShare fileShare = FileShare.Read)
- {
- if (string.IsNullOrEmpty(path))
- {
- throw new ArgumentNullException(nameof(path));
- }
-
- return GetStaticFileResult(requestContext, new StaticFileResultOptions
- {
- Path = path,
- FileShare = fileShare
- });
- }
-
- public Task<object> GetStaticFileResult(IRequest requestContext, StaticFileResultOptions options)
- {
- var path = options.Path;
- var fileShare = options.FileShare;
-
- if (string.IsNullOrEmpty(path))
- {
- throw new ArgumentException("Path can't be empty.", nameof(options));
- }
-
- if (fileShare != FileShare.Read && fileShare != FileShare.ReadWrite)
- {
- throw new ArgumentException("FileShare must be either Read or ReadWrite");
- }
-
- if (string.IsNullOrEmpty(options.ContentType))
- {
- options.ContentType = MimeTypes.GetMimeType(path);
- }
-
- if (!options.DateLastModified.HasValue)
- {
- options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path);
- }
-
- options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare));
-
- options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
- return GetStaticResult(requestContext, options);
- }
-
- /// <summary>
- /// Gets the file stream.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <param name="fileShare">The file share.</param>
- /// <returns>Stream.</returns>
- private Stream GetFileStream(string path, FileShare fileShare)
- {
- return new FileStream(path, FileMode.Open, FileAccess.Read, fileShare);
- }
-
- public Task<object> GetStaticResult(IRequest requestContext,
- Guid cacheKey,
- DateTime? lastDateModified,
- TimeSpan? cacheDuration,
- string contentType,
- Func<Task<Stream>> factoryFn,
- IDictionary<string, string> responseHeaders = null,
- bool isHeadRequest = false)
- {
- return GetStaticResult(requestContext, new StaticResultOptions
- {
- CacheDuration = cacheDuration,
- ContentFactory = factoryFn,
- ContentType = contentType,
- DateLastModified = lastDateModified,
- IsHeadRequest = isHeadRequest,
- ResponseHeaders = responseHeaders
- });
- }
-
- public async Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options)
- {
- options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
- var contentType = options.ContentType;
- if (!StringValues.IsNullOrEmpty(requestContext.Headers[HeaderNames.IfModifiedSince]))
- {
- // See if the result is already cached in the browser
- var result = GetCachedResult(requestContext, options.ResponseHeaders, options);
-
- if (result != null)
- {
- return result;
- }
- }
-
- // TODO: We don't really need the option value
- var isHeadRequest = options.IsHeadRequest || string.Equals(requestContext.Verb, "HEAD", StringComparison.OrdinalIgnoreCase);
- var factoryFn = options.ContentFactory;
- var responseHeaders = options.ResponseHeaders;
- AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified);
- AddAgeHeader(responseHeaders, options.DateLastModified);
-
- var rangeHeader = requestContext.Headers[HeaderNames.Range];
-
- if (!isHeadRequest && !string.IsNullOrEmpty(options.Path))
- {
- var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem, _streamHelper)
- {
- OnComplete = options.OnComplete,
- OnError = options.OnError,
- FileShare = options.FileShare
- };
-
- AddResponseHeaders(hasHeaders, options.ResponseHeaders);
- return hasHeaders;
- }
-
- var stream = await factoryFn().ConfigureAwait(false);
-
- var totalContentLength = options.ContentLength;
- if (!totalContentLength.HasValue)
- {
- try
- {
- totalContentLength = stream.Length;
- }
- catch (NotSupportedException)
- {
-
- }
- }
-
- if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue)
- {
- var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest, _logger)
- {
- OnComplete = options.OnComplete
- };
-
- AddResponseHeaders(hasHeaders, options.ResponseHeaders);
- return hasHeaders;
- }
- else
- {
- if (totalContentLength.HasValue)
- {
- responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture);
- }
-
- if (isHeadRequest)
- {
- using (stream)
- {
- return GetHttpResult(requestContext, Array.Empty<byte>(), contentType, true, responseHeaders);
- }
- }
-
- var hasHeaders = new StreamWriter(stream, contentType)
- {
- OnComplete = options.OnComplete,
- OnError = options.OnError
- };
-
- AddResponseHeaders(hasHeaders, options.ResponseHeaders);
- return hasHeaders;
- }
- }
-
- /// <summary>
- /// Adds the caching responseHeaders.
- /// </summary>
- private void AddCachingHeaders(IDictionary<string, string> responseHeaders, TimeSpan? cacheDuration,
- bool noCache, DateTime? lastModifiedDate)
- {
- if (noCache)
- {
- responseHeaders[HeaderNames.CacheControl] = "no-cache, no-store, must-revalidate";
- responseHeaders[HeaderNames.Pragma] = "no-cache, no-store, must-revalidate";
- return;
- }
-
- if (cacheDuration.HasValue)
- {
- responseHeaders[HeaderNames.CacheControl] = "public, max-age=" + cacheDuration.Value.TotalSeconds;
- }
- else
- {
- responseHeaders[HeaderNames.CacheControl] = "public";
- }
-
- if (lastModifiedDate.HasValue)
- {
- responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture);
- }
- }
-
- /// <summary>
- /// Adds the age header.
- /// </summary>
- /// <param name="responseHeaders">The responseHeaders.</param>
- /// <param name="lastDateModified">The last date modified.</param>
- private static void AddAgeHeader(IDictionary<string, string> responseHeaders, DateTime? lastDateModified)
- {
- if (lastDateModified.HasValue)
- {
- responseHeaders[HeaderNames.Age] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture);
- }
- }
-
- /// <summary>
- /// Determines whether [is not modified] [the specified if modified since].
- /// </summary>
- /// <param name="ifModifiedSince">If modified since.</param>
- /// <param name="cacheDuration">Duration of the cache.</param>
- /// <param name="dateModified">The date modified.</param>
- /// <returns><c>true</c> if [is not modified] [the specified if modified since]; otherwise, <c>false</c>.</returns>
- private bool IsNotModified(DateTime ifModifiedSince, TimeSpan? cacheDuration, DateTime? dateModified)
- {
- if (dateModified.HasValue)
- {
- var lastModified = NormalizeDateForComparison(dateModified.Value);
- ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
-
- return lastModified <= ifModifiedSince;
- }
-
- if (cacheDuration.HasValue)
- {
- var cacheExpirationDate = ifModifiedSince.Add(cacheDuration.Value);
-
- if (DateTime.UtcNow < cacheExpirationDate)
- {
- return true;
- }
- }
-
- return false;
- }
-
-
- /// <summary>
- /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
- /// </summary>
- /// <param name="date">The date.</param>
- /// <returns>DateTime.</returns>
- private static DateTime NormalizeDateForComparison(DateTime date)
- {
- return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind);
- }
-
- /// <summary>
- /// Adds the response headers.
- /// </summary>
- /// <param name="hasHeaders">The has options.</param>
- /// <param name="responseHeaders">The response headers.</param>
- private static void AddResponseHeaders(IHasHeaders hasHeaders, IEnumerable<KeyValuePair<string, string>> responseHeaders)
- {
- foreach (var item in responseHeaders)
- {
- hasHeaders.Headers[item.Key] = item.Value;
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs
deleted file mode 100644
index 8b9028f6b..000000000
--- a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs
+++ /dev/null
@@ -1,225 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
- public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult
- {
- /// <summary>
- /// Gets or sets the source stream.
- /// </summary>
- /// <value>The source stream.</value>
- private Stream SourceStream { get; set; }
- private string RangeHeader { get; set; }
- private bool IsHeadRequest { get; set; }
-
- private long RangeStart { get; set; }
- private long RangeEnd { get; set; }
- private long RangeLength { get; set; }
- private long TotalContentLength { get; set; }
-
- public Action OnComplete { get; set; }
- private readonly ILogger _logger;
-
- private const int BufferSize = 81920;
-
- /// <summary>
- /// The _options
- /// </summary>
- private readonly Dictionary<string, string> _options = new Dictionary<string, string>();
-
- /// <summary>
- /// The us culture
- /// </summary>
- private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
- /// <summary>
- /// Additional HTTP Headers
- /// </summary>
- /// <value>The headers.</value>
- public IDictionary<string, string> Headers => _options;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="RangeRequestWriter" /> class.
- /// </summary>
- /// <param name="rangeHeader">The range header.</param>
- /// <param name="contentLength">The content length.</param>
- /// <param name="source">The source.</param>
- /// <param name="contentType">Type of the content.</param>
- /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
- /// <param name="logger">The logger instance.</param>
- public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest, ILogger logger)
- {
- if (string.IsNullOrEmpty(contentType))
- {
- throw new ArgumentNullException(nameof(contentType));
- }
-
- RangeHeader = rangeHeader;
- SourceStream = source;
- IsHeadRequest = isHeadRequest;
- this._logger = logger;
-
- ContentType = contentType;
- Headers[HeaderNames.ContentType] = contentType;
- Headers[HeaderNames.AcceptRanges] = "bytes";
- StatusCode = HttpStatusCode.PartialContent;
-
- SetRangeValues(contentLength);
- }
-
- /// <summary>
- /// Sets the range values.
- /// </summary>
- private void SetRangeValues(long contentLength)
- {
- var requestedRange = RequestedRanges[0];
-
- TotalContentLength = contentLength;
-
- // If the requested range is "0-", we can optimize by just doing a stream copy
- if (!requestedRange.Value.HasValue)
- {
- RangeEnd = TotalContentLength - 1;
- }
- else
- {
- RangeEnd = requestedRange.Value.Value;
- }
-
- RangeStart = requestedRange.Key;
- RangeLength = 1 + RangeEnd - RangeStart;
-
- Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
- Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
-
- if (RangeStart > 0 && SourceStream.CanSeek)
- {
- SourceStream.Position = RangeStart;
- }
- }
-
- /// <summary>
- /// The _requested ranges
- /// </summary>
- private List<KeyValuePair<long, long?>> _requestedRanges;
- /// <summary>
- /// Gets the requested ranges.
- /// </summary>
- /// <value>The requested ranges.</value>
- protected List<KeyValuePair<long, long?>> RequestedRanges
- {
- get
- {
- if (_requestedRanges == null)
- {
- _requestedRanges = new List<KeyValuePair<long, long?>>();
-
- // Example: bytes=0-,32-63
- var ranges = RangeHeader.Split('=')[1].Split(',');
-
- foreach (var range in ranges)
- {
- var vals = range.Split('-');
-
- long start = 0;
- long? end = null;
-
- if (!string.IsNullOrEmpty(vals[0]))
- {
- start = long.Parse(vals[0], UsCulture);
- }
- if (!string.IsNullOrEmpty(vals[1]))
- {
- end = long.Parse(vals[1], UsCulture);
- }
-
- _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
- }
- }
-
- return _requestedRanges;
- }
- }
-
- public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
- {
- try
- {
- // Headers only
- if (IsHeadRequest)
- {
- return;
- }
-
- using (var source = SourceStream)
- {
- // If the requested range is "0-", we can optimize by just doing a stream copy
- if (RangeEnd >= TotalContentLength - 1)
- {
- await source.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false);
- }
- else
- {
- await CopyToInternalAsync(source, responseStream, RangeLength).ConfigureAwait(false);
- }
- }
- }
- finally
- {
- if (OnComplete != null)
- {
- OnComplete();
- }
- }
- }
-
- private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength)
- {
- var array = new byte[BufferSize];
- int bytesRead;
- while ((bytesRead = await source.ReadAsync(array, 0, array.Length).ConfigureAwait(false)) != 0)
- {
- if (bytesRead == 0)
- {
- break;
- }
-
- var bytesToCopy = Math.Min(bytesRead, copyLength);
-
- await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy)).ConfigureAwait(false);
-
- copyLength -= bytesToCopy;
-
- if (copyLength <= 0)
- {
- break;
- }
- }
- }
-
- public string ContentType { get; set; }
-
- public IRequest RequestContext { get; set; }
-
- public object Response { get; set; }
-
- public int Status { get; set; }
-
- public HttpStatusCode StatusCode
- {
- get => (HttpStatusCode)Status;
- set => Status = (int)value;
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
deleted file mode 100644
index 85c3db9b2..000000000
--- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
+++ /dev/null
@@ -1,114 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Text;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
- /// <summary>
- /// Class ResponseFilter.
- /// </summary>
- public class ResponseFilter
- {
- private readonly IHttpServer _server;
- private readonly ILogger _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ResponseFilter"/> class.
- /// </summary>
- /// <param name="server">The HTTP server.</param>
- /// <param name="logger">The logger.</param>
- public ResponseFilter(IHttpServer server, ILogger logger)
- {
- _server = server;
- _logger = logger;
- }
-
- /// <summary>
- /// Filters the response.
- /// </summary>
- /// <param name="req">The req.</param>
- /// <param name="res">The res.</param>
- /// <param name="dto">The dto.</param>
- public void FilterResponse(IRequest req, HttpResponse res, object dto)
- {
- foreach(var (key, value) in _server.GetDefaultCorsHeaders(req))
- {
- res.Headers.Add(key, value);
- }
- // Try to prevent compatibility view
- res.Headers["Access-Control-Allow-Headers"] = ("Accept, Accept-Language, Authorization, Cache-Control, " +
- "Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " +
- "Content-Type, Cookie, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, " +
- "Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, " +
- "X-Emby-Authorization");
-
- if (dto is Exception exception)
- {
- _logger.LogError(exception, "Error processing request for {RawUrl}", req.RawUrl);
-
- if (!string.IsNullOrEmpty(exception.Message))
- {
- var error = exception.Message.Replace(Environment.NewLine, " ", StringComparison.Ordinal);
- error = RemoveControlCharacters(error);
-
- res.Headers.Add("X-Application-Error-Code", error);
- }
- }
-
- if (dto is IHasHeaders hasHeaders)
- {
- if (!hasHeaders.Headers.ContainsKey(HeaderNames.Server))
- {
- hasHeaders.Headers[HeaderNames.Server] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50";
- }
-
- // Content length has to be explicitly set on on HttpListenerResponse or it won't be happy
- if (hasHeaders.Headers.TryGetValue(HeaderNames.ContentLength, out string contentLength)
- && !string.IsNullOrEmpty(contentLength))
- {
- var length = long.Parse(contentLength, CultureInfo.InvariantCulture);
-
- if (length > 0)
- {
- res.ContentLength = length;
- }
- }
- }
- }
-
- /// <summary>
- /// Removes the control characters.
- /// </summary>
- /// <param name="inString">The in string.</param>
- /// <returns>System.String.</returns>
- public static string RemoveControlCharacters(string inString)
- {
- if (inString == null)
- {
- return null;
- }
- else if (inString.Length == 0)
- {
- return inString;
- }
-
- var newString = new StringBuilder(inString.Length);
-
- foreach (var ch in inString)
- {
- if (!char.IsControl(ch))
- {
- newString.Append(ch);
- }
- }
-
- return newString.ToString();
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index 256b24924..68d981ad1 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -1,237 +1,35 @@
#pragma warning disable CS1591
-using System;
-using System.Linq;
-using System.Security.Authentication;
-using Emby.Server.Implementations.SocketSharp;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.HttpServer.Security
{
public class AuthService : IAuthService
{
- private readonly ILogger<AuthService> _logger;
private readonly IAuthorizationContext _authorizationContext;
- private readonly ISessionManager _sessionManager;
- private readonly IServerConfigurationManager _config;
- private readonly INetworkManager _networkManager;
public AuthService(
- ILogger<AuthService> logger,
- IAuthorizationContext authorizationContext,
- IServerConfigurationManager config,
- ISessionManager sessionManager,
- INetworkManager networkManager)
+ IAuthorizationContext authorizationContext)
{
- _logger = logger;
_authorizationContext = authorizationContext;
- _config = config;
- _sessionManager = sessionManager;
- _networkManager = networkManager;
}
- public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues)
+ public AuthorizationInfo Authenticate(HttpRequest request)
{
- ValidateUser(request, authAttribtues);
- }
-
- public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
- {
- var req = new WebSocketSharpRequest(request, null, request.Path, _logger);
- var user = ValidateUser(req, authAttributes);
- return user;
- }
-
- private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
- {
- // This code is executed before the service
var auth = _authorizationContext.GetAuthorizationInfo(request);
-
- if (!IsExemptFromAuthenticationToken(authAttribtues, request))
- {
- ValidateSecurityToken(request, auth.Token);
- }
-
- if (authAttribtues.AllowLocalOnly && !request.IsLocal)
+ if (auth?.User == null)
{
- throw new SecurityException("Operation not found.");
+ return null;
}
- var user = auth.User;
-
- if (user == null && auth.UserId != Guid.Empty)
- {
- throw new AuthenticationException("User with Id " + auth.UserId + " not found");
- }
-
- if (user != null)
- {
- ValidateUserAccess(user, request, authAttribtues, auth);
- }
-
- var info = GetTokenInfo(request);
-
- if (!IsExemptFromRoles(auth, authAttribtues, request, info))
- {
- var roles = authAttribtues.GetRoles();
-
- ValidateRoles(roles, user);
- }
-
- if (!string.IsNullOrEmpty(auth.DeviceId) &&
- !string.IsNullOrEmpty(auth.Client) &&
- !string.IsNullOrEmpty(auth.Device))
- {
- _sessionManager.LogSessionActivity(auth.Client,
- auth.Version,
- auth.DeviceId,
- auth.Device,
- request.RemoteIp,
- user);
- }
-
- return user;
- }
-
- private void ValidateUserAccess(
- User user,
- IRequest request,
- IAuthenticationAttributes authAttribtues,
- AuthorizationInfo auth)
- {
- if (user.Policy.IsDisabled)
+ if (auth.User.HasPermission(PermissionKind.IsDisabled))
{
throw new SecurityException("User account has been disabled.");
}
- if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(request.RemoteIp))
- {
- throw new SecurityException("User account has been disabled.");
- }
-
- if (!user.Policy.IsAdministrator
- && !authAttribtues.EscapeParentalControl
- && !user.IsParentalScheduleAllowed())
- {
- request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl");
-
- throw new SecurityException("This user account is not allowed access at this time.");
- }
- }
-
- private bool IsExemptFromAuthenticationToken(IAuthenticationAttributes authAttribtues, IRequest request)
- {
- if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
- {
- return true;
- }
-
- if (authAttribtues.AllowLocal && request.IsLocal)
- {
- return true;
- }
- if (authAttribtues.AllowLocalOnly && request.IsLocal)
- {
- return true;
- }
-
- return false;
- }
-
- private bool IsExemptFromRoles(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, IRequest request, AuthenticationInfo tokenInfo)
- {
- if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
- {
- return true;
- }
-
- if (authAttribtues.AllowLocal && request.IsLocal)
- {
- return true;
- }
-
- if (authAttribtues.AllowLocalOnly && request.IsLocal)
- {
- return true;
- }
-
- if (string.IsNullOrEmpty(auth.Token))
- {
- return true;
- }
-
- if (tokenInfo != null && tokenInfo.UserId.Equals(Guid.Empty))
- {
- return true;
- }
-
- return false;
- }
-
- private static void ValidateRoles(string[] roles, User user)
- {
- if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase))
- {
- if (user == null || !user.Policy.IsAdministrator)
- {
- throw new SecurityException("User does not have admin access.");
- }
- }
-
- if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase))
- {
- if (user == null || !user.Policy.EnableContentDeletion)
- {
- throw new SecurityException("User does not have delete access.");
- }
- }
-
- if (roles.Contains("download", StringComparer.OrdinalIgnoreCase))
- {
- if (user == null || !user.Policy.EnableContentDownloading)
- {
- throw new SecurityException("User does not have download access.");
- }
- }
- }
-
- private static AuthenticationInfo GetTokenInfo(IRequest request)
- {
- request.Items.TryGetValue("OriginalAuthenticationInfo", out var info);
- return info as AuthenticationInfo;
- }
-
- private void ValidateSecurityToken(IRequest request, string token)
- {
- if (string.IsNullOrEmpty(token))
- {
- throw new AuthenticationException("Access token is required.");
- }
-
- var info = GetTokenInfo(request);
-
- if (info == null)
- {
- throw new AuthenticationException("Access token is invalid or expired.");
- }
-
- //if (!string.IsNullOrEmpty(info.UserId))
- //{
- // var user = _userManager.GetUserById(info.UserId);
-
- // if (user == null || user.Configuration.IsDisabled)
- // {
- // throw new SecurityException("User account has been disabled.");
- // }
- //}
+ return auth;
}
}
}
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
index 129faeaab..4b407dd9d 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
@@ -7,7 +7,7 @@ using System.Net;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
-using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer.Security
@@ -23,14 +23,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
_userManager = userManager;
}
- public AuthorizationInfo GetAuthorizationInfo(object requestContext)
+ public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext)
{
- return GetAuthorizationInfo((IRequest)requestContext);
- }
-
- public AuthorizationInfo GetAuthorizationInfo(IRequest requestContext)
- {
- if (requestContext.Items.TryGetValue("AuthorizationInfo", out var cached))
+ if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
{
return (AuthorizationInfo)cached;
}
@@ -38,15 +33,39 @@ namespace Emby.Server.Implementations.HttpServer.Security
return GetAuthorization(requestContext);
}
+ public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
+ {
+ var auth = GetAuthorizationDictionary(requestContext);
+ var (authInfo, _) =
+ GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
+ return authInfo;
+ }
+
/// <summary>
/// Gets the authorization.
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
- private AuthorizationInfo GetAuthorization(IRequest httpReq)
+ private AuthorizationInfo GetAuthorization(HttpContext httpReq)
{
var auth = GetAuthorizationDictionary(httpReq);
+ var (authInfo, originalAuthInfo) =
+ GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
+
+ if (originalAuthInfo != null)
+ {
+ httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
+ }
+ httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
+ return authInfo;
+ }
+
+ private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
+ in Dictionary<string, string> auth,
+ in IHeaderDictionary headers,
+ in IQueryCollection queryString)
+ {
string deviceId = null;
string device = null;
string client = null;
@@ -64,19 +83,26 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (string.IsNullOrEmpty(token))
{
- token = httpReq.Headers["X-Emby-Token"];
+ token = headers["X-Emby-Token"];
+ }
+
+ if (string.IsNullOrEmpty(token))
+ {
+ token = headers["X-MediaBrowser-Token"];
}
if (string.IsNullOrEmpty(token))
{
- token = httpReq.Headers["X-MediaBrowser-Token"];
+ token = queryString["ApiKey"];
}
+
+ // TODO deprecate this query parameter.
if (string.IsNullOrEmpty(token))
{
- token = httpReq.QueryString["api_key"];
+ token = queryString["api_key"];
}
- var info = new AuthorizationInfo
+ var authInfo = new AuthorizationInfo
{
Client = client,
Device = device,
@@ -85,6 +111,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
Token = token
};
+ AuthenticationInfo originalAuthenticationInfo = null;
if (!string.IsNullOrWhiteSpace(token))
{
var result = _authRepo.Get(new AuthenticationInfoQuery
@@ -92,81 +119,94 @@ namespace Emby.Server.Implementations.HttpServer.Security
AccessToken = token
});
- var tokenInfo = result.Items.Count > 0 ? result.Items[0] : null;
+ originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
- if (tokenInfo != null)
+ if (originalAuthenticationInfo != null)
{
var updateToken = false;
// TODO: Remove these checks for IsNullOrWhiteSpace
- if (string.IsNullOrWhiteSpace(info.Client))
+ if (string.IsNullOrWhiteSpace(authInfo.Client))
{
- info.Client = tokenInfo.AppName;
+ authInfo.Client = originalAuthenticationInfo.AppName;
}
- if (string.IsNullOrWhiteSpace(info.DeviceId))
+ if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
{
- info.DeviceId = tokenInfo.DeviceId;
+ authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
}
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
- var allowTokenInfoUpdate = info.Client == null || info.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
+ var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
- if (string.IsNullOrWhiteSpace(info.Device))
+ if (string.IsNullOrWhiteSpace(authInfo.Device))
{
- info.Device = tokenInfo.DeviceName;
+ authInfo.Device = originalAuthenticationInfo.DeviceName;
}
-
- else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
+ else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
- tokenInfo.DeviceName = info.Device;
+ originalAuthenticationInfo.DeviceName = authInfo.Device;
}
}
- if (string.IsNullOrWhiteSpace(info.Version))
+ if (string.IsNullOrWhiteSpace(authInfo.Version))
{
- info.Version = tokenInfo.AppVersion;
+ authInfo.Version = originalAuthenticationInfo.AppVersion;
}
- else if (!string.Equals(info.Version, tokenInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
+ else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
- tokenInfo.AppVersion = info.Version;
+ originalAuthenticationInfo.AppVersion = authInfo.Version;
}
}
- if ((DateTime.UtcNow - tokenInfo.DateLastActivity).TotalMinutes > 3)
+ if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
{
- tokenInfo.DateLastActivity = DateTime.UtcNow;
+ originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
updateToken = true;
}
- if (!tokenInfo.UserId.Equals(Guid.Empty))
+ if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
{
- info.User = _userManager.GetUserById(tokenInfo.UserId);
+ authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
- if (info.User != null && !string.Equals(info.User.Name, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
+ if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
{
- tokenInfo.UserName = info.User.Name;
+ originalAuthenticationInfo.UserName = authInfo.User.Username;
updateToken = true;
}
}
if (updateToken)
{
- _authRepo.Update(tokenInfo);
+ _authRepo.Update(originalAuthenticationInfo);
}
}
- httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo;
}
- httpReq.Items["AuthorizationInfo"] = info;
+ return (authInfo, originalAuthenticationInfo);
+ }
+
+ /// <summary>
+ /// Gets the auth.
+ /// </summary>
+ /// <param name="httpReq">The HTTP req.</param>
+ /// <returns>Dictionary{System.StringSystem.String}.</returns>
+ private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq)
+ {
+ var auth = httpReq.Request.Headers["X-Emby-Authorization"];
- return info;
+ if (string.IsNullOrEmpty(auth))
+ {
+ auth = httpReq.Request.Headers[HeaderNames.Authorization];
+ }
+
+ return GetAuthorization(auth);
}
/// <summary>
@@ -174,7 +214,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
- private Dictionary<string, string> GetAuthorizationDictionary(IRequest httpReq)
+ private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
{
var auth = httpReq.Headers["X-Emby-Authorization"];
@@ -236,12 +276,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
private static string NormalizeValue(string value)
{
- if (string.IsNullOrEmpty(value))
- {
- return value;
- }
-
- return WebUtility.HtmlEncode(value);
+ return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
}
}
}
diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
index 166952c64..86914dea2 100644
--- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
@@ -1,12 +1,12 @@
#pragma warning disable CS1591
using System;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
namespace Emby.Server.Implementations.HttpServer.Security
{
@@ -23,26 +23,20 @@ namespace Emby.Server.Implementations.HttpServer.Security
_sessionManager = sessionManager;
}
- public SessionInfo GetSession(IRequest requestContext)
+ public SessionInfo GetSession(HttpContext requestContext)
{
var authorization = _authContext.GetAuthorizationInfo(requestContext);
var user = authorization.User;
- return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user);
- }
-
- private AuthenticationInfo GetTokenInfo(IRequest request)
- {
- request.Items.TryGetValue("OriginalAuthenticationInfo", out var info);
- return info as AuthenticationInfo;
+ return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp(), user);
}
public SessionInfo GetSession(object requestContext)
{
- return GetSession((IRequest)requestContext);
+ return GetSession((HttpContext)requestContext);
}
- public User GetUser(IRequest requestContext)
+ public User GetUser(HttpContext requestContext)
{
var session = GetSession(requestContext);
@@ -51,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
public User GetUser(object requestContext)
{
- return GetUser((IRequest)requestContext);
+ return GetUser((HttpContext)requestContext);
}
}
}
diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs
deleted file mode 100644
index 5afc51dbc..000000000
--- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs
+++ /dev/null
@@ -1,120 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer
-{
- /// <summary>
- /// Class StreamWriter.
- /// </summary>
- public class StreamWriter : IAsyncStreamWriter, IHasHeaders
- {
- /// <summary>
- /// The options.
- /// </summary>
- private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
-
- /// <summary>
- /// Initializes a new instance of the <see cref="StreamWriter" /> class.
- /// </summary>
- /// <param name="source">The source.</param>
- /// <param name="contentType">Type of the content.</param>
- public StreamWriter(Stream source, string contentType)
- {
- if (string.IsNullOrEmpty(contentType))
- {
- throw new ArgumentNullException(nameof(contentType));
- }
-
- SourceStream = source;
-
- Headers["Content-Type"] = contentType;
-
- if (source.CanSeek)
- {
- Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture);
- }
-
- Headers[HeaderNames.ContentType] = contentType;
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="StreamWriter"/> class.
- /// </summary>
- /// <param name="source">The source.</param>
- /// <param name="contentType">Type of the content.</param>
- /// <param name="contentLength">The content length.</param>
- public StreamWriter(byte[] source, string contentType, int contentLength)
- {
- if (string.IsNullOrEmpty(contentType))
- {
- throw new ArgumentNullException(nameof(contentType));
- }
-
- SourceBytes = source;
-
- Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture);
- Headers[HeaderNames.ContentType] = contentType;
- }
-
- /// <summary>
- /// Gets or sets the source stream.
- /// </summary>
- /// <value>The source stream.</value>
- private Stream SourceStream { get; set; }
-
- private byte[] SourceBytes { get; set; }
-
- /// <summary>
- /// Gets the options.
- /// </summary>
- /// <value>The options.</value>
- public IDictionary<string, string> Headers => _options;
-
- /// <summary>
- /// Fires when complete.
- /// </summary>
- public Action OnComplete { get; set; }
-
- /// <summary>
- /// Fires when an error occours.
- /// </summary>
- public Action OnError { get; set; }
-
- /// <inheritdoc />
- public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
- {
- try
- {
- var bytes = SourceBytes;
-
- if (bytes != null)
- {
- await responseStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
- }
- else
- {
- using (var src = SourceStream)
- {
- await src.CopyToAsync(responseStream).ConfigureAwait(false);
- }
- }
- }
- catch
- {
- OnError?.Invoke();
-
- throw;
- }
- finally
- {
- OnComplete?.Invoke();
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 0680c5ffe..fed2addf8 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -11,6 +11,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -19,12 +20,12 @@ namespace Emby.Server.Implementations.HttpServer
/// <summary>
/// Class WebSocketConnection.
/// </summary>
- public class WebSocketConnection : IWebSocketConnection
+ public class WebSocketConnection : IWebSocketConnection, IDisposable
{
/// <summary>
/// The logger.
/// </summary>
- private readonly ILogger _logger;
+ private readonly ILogger<WebSocketConnection> _logger;
/// <summary>
/// The json serializer options.
@@ -119,7 +120,7 @@ namespace Emby.Server.Implementations.HttpServer
Memory<byte> memory = writer.GetMemory(512);
try
{
- receiveresult = await _socket.ReceiveAsync(memory, cancellationToken);
+ receiveresult = await _socket.ReceiveAsync(memory, cancellationToken).ConfigureAwait(false);
}
catch (WebSocketException ex)
{
@@ -137,7 +138,7 @@ namespace Emby.Server.Implementations.HttpServer
writer.Advance(bytesRead);
// Make the data available to the PipeReader
- FlushResult flushResult = await writer.FlushAsync();
+ FlushResult flushResult = await writer.FlushAsync().ConfigureAwait(false);
if (flushResult.IsCompleted)
{
// The PipeReader stopped reading
@@ -179,7 +180,7 @@ namespace Emby.Server.Implementations.HttpServer
return;
}
- WebSocketMessage<object> stub;
+ WebSocketMessage<object>? stub;
try
{
@@ -209,6 +210,12 @@ namespace Emby.Server.Implementations.HttpServer
return;
}
+ if (stub == null)
+ {
+ _logger.LogError("Error processing web socket message");
+ return;
+ }
+
// Tell the PipeReader how much of the buffer we have consumed
reader.AdvanceTo(buffer.End);
@@ -221,9 +228,9 @@ namespace Emby.Server.Implementations.HttpServer
Connection = this
};
- if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
+ if (info.MessageType == SessionMessageType.KeepAlive)
{
- await SendKeepAliveResponse();
+ await SendKeepAliveResponse().ConfigureAwait(false);
}
else
{
@@ -234,10 +241,12 @@ namespace Emby.Server.Implementations.HttpServer
private Task SendKeepAliveResponse()
{
LastKeepAliveDate = DateTime.UtcNow;
- return SendAsync(new WebSocketMessage<string>
- {
- MessageType = "KeepAlive"
- }, CancellationToken.None);
+ return SendAsync(
+ new WebSocketMessage<string>
+ {
+ MessageId = Guid.NewGuid(),
+ MessageType = SessionMessageType.KeepAlive
+ }, CancellationToken.None);
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
new file mode 100644
index 000000000..71ece80a7
--- /dev/null
+++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
@@ -0,0 +1,95 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+using Jellyfin.Data.Events;
+using MediaBrowser.Controller.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.HttpServer
+{
+ public class WebSocketManager : IWebSocketManager
+ {
+ private readonly Lazy<IEnumerable<IWebSocketListener>> _webSocketListeners;
+ private readonly ILogger<WebSocketManager> _logger;
+ private readonly ILoggerFactory _loggerFactory;
+
+ private bool _disposed = false;
+
+ public WebSocketManager(
+ Lazy<IEnumerable<IWebSocketListener>> webSocketListeners,
+ ILogger<WebSocketManager> logger,
+ ILoggerFactory loggerFactory)
+ {
+ _webSocketListeners = webSocketListeners;
+ _logger = logger;
+ _loggerFactory = loggerFactory;
+ }
+
+ public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
+
+ /// <inheritdoc />
+ public async Task WebSocketRequestHandler(HttpContext context)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ try
+ {
+ _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
+
+ WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
+
+ using var connection = new WebSocketConnection(
+ _loggerFactory.CreateLogger<WebSocketConnection>(),
+ webSocket,
+ context.Connection.RemoteIpAddress,
+ context.Request.Query)
+ {
+ OnReceive = ProcessWebSocketMessageReceived
+ };
+
+ WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
+
+ await connection.ProcessAsync().ConfigureAwait(false);
+ _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
+ }
+ catch (Exception ex) // Otherwise ASP.Net will ignore the exception
+ {
+ _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress);
+ if (!context.Response.HasStarted)
+ {
+ context.Response.StatusCode = 500;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Processes the web socket message received.
+ /// </summary>
+ /// <param name="result">The result.</param>
+ private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
+ {
+ if (_disposed)
+ {
+ return Task.CompletedTask;
+ }
+
+ IEnumerable<Task> GetTasks()
+ {
+ var listeners = _webSocketListeners.Value;
+ foreach (var x in listeners)
+ {
+ yield return x.ProcessMessageAsync(result);
+ }
+ }
+
+ return Task.WhenAll(GetTasks());
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs
index ef93779aa..7435e9d0b 100644
--- a/Emby.Server.Implementations/IO/FileRefresher.cs
+++ b/Emby.Server.Implementations/IO/FileRefresher.cs
@@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.IO
private readonly List<string> _affectedPaths = new List<string>();
private readonly object _timerLock = new object();
private Timer _timer;
+ private bool _disposed;
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
{
@@ -148,7 +149,7 @@ namespace Emby.Server.Implementations.IO
continue;
}
- _logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path);
+ _logger.LogInformation("{Name} ({Path}) will be refreshed.", item.Name, item.Path);
try
{
@@ -159,11 +160,11 @@ namespace Emby.Server.Implementations.IO
// For now swallow and log.
// Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
// Should we remove it from it's parent?
- _logger.LogError(ex, "Error refreshing {name}", item.Name);
+ _logger.LogError(ex, "Error refreshing {Name}", item.Name);
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error refreshing {name}", item.Name);
+ _logger.LogError(ex, "Error refreshing {Name}", item.Name);
}
}
}
@@ -213,11 +214,12 @@ namespace Emby.Server.Implementations.IO
}
}
- private bool _disposed;
+ /// <inheritdoc />
public void Dispose()
{
_disposed = true;
DisposeTimer();
+ GC.SuppressFinalize(this);
}
}
}
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index eb5e190aa..3353fae9d 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -6,19 +6,18 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using Emby.Server.Implementations.Library;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.IO;
-using Emby.Server.Implementations.Library;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO
{
public class LibraryMonitor : ILibraryMonitor
{
- private readonly ILogger _logger;
+ private readonly ILogger<LibraryMonitor> _logger;
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem;
@@ -38,6 +37,8 @@ namespace Emby.Server.Implementations.IO
/// </summary>
private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ private bool _disposed = false;
+
/// <summary>
/// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
/// </summary>
@@ -87,7 +88,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error in ReportFileSystemChanged for {path}", path);
+ _logger.LogError(ex, "Error in ReportFileSystemChanged for {Path}", path);
}
}
}
@@ -266,7 +267,6 @@ namespace Emby.Server.Implementations.IO
{
DisposeWatcher(newWatcher, false);
}
-
}
catch (Exception ex)
{
@@ -393,7 +393,6 @@ namespace Emby.Server.Implementations.IO
}
return false;
-
}))
{
monitorPath = false;
@@ -494,8 +493,6 @@ namespace Emby.Server.Implementations.IO
}
}
- private bool _disposed = false;
-
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
@@ -524,24 +521,4 @@ namespace Emby.Server.Implementations.IO
_disposed = true;
}
}
-
- public class LibraryMonitorStartup : IServerEntryPoint
- {
- private readonly ILibraryMonitor _monitor;
-
- public LibraryMonitorStartup(ILibraryMonitor monitor)
- {
- _monitor = monitor;
- }
-
- public Task RunAsync()
- {
- _monitor.Start();
- return Task.CompletedTask;
- }
-
- public void Dispose()
- {
- }
- }
}
diff --git a/Emby.Server.Implementations/IO/LibraryMonitorStartup.cs b/Emby.Server.Implementations/IO/LibraryMonitorStartup.cs
new file mode 100644
index 000000000..c51cf0545
--- /dev/null
+++ b/Emby.Server.Implementations/IO/LibraryMonitorStartup.cs
@@ -0,0 +1,35 @@
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Plugins;
+
+namespace Emby.Server.Implementations.IO
+{
+ /// <summary>
+ /// <see cref="IServerEntryPoint" /> which is responsible for starting the library monitor.
+ /// </summary>
+ public sealed class LibraryMonitorStartup : IServerEntryPoint
+ {
+ private readonly ILibraryMonitor _monitor;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LibraryMonitorStartup"/> class.
+ /// </summary>
+ /// <param name="monitor">The library monitor.</param>
+ public LibraryMonitorStartup(ILibraryMonitor monitor)
+ {
+ _monitor = monitor;
+ }
+
+ /// <inheritdoc />
+ public Task RunAsync()
+ {
+ _monitor.Start();
+ return Task.CompletedTask;
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 7461ec4f1..3cb025111 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -20,7 +20,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
public class ManagedFileSystem : IFileSystem
{
- protected ILogger Logger;
+ protected ILogger<ManagedFileSystem> Logger;
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
private readonly string _tempPath;
@@ -237,7 +237,7 @@ namespace Emby.Server.Implementations.IO
{
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
- //if (!result.IsDirectory)
+ // if (!result.IsDirectory)
//{
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
//}
@@ -245,6 +245,16 @@ namespace Emby.Server.Implementations.IO
if (info is FileInfo fileInfo)
{
result.Length = fileInfo.Length;
+
+ // Issue #2354 get the size of files behind symbolic links
+ if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
+ {
+ using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
+ {
+ result.Length = thisFileStream.Length;
+ }
+ }
+
result.DirectoryName = fileInfo.DirectoryName;
}
@@ -388,30 +398,6 @@ namespace Emby.Server.Implementations.IO
}
}
- public virtual void SetReadOnly(string path, bool isReadOnly)
- {
- if (OperatingSystem.Id != OperatingSystemId.Windows)
- {
- return;
- }
-
- var info = GetExtendedFileSystemInfo(path);
-
- if (info.Exists && info.IsReadOnly != isReadOnly)
- {
- if (isReadOnly)
- {
- File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.ReadOnly);
- }
- else
- {
- var attributes = File.GetAttributes(path);
- attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly);
- File.SetAttributes(path, attributes);
- }
- }
- }
-
public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly)
{
if (OperatingSystem.Id != OperatingSystemId.Windows)
@@ -628,6 +614,7 @@ namespace Emby.Server.Implementations.IO
{
return false;
}
+
return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
});
}
@@ -682,6 +669,7 @@ namespace Emby.Server.Implementations.IO
{
return false;
}
+
return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
});
}
@@ -695,14 +683,6 @@ namespace Emby.Server.Implementations.IO
return Directory.EnumerateFileSystemEntries(path, "*", searchOption);
}
- public virtual void SetExecutable(string path)
- {
- if (OperatingSystem.Id == OperatingSystemId.Darwin)
- {
- RunProcess("chmod", "+x \"" + path + "\"", Path.GetDirectoryName(path));
- }
- }
-
private static void RunProcess(string path, string args, string workingDirectory)
{
using (var process = Process.Start(new ProcessStartInfo
diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/Emby.Server.Implementations/IO/StreamHelper.cs
index 40b397edc..c16ebd61b 100644
--- a/Emby.Server.Implementations/IO/StreamHelper.cs
+++ b/Emby.Server.Implementations/IO/StreamHelper.cs
@@ -11,8 +11,6 @@ namespace Emby.Server.Implementations.IO
{
public class StreamHelper : IStreamHelper
{
- private const int StreamCopyToBufferSize = 81920;
-
public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
@@ -83,37 +81,9 @@ namespace Emby.Server.Implementations.IO
}
}
- public async Task<int> CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken)
- {
- byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
- try
- {
- int totalBytesRead = 0;
-
- int bytesRead;
- while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
- {
- var bytesToWrite = bytesRead;
-
- if (bytesToWrite > 0)
- {
- await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
-
- totalBytesRead += bytesRead;
- }
- }
-
- return totalBytesRead;
- }
- finally
- {
- ArrayPool<byte>.Shared.Return(buffer);
- }
- }
-
public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
{
- byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
+ byte[] buffer = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
try
{
int bytesRead;
diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs
index acae702f3..4bef59543 100644
--- a/Emby.Server.Implementations/IStartupOptions.cs
+++ b/Emby.Server.Implementations/IStartupOptions.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
namespace Emby.Server.Implementations
@@ -15,11 +17,6 @@ namespace Emby.Server.Implementations
bool IsService { get; }
/// <summary>
- /// Gets the value of the --noautorunwebapp command line option.
- /// </summary>
- bool NoAutoRunWebApp { get; }
-
- /// <summary>
/// Gets the value of the --package-name command line option.
/// </summary>
string PackageName { get; }
@@ -35,11 +32,6 @@ namespace Emby.Server.Implementations
string RestartArgs { get; }
/// <summary>
- /// Gets the value of the --plugin-manifest-url command line option.
- /// </summary>
- string PluginManifestUrl { get; }
-
- /// <summary>
/// Gets the value of the --published-server-url command line option.
/// </summary>
Uri PublishedServerUrl { get; }
diff --git a/Emby.Server.Implementations/Images/ArtistImageProvider.cs b/Emby.Server.Implementations/Images/ArtistImageProvider.cs
new file mode 100644
index 000000000..bf57382ed
--- /dev/null
+++ b/Emby.Server.Implementations/Images/ArtistImageProvider.cs
@@ -0,0 +1,54 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Emby.Server.Implementations.Images;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Querying;
+
+namespace Emby.Server.Implementations.Images
+{
+ /// <summary>
+ /// Class ArtistImageProvider.
+ /// </summary>
+ public class ArtistImageProvider : BaseDynamicImageProvider<MusicArtist>
+ {
+ public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor)
+ : base(fileSystem, providerManager, applicationPaths, imageProcessor)
+ {
+ }
+
+ /// <summary>
+ /// Get children objects used to create an artist image.
+ /// </summary>
+ /// <param name="item">The artist used to create the image.</param>
+ /// <returns>Any relevant children objects.</returns>
+ protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
+ {
+ return Array.Empty<BaseItem>();
+
+ // TODO enable this when BaseDynamicImageProvider objects are configurable
+ // return _libraryManager.GetItemList(new InternalItemsQuery
+ // {
+ // ArtistIds = new[] { item.Id },
+ // IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
+ // OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
+ // Limit = 4,
+ // Recursive = true,
+ // ImageTypes = new[] { ImageType.Primary },
+ // DtoOptions = new DtoOptions(false)
+ // });
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index fd50f156a..5f7e51858 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -133,9 +133,20 @@ namespace Emby.Server.Implementations.Images
protected virtual IEnumerable<string> GetStripCollageImagePaths(BaseItem primaryItem, IEnumerable<BaseItem> items)
{
+ var useBackdrop = primaryItem is CollectionFolder;
return items
.Select(i =>
{
+ // Use Backdrop instead of Primary image for Library images.
+ if (useBackdrop)
+ {
+ var backdrop = i.GetImageInfo(ImageType.Backdrop, 0);
+ if (backdrop != null && backdrop.IsLocalFile)
+ {
+ return backdrop.Path;
+ }
+ }
+
var image = i.GetImageInfo(ImageType.Primary, 0);
if (image != null && image.IsLocalFile)
{
@@ -190,11 +201,12 @@ namespace Emby.Server.Implementations.Images
return null;
}
- ImageProcessor.CreateImageCollage(options);
+ ImageProcessor.CreateImageCollage(options, primaryItem.Name);
return outputPath;
}
- protected virtual string CreateImage(BaseItem item,
+ protected virtual string CreateImage(
+ BaseItem item,
IReadOnlyCollection<BaseItem> itemsWithImages,
string outputPathWithoutExtension,
ImageType imageType,
@@ -214,7 +226,12 @@ namespace Emby.Server.Implementations.Images
if (imageType == ImageType.Primary)
{
- if (item is UserView || item is Playlist || item is MusicGenre || item is Genre || item is PhotoAlbum)
+ if (item is UserView
+ || item is Playlist
+ || item is MusicGenre
+ || item is Genre
+ || item is PhotoAlbum
+ || item is MusicArtist)
{
return CreateSquareCollage(item, itemsWithImages, outputPath);
}
@@ -225,7 +242,7 @@ namespace Emby.Server.Implementations.Images
throw new ArgumentException("Unexpected image type", nameof(imageType));
}
- public bool HasChanged(BaseItem item, IDirectoryService directoryServicee)
+ public bool HasChanged(BaseItem item, IDirectoryService directoryService)
{
if (!Supports(item))
{
@@ -236,6 +253,7 @@ namespace Emby.Server.Implementations.Images
{
return true;
}
+
if (SupportedImages.Contains(ImageType.Thumb) && HasChanged(item, ImageType.Thumb))
{
return true;
diff --git a/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
index a3f3f6cb4..161b4c452 100644
--- a/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
@@ -1,7 +1,9 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.IO;
-using Emby.Server.Implementations.Images;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@@ -11,7 +13,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
-namespace Emby.Server.Implementations.UserViews
+namespace Emby.Server.Implementations.Images
{
public class CollectionFolderImageProvider : BaseDynamicImageProvider<CollectionFolder>
{
@@ -69,7 +71,6 @@ namespace Emby.Server.Implementations.UserViews
new ValueTuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending)
},
IncludeItemTypes = includeItemTypes
-
});
}
diff --git a/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
index 78ac95f85..462eb03a8 100644
--- a/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.IO;
@@ -14,18 +16,16 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
-namespace Emby.Server.Implementations.UserViews
+namespace Emby.Server.Implementations.Images
{
public class DynamicImageProvider : BaseDynamicImageProvider<UserView>
{
private readonly IUserManager _userManager;
- private readonly ILibraryManager _libraryManager;
- public DynamicImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, IUserManager userManager, ILibraryManager libraryManager)
+ public DynamicImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, IUserManager userManager)
: base(fileSystem, providerManager, applicationPaths, imageProcessor)
{
_userManager = userManager;
- _libraryManager = libraryManager;
}
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
@@ -78,7 +78,6 @@ namespace Emby.Server.Implementations.UserViews
}
return i;
-
}).GroupBy(x => x.Id)
.Select(x => x.First());
diff --git a/Emby.Server.Implementations/UserViews/FolderImageProvider.cs b/Emby.Server.Implementations/Images/FolderImageProvider.cs
index 4655cd928..0224ab32a 100644
--- a/Emby.Server.Implementations/UserViews/FolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/FolderImageProvider.cs
@@ -1,5 +1,7 @@
+#pragma warning disable CS1591
+
using System.Collections.Generic;
-using Emby.Server.Implementations.Images;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@@ -11,7 +13,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
-namespace Emby.Server.Implementations.UserViews
+namespace Emby.Server.Implementations.Images
{
public abstract class BaseFolderImageProvider<T> : BaseDynamicImageProvider<T>
where T : Folder, new()
@@ -75,16 +77,12 @@ namespace Emby.Server.Implementations.UserViews
return false;
}
- var folder = item as Folder;
- if (folder != null)
+ if (item is Folder && item.IsTopParent)
{
- if (folder.IsTopParent)
- {
- return false;
- }
+ return false;
}
+
return true;
- //return item.SourceType == SourceType.Library;
}
}
diff --git a/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs
index bb56d9771..1cd4cd66b 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs
+++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs
@@ -1,6 +1,7 @@
+#pragma warning disable CS1591
+
using System.Collections.Generic;
-using System.Linq;
-using Emby.Server.Implementations.Images;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@@ -9,66 +10,21 @@ using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
-namespace Emby.Server.Implementations.Playlists
+namespace Emby.Server.Implementations.Images
{
- public class PlaylistImageProvider : BaseDynamicImageProvider<Playlist>
- {
- public PlaylistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
- {
- }
-
- protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
- {
- var playlist = (Playlist)item;
-
- return playlist.GetManageableItems()
- .Select(i =>
- {
- var subItem = i.Item2;
-
- var episode = subItem as Episode;
-
- if (episode != null)
- {
- var series = episode.Series;
- if (series != null && series.HasImage(ImageType.Primary))
- {
- return series;
- }
- }
-
- if (subItem.HasImage(ImageType.Primary))
- {
- return subItem;
- }
-
- var parent = subItem.GetOwner() ?? subItem.GetParent();
-
- if (parent != null && parent.HasImage(ImageType.Primary))
- {
- if (parent is MusicAlbum)
- {
- return parent;
- }
- }
-
- return null;
- })
- .Where(i => i != null)
- .GroupBy(x => x.Id)
- .Select(x => x.First())
- .ToList();
- }
- }
-
+ /// <summary>
+ /// Class MusicGenreImageProvider.
+ /// </summary>
public class MusicGenreImageProvider : BaseDynamicImageProvider<MusicGenre>
{
+ /// <summary>
+ /// The library manager.
+ /// </summary>
private readonly ILibraryManager _libraryManager;
public MusicGenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
@@ -76,6 +32,11 @@ namespace Emby.Server.Implementations.Playlists
_libraryManager = libraryManager;
}
+ /// <summary>
+ /// Get children objects used to create an music genre image.
+ /// </summary>
+ /// <param name="item">The music genre used to create the image.</param>
+ /// <returns>Any relevant children objects.</returns>
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
return _libraryManager.GetItemList(new InternalItemsQuery
@@ -91,8 +52,14 @@ namespace Emby.Server.Implementations.Playlists
}
}
+ /// <summary>
+ /// Class GenreImageProvider.
+ /// </summary>
public class GenreImageProvider : BaseDynamicImageProvider<Genre>
{
+ /// <summary>
+ /// The library manager.
+ /// </summary>
private readonly ILibraryManager _libraryManager;
public GenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
@@ -100,6 +67,11 @@ namespace Emby.Server.Implementations.Playlists
_libraryManager = libraryManager;
}
+ /// <summary>
+ /// Get children objects used to create an genre image.
+ /// </summary>
+ /// <param name="item">The genre used to create the image.</param>
+ /// <returns>Any relevant children objects.</returns>
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
return _libraryManager.GetItemList(new InternalItemsQuery
diff --git a/Emby.Server.Implementations/Images/PlaylistImageProvider.cs b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs
new file mode 100644
index 000000000..0ce1b91e8
--- /dev/null
+++ b/Emby.Server.Implementations/Images/PlaylistImageProvider.cs
@@ -0,0 +1,66 @@
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Images
+{
+ public class PlaylistImageProvider : BaseDynamicImageProvider<Playlist>
+ {
+ public PlaylistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
+ {
+ }
+
+ protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
+ {
+ var playlist = (Playlist)item;
+
+ return playlist.GetManageableItems()
+ .Select(i =>
+ {
+ var subItem = i.Item2;
+
+ var episode = subItem as Episode;
+
+ if (episode != null)
+ {
+ var series = episode.Series;
+ if (series != null && series.HasImage(ImageType.Primary))
+ {
+ return series;
+ }
+ }
+
+ if (subItem.HasImage(ImageType.Primary))
+ {
+ return subItem;
+ }
+
+ var parent = subItem.GetOwner() ?? subItem.GetParent();
+
+ if (parent != null && parent.HasImage(ImageType.Primary))
+ {
+ if (parent is MusicAlbum)
+ {
+ return parent;
+ }
+ }
+
+ return null;
+ })
+ .Where(i => i != null)
+ .GroupBy(x => x.Id)
+ .Select(x => x.First())
+ .ToList();
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index 218e5a0c6..3380e29d4 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
@@ -8,24 +9,33 @@ using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Library
{
/// <summary>
- /// Provides the core resolver ignore rules
+ /// Provides the core resolver ignore rules.
/// </summary>
public class CoreResolutionIgnoreRule : IResolverIgnoreRule
{
private readonly ILibraryManager _libraryManager;
+ private readonly IServerApplicationPaths _serverApplicationPaths;
/// <summary>
/// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
- public CoreResolutionIgnoreRule(ILibraryManager libraryManager)
+ /// <param name="serverApplicationPaths">The server application paths.</param>
+ public CoreResolutionIgnoreRule(ILibraryManager libraryManager, IServerApplicationPaths serverApplicationPaths)
{
_libraryManager = libraryManager;
+ _serverApplicationPaths = serverApplicationPaths;
}
/// <inheritdoc />
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent)
{
+ // Don't ignore application folders
+ if (fileInfo.FullName.Contains(_serverApplicationPaths.RootFolderPath, StringComparison.InvariantCulture))
+ {
+ return false;
+ }
+
// Don't ignore top level folders
if (fileInfo.IsDirectory && parent is AggregateFolder)
{
@@ -67,7 +77,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null)
{
// Don't resolve these into audio files
- if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename)
+ if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal)
&& _libraryManager.IsAudioFile(filename))
{
return true;
diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
deleted file mode 100644
index 52c8facc3..000000000
--- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
+++ /dev/null
@@ -1,177 +0,0 @@
-using System;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Cryptography;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Cryptography;
-
-namespace Emby.Server.Implementations.Library
-{
- /// <summary>
- /// The default authentication provider.
- /// </summary>
- public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser
- {
- private readonly ICryptoProvider _cryptographyProvider;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="DefaultAuthenticationProvider"/> class.
- /// </summary>
- /// <param name="cryptographyProvider">The cryptography provider.</param>
- public DefaultAuthenticationProvider(ICryptoProvider cryptographyProvider)
- {
- _cryptographyProvider = cryptographyProvider;
- }
-
- /// <inheritdoc />
- public string Name => "Default";
-
- /// <inheritdoc />
- public bool IsEnabled => true;
-
- /// <inheritdoc />
- // This is dumb and an artifact of the backwards way auth providers were designed.
- // This version of authenticate was never meant to be called, but needs to be here for interface compat
- // Only the providers that don't provide local user support use this
- public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
- {
- throw new NotImplementedException();
- }
-
- /// <inheritdoc />
- // This is the version that we need to use for local users. Because reasons.
- public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
- {
- if (resolvedUser == null)
- {
- throw new AuthenticationException($"Specified user does not exist.");
- }
-
- bool success = false;
-
- // As long as jellyfin supports passwordless users, we need this little block here to accommodate
- if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password))
- {
- return Task.FromResult(new ProviderAuthenticationResult
- {
- Username = username
- });
- }
-
- byte[] passwordbytes = Encoding.UTF8.GetBytes(password);
-
- PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password);
- if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)
- || _cryptographyProvider.DefaultHashMethod == readyHash.Id)
- {
- byte[] calculatedHash = _cryptographyProvider.ComputeHash(
- readyHash.Id,
- passwordbytes,
- readyHash.Salt.ToArray());
-
- if (readyHash.Hash.SequenceEqual(calculatedHash))
- {
- success = true;
- }
- }
- else
- {
- throw new AuthenticationException($"Requested crypto method not available in provider: {readyHash.Id}");
- }
-
- if (!success)
- {
- throw new AuthenticationException("Invalid username or password");
- }
-
- return Task.FromResult(new ProviderAuthenticationResult
- {
- Username = username
- });
- }
-
- /// <inheritdoc />
- public bool HasPassword(User user)
- => !string.IsNullOrEmpty(user.Password);
-
- /// <inheritdoc />
- public Task ChangePassword(User user, string newPassword)
- {
- if (string.IsNullOrEmpty(newPassword))
- {
- user.Password = null;
- return Task.CompletedTask;
- }
-
- PasswordHash newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword);
- user.Password = newPasswordHash.ToString();
-
- return Task.CompletedTask;
- }
-
- /// <inheritdoc />
- public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
- {
- if (newPassword != null)
- {
- newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword).ToString();
- }
-
- if (string.IsNullOrWhiteSpace(newPasswordHash))
- {
- throw new ArgumentNullException(nameof(newPasswordHash));
- }
-
- user.EasyPassword = newPasswordHash;
- }
-
- /// <inheritdoc />
- public string GetEasyPasswordHash(User user)
- {
- return string.IsNullOrEmpty(user.EasyPassword)
- ? null
- : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash);
- }
-
- /// <summary>
- /// Gets the hashed string.
- /// </summary>
- public string GetHashedString(User user, string str)
- {
- if (string.IsNullOrEmpty(user.Password))
- {
- return _cryptographyProvider.CreatePasswordHash(str).ToString();
- }
-
- // TODO: make use of iterations parameter?
- PasswordHash passwordHash = PasswordHash.Parse(user.Password);
- var salt = passwordHash.Salt.ToArray();
- return new PasswordHash(
- passwordHash.Id,
- _cryptographyProvider.ComputeHash(
- passwordHash.Id,
- Encoding.UTF8.GetBytes(str),
- salt),
- salt,
- passwordHash.Parameters.ToDictionary(x => x.Key, y => y.Value)).ToString();
- }
-
- public ReadOnlySpan<byte> GetHashed(User user, string str)
- {
- if (string.IsNullOrEmpty(user.Password))
- {
- return _cryptographyProvider.CreatePasswordHash(str).Hash;
- }
-
- // TODO: make use of iterations parameter?
- PasswordHash passwordHash = PasswordHash.Parse(user.Password);
- return _cryptographyProvider.ComputeHash(
- passwordHash.Id,
- Encoding.UTF8.GetBytes(str),
- passwordHash.Salt.ToArray());
- }
- }
-}
diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs
deleted file mode 100644
index 6c6fbd86f..000000000
--- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs
+++ /dev/null
@@ -1,140 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Security.Cryptography;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Users;
-
-namespace Emby.Server.Implementations.Library
-{
- /// <summary>
- /// The default password reset provider.
- /// </summary>
- public class DefaultPasswordResetProvider : IPasswordResetProvider
- {
- private const string BaseResetFileName = "passwordreset";
-
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IUserManager _userManager;
-
- private readonly string _passwordResetFileBase;
- private readonly string _passwordResetFileBaseDir;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="DefaultPasswordResetProvider"/> class.
- /// </summary>
- /// <param name="configurationManager">The configuration manager.</param>
- /// <param name="jsonSerializer">The JSON serializer.</param>
- /// <param name="userManager">The user manager.</param>
- public DefaultPasswordResetProvider(
- IServerConfigurationManager configurationManager,
- IJsonSerializer jsonSerializer,
- IUserManager userManager)
- {
- _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
- _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName);
- _jsonSerializer = jsonSerializer;
- _userManager = userManager;
- }
-
- /// <inheritdoc />
- public string Name => "Default Password Reset Provider";
-
- /// <inheritdoc />
- public bool IsEnabled => true;
-
- /// <inheritdoc />
- public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
- {
- SerializablePasswordReset spr;
- List<string> usersreset = new List<string>();
- foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
- {
- using (var str = File.OpenRead(resetfile))
- {
- spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
- }
-
- if (spr.ExpirationDate < DateTime.Now)
- {
- File.Delete(resetfile);
- }
- else if (string.Equals(
- spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal),
- pin.Replace("-", string.Empty, StringComparison.Ordinal),
- StringComparison.InvariantCultureIgnoreCase))
- {
- var resetUser = _userManager.GetUserByName(spr.UserName);
- if (resetUser == null)
- {
- throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
- }
-
- await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
- usersreset.Add(resetUser.Name);
- File.Delete(resetfile);
- }
- }
-
- if (usersreset.Count < 1)
- {
- throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}");
- }
- else
- {
- return new PinRedeemResult
- {
- Success = true,
- UsersReset = usersreset.ToArray()
- };
- }
- }
-
- /// <inheritdoc />
- public async Task<ForgotPasswordResult> StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork)
- {
- string pin = string.Empty;
- using (var cryptoRandom = RandomNumberGenerator.Create())
- {
- byte[] bytes = new byte[4];
- cryptoRandom.GetBytes(bytes);
- pin = BitConverter.ToString(bytes);
- }
-
- DateTime expireTime = DateTime.Now.AddMinutes(30);
- string filePath = _passwordResetFileBase + user.InternalId + ".json";
- SerializablePasswordReset spr = new SerializablePasswordReset
- {
- ExpirationDate = expireTime,
- Pin = pin,
- PinFile = filePath,
- UserName = user.Name
- };
-
- using (FileStream fileStream = File.OpenWrite(filePath))
- {
- _jsonSerializer.SerializeToStream(spr, fileStream);
- await fileStream.FlushAsync().ConfigureAwait(false);
- }
-
- return new ForgotPasswordResult
- {
- Action = ForgotPasswordAction.PinCode,
- PinExpirationDate = expireTime,
- PinFile = filePath
- };
- }
-
- private class SerializablePasswordReset : PasswordPinCreationResult
- {
- public string Pin { get; set; }
-
- public string UserName { get; set; }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
index 9a7186898..236453e80 100644
--- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
+++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs
@@ -11,17 +11,7 @@ namespace Emby.Server.Implementations.Library
{
public class ExclusiveLiveStream : ILiveStream
{
- public int ConsumerCount { get; set; }
- public string OriginalStreamId { get; set; }
-
- public string TunerHostId => null;
-
- public bool EnableStreamSharing { get; set; }
- public MediaSourceInfo MediaSource { get; set; }
-
- public string UniqueId { get; private set; }
-
- private Func<Task> _closeFn;
+ private readonly Func<Task> _closeFn;
public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func<Task> closeFn)
{
@@ -32,6 +22,18 @@ namespace Emby.Server.Implementations.Library
UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
}
+ public int ConsumerCount { get; set; }
+
+ public string OriginalStreamId { get; set; }
+
+ public string TunerHostId => null;
+
+ public bool EnableStreamSharing { get; set; }
+
+ public MediaSourceInfo MediaSource { get; set; }
+
+ public string UniqueId { get; }
+
public Task Close()
{
return _closeFn();
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index d12b5855b..e30a67593 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -1,50 +1,89 @@
+#nullable enable
+
+using System;
using System.Linq;
using DotNet.Globbing;
namespace Emby.Server.Implementations.Library
{
/// <summary>
- /// Glob patterns for files to ignore
+ /// Glob patterns for files to ignore.
/// </summary>
public static class IgnorePatterns
{
/// <summary>
- /// Files matching these glob patterns will be ignored
+ /// Files matching these glob patterns will be ignored.
/// </summary>
- public static readonly string[] Patterns = new string[]
+ private static readonly string[] _patterns =
{
"**/small.jpg",
"**/albumart.jpg",
- "**/*sample*",
+
+ // We have neither non-greedy matching or character group repetitions, working around that here.
+ // https://github.com/dazinator/DotNet.Glob#patterns
+ // .*/sample\..{1,5}
+ "**/sample.?",
+ "**/sample.??",
+ "**/sample.???", // Matches sample.mkv
+ "**/sample.????", // Matches sample.webm
+ "**/sample.?????",
+ "**/*.sample.?",
+ "**/*.sample.??",
+ "**/*.sample.???",
+ "**/*.sample.????",
+ "**/*.sample.?????",
+ "**/sample/*",
// Directories
"**/metadata/**",
+ "**/metadata",
"**/ps3_update/**",
+ "**/ps3_update",
"**/ps3_vprm/**",
+ "**/ps3_vprm",
"**/extrafanart/**",
+ "**/extrafanart",
"**/extrathumbs/**",
+ "**/extrathumbs",
"**/.actors/**",
+ "**/.actors",
"**/.wd_tv/**",
+ "**/.wd_tv",
"**/lost+found/**",
+ "**/lost+found",
// WMC temp recording directories that will constantly be written to
"**/TempRec/**",
+ "**/TempRec",
"**/TempSBE/**",
+ "**/TempSBE",
// Synology
"**/eaDir/**",
+ "**/eaDir",
"**/@eaDir/**",
+ "**/@eaDir",
"**/#recycle/**",
+ "**/#recycle",
// Qnap
"**/@Recycle/**",
+ "**/@Recycle",
"**/.@__thumb/**",
+ "**/.@__thumb",
"**/$RECYCLE.BIN/**",
+ "**/$RECYCLE.BIN",
"**/System Volume Information/**",
+ "**/System Volume Information",
"**/.grab/**",
+ "**/.grab",
+
+ // Unix hidden files
+ "**/.*",
- // Unix hidden files and directories
- "**/.*/**",
+ // Mac - if you ever remove the above.
+ // "**/._*",
+ // "**/.DS_Store",
// thumbs.db
"**/thumbs.db",
@@ -56,19 +95,31 @@ namespace Emby.Server.Implementations.Library
private static readonly GlobOptions _globOptions = new GlobOptions
{
- Evaluation = {
+ Evaluation =
+ {
CaseInsensitive = true
}
};
- private static readonly Glob[] _globs = Patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray();
+ private static readonly Glob[] _globs = _patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray();
/// <summary>
- /// Returns true if the supplied path should be ignored
+ /// Returns true if the supplied path should be ignored.
/// </summary>
- public static bool ShouldIgnore(string path)
+ /// <param name="path">The path to test.</param>
+ /// <returns>Whether to ignore the path.</returns>
+ public static bool ShouldIgnore(ReadOnlySpan<char> path)
{
- return _globs.Any(g => g.IsMatch(path));
+ int len = _globs.Length;
+ for (int i = 0; i < len; i++)
+ {
+ if (_globs[i].IsMatch(path))
+ {
+ return true;
+ }
+ }
+
+ return false;
}
}
}
diff --git a/Emby.Server.Implementations/Library/InvalidAuthProvider.cs b/Emby.Server.Implementations/Library/InvalidAuthProvider.cs
deleted file mode 100644
index dc61aacd7..000000000
--- a/Emby.Server.Implementations/Library/InvalidAuthProvider.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Entities;
-
-namespace Emby.Server.Implementations.Library
-{
- /// <summary>
- /// An invalid authentication provider.
- /// </summary>
- public class InvalidAuthProvider : IAuthenticationProvider
- {
- /// <inheritdoc />
- public string Name => "InvalidOrMissingAuthenticationProvider";
-
- /// <inheritdoc />
- public bool IsEnabled => true;
-
- /// <inheritdoc />
- public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
- {
- throw new AuthenticationException("User Account cannot login with this provider. The Normal provider for this user cannot be found");
- }
-
- /// <inheritdoc />
- public bool HasPassword(User user)
- {
- return true;
- }
-
- /// <inheritdoc />
- public Task ChangePassword(User user, string newPassword)
- {
- return Task.CompletedTask;
- }
-
- /// <inheritdoc />
- public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
- {
- // Nothing here
- }
-
- /// <inheritdoc />
- public string GetPasswordHash(User user)
- {
- return string.Empty;
- }
-
- /// <inheritdoc />
- public string GetEasyPasswordHash(User user)
- {
- return string.Empty;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 0b86b2db7..00282b71a 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -1,7 +1,6 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -17,14 +16,16 @@ using Emby.Server.Implementations.Library.Resolvers;
using Emby.Server.Implementations.Library.Validators;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.ScheduledTasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
@@ -35,6 +36,7 @@ using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@@ -43,18 +45,24 @@ using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.MediaInfo;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
-using SortOrder = MediaBrowser.Model.Entities.SortOrder;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using Person = MediaBrowser.Controller.Entities.Person;
using VideoResolver = Emby.Naming.Video.VideoResolver;
namespace Emby.Server.Implementations.Library
{
/// <summary>
- /// Class LibraryManager
+ /// Class LibraryManager.
/// </summary>
public class LibraryManager : ILibraryManager
{
- private readonly ILogger _logger;
+ private const string ShortcutFileExtension = ".mblink";
+
+ private readonly ILogger<LibraryManager> _logger;
+ private readonly IMemoryCache _memoryCache;
private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataRepository;
@@ -66,75 +74,44 @@ namespace Emby.Server.Implementations.Library
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
private readonly IItemRepository _itemRepository;
- private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
-
- private NamingOptions _namingOptions;
- private string[] _videoFileExtensions;
-
- private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
-
- private IProviderManager ProviderManager => _providerManagerFactory.Value;
-
- private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
-
- /// <summary>
- /// Gets or sets the postscan tasks.
- /// </summary>
- /// <value>The postscan tasks.</value>
- private ILibraryPostScanTask[] PostscanTasks { get; set; }
-
- /// <summary>
- /// Gets or sets the intro providers.
- /// </summary>
- /// <value>The intro providers.</value>
- private IIntroProvider[] IntroProviders { get; set; }
-
- /// <summary>
- /// Gets or sets the list of entity resolution ignore rules
- /// </summary>
- /// <value>The entity resolution ignore rules.</value>
- private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
-
- /// <summary>
- /// Gets or sets the list of currently registered entity resolvers
- /// </summary>
- /// <value>The entity resolvers enumerable.</value>
- private IItemResolver[] EntityResolvers { get; set; }
-
- private IMultiItemResolver[] MultiItemResolvers { get; set; }
+ private readonly IImageProcessor _imageProcessor;
/// <summary>
- /// Gets or sets the comparers.
+ /// The _root folder sync lock.
/// </summary>
- /// <value>The comparers.</value>
- private IBaseItemComparer[] Comparers { get; set; }
+ private readonly object _rootFolderSyncLock = new object();
+ private readonly object _userRootFolderSyncLock = new object();
- /// <summary>
- /// Occurs when [item added].
- /// </summary>
- public event EventHandler<ItemChangeEventArgs> ItemAdded;
+ private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
- /// <summary>
- /// Occurs when [item updated].
- /// </summary>
- public event EventHandler<ItemChangeEventArgs> ItemUpdated;
+ private NamingOptions _namingOptions;
+ private string[] _videoFileExtensions;
/// <summary>
- /// Occurs when [item removed].
+ /// The _root folder.
/// </summary>
- public event EventHandler<ItemChangeEventArgs> ItemRemoved;
+ private volatile AggregateFolder _rootFolder;
+ private volatile UserRootFolder _userRootFolder;
- public bool IsScanRunning { get; private set; }
+ private bool _wizardCompleted;
/// <summary>
/// Initializes a new instance of the <see cref="LibraryManager" /> class.
/// </summary>
- /// <param name="appHost">The application host</param>
+ /// <param name="appHost">The application host.</param>
/// <param name="logger">The logger.</param>
/// <param name="taskManager">The task manager.</param>
/// <param name="userManager">The user manager.</param>
/// <param name="configurationManager">The configuration manager.</param>
/// <param name="userDataRepository">The user data repository.</param>
+ /// <param name="libraryMonitorFactory">The library monitor.</param>
+ /// <param name="fileSystem">The file system.</param>
+ /// <param name="providerManagerFactory">The provider manager.</param>
+ /// <param name="userviewManagerFactory">The userview manager.</param>
+ /// <param name="mediaEncoder">The media encoder.</param>
+ /// <param name="itemRepository">The item repository.</param>
+ /// <param name="imageProcessor">The image processor.</param>
+ /// <param name="memoryCache">The memory cache.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILogger<LibraryManager> logger,
@@ -147,7 +124,9 @@ namespace Emby.Server.Implementations.Library
Lazy<IProviderManager> providerManagerFactory,
Lazy<IUserViewManager> userviewManagerFactory,
IMediaEncoder mediaEncoder,
- IItemRepository itemRepository)
+ IItemRepository itemRepository,
+ IImageProcessor imageProcessor,
+ IMemoryCache memoryCache)
{
_appHost = appHost;
_logger = logger;
@@ -161,8 +140,8 @@ namespace Emby.Server.Implementations.Library
_userviewManagerFactory = userviewManagerFactory;
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
-
- _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
+ _imageProcessor = imageProcessor;
+ _memoryCache = memoryCache;
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -170,37 +149,19 @@ namespace Emby.Server.Implementations.Library
}
/// <summary>
- /// Adds the parts.
+ /// Occurs when [item added].
/// </summary>
- /// <param name="rules">The rules.</param>
- /// <param name="resolvers">The resolvers.</param>
- /// <param name="introProviders">The intro providers.</param>
- /// <param name="itemComparers">The item comparers.</param>
- /// <param name="postscanTasks">The post scan tasks.</param>
- public void AddParts(
- IEnumerable<IResolverIgnoreRule> rules,
- IEnumerable<IItemResolver> resolvers,
- IEnumerable<IIntroProvider> introProviders,
- IEnumerable<IBaseItemComparer> itemComparers,
- IEnumerable<ILibraryPostScanTask> postscanTasks)
- {
- EntityResolutionIgnoreRules = rules.ToArray();
- EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
- MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
- IntroProviders = introProviders.ToArray();
- Comparers = itemComparers.ToArray();
- PostscanTasks = postscanTasks.ToArray();
- }
+ public event EventHandler<ItemChangeEventArgs> ItemAdded;
/// <summary>
- /// The _root folder
+ /// Occurs when [item updated].
/// </summary>
- private volatile AggregateFolder _rootFolder;
+ public event EventHandler<ItemChangeEventArgs> ItemUpdated;
/// <summary>
- /// The _root folder sync lock
+ /// Occurs when [item removed].
/// </summary>
- private readonly object _rootFolderSyncLock = new object();
+ public event EventHandler<ItemChangeEventArgs> ItemRemoved;
/// <summary>
/// Gets the root folder.
@@ -225,7 +186,68 @@ namespace Emby.Server.Implementations.Library
}
}
- private bool _wizardCompleted;
+ private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
+
+ private IProviderManager ProviderManager => _providerManagerFactory.Value;
+
+ private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
+
+ /// <summary>
+ /// Gets or sets the postscan tasks.
+ /// </summary>
+ /// <value>The postscan tasks.</value>
+ private ILibraryPostScanTask[] PostscanTasks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the intro providers.
+ /// </summary>
+ /// <value>The intro providers.</value>
+ private IIntroProvider[] IntroProviders { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of entity resolution ignore rules.
+ /// </summary>
+ /// <value>The entity resolution ignore rules.</value>
+ private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of currently registered entity resolvers.
+ /// </summary>
+ /// <value>The entity resolvers enumerable.</value>
+ private IItemResolver[] EntityResolvers { get; set; }
+
+ private IMultiItemResolver[] MultiItemResolvers { get; set; }
+
+ /// <summary>
+ /// Gets or sets the comparers.
+ /// </summary>
+ /// <value>The comparers.</value>
+ private IBaseItemComparer[] Comparers { get; set; }
+
+ public bool IsScanRunning { get; private set; }
+
+ /// <summary>
+ /// Adds the parts.
+ /// </summary>
+ /// <param name="rules">The rules.</param>
+ /// <param name="resolvers">The resolvers.</param>
+ /// <param name="introProviders">The intro providers.</param>
+ /// <param name="itemComparers">The item comparers.</param>
+ /// <param name="postscanTasks">The post scan tasks.</param>
+ public void AddParts(
+ IEnumerable<IResolverIgnoreRule> rules,
+ IEnumerable<IItemResolver> resolvers,
+ IEnumerable<IIntroProvider> introProviders,
+ IEnumerable<IBaseItemComparer> itemComparers,
+ IEnumerable<ILibraryPostScanTask> postscanTasks)
+ {
+ EntityResolutionIgnoreRules = rules.ToArray();
+ EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
+ MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
+ IntroProviders = introProviders.ToArray();
+ Comparers = itemComparers.ToArray();
+ PostscanTasks = postscanTasks.ToArray();
+ }
/// <summary>
/// Records the configuration values.
@@ -277,7 +299,7 @@ namespace Emby.Server.Implementations.Library
}
}
- _libraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; });
+ _memoryCache.Set(item.Id, item);
}
public void DeleteItem(BaseItem item, DeleteOptions options)
@@ -325,7 +347,7 @@ namespace Emby.Server.Implementations.Library
if (item is LiveTvProgram)
{
_logger.LogDebug(
- "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name,
item.Name ?? "Unknown name",
item.Path ?? string.Empty,
@@ -334,7 +356,7 @@ namespace Emby.Server.Implementations.Library
else
{
_logger.LogInformation(
- "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
item.GetType().Name,
item.Name ?? "Unknown name",
item.Path ?? string.Empty,
@@ -352,7 +374,12 @@ namespace Emby.Server.Implementations.Library
continue;
}
- _logger.LogDebug("Deleting path {MetadataPath}", metadataPath);
+ _logger.LogDebug(
+ "Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ item.GetType().Name,
+ item.Name ?? "Unknown name",
+ metadataPath,
+ item.Id);
try
{
@@ -376,7 +403,13 @@ namespace Emby.Server.Implementations.Library
{
try
{
- _logger.LogDebug("Deleting path {path}", fileSystemInfo.FullName);
+ _logger.LogInformation(
+ "Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}",
+ item.GetType().Name,
+ item.Name ?? "Unknown name",
+ fileSystemInfo.FullName,
+ item.Id);
+
if (fileSystemInfo.IsDirectory)
{
Directory.Delete(fileSystemInfo.FullName, true);
@@ -414,7 +447,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository.DeleteItem(child.Id);
}
- _libraryItemsCache.TryRemove(item.Id, out BaseItem removed);
+ _memoryCache.Remove(item.Id);
ReportItemRemoved(item, parent);
}
@@ -480,12 +513,13 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(type));
}
- if (key.StartsWith(_configurationManager.ApplicationPaths.ProgramDataPath, StringComparison.Ordinal))
+ string programDataPath = _configurationManager.ApplicationPaths.ProgramDataPath;
+ if (key.StartsWith(programDataPath, StringComparison.Ordinal))
{
// Try to normalize paths located underneath program-data in an attempt to make them more portable
- key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
- .TrimStart(new[] { '/', '\\' })
- .Replace("/", "\\");
+ key = key.Substring(programDataPath.Length)
+ .TrimStart('/', '\\')
+ .Replace('/', '\\');
}
if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
@@ -610,7 +644,7 @@ namespace Emby.Server.Implementations.Library
}
/// <summary>
- /// Determines whether a path should be ignored based on its contents - called after the contents have been read
+ /// Determines whether a path should be ignored based on its contents - called after the contents have been read.
/// </summary>
/// <param name="args">The args.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
@@ -695,7 +729,9 @@ namespace Emby.Server.Implementations.Library
Directory.CreateDirectory(rootFolderPath);
- var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath))).DeepCopy<Folder, AggregateFolder>();
+ var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
+ ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)))
+ .DeepCopy<Folder, AggregateFolder>();
// In case program data folder was moved
if (!string.Equals(rootFolder.Path, rootFolderPath, StringComparison.Ordinal))
@@ -736,7 +772,7 @@ namespace Emby.Server.Implementations.Library
if (folder.ParentId != rootFolder.Id)
{
folder.ParentId = rootFolder.Id;
- folder.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None);
+ folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult();
}
rootFolder.AddVirtualChild(folder);
@@ -746,14 +782,11 @@ namespace Emby.Server.Implementations.Library
return rootFolder;
}
- private volatile UserRootFolder _userRootFolder;
- private readonly object _syncLock = new object();
-
public Folder GetUserRootFolder()
{
if (_userRootFolder == null)
{
- lock (_syncLock)
+ lock (_userRootFolderSyncLock)
{
if (_userRootFolder == null)
{
@@ -839,17 +872,17 @@ namespace Emby.Server.Implementations.Library
public Guid GetStudioId(string name)
{
- return GetItemByNameId<Studio>(Studio.GetPath, name);
+ return GetItemByNameId<Studio>(Studio.GetPath(name));
}
public Guid GetGenreId(string name)
{
- return GetItemByNameId<Genre>(Genre.GetPath, name);
+ return GetItemByNameId<Genre>(Genre.GetPath(name));
}
public Guid GetMusicGenreId(string name)
{
- return GetItemByNameId<MusicGenre>(MusicGenre.GetPath, name);
+ return GetItemByNameId<MusicGenre>(MusicGenre.GetPath(name));
}
/// <summary>
@@ -890,7 +923,7 @@ namespace Emby.Server.Implementations.Library
}
/// <summary>
- /// Gets a Genre
+ /// Gets a Genre.
/// </summary>
/// <param name="name">The name.</param>
/// <returns>Task{Genre}.</returns>
@@ -911,7 +944,7 @@ namespace Emby.Server.Implementations.Library
{
var existing = GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { typeof(T).Name },
+ IncludeItemTypes = new[] { nameof(MusicArtist) },
Name = name,
DtoOptions = options
}).Cast<MusicArtist>()
@@ -925,13 +958,11 @@ namespace Emby.Server.Implementations.Library
}
}
- var id = GetItemByNameId<T>(getPathFn, name);
-
+ var path = getPathFn(name);
+ var id = GetItemByNameId<T>(path);
var item = GetItemById(id) as T;
-
if (item == null)
{
- var path = getPathFn(name);
item = new T
{
Name = name,
@@ -947,10 +978,9 @@ namespace Emby.Server.Implementations.Library
return item;
}
- private Guid GetItemByNameId<T>(Func<string, string> getPathFn, string name)
+ private Guid GetItemByNameId<T>(string path)
where T : BaseItem, new()
{
- var path = getPathFn(name);
var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds;
return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
}
@@ -971,7 +1001,7 @@ namespace Emby.Server.Implementations.Library
}
/// <summary>
- /// Reloads the root media folder
+ /// Reloads the root media folder.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
@@ -1216,7 +1246,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("Guid can't be empty", nameof(id));
}
- if (_libraryItemsCache.TryGetValue(id, out BaseItem item))
+ if (_memoryCache.TryGetValue(id, out BaseItem item))
{
return item;
}
@@ -1303,7 +1333,7 @@ namespace Emby.Server.Implementations.Library
return new QueryResult<BaseItem>
{
- Items = _itemRepository.GetItemList(query).ToArray()
+ Items = _itemRepository.GetItemList(query)
};
}
@@ -1434,11 +1464,9 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetItems(query);
}
- var list = _itemRepository.GetItemList(query);
-
return new QueryResult<BaseItem>
{
- Items = list
+ Items = _itemRepository.GetItemList(query)
};
}
@@ -1524,7 +1552,8 @@ namespace Emby.Server.Implementations.Library
}
// Handle grouping
- if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) && user.Configuration.GroupedFolders.Length > 0)
+ if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType)
+ && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
{
return GetUserRootFolder()
.GetChildren(user, true)
@@ -1560,7 +1589,6 @@ namespace Emby.Server.Implementations.Library
public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
{
var tasks = IntroProviders
- .OrderBy(i => i.GetType().Name.Contains("Default", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
.Take(1)
.Select(i => GetIntros(i, item, user));
@@ -1773,23 +1801,20 @@ namespace Emby.Server.Implementations.Library
/// Creates the items.
/// </summary>
/// <param name="items">The items.</param>
- /// <param name="parent">The parent item</param>
+ /// <param name="parent">The parent item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- public void CreateItems(IEnumerable<BaseItem> items, BaseItem parent, CancellationToken cancellationToken)
+ public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem parent, CancellationToken cancellationToken)
{
- // Don't iterate multiple times
- var itemsList = items.ToList();
-
- _itemRepository.SaveItems(itemsList, cancellationToken);
+ _itemRepository.SaveItems(items, cancellationToken);
- foreach (var item in itemsList)
+ foreach (var item in items)
{
RegisterItem(item);
}
if (ItemAdded != null)
{
- foreach (var item in itemsList)
+ foreach (var item in items)
{
// With the live tv guide this just creates too much noise
if (item.SourceType != SourceType.Library)
@@ -1815,22 +1840,109 @@ namespace Emby.Server.Implementations.Library
}
}
- public void UpdateImages(BaseItem item)
+ private bool ImageNeedsRefresh(ItemImageInfo image)
{
- _itemRepository.SaveImages(item);
+ if (image.Path != null && image.IsLocalFile)
+ {
+ if (image.Width == 0 || image.Height == 0 || string.IsNullOrEmpty(image.BlurHash))
+ {
+ return true;
+ }
- RegisterItem(item);
+ try
+ {
+ return _fileSystem.GetLastWriteTimeUtc(image.Path) != image.DateModified;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Cannot get file info for {0}", image.Path);
+ return false;
+ }
+ }
+
+ return image.Path != null && !image.IsLocalFile;
}
- /// <summary>
- /// Updates the item.
- /// </summary>
- public void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public async Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false)
{
- // Don't iterate multiple times
- var itemsList = items.ToList();
+ if (item == null)
+ {
+ throw new ArgumentNullException(nameof(item));
+ }
- foreach (var item in itemsList)
+ var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
+ // Skip image processing if current or live tv source
+ if (outdated.Length == 0 || item.SourceType != SourceType.Library)
+ {
+ RegisterItem(item);
+ return;
+ }
+
+ foreach (var img in outdated)
+ {
+ var image = img;
+ if (!img.IsLocalFile)
+ {
+ try
+ {
+ var index = item.GetImageIndex(img);
+ image = await ConvertImageToLocal(item, img, index).ConfigureAwait(false);
+ }
+ catch (ArgumentException)
+ {
+ _logger.LogWarning("Cannot get image index for {0}", img.Path);
+ continue;
+ }
+ catch (InvalidOperationException)
+ {
+ _logger.LogWarning("Cannot fetch image from {0}", img.Path);
+ continue;
+ }
+ }
+
+ try
+ {
+ ImageDimensions size = _imageProcessor.GetImageDimensions(item, image);
+ image.Width = size.Width;
+ image.Height = size.Height;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Cannot get image dimensions for {0}", image.Path);
+ image.Width = 0;
+ image.Height = 0;
+ continue;
+ }
+
+ try
+ {
+ image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Cannot compute blurhash for {0}", image.Path);
+ image.BlurHash = string.Empty;
+ }
+
+ try
+ {
+ image.DateModified = _fileSystem.GetLastWriteTimeUtc(image.Path);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Cannot update DateModified for {0}", image.Path);
+ }
+ }
+
+ _itemRepository.SaveImages(item);
+ RegisterItem(item);
+ }
+
+ /// <inheritdoc />
+ public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
+ {
+ foreach (var item in items)
{
if (item.IsFileProtocol)
{
@@ -1839,14 +1951,14 @@ namespace Emby.Server.Implementations.Library
item.DateLastSaved = DateTime.UtcNow;
- RegisterItem(item);
+ await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
}
- _itemRepository.SaveItems(itemsList, cancellationToken);
+ _itemRepository.SaveItems(items, cancellationToken);
if (ItemUpdated != null)
{
- foreach (var item in itemsList)
+ foreach (var item in items)
{
// With the live tv guide this just creates too much noise
if (item.SourceType != SourceType.Library)
@@ -1873,17 +1985,9 @@ namespace Emby.Server.Implementations.Library
}
}
- /// <summary>
- /// Updates the item.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="parent">The parent item.</param>
- /// <param name="updateReason">The update reason.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- public void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
- {
- UpdateItems(new[] { item }, parent, updateReason, cancellationToken);
- }
+ /// <inheritdoc />
+ public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
+ => UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken);
/// <summary>
/// Reports the item removed.
@@ -2069,8 +2173,6 @@ namespace Emby.Server.Implementations.Library
.FirstOrDefault(i => !string.IsNullOrEmpty(i));
}
- private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
-
public UserView GetNamedView(
User user,
string name,
@@ -2117,7 +2219,7 @@ namespace Emby.Server.Implementations.Library
if (refresh)
{
- item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None);
+ item.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult();
ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal);
}
@@ -2304,7 +2406,7 @@ namespace Emby.Server.Implementations.Library
if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase))
{
item.ViewType = viewType;
- item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+ item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
}
var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
@@ -2368,14 +2470,9 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
- var episodeInfo = episode.IsFileProtocol ?
- resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) :
- new Naming.TV.EpisodeInfo();
-
- if (episodeInfo == null)
- {
- episodeInfo = new Naming.TV.EpisodeInfo();
- }
+ var episodeInfo = episode.IsFileProtocol
+ ? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo()
+ : new Naming.TV.EpisodeInfo();
try
{
@@ -2383,11 +2480,13 @@ namespace Emby.Server.Implementations.Library
if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(episodeInfo.Container, "mp4", StringComparison.OrdinalIgnoreCase))
{
// Read from metadata
- var mediaInfo = _mediaEncoder.GetMediaInfo(new MediaInfoRequest
- {
- MediaSource = episode.GetMediaSources(false)[0],
- MediaType = DlnaProfileType.Video
- }, CancellationToken.None).GetAwaiter().GetResult();
+ var mediaInfo = _mediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaSource = episode.GetMediaSources(false)[0],
+ MediaType = DlnaProfileType.Video
+ },
+ CancellationToken.None).GetAwaiter().GetResult();
if (mediaInfo.ParentIndexNumber > 0)
{
episodeInfo.SeasonNumber = mediaInfo.ParentIndexNumber;
@@ -2495,7 +2594,7 @@ namespace Emby.Server.Implementations.Library
Anime series don't generally have a season in their file name, however,
tvdb needs a season to correctly get the metadata.
Hence, a null season needs to be filled with something. */
- //FIXME perhaps this would be better for tvdb parser to ask for season 1 if no season is specified
+ // FIXME perhaps this would be better for tvdb parser to ask for season 1 if no season is specified
episode.ParentIndexNumber = 1;
}
@@ -2545,7 +2644,7 @@ namespace Emby.Server.Implementations.Library
var videos = videoListResolver.Resolve(fileSystemChildren);
- var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase));
+ var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
if (currentVideo != null)
{
@@ -2562,9 +2661,7 @@ namespace Emby.Server.Implementations.Library
.Select(video =>
{
// Try to retrieve it from the db. If we don't find it, use the resolved version
- var dbItem = GetItemById(video.Id) as Trailer;
-
- if (dbItem != null)
+ if (GetItemById(video.Id) is Trailer dbItem)
{
video = dbItem;
}
@@ -2684,10 +2781,12 @@ namespace Emby.Server.Implementations.Library
{
throw new ArgumentNullException(nameof(path));
}
+
if (string.IsNullOrWhiteSpace(from))
{
throw new ArgumentNullException(nameof(from));
}
+
if (string.IsNullOrWhiteSpace(to))
{
throw new ArgumentNullException(nameof(to));
@@ -2761,7 +2860,6 @@ namespace Emby.Server.Implementations.Library
_logger.LogError(ex, "Error getting person");
return null;
}
-
}).Where(i => i != null).ToList();
}
@@ -2790,13 +2888,14 @@ namespace Emby.Server.Implementations.Library
await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).ConfigureAwait(false);
- item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return item.GetImageInfo(image.Type, imageIndex);
}
catch (HttpException ex)
{
- if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
+ if (ex.StatusCode.HasValue
+ && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forbidden))
{
continue;
}
@@ -2807,7 +2906,7 @@ namespace Emby.Server.Implementations.Library
// Remove this image to prevent it from retrying over and over
item.RemoveImage(image);
- item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
+ await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
throw new InvalidOperationException();
}
@@ -2889,23 +2988,6 @@ namespace Emby.Server.Implementations.Library
});
}
- private static bool ValidateNetworkPath(string path)
- {
- //if (Environment.OSVersion.Platform == PlatformID.Win32NT)
- //{
- // // We can't validate protocol-based paths, so just allow them
- // if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) == -1)
- // {
- // return Directory.Exists(path);
- // }
- //}
-
- // Without native support for unc, we cannot validate this when running under mono
- return true;
- }
-
- private const string ShortcutFileExtension = ".mblink";
-
public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
{
AddMediaPathInternal(virtualFolderName, pathInfo, true);
@@ -2930,11 +3012,6 @@ namespace Emby.Server.Implementations.Library
throw new FileNotFoundException("The path does not exist.");
}
- if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
- {
- throw new FileNotFoundException("The network path does not exist.");
- }
-
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
@@ -2973,11 +3050,6 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(pathInfo));
}
- if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
- {
- throw new FileNotFoundException("The network path does not exist.");
- }
-
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
@@ -3109,7 +3181,8 @@ namespace Emby.Server.Implementations.Library
if (!Directory.Exists(virtualFolderPath))
{
- throw new FileNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName));
+ throw new FileNotFoundException(
+ string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
}
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index ed7d8aa40..041619d1e 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -23,9 +23,8 @@ namespace Emby.Server.Implementations.Library
{
private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger _logger;
-
- private IJsonSerializer _json;
- private IApplicationPaths _appPaths;
+ private readonly IJsonSerializer _json;
+ private readonly IApplicationPaths _appPaths;
public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths)
{
@@ -50,7 +49,7 @@ namespace Emby.Server.Implementations.Library
{
mediaInfo = _json.DeserializeFromFile<MediaInfo>(cacheFilePath);
- //_logger.LogDebug("Found cached media info");
+ // _logger.LogDebug("Found cached media info");
}
catch
{
@@ -72,20 +71,21 @@ namespace Emby.Server.Implementations.Library
mediaSource.AnalyzeDurationMs = 3000;
- mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
- {
- MediaSource = mediaSource,
- MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
- ExtractChapters = false
-
- }, cancellationToken).ConfigureAwait(false);
+ mediaInfo = await _mediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaSource = mediaSource,
+ MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
+ ExtractChapters = false
+ },
+ cancellationToken).ConfigureAwait(false);
if (cacheFilePath != null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
_json.SerializeToFile(mediaInfo, cacheFilePath);
- //_logger.LogDebug("Saved media info to {0}", cacheFilePath);
+ // _logger.LogDebug("Saved media info to {0}", cacheFilePath);
}
}
@@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library
mediaSource.RunTimeTicks = null;
}
- var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio);
+ var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
if (audioStream == null || audioStream.Index == -1)
{
@@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.Library
mediaSource.DefaultAudioStreamIndex = audioStream.Index;
}
- var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video);
+ var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
if (videoStream != null)
{
if (!videoStream.BitRate.HasValue)
@@ -148,17 +148,14 @@ namespace Emby.Server.Implementations.Library
{
videoStream.BitRate = 30000000;
}
-
else if (width >= 1900)
{
videoStream.BitRate = 20000000;
}
-
else if (width >= 1200)
{
videoStream.BitRate = 8000000;
}
-
else if (width >= 700)
{
videoStream.BitRate = 2000000;
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 01fe98f3a..376a15570 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -1,12 +1,15 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
@@ -14,7 +17,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -28,17 +30,23 @@ namespace Emby.Server.Implementations.Library
{
public class MediaSourceManager : IMediaSourceManager, IDisposable
{
+ // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
+ private const char LiveStreamIdDelimeter = '_';
+
private readonly IItemRepository _itemRepo;
private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager;
private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
- private readonly ILogger _logger;
+ private readonly ILogger<MediaSourceManager> _logger;
private readonly IUserDataManager _userDataManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths;
+ private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
+ private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
+
private IMediaSourceProvider[] _providers;
public MediaSourceManager(
@@ -190,10 +198,7 @@ namespace Emby.Server.Implementations.Library
{
if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
{
- if (!user.Policy.EnableAudioPlaybackTranscoding)
- {
- source.SupportsTranscoding = false;
- }
+ source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
}
}
}
@@ -207,22 +212,27 @@ namespace Emby.Server.Implementations.Library
{
return MediaProtocol.Rtsp;
}
+
if (path.StartsWith("Rtmp", StringComparison.OrdinalIgnoreCase))
{
return MediaProtocol.Rtmp;
}
+
if (path.StartsWith("Http", StringComparison.OrdinalIgnoreCase))
{
return MediaProtocol.Http;
}
+
if (path.StartsWith("rtp", StringComparison.OrdinalIgnoreCase))
{
return MediaProtocol.Rtp;
}
+
if (path.StartsWith("ftp", StringComparison.OrdinalIgnoreCase))
{
return MediaProtocol.Ftp;
}
+
if (path.StartsWith("udp", StringComparison.OrdinalIgnoreCase))
{
return MediaProtocol.Udp;
@@ -352,7 +362,9 @@ namespace Emby.Server.Implementations.Library
private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
{
- if (userData.SubtitleStreamIndex.HasValue && user.Configuration.RememberSubtitleSelections && user.Configuration.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection)
+ if (userData.SubtitleStreamIndex.HasValue
+ && user.RememberSubtitleSelections
+ && user.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection)
{
var index = userData.SubtitleStreamIndex.Value;
// Make sure the saved index is still valid
@@ -363,26 +375,26 @@ namespace Emby.Server.Implementations.Library
}
}
- var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference)
- ? Array.Empty<string>() : NormalizeLanguage(user.Configuration.SubtitleLanguagePreference);
+ var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
+ ? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
var defaultAudioIndex = source.DefaultAudioStreamIndex;
var audioLangage = defaultAudioIndex == null
? null
: source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault();
- source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(source.MediaStreams,
+ source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(
+ source.MediaStreams,
preferredSubs,
- user.Configuration.SubtitleMode,
+ user.SubtitleMode,
audioLangage);
- MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs,
- user.Configuration.SubtitleMode, audioLangage);
+ MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLangage);
}
private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
{
- if (userData.AudioStreamIndex.HasValue && user.Configuration.RememberAudioSelections && allowRememberingSelection)
+ if (userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection)
{
var index = userData.AudioStreamIndex.Value;
// Make sure the saved index is still valid
@@ -393,11 +405,11 @@ namespace Emby.Server.Implementations.Library
}
}
- var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference)
+ var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference)
? Array.Empty<string>()
- : NormalizeLanguage(user.Configuration.AudioLanguagePreference);
+ : NormalizeLanguage(user.AudioLanguagePreference);
- source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack);
+ source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
}
public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user)
@@ -435,7 +447,6 @@ namespace Emby.Server.Implementations.Library
}
return 1;
-
}).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
.ThenByDescending(i =>
{
@@ -446,9 +457,6 @@ namespace Emby.Server.Implementations.Library
.ToList();
}
- private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
- private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
-
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
{
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -521,11 +529,7 @@ namespace Emby.Server.Implementations.Library
SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user);
}
- return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse
- {
- MediaSource = clone
-
- }, liveStream as IDirectStreamProvider);
+ return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider);
}
private static void AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio)
@@ -538,7 +542,7 @@ namespace Emby.Server.Implementations.Library
mediaSource.RunTimeTicks = null;
}
- var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio);
+ var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
if (audioStream == null || audioStream.Index == -1)
{
@@ -549,7 +553,7 @@ namespace Emby.Server.Implementations.Library
mediaSource.DefaultAudioStreamIndex = audioStream.Index;
}
- var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video);
+ var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
if (videoStream != null)
{
if (!videoStream.BitRate.HasValue)
@@ -560,17 +564,14 @@ namespace Emby.Server.Implementations.Library
{
videoStream.BitRate = 30000000;
}
-
else if (width >= 1900)
{
videoStream.BitRate = 20000000;
}
-
else if (width >= 1200)
{
videoStream.BitRate = 8000000;
}
-
else if (width >= 700)
{
videoStream.BitRate = 2000000;
@@ -582,29 +583,20 @@ namespace Emby.Server.Implementations.Library
mediaSource.InferTotalBitrate();
}
- public async Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
+ public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
{
- await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ var info = _openStreams.Values.FirstOrDefault(i =>
{
- var info = _openStreams.Values.FirstOrDefault(i =>
+ var liveStream = i as ILiveStream;
+ if (liveStream != null)
{
- var liveStream = i as ILiveStream;
- if (liveStream != null)
- {
- return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
- }
+ return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
+ }
- return false;
- });
+ return false;
+ });
- return info as IDirectStreamProvider;
- }
- finally
- {
- _liveStreamSemaphore.Release();
- }
+ return Task.FromResult(info as IDirectStreamProvider);
}
public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
@@ -621,13 +613,14 @@ namespace Emby.Server.Implementations.Library
if (liveStreamInfo is IDirectStreamProvider)
{
- var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
- {
- MediaSource = mediaSource,
- ExtractChapters = false,
- MediaType = DlnaProfileType.Video
-
- }, cancellationToken).ConfigureAwait(false);
+ var info = await _mediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaSource = mediaSource,
+ ExtractChapters = false,
+ MediaType = DlnaProfileType.Video
+ },
+ cancellationToken).ConfigureAwait(false);
mediaSource.MediaStreams = info.MediaStreams;
mediaSource.Container = info.Container;
@@ -652,7 +645,7 @@ namespace Emby.Server.Implementations.Library
{
mediaInfo = _jsonSerializer.DeserializeFromFile<MediaInfo>(cacheFilePath);
- //_logger.LogDebug("Found cached media info");
+ // _logger.LogDebug("Found cached media info");
}
catch (Exception ex)
{
@@ -674,20 +667,21 @@ namespace Emby.Server.Implementations.Library
mediaSource.AnalyzeDurationMs = 3000;
}
- mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
+ mediaInfo = await _mediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
{
MediaSource = mediaSource,
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
ExtractChapters = false
-
- }, cancellationToken).ConfigureAwait(false);
+ },
+ cancellationToken).ConfigureAwait(false);
if (cacheFilePath != null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
_jsonSerializer.SerializeToFile(mediaInfo, cacheFilePath);
- //_logger.LogDebug("Saved media info to {0}", cacheFilePath);
+ // _logger.LogDebug("Saved media info to {0}", cacheFilePath);
}
}
@@ -753,17 +747,14 @@ namespace Emby.Server.Implementations.Library
{
videoStream.BitRate = 30000000;
}
-
else if (width >= 1900)
{
videoStream.BitRate = 20000000;
}
-
else if (width >= 1200)
{
videoStream.BitRate = 8000000;
}
-
else if (width >= 700)
{
videoStream.BitRate = 2000000;
@@ -794,29 +785,20 @@ namespace Emby.Server.Implementations.Library
return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider);
}
- private async Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken)
+ private Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(id))
{
throw new ArgumentNullException(nameof(id));
}
- await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ if (_openStreams.TryGetValue(id, out ILiveStream info))
{
- if (_openStreams.TryGetValue(id, out ILiveStream info))
- {
- return info;
- }
- else
- {
- throw new ResourceNotFoundException();
- }
+ return Task.FromResult(info);
}
- finally
+ else
{
- _liveStreamSemaphore.Release();
+ return Task.FromException<ILiveStream>(new ResourceNotFoundException());
}
}
@@ -845,7 +827,7 @@ namespace Emby.Server.Implementations.Library
if (liveStream.ConsumerCount <= 0)
{
- _openStreams.Remove(id);
+ _openStreams.TryRemove(id, out _);
_logger.LogInformation("Closing live stream {0}", id);
@@ -860,24 +842,21 @@ namespace Emby.Server.Implementations.Library
}
}
- // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
- private const char LiveStreamIdDelimeter = '_';
-
- private Tuple<IMediaSourceProvider, string> GetProvider(string key)
+ private (IMediaSourceProvider, string) GetProvider(string key)
{
if (string.IsNullOrEmpty(key))
{
- throw new ArgumentException("key");
+ throw new ArgumentException("Key can't be empty.", nameof(key));
}
var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2);
var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
- var splitIndex = key.IndexOf(LiveStreamIdDelimeter);
+ var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
var keyId = key.Substring(splitIndex + 1);
- return new Tuple<IMediaSourceProvider, string>(provider, keyId);
+ return (provider, keyId);
}
/// <summary>
@@ -886,9 +865,9 @@ namespace Emby.Server.Implementations.Library
public void Dispose()
{
Dispose(true);
+ GC.SuppressFinalize(this);
}
- private readonly object _disposeLock = new object();
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
@@ -897,15 +876,12 @@ namespace Emby.Server.Implementations.Library
{
if (dispose)
{
- lock (_disposeLock)
+ foreach (var key in _openStreams.Keys.ToList())
{
- foreach (var key in _openStreams.Keys.ToList())
- {
- var task = CloseLiveStream(key);
-
- Task.WaitAll(task);
- }
+ CloseLiveStream(key).GetAwaiter().GetResult();
}
+
+ _liveStreamSemaphore.Dispose();
}
}
}
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index e27145a1d..179e0ed98 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -3,7 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using MediaBrowser.Model.Configuration;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library
@@ -89,7 +89,7 @@ namespace Emby.Server.Implementations.Library
}
// load forced subs if we have found no suitable full subtitles
- stream = stream ?? streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
+ stream ??= streams.FirstOrDefault(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase));
if (stream != null)
{
diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs
index 1ec578371..877fdec86 100644
--- a/Emby.Server.Implementations/Library/MusicManager.cs
+++ b/Emby.Server.Implementations/Library/MusicManager.cs
@@ -3,13 +3,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
namespace Emby.Server.Implementations.Library
{
@@ -75,7 +77,6 @@ namespace Emby.Server.Implementations.Library
{
return Guid.Empty;
}
-
}).Where(i => !i.Equals(Guid.Empty)).ToArray();
return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
@@ -105,32 +106,27 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions);
}
- var playlist = item as Playlist;
- if (playlist != null)
+ if (item is Playlist playlist)
{
return GetInstantMixFromPlaylist(playlist, user, dtoOptions);
}
- var album = item as MusicAlbum;
- if (album != null)
+ if (item is MusicAlbum album)
{
return GetInstantMixFromAlbum(album, user, dtoOptions);
}
- var artist = item as MusicArtist;
- if (artist != null)
+ if (item is MusicArtist artist)
{
return GetInstantMixFromArtist(artist, user, dtoOptions);
}
- var song = item as Audio;
- if (song != null)
+ if (item is Audio song)
{
return GetInstantMixFromSong(song, user, dtoOptions);
}
- var folder = item as Folder;
- if (folder != null)
+ if (item is Folder folder)
{
return GetInstantMixFromFolder(folder, user, dtoOptions);
}
diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs
index 7ca15b4e5..4e4cac75b 100644
--- a/Emby.Server.Implementations/Library/ResolverHelper.cs
+++ b/Emby.Server.Implementations/Library/ResolverHelper.cs
@@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Library
}
/// <summary>
- /// Ensures DateCreated and DateModified have values
+ /// Ensures DateCreated and DateModified have values.
/// </summary>
/// <param name="fileSystem">The file system.</param>
/// <param name="item">The item.</param>
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index fefc8e789..70be52411 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -32,7 +32,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <value>The priority.</value>
public override ResolverPriority Priority => ResolverPriority.Fourth;
- public MultiItemResolverResult ResolveMultiple(Folder parent,
+ public MultiItemResolverResult ResolveMultiple(
+ Folder parent,
List<FileSystemMetadata> files,
string collectionType,
IDirectoryService directoryService)
@@ -50,7 +51,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return result;
}
- private MultiItemResolverResult ResolveMultipleInternal(Folder parent,
+ private MultiItemResolverResult ResolveMultipleInternal(
+ Folder parent,
List<FileSystemMetadata> files,
string collectionType,
IDirectoryService directoryService)
@@ -209,8 +211,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
Name = parseName ?
resolvedItem.Name :
Path.GetFileNameWithoutExtension(firstMedia.Path),
- //AdditionalParts = resolvedItem.Files.Skip(1).Select(i => i.Path).ToArray(),
- //LocalAlternateVersions = resolvedItem.AlternateVersions.Select(i => i.Path).ToArray()
+ // AdditionalParts = resolvedItem.Files.Skip(1).Select(i => i.Path).ToArray(),
+ // LocalAlternateVersions = resolvedItem.AlternateVersions.Select(i => i.Path).ToArray()
};
result.Items.Add(libraryItem);
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index 6c9ba7c27..18ceb5e76 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using Emby.Naming.Audio;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@@ -92,7 +95,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
// Args points to an album if parent is an Artist folder or it directly contains music
if (args.IsDirectory)
{
- // if (args.Parent is MusicArtist) return true; //saves us from testing children twice
+ // if (args.Parent is MusicArtist) return true; // saves us from testing children twice
if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService, _logger, _fileSystem, _libraryManager))
{
return true;
@@ -109,56 +112,52 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
IEnumerable<FileSystemMetadata> list,
bool allowSubfolders,
IDirectoryService directoryService,
- ILogger logger,
+ ILogger<MusicAlbumResolver> logger,
IFileSystem fileSystem,
ILibraryManager libraryManager)
{
+ // check for audio files before digging down into directories
+ var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && libraryManager.IsAudioFile(fileSystemInfo.FullName));
+ if (foundAudioFile)
+ {
+ // at least one audio file exists
+ return true;
+ }
+
+ if (!allowSubfolders)
+ {
+ // not music since no audio file exists and we're not looking into subfolders
+ return false;
+ }
+
var discSubfolderCount = 0;
- var notMultiDisc = false;
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
var parser = new AlbumParser(namingOptions);
- foreach (var fileSystemInfo in list)
+
+ var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory);
+
+ var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
{
- if (fileSystemInfo.IsDirectory)
+ var path = fileSystemInfo.FullName;
+ var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
+
+ if (hasMusic)
{
- if (allowSubfolders)
+ if (parser.IsMultiPart(path))
{
- if (notMultiDisc)
- {
- continue;
- }
-
- var path = fileSystemInfo.FullName;
- var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
-
- if (hasMusic)
- {
- if (parser.IsMultiPart(path))
- {
- logger.LogDebug("Found multi-disc folder: " + path);
- discSubfolderCount++;
- }
- else
- {
- // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
- notMultiDisc = true;
- }
- }
+ logger.LogDebug("Found multi-disc folder: " + path);
+ Interlocked.Increment(ref discSubfolderCount);
}
- }
- else
- {
- var fullName = fileSystemInfo.FullName;
-
- if (libraryManager.IsAudioFile(fullName))
+ else
{
- return true;
+ // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
+ state.Stop();
}
}
- }
+ });
- if (notMultiDisc)
+ if (!result.IsCompleted)
{
return false;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
index 5f5cd0e92..e9e688fa6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@@ -94,7 +95,18 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
// If we contain an album assume we are an artist folder
- return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService)) ? new MusicArtist() : null;
+ var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
+
+ var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
+ {
+ if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService))
+ {
+ // stop once we see a music album
+ state.Stop();
+ }
+ });
+
+ return !result.IsCompleted ? new MusicArtist() : null;
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index fb75593bd..2f5e46038 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -292,7 +292,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
return true;
- //var blurayExtensions = new[]
+ // var blurayExtensions = new[]
//{
// ".mts",
// ".m2ts",
@@ -300,7 +300,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
// ".mpls"
//};
- //return directoryService.GetFiles(fullPath).Any(i => blurayExtensions.Contains(i.Extension ?? string.Empty, StringComparer.OrdinalIgnoreCase));
+ // return directoryService.GetFiles(fullPath).Any(i => blurayExtensions.Contains(i.Extension ?? string.Empty, StringComparer.OrdinalIgnoreCase));
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 503de0b4e..59af7ce8a 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -19,7 +19,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
// Only process items that are in a collection folder containing books
if (!string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
+ {
return null;
+ }
if (args.IsDirectory)
{
@@ -48,14 +50,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
var fileExtension = Path.GetExtension(f.FullName) ??
string.Empty;
- return _validExtensions.Contains(fileExtension,
+ return _validExtensions.Contains(
+ fileExtension,
StringComparer
.OrdinalIgnoreCase);
}).ToList();
// Don't return a Book if there is more (or less) than one document in the directory
if (bookFiles.Count != 1)
+ {
return null;
+ }
return new Book
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
index 32ccc7fdd..9ca76095b 100644
--- a/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/ItemResolver.cs
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
public virtual ResolverPriority Priority => ResolverPriority.First;
/// <summary>
- /// Sets initial values on the newly resolved item
+ /// Sets initial values on the newly resolved item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="args">The args.</param>
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
index e4bc4a469..295e9e120 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
@@ -69,7 +69,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (!string.IsNullOrEmpty(id))
{
- item.SetProviderId(MetadataProviders.Tmdb, id);
+ item.SetProviderId(MetadataProvider.Tmdb, id);
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index cb67c8aa7..baf0e3cf9 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -350,7 +350,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (!string.IsNullOrWhiteSpace(tmdbid))
{
- item.SetProviderId(MetadataProviders.Tmdb, tmdbid);
+ item.SetProviderId(MetadataProvider.Tmdb, tmdbid);
}
}
@@ -361,7 +361,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (!string.IsNullOrWhiteSpace(imdbid))
{
- item.SetProviderId(MetadataProviders.Imdb, imdbid);
+ item.SetProviderId(MetadataProvider.Imdb, imdbid);
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
index 1030ed39d..99f304190 100644
--- a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
@@ -41,10 +41,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
return new AggregateFolder();
}
+
if (string.Equals(args.Path, _appPaths.DefaultUserViewsPath, StringComparison.OrdinalIgnoreCase))
{
- return new UserRootFolder(); //if we got here and still a root - must be user root
+ return new UserRootFolder(); // if we got here and still a root - must be user root
}
+
if (args.IsVf)
{
return new CollectionFolder
@@ -73,7 +75,6 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
return false;
}
-
})
.Select(i => _fileSystem.GetFileNameWithoutExtension(i))
.FirstOrDefault();
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
index 7f477a0f0..2f7af60c0 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
@@ -55,6 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
episode.SeriesId = series.Id;
episode.SeriesName = series.Name;
}
+
if (season != null)
{
episode.SeasonId = season.Id;
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index 18145b7f1..3332e1806 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -1,6 +1,5 @@
using System.Globalization;
using Emby.Naming.TV;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Globalization;
@@ -13,25 +12,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary>
public class SeasonResolver : FolderResolver<Season>
{
- private readonly IServerConfigurationManager _config;
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization;
- private readonly ILogger _logger;
+ private readonly ILogger<SeasonResolver> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="SeasonResolver"/> class.
/// </summary>
- /// <param name="config">The config.</param>
/// <param name="libraryManager">The library manager.</param>
- /// <param name="localization">The localization</param>
- /// <param name="logger">The logger</param>
+ /// <param name="localization">The localization.</param>
+ /// <param name="logger">The logger.</param>
public SeasonResolver(
- IServerConfigurationManager config,
ILibraryManager libraryManager,
ILocalizationManager localization,
ILogger<SeasonResolver> logger)
{
- _config = config;
_libraryManager = libraryManager;
_localization = localization;
_logger = logger;
@@ -94,7 +89,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
_localization.GetLocalizedString("NameSeasonNumber"),
seasonNumber,
args.GetLibraryOptions().PreferredMetadataLanguage);
-
}
return season;
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index dd6bd8ee8..732bfd94d 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -20,7 +20,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
public class SeriesResolver : FolderResolver<Series>
{
private readonly IFileSystem _fileSystem;
- private readonly ILogger _logger;
+ private readonly ILogger<SeriesResolver> _logger;
private readonly ILibraryManager _libraryManager;
/// <summary>
@@ -59,7 +59,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var collectionType = args.GetCollectionType();
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
{
- //if (args.ContainsFileSystemEntryByName("tvshow.nfo"))
+ // if (args.ContainsFileSystemEntryByName("tvshow.nfo"))
//{
// return new Series
// {
@@ -119,7 +119,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
IEnumerable<FileSystemMetadata> fileSystemChildren,
IDirectoryService directoryService,
IFileSystem fileSystem,
- ILogger logger,
+ ILogger<SeriesResolver> logger,
ILibraryManager libraryManager,
bool isTvContentType)
{
@@ -217,7 +217,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
if (!string.IsNullOrEmpty(id))
{
- item.SetProviderId(MetadataProviders.Tvdb, id);
+ item.SetProviderId(MetadataProvider.Tvdb, id);
}
}
}
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 59a77607d..9a69bce0e 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -3,27 +3,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Search;
using Microsoft.Extensions.Logging;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using Person = MediaBrowser.Controller.Entities.Person;
namespace Emby.Server.Implementations.Library
{
public class SearchEngine : ISearchEngine
{
- private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
- public SearchEngine(ILogger<SearchEngine> logger, ILibraryManager libraryManager, IUserManager userManager)
+ public SearchEngine(ILibraryManager libraryManager, IUserManager userManager)
{
- _logger = logger;
_libraryManager = libraryManager;
_userManager = userManager;
}
@@ -31,11 +32,7 @@ namespace Emby.Server.Implementations.Library
public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
{
User user = null;
-
- if (query.UserId.Equals(Guid.Empty))
- {
- }
- else
+ if (query.UserId != Guid.Empty)
{
user = _userManager.GetUserById(query.UserId);
}
@@ -45,19 +42,19 @@ namespace Emby.Server.Implementations.Library
if (query.StartIndex.HasValue)
{
- results = results.Skip(query.StartIndex.Value).ToList();
+ results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value);
}
if (query.Limit.HasValue)
{
- results = results.Take(query.Limit.Value).ToList();
+ results = results.GetRange(0, query.Limit.Value);
}
return new QueryResult<SearchHintInfo>
{
TotalRecordCount = totalRecordCount,
- Items = results.ToArray()
+ Items = results
};
}
@@ -82,7 +79,7 @@ namespace Emby.Server.Implementations.Library
if (string.IsNullOrEmpty(searchTerm))
{
- throw new ArgumentNullException("SearchTerm can't be empty.", nameof(searchTerm));
+ throw new ArgumentException("SearchTerm can't be empty.", nameof(query));
}
searchTerm = searchTerm.Trim().RemoveDiacritics();
@@ -191,6 +188,7 @@ namespace Emby.Server.Implementations.Library
{
searchQuery.AncestorIds = new[] { searchQuery.ParentId };
}
+
searchQuery.ParentId = Guid.Empty;
searchQuery.IncludeItemsByName = true;
searchQuery.IncludeItemTypes = Array.Empty<string>();
@@ -204,7 +202,6 @@ namespace Emby.Server.Implementations.Library
return mediaItems.Select(i => new SearchHintInfo
{
Item = i
-
}).ToList();
}
}
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index a9772a078..f9e5e6bbc 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -5,6 +5,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -12,7 +13,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using Microsoft.Extensions.Logging;
+using Book = MediaBrowser.Controller.Entities.Book;
namespace Emby.Server.Implementations.Library
{
@@ -26,18 +27,15 @@ namespace Emby.Server.Implementations.Library
private readonly ConcurrentDictionary<string, UserItemData> _userData =
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
- private readonly ILogger _logger;
private readonly IServerConfigurationManager _config;
private readonly IUserManager _userManager;
private readonly IUserDataRepository _repository;
public UserDataManager(
- ILogger<UserDataManager> logger,
IServerConfigurationManager config,
IUserManager userManager,
IUserDataRepository repository)
{
- _logger = logger;
_config = config;
_userManager = userManager;
_repository = repository;
@@ -101,7 +99,7 @@ namespace Emby.Server.Implementations.Library
}
/// <summary>
- /// Retrieve all user data for the given user
+ /// Retrieve all user data for the given user.
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
@@ -186,7 +184,7 @@ namespace Emby.Server.Implementations.Library
}
/// <summary>
- /// Converts a UserItemData to a DTOUserItemData
+ /// Converts a UserItemData to a DTOUserItemData.
/// </summary>
/// <param name="data">The data.</param>
/// <returns>DtoUserItemData.</returns>
@@ -240,7 +238,7 @@ namespace Emby.Server.Implementations.Library
{
// Enforce MinResumeDuration
var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds;
- if (durationSeconds < _config.Configuration.MinResumeDurationSeconds)
+ if (durationSeconds < _config.Configuration.MinResumeDurationSeconds && !(item is Book))
{
positionTicks = 0;
data.Played = playedToCompletion = true;
diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs
deleted file mode 100644
index d63bc6bda..000000000
--- a/Emby.Server.Implementations/Library/UserManager.cs
+++ /dev/null
@@ -1,1107 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Cryptography;
-using MediaBrowser.Common.Events;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Cryptography;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Users;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Library
-{
- /// <summary>
- /// Class UserManager.
- /// </summary>
- public class UserManager : IUserManager
- {
- private readonly object _policySyncLock = new object();
- private readonly object _configSyncLock = new object();
-
- private readonly ILogger _logger;
- private readonly IUserRepository _userRepository;
- private readonly IXmlSerializer _xmlSerializer;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly INetworkManager _networkManager;
- private readonly IImageProcessor _imageProcessor;
- private readonly Lazy<IDtoService> _dtoServiceFactory;
- private readonly IServerApplicationHost _appHost;
- private readonly IFileSystem _fileSystem;
- private readonly ICryptoProvider _cryptoProvider;
-
- private ConcurrentDictionary<Guid, User> _users;
-
- private IAuthenticationProvider[] _authenticationProviders;
- private DefaultAuthenticationProvider _defaultAuthenticationProvider;
-
- private InvalidAuthProvider _invalidAuthProvider;
-
- private IPasswordResetProvider[] _passwordResetProviders;
- private DefaultPasswordResetProvider _defaultPasswordResetProvider;
-
- private IDtoService DtoService => _dtoServiceFactory.Value;
-
- public UserManager(
- ILogger<UserManager> logger,
- IUserRepository userRepository,
- IXmlSerializer xmlSerializer,
- INetworkManager networkManager,
- IImageProcessor imageProcessor,
- Lazy<IDtoService> dtoServiceFactory,
- IServerApplicationHost appHost,
- IJsonSerializer jsonSerializer,
- IFileSystem fileSystem,
- ICryptoProvider cryptoProvider)
- {
- _logger = logger;
- _userRepository = userRepository;
- _xmlSerializer = xmlSerializer;
- _networkManager = networkManager;
- _imageProcessor = imageProcessor;
- _dtoServiceFactory = dtoServiceFactory;
- _appHost = appHost;
- _jsonSerializer = jsonSerializer;
- _fileSystem = fileSystem;
- _cryptoProvider = cryptoProvider;
- _users = null;
- }
-
- public event EventHandler<GenericEventArgs<User>> UserPasswordChanged;
-
- /// <summary>
- /// Occurs when [user updated].
- /// </summary>
- public event EventHandler<GenericEventArgs<User>> UserUpdated;
-
- public event EventHandler<GenericEventArgs<User>> UserPolicyUpdated;
-
- public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated;
-
- public event EventHandler<GenericEventArgs<User>> UserLockedOut;
-
- public event EventHandler<GenericEventArgs<User>> UserCreated;
-
- /// <summary>
- /// Occurs when [user deleted].
- /// </summary>
- public event EventHandler<GenericEventArgs<User>> UserDeleted;
-
- /// <inheritdoc />
- public IEnumerable<User> Users => _users.Values;
-
- /// <inheritdoc />
- public IEnumerable<Guid> UsersIds => _users.Keys;
-
- /// <summary>
- /// Called when [user updated].
- /// </summary>
- /// <param name="user">The user.</param>
- private void OnUserUpdated(User user)
- {
- UserUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user });
- }
-
- /// <summary>
- /// Called when [user deleted].
- /// </summary>
- /// <param name="user">The user.</param>
- private void OnUserDeleted(User user)
- {
- UserDeleted?.Invoke(this, new GenericEventArgs<User> { Argument = user });
- }
-
- public NameIdPair[] GetAuthenticationProviders()
- {
- return _authenticationProviders
- .Where(i => i.IsEnabled)
- .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1)
- .ThenBy(i => i.Name)
- .Select(i => new NameIdPair
- {
- Name = i.Name,
- Id = GetAuthenticationProviderId(i)
- })
- .ToArray();
- }
-
- public NameIdPair[] GetPasswordResetProviders()
- {
- return _passwordResetProviders
- .Where(i => i.IsEnabled)
- .OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
- .ThenBy(i => i.Name)
- .Select(i => new NameIdPair
- {
- Name = i.Name,
- Id = GetPasswordResetProviderId(i)
- })
- .ToArray();
- }
-
- public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders)
- {
- _authenticationProviders = authenticationProviders.ToArray();
-
- _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
-
- _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
-
- _passwordResetProviders = passwordResetProviders.ToArray();
-
- _defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
- }
-
- /// <inheritdoc />
- public User GetUserById(Guid id)
- {
- if (id == Guid.Empty)
- {
- throw new ArgumentException("Guid can't be empty", nameof(id));
- }
-
- _users.TryGetValue(id, out User user);
- return user;
- }
-
- public User GetUserByName(string name)
- {
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentException("Invalid username", nameof(name));
- }
-
- return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase));
- }
-
- public void Initialize()
- {
- LoadUsers();
-
- var users = Users;
-
- // If there are no local users with admin rights, make them all admins
- if (!users.Any(i => i.Policy.IsAdministrator))
- {
- foreach (var user in users)
- {
- user.Policy.IsAdministrator = true;
- UpdateUserPolicy(user, user.Policy, false);
- }
- }
- }
-
- public static bool IsValidUsername(string username)
- {
- // This is some regex that matches only on unicode "word" characters, as well as -, _ and @
- // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
- // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.)
- return Regex.IsMatch(username, @"^[\w\-'._@]*$");
- }
-
- private static bool IsValidUsernameCharacter(char i)
- => IsValidUsername(i.ToString(CultureInfo.InvariantCulture));
-
- public string MakeValidUsername(string username)
- {
- if (IsValidUsername(username))
- {
- return username;
- }
-
- // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
- var builder = new StringBuilder();
-
- foreach (var c in username)
- {
- if (IsValidUsernameCharacter(c))
- {
- builder.Append(c);
- }
- }
-
- return builder.ToString();
- }
-
- public async Task<User> AuthenticateUser(
- string username,
- string password,
- string hashedPassword,
- string remoteEndPoint,
- bool isUserSession)
- {
- if (string.IsNullOrWhiteSpace(username))
- {
- _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint);
- throw new ArgumentNullException(nameof(username));
- }
-
- var user = Users.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
-
- var success = false;
- IAuthenticationProvider authenticationProvider = null;
-
- if (user != null)
- {
- var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false);
- authenticationProvider = authResult.authenticationProvider;
- success = authResult.success;
- }
- else
- {
- // user is null
- var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false);
- authenticationProvider = authResult.authenticationProvider;
- string updatedUsername = authResult.username;
- success = authResult.success;
-
- if (success
- && authenticationProvider != null
- && !(authenticationProvider is DefaultAuthenticationProvider))
- {
- // Trust the username returned by the authentication provider
- username = updatedUsername;
-
- // Search the database for the user again
- // the authentication provider might have created it
- user = Users
- .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
-
- if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
- {
- var policy = hasNewUserPolicy.GetNewUserPolicy();
- UpdateUserPolicy(user, policy, true);
- }
- }
- }
-
- if (success && user != null && authenticationProvider != null)
- {
- var providerId = GetAuthenticationProviderId(authenticationProvider);
-
- if (!string.Equals(providerId, user.Policy.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
- {
- user.Policy.AuthenticationProviderId = providerId;
- UpdateUserPolicy(user, user.Policy, true);
- }
- }
-
- if (user == null)
- {
- _logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", username, remoteEndPoint);
- throw new AuthenticationException("Invalid username or password entered.");
- }
-
- if (user.Policy.IsDisabled)
- {
- _logger.LogInformation("Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).", username, remoteEndPoint);
- throw new SecurityException($"The {user.Name} account is currently disabled. Please consult with your administrator.");
- }
-
- if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint))
- {
- _logger.LogInformation("Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).", username, remoteEndPoint);
- throw new SecurityException("Forbidden.");
- }
-
- if (!user.IsParentalScheduleAllowed())
- {
- _logger.LogInformation("Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).", username, remoteEndPoint);
- throw new SecurityException("User is not allowed access at this time.");
- }
-
- // Update LastActivityDate and LastLoginDate, then save
- if (success)
- {
- if (isUserSession)
- {
- user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
- UpdateUser(user);
- }
-
- ResetInvalidLoginAttemptCount(user);
- _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Name);
- }
- else
- {
- IncrementInvalidLoginAttemptCount(user);
- _logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", user.Name, remoteEndPoint);
- }
-
- return success ? user : null;
- }
-
-#nullable enable
-
- private static string GetAuthenticationProviderId(IAuthenticationProvider provider)
- {
- return provider.GetType().FullName;
- }
-
- private static string GetPasswordResetProviderId(IPasswordResetProvider provider)
- {
- return provider.GetType().FullName;
- }
-
- private IAuthenticationProvider GetAuthenticationProvider(User user)
- {
- return GetAuthenticationProviders(user)[0];
- }
-
- private IPasswordResetProvider GetPasswordResetProvider(User user)
- {
- return GetPasswordResetProviders(user)[0];
- }
-
- private IAuthenticationProvider[] GetAuthenticationProviders(User? user)
- {
- var authenticationProviderId = user?.Policy.AuthenticationProviderId;
-
- var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray();
-
- if (!string.IsNullOrEmpty(authenticationProviderId))
- {
- providers = providers.Where(i => string.Equals(authenticationProviderId, GetAuthenticationProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray();
- }
-
- if (providers.Length == 0)
- {
- // Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
- _logger.LogWarning("User {UserName} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected", user?.Name, user?.Policy.AuthenticationProviderId);
- providers = new IAuthenticationProvider[] { _invalidAuthProvider };
- }
-
- return providers;
- }
-
- private IPasswordResetProvider[] GetPasswordResetProviders(User? user)
- {
- var passwordResetProviderId = user?.Policy.PasswordResetProviderId;
-
- var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
-
- if (!string.IsNullOrEmpty(passwordResetProviderId))
- {
- providers = providers.Where(i => string.Equals(passwordResetProviderId, GetPasswordResetProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray();
- }
-
- if (providers.Length == 0)
- {
- providers = new IPasswordResetProvider[] { _defaultPasswordResetProvider };
- }
-
- return providers;
- }
-
- private async Task<(string username, bool success)> AuthenticateWithProvider(
- IAuthenticationProvider provider,
- string username,
- string password,
- User? resolvedUser)
- {
- try
- {
- var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser
- ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false)
- : await provider.Authenticate(username, password).ConfigureAwait(false);
-
- if (authenticationResult.Username != username)
- {
- _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username);
- username = authenticationResult.Username;
- }
-
- return (username, true);
- }
- catch (AuthenticationException ex)
- {
- _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name);
-
- return (username, false);
- }
- }
-
- private async Task<(IAuthenticationProvider? authenticationProvider, string username, bool success)> AuthenticateLocalUser(
- string username,
- string password,
- string hashedPassword,
- User? user,
- string remoteEndPoint)
- {
- bool success = false;
- IAuthenticationProvider? authenticationProvider = null;
-
- foreach (var provider in GetAuthenticationProviders(user))
- {
- var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
- var updatedUsername = providerAuthResult.username;
- success = providerAuthResult.success;
-
- if (success)
- {
- authenticationProvider = provider;
- username = updatedUsername;
- break;
- }
- }
-
- if (!success
- && _networkManager.IsInLocalNetwork(remoteEndPoint)
- && user?.Configuration.EnableLocalPassword == true
- && !string.IsNullOrEmpty(user.EasyPassword))
- {
- // Check easy password
- var passwordHash = PasswordHash.Parse(user.EasyPassword);
- var hash = _cryptoProvider.ComputeHash(
- passwordHash.Id,
- Encoding.UTF8.GetBytes(password),
- passwordHash.Salt.ToArray());
- success = passwordHash.Hash.SequenceEqual(hash);
- }
-
- return (authenticationProvider, username, success);
- }
-
- private void ResetInvalidLoginAttemptCount(User user)
- {
- user.Policy.InvalidLoginAttemptCount = 0;
- UpdateUserPolicy(user, user.Policy, false);
- }
-
- private void IncrementInvalidLoginAttemptCount(User user)
- {
- int invalidLogins = ++user.Policy.InvalidLoginAttemptCount;
- int maxInvalidLogins = user.Policy.LoginAttemptsBeforeLockout;
- if (maxInvalidLogins > 0
- && invalidLogins >= maxInvalidLogins)
- {
- user.Policy.IsDisabled = true;
- UserLockedOut?.Invoke(this, new GenericEventArgs<User>(user));
- _logger.LogWarning(
- "Disabling user {UserName} due to {Attempts} unsuccessful login attempts.",
- user.Name,
- invalidLogins);
- }
-
- UpdateUserPolicy(user, user.Policy, false);
- }
-
- /// <summary>
- /// Loads the users from the repository.
- /// </summary>
- private void LoadUsers()
- {
- var users = _userRepository.RetrieveAllUsers();
-
- // There always has to be at least one user.
- if (users.Count != 0)
- {
- _users = new ConcurrentDictionary<Guid, User>(
- users.Select(x => new KeyValuePair<Guid, User>(x.Id, x)));
- return;
- }
-
- var defaultName = Environment.UserName;
- if (string.IsNullOrWhiteSpace(defaultName))
- {
- defaultName = "MyJellyfinUser";
- }
-
- _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
-
- var name = MakeValidUsername(defaultName);
-
- var user = InstantiateNewUser(name);
-
- user.DateLastSaved = DateTime.UtcNow;
-
- _userRepository.CreateUser(user);
-
- user.Policy.IsAdministrator = true;
- user.Policy.EnableContentDeletion = true;
- user.Policy.EnableRemoteControlOfOtherUsers = true;
- UpdateUserPolicy(user, user.Policy, false);
-
- _users = new ConcurrentDictionary<Guid, User>();
- _users[user.Id] = user;
- }
-
-#nullable restore
-
- public UserDto GetUserDto(User user, string remoteEndPoint = null)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user);
- bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user));
-
- bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
- hasConfiguredEasyPassword :
- hasConfiguredPassword;
-
- UserDto dto = new UserDto
- {
- Id = user.Id,
- Name = user.Name,
- HasPassword = hasPassword,
- HasConfiguredPassword = hasConfiguredPassword,
- HasConfiguredEasyPassword = hasConfiguredEasyPassword,
- LastActivityDate = user.LastActivityDate,
- LastLoginDate = user.LastLoginDate,
- Configuration = user.Configuration,
- ServerId = _appHost.SystemId,
- Policy = user.Policy
- };
-
- if (!hasPassword && _users.Count == 1)
- {
- dto.EnableAutoLogin = true;
- }
-
- ItemImageInfo image = user.GetImageInfo(ImageType.Primary, 0);
-
- if (image != null)
- {
- dto.PrimaryImageTag = GetImageCacheTag(user, image);
-
- try
- {
- DtoService.AttachPrimaryImageAspectRatio(dto, user);
- }
- catch (Exception ex)
- {
- // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
- _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {User}", user.Name);
- }
- }
-
- return dto;
- }
-
- public UserDto GetOfflineUserDto(User user)
- {
- var dto = GetUserDto(user);
-
- dto.ServerName = _appHost.FriendlyName;
-
- return dto;
- }
-
- private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
- {
- try
- {
- return _imageProcessor.GetImageCacheTag(item, image);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error getting {ImageType} image info for {ImagePath}", image.Type, image.Path);
- return null;
- }
- }
-
- /// <summary>
- /// Refreshes metadata for each user
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public async Task RefreshUsersMetadata(CancellationToken cancellationToken)
- {
- foreach (var user in Users)
- {
- await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false);
- }
- }
-
- /// <summary>
- /// Renames the user.
- /// </summary>
- /// <param name="user">The user.</param>
- /// <param name="newName">The new name.</param>
- /// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">user</exception>
- /// <exception cref="ArgumentException"></exception>
- public async Task RenameUser(User user, string newName)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- if (string.IsNullOrWhiteSpace(newName))
- {
- throw new ArgumentException("Invalid username", nameof(newName));
- }
-
- if (user.Name.Equals(newName, StringComparison.Ordinal))
- {
- throw new ArgumentException("The new and old names must be different.");
- }
-
- if (Users.Any(
- u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase)))
- {
- throw new ArgumentException(string.Format(
- CultureInfo.InvariantCulture,
- "A user with the name '{0}' already exists.",
- newName));
- }
-
- await user.Rename(newName).ConfigureAwait(false);
-
- OnUserUpdated(user);
- }
-
- /// <summary>
- /// Updates the user.
- /// </summary>
- /// <param name="user">The user.</param>
- /// <exception cref="ArgumentNullException">user</exception>
- /// <exception cref="ArgumentException"></exception>
- public void UpdateUser(User user)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- if (user.Id == Guid.Empty)
- {
- throw new ArgumentException("Id can't be empty.", nameof(user));
- }
-
- if (!_users.ContainsKey(user.Id))
- {
- throw new ArgumentException(
- string.Format(
- CultureInfo.InvariantCulture,
- "A user '{0}' with Id {1} does not exist.",
- user.Name,
- user.Id),
- nameof(user));
- }
-
- user.DateModified = DateTime.UtcNow;
- user.DateLastSaved = DateTime.UtcNow;
-
- _userRepository.UpdateUser(user);
-
- OnUserUpdated(user);
- }
-
- /// <summary>
- /// Creates the user.
- /// </summary>
- /// <param name="name">The name.</param>
- /// <returns>User.</returns>
- /// <exception cref="ArgumentNullException">name</exception>
- /// <exception cref="ArgumentException"></exception>
- public User CreateUser(string name)
- {
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentNullException(nameof(name));
- }
-
- if (!IsValidUsername(name))
- {
- throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
- }
-
- if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
- {
- throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name));
- }
-
- var user = InstantiateNewUser(name);
-
- _users[user.Id] = user;
-
- user.DateLastSaved = DateTime.UtcNow;
-
- _userRepository.CreateUser(user);
-
- EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger);
-
- return user;
- }
-
- /// <inheritdoc />
- /// <exception cref="ArgumentNullException">The <c>user</c> is <c>null</c>.</exception>
- /// <exception cref="ArgumentException">The <c>user</c> doesn't exist, or is the last administrator.</exception>
- /// <exception cref="InvalidOperationException">The <c>user</c> can't be deleted; there are no other users.</exception>
- public void DeleteUser(User user)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- if (!_users.ContainsKey(user.Id))
- {
- throw new ArgumentException(string.Format(
- CultureInfo.InvariantCulture,
- "The user cannot be deleted because there is no user with the Name {0} and Id {1}.",
- user.Name,
- user.Id));
- }
-
- if (_users.Count == 1)
- {
- throw new InvalidOperationException(string.Format(
- CultureInfo.InvariantCulture,
- "The user '{0}' cannot be deleted because there must be at least one user in the system.",
- user.Name));
- }
-
- if (user.Policy.IsAdministrator
- && Users.Count(i => i.Policy.IsAdministrator) == 1)
- {
- throw new ArgumentException(
- string.Format(
- CultureInfo.InvariantCulture,
- "The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
- user.Name),
- nameof(user));
- }
-
- var configPath = GetConfigurationFilePath(user);
-
- _userRepository.DeleteUser(user);
-
- // Delete user config dir
- lock (_configSyncLock)
- lock (_policySyncLock)
- {
- try
- {
- Directory.Delete(user.ConfigurationDirectoryPath, true);
- }
- catch (IOException ex)
- {
- _logger.LogError(ex, "Error deleting user config dir: {Path}", user.ConfigurationDirectoryPath);
- }
- }
-
- _users.TryRemove(user.Id, out _);
-
- OnUserDeleted(user);
- }
-
- /// <summary>
- /// Resets the password by clearing it.
- /// </summary>
- /// <returns>Task.</returns>
- public Task ResetPassword(User user)
- {
- return ChangePassword(user, string.Empty);
- }
-
- public void ResetEasyPassword(User user)
- {
- ChangeEasyPassword(user, string.Empty, null);
- }
-
- public async Task ChangePassword(User user, string newPassword)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
-
- UpdateUser(user);
-
- UserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
- }
-
- public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
-
- GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordHash);
-
- UpdateUser(user);
-
- UserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
- }
-
- /// <summary>
- /// Instantiates the new user.
- /// </summary>
- /// <param name="name">The name.</param>
- /// <returns>User.</returns>
- private static User InstantiateNewUser(string name)
- {
- return new User
- {
- Name = name,
- Id = Guid.NewGuid(),
- DateCreated = DateTime.UtcNow,
- DateModified = DateTime.UtcNow
- };
- }
-
- public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
- {
- var user = string.IsNullOrWhiteSpace(enteredUsername) ?
- null :
- GetUserByName(enteredUsername);
-
- var action = ForgotPasswordAction.InNetworkRequired;
-
- if (user != null && isInNetwork)
- {
- var passwordResetProvider = GetPasswordResetProvider(user);
- return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false);
- }
- else
- {
- return new ForgotPasswordResult
- {
- Action = action,
- PinFile = string.Empty
- };
- }
- }
-
- public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
- {
- foreach (var provider in _passwordResetProviders)
- {
- var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
- if (result.Success)
- {
- return result;
- }
- }
-
- return new PinRedeemResult
- {
- Success = false,
- UsersReset = Array.Empty<string>()
- };
- }
-
- public UserPolicy GetUserPolicy(User user)
- {
- var path = GetPolicyFilePath(user);
- if (!File.Exists(path))
- {
- return GetDefaultPolicy();
- }
-
- try
- {
- lock (_policySyncLock)
- {
- return (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), path);
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error reading policy file: {Path}", path);
-
- return GetDefaultPolicy();
- }
- }
-
- private static UserPolicy GetDefaultPolicy()
- {
- return new UserPolicy
- {
- EnableContentDownloading = true,
- EnableSyncTranscoding = true
- };
- }
-
- public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy)
- {
- var user = GetUserById(userId);
- UpdateUserPolicy(user, userPolicy, true);
- }
-
- private void UpdateUserPolicy(User user, UserPolicy userPolicy, bool fireEvent)
- {
- // The xml serializer will output differently if the type is not exact
- if (userPolicy.GetType() != typeof(UserPolicy))
- {
- var json = _jsonSerializer.SerializeToString(userPolicy);
- userPolicy = _jsonSerializer.DeserializeFromString<UserPolicy>(json);
- }
-
- var path = GetPolicyFilePath(user);
-
- Directory.CreateDirectory(Path.GetDirectoryName(path));
-
- lock (_policySyncLock)
- {
- _xmlSerializer.SerializeToFile(userPolicy, path);
- user.Policy = userPolicy;
- }
-
- if (fireEvent)
- {
- UserPolicyUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user });
- }
- }
-
- private static string GetPolicyFilePath(User user)
- {
- return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml");
- }
-
- private static string GetConfigurationFilePath(User user)
- {
- return Path.Combine(user.ConfigurationDirectoryPath, "config.xml");
- }
-
- public UserConfiguration GetUserConfiguration(User user)
- {
- var path = GetConfigurationFilePath(user);
-
- if (!File.Exists(path))
- {
- return new UserConfiguration();
- }
-
- try
- {
- lock (_configSyncLock)
- {
- return (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), path);
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error reading policy file: {Path}", path);
-
- return new UserConfiguration();
- }
- }
-
- public void UpdateConfiguration(Guid userId, UserConfiguration config)
- {
- var user = GetUserById(userId);
- UpdateConfiguration(user, config);
- }
-
- public void UpdateConfiguration(User user, UserConfiguration config)
- {
- UpdateConfiguration(user, config, true);
- }
-
- private void UpdateConfiguration(User user, UserConfiguration config, bool fireEvent)
- {
- var path = GetConfigurationFilePath(user);
-
- // The xml serializer will output differently if the type is not exact
- if (config.GetType() != typeof(UserConfiguration))
- {
- var json = _jsonSerializer.SerializeToString(config);
- config = _jsonSerializer.DeserializeFromString<UserConfiguration>(json);
- }
-
- Directory.CreateDirectory(Path.GetDirectoryName(path));
-
- lock (_configSyncLock)
- {
- _xmlSerializer.SerializeToFile(config, path);
- user.Configuration = config;
- }
-
- if (fireEvent)
- {
- UserConfigurationUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user });
- }
- }
- }
-
- public class DeviceAccessEntryPoint : IServerEntryPoint
- {
- private IUserManager _userManager;
- private IAuthenticationRepository _authRepo;
- private IDeviceManager _deviceManager;
- private ISessionManager _sessionManager;
-
- public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager)
- {
- _userManager = userManager;
- _authRepo = authRepo;
- _deviceManager = deviceManager;
- _sessionManager = sessionManager;
- }
-
- public Task RunAsync()
- {
- _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated;
-
- return Task.CompletedTask;
- }
-
- private void _userManager_UserPolicyUpdated(object sender, GenericEventArgs<User> e)
- {
- var user = e.Argument;
- if (!user.Policy.EnableAllDevices)
- {
- UpdateDeviceAccess(user);
- }
- }
-
- private void UpdateDeviceAccess(User user)
- {
- var existing = _authRepo.Get(new AuthenticationInfoQuery
- {
- UserId = user.Id
-
- }).Items;
-
- foreach (var authInfo in existing)
- {
- if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId))
- {
- _sessionManager.Logout(authInfo);
- }
- }
- }
-
- public void Dispose()
- {
-
- }
- }
-}
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index 322819b05..f51657c63 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -5,6 +5,8 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
@@ -17,6 +19,8 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using Person = MediaBrowser.Controller.Entities.Person;
namespace Emby.Server.Implementations.Library
{
@@ -125,12 +129,12 @@ namespace Emby.Server.Implementations.Library
if (!query.IncludeHidden)
{
- list = list.Where(i => !user.Configuration.MyMediaExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList();
+ list = list.Where(i => !user.GetPreference(PreferenceKind.MyMediaExcludes).Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList();
}
var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
- var orders = user.Configuration.OrderedViews.ToList();
+ var orders = user.GetPreference(PreferenceKind.OrderedViews).ToList();
return list
.OrderBy(i =>
@@ -165,7 +169,13 @@ namespace Emby.Server.Implementations.Library
return GetUserSubViewWithName(name, parentId, type, sortName);
}
- private Folder GetUserView(List<ICollectionFolder> parents, string viewType, string localizationKey, string sortName, User user, string[] presetViews)
+ private Folder GetUserView(
+ List<ICollectionFolder> parents,
+ string viewType,
+ string localizationKey,
+ string sortName,
+ Jellyfin.Data.Entities.User user,
+ string[] presetViews)
{
if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase)))
{
@@ -270,7 +280,8 @@ namespace Emby.Server.Implementations.Library
{
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
- .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
+ .Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes)
+ .Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
.ToList();
}
@@ -331,12 +342,11 @@ namespace Emby.Server.Implementations.Library
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 ? new[]
{
- typeof(Person).Name,
- typeof(Studio).Name,
- typeof(Year).Name,
- typeof(MusicGenre).Name,
- typeof(Genre).Name
-
+ nameof(Person),
+ nameof(Studio),
+ nameof(Year),
+ nameof(MusicGenre),
+ nameof(Genre)
} : Array.Empty<string>();
var query = new InternalItemsQuery(user)
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs
index 2af8ff5cb..d51f9aaa7 100644
--- a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Validators
/// The _library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
- private readonly ILogger _logger;
+ private readonly ILogger<ArtistsValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
@@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Library.Validators
/// <param name="itemRepo">The item repository.</param>
public ArtistsPostScanTask(
ILibraryManager libraryManager,
- ILogger<ArtistsPostScanTask> logger,
+ ILogger<ArtistsValidator> logger,
IItemRepository itemRepo)
{
_libraryManager = libraryManager;
diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
index 1497f4a3a..d4c8c35e6 100644
--- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
@@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.Library.Validators
/// <summary>
/// The logger.
/// </summary>
- private readonly ILogger _logger;
+ private readonly ILogger<ArtistsValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
@@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.Library.Validators
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
- public ArtistsValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo)
{
_libraryManager = libraryManager;
_logger = logger;
@@ -98,7 +98,6 @@ namespace Emby.Server.Implementations.Library.Validators
_libraryManager.DeleteItem(item, new DeleteOptions
{
DeleteFileLocation = false
-
}, false);
}
diff --git a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs
index 251785dfd..d21d2887b 100644
--- a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Validators
/// The _library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
- private readonly ILogger _logger;
+ private readonly ILogger<GenresValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
@@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Library.Validators
/// <param name="itemRepo">The item repository.</param>
public GenresPostScanTask(
ILibraryManager libraryManager,
- ILogger<GenresPostScanTask> logger,
+ ILogger<GenresValidator> logger,
IItemRepository itemRepo)
{
_libraryManager = libraryManager;
diff --git a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
index b0cd5f87a..e59c62e23 100644
--- a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs
@@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.Library.Validators
/// <summary>
/// The logger.
/// </summary>
- private readonly ILogger _logger;
+ private readonly ILogger<GenresValidator> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="GenresValidator"/> class.
@@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Library.Validators
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
- public GenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo)
{
_libraryManager = libraryManager;
_logger = logger;
diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs
index 9d8690116..be119866b 100644
--- a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Validators
/// The library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
- private readonly ILogger _logger;
+ private readonly ILogger<MusicGenresValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
@@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Library.Validators
/// <param name="itemRepo">The item repository.</param>
public MusicGenresPostScanTask(
ILibraryManager libraryManager,
- ILogger<MusicGenresPostScanTask> logger,
+ ILogger<MusicGenresValidator> logger,
IItemRepository itemRepo)
{
_libraryManager = libraryManager;
diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
index 5ee4ca72e..1ecf4c87c 100644
--- a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
@@ -20,7 +20,7 @@ namespace Emby.Server.Implementations.Library.Validators
/// <summary>
/// The logger.
/// </summary>
- private readonly ILogger _logger;
+ private readonly ILogger<MusicGenresValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
@@ -29,7 +29,7 @@ namespace Emby.Server.Implementations.Library.Validators
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
- public MusicGenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo)
{
_libraryManager = libraryManager;
_logger = logger;
diff --git a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs
index 2f8f906b9..c682b156b 100644
--- a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.Library.Validators
/// </summary>
private readonly ILibraryManager _libraryManager;
- private readonly ILogger _logger;
+ private readonly ILogger<StudiosValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Library.Validators
/// <param name="itemRepo">The item repository.</param>
public StudiosPostScanTask(
ILibraryManager libraryManager,
- ILogger<StudiosPostScanTask> logger,
+ ILogger<StudiosValidator> logger,
IItemRepository itemRepo)
{
_libraryManager = libraryManager;
diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
index 15e7a0dbb..ca35adfff 100644
--- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
+++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
@@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.Library.Validators
/// <summary>
/// The logger.
/// </summary>
- private readonly ILogger _logger;
+ private readonly ILogger<StudiosValidator> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="StudiosValidator" /> class.
@@ -32,7 +32,7 @@ namespace Emby.Server.Implementations.Library.Validators
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
- public StudiosValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo)
+ public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo)
{
_libraryManager = libraryManager;
_logger = logger;
@@ -92,7 +92,6 @@ namespace Emby.Server.Implementations.Library.Validators
_libraryManager.DeleteItem(item, new DeleteOptions
{
DeleteFileLocation = false
-
}, false);
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index 2e13a3bb3..44560d1e2 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -16,13 +16,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public class DirectRecorder : IRecorder
{
private readonly ILogger _logger;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IStreamHelper _streamHelper;
- public DirectRecorder(ILogger logger, IHttpClient httpClient, IStreamHelper streamHelper)
+ public DirectRecorder(ILogger logger, IHttpClientFactory httpClientFactory, IStreamHelper streamHelper)
{
_logger = logger;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_streamHelper = streamHelper;
}
@@ -52,10 +52,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_logger.LogInformation("Copying recording stream to file {0}", targetFile);
// The media source is infinite so we need to handle stopping ourselves
- var durationToken = new CancellationTokenSource(duration);
- cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+ using var durationToken = new CancellationTokenSource(duration);
+ using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
- await directStreamProvider.CopyToAsync(output, cancellationToken).ConfigureAwait(false);
+ await directStreamProvider.CopyToAsync(output, cancellationTokenSource.Token).ConfigureAwait(false);
}
_logger.LogInformation("Recording completed to file {0}", targetFile);
@@ -63,37 +63,28 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private async Task RecordFromMediaSource(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
- var httpRequestOptions = new HttpRequestOptions
- {
- Url = mediaSource.Path,
- BufferContent = false,
-
- // Some remote urls will expect a user agent to be supplied
- UserAgent = "Emby/3.0",
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetAsync(mediaSource.Path, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- // Shouldn't matter but may cause issues
- DecompressionMethod = CompressionMethods.None
- };
+ _logger.LogInformation("Opened recording stream from tuner provider");
- using (var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false))
- {
- _logger.LogInformation("Opened recording stream from tuner provider");
+ Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
- Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
+ await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read);
- using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read))
- {
- onStarted();
+ onStarted();
- _logger.LogInformation("Copying recording stream to file {0}", targetFile);
+ _logger.LogInformation("Copying recording stream to file {0}", targetFile);
- // The media source if infinite so we need to handle stopping ourselves
- var durationToken = new CancellationTokenSource(duration);
- cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+ // The media source if infinite so we need to handle stopping ourselves
+ var durationToken = new CancellationTokenSource(duration);
+ cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
- await _streamHelper.CopyUntilCancelled(response.Content, output, 81920, cancellationToken).ConfigureAwait(false);
- }
- }
+ await _streamHelper.CopyUntilCancelled(
+ await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+ output,
+ IODefaults.CopyToBufferSize,
+ cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Recording completed to file {0}", targetFile);
}
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 3efe1ee25..ca60c3366 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -7,11 +7,14 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Emby.Server.Implementations.Library;
+using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
@@ -28,7 +31,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
@@ -46,8 +48,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private const int TunerDiscoveryDurationMs = 3000;
private readonly IServerApplicationHost _appHost;
- private readonly ILogger _logger;
- private readonly IHttpClient _httpClient;
+ private readonly ILogger<EmbyTV> _logger;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config;
private readonly IJsonSerializer _jsonSerializer;
@@ -80,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
IMediaSourceManager mediaSourceManager,
ILogger<EmbyTV> logger,
IJsonSerializer jsonSerializer,
- IHttpClient httpClient,
+ IHttpClientFactory httpClientFactory,
IServerConfigurationManager config,
ILiveTvManager liveTvManager,
IFileSystem fileSystem,
@@ -93,7 +95,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_appHost = appHost;
_logger = logger;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_config = config;
_fileSystem = fileSystem;
_libraryManager = libraryManager;
@@ -140,11 +142,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
+ private async void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
{
if (string.Equals(e.Key, "livetv", StringComparison.OrdinalIgnoreCase))
{
- OnRecordingFoldersChanged();
+ await CreateRecordingFolders().ConfigureAwait(false);
}
}
@@ -155,11 +157,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return CreateRecordingFolders();
}
- private async void OnRecordingFoldersChanged()
- {
- await CreateRecordingFolders().ConfigureAwait(false);
- }
-
internal async Task CreateRecordingFolders()
{
try
@@ -608,11 +605,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return Task.CompletedTask;
}
- public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken)
- {
- return Task.CompletedTask;
- }
-
public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
{
throw new NotImplementedException();
@@ -812,11 +804,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return null;
}
- public IEnumerable<ActiveRecordingInfo> GetAllActiveRecordings()
- {
- return _activeRecordings.Values.Where(i => i.Timer.Status == RecordingStatus.InProgress && !i.CancellationTokenSource.IsCancellationRequested);
- }
-
public ActiveRecordingInfo GetActiveRecordingInfo(string path)
{
if (string.IsNullOrWhiteSpace(path))
@@ -1019,16 +1006,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
throw new Exception("Tuner not found.");
}
- private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, bool enableStreamSharing)
- {
- var json = _jsonSerializer.SerializeToString(mediaSource);
- mediaSource = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
-
- mediaSource.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture) + "_" + mediaSource.Id;
-
- return mediaSource;
- }
-
public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(channelId))
@@ -1334,7 +1311,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
await CreateRecordingFolders().ConfigureAwait(false);
TriggerRefresh(recordPath);
- EnforceKeepUpTo(timer, seriesPath);
+ await EnforceKeepUpTo(timer, seriesPath).ConfigureAwait(false);
};
await recorder.Record(directStreamProvider, mediaStreamInfo, recordPath, duration, onStarted, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false);
@@ -1494,7 +1471,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return item;
}
- private async void EnforceKeepUpTo(TimerInfo timer, string seriesPath)
+ private async Task EnforceKeepUpTo(TimerInfo timer, string seriesPath)
{
if (string.IsNullOrWhiteSpace(timer.SeriesTimerId))
{
@@ -1552,7 +1529,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
IsFolder = false,
Recursive = true,
DtoOptions = new DtoOptions(true)
-
})
.Where(i => i.IsFileProtocol && File.Exists(i.Path))
.Skip(seriesTimer.KeepUpTo - 1)
@@ -1659,10 +1635,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http))
{
- return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config);
+ return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer);
}
- return new DirectRecorder(_logger, _httpClient, _streamHelper);
+ return new DirectRecorder(_logger, _httpClientFactory, _streamHelper);
}
private void OnSuccessfulRecording(TimerInfo timer, string path)
@@ -1898,22 +1874,22 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
writer.WriteStartDocument(true);
writer.WriteStartElement("tvshow");
string id;
- if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id))
+ if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out id))
{
writer.WriteElementString("id", id);
}
- if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id))
+ if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out id))
{
writer.WriteElementString("imdb_id", id);
}
- if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Tmdb.ToString(), out id))
+ if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out id))
{
writer.WriteElementString("tmdbid", id);
}
- if (timer.SeriesProviderIds.TryGetValue(MetadataProviders.Zap2It.ToString(), out id))
+ if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Zap2It.ToString(), out id))
{
writer.WriteElementString("zap2itid", id);
}
@@ -2080,14 +2056,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
writer.WriteElementString("credits", person);
}
- var tmdbCollection = item.GetProviderId(MetadataProviders.TmdbCollection);
+ var tmdbCollection = item.GetProviderId(MetadataProvider.TmdbCollection);
if (!string.IsNullOrEmpty(tmdbCollection))
{
writer.WriteElementString("collectionnumber", tmdbCollection);
}
- var imdb = item.GetProviderId(MetadataProviders.Imdb);
+ var imdb = item.GetProviderId(MetadataProvider.Imdb);
if (!string.IsNullOrEmpty(imdb))
{
if (!isSeriesEpisode)
@@ -2101,7 +2077,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
lockData = false;
}
- var tvdb = item.GetProviderId(MetadataProviders.Tvdb);
+ var tvdb = item.GetProviderId(MetadataProvider.Tvdb);
if (!string.IsNullOrEmpty(tvdb))
{
writer.WriteElementString("tvdbid", tvdb);
@@ -2110,7 +2086,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
lockData = false;
}
- var tmdb = item.GetProviderId(MetadataProviders.Tmdb);
+ var tmdb = item.GetProviderId(MetadataProvider.Tmdb);
if (!string.IsNullOrEmpty(tmdb))
{
writer.WriteElementString("tmdbid", tmdb);
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index bc86cc59a..3e5457dbd 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -8,12 +8,9 @@ using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
@@ -26,26 +23,24 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly ILogger _logger;
private readonly IMediaEncoder _mediaEncoder;
private readonly IServerApplicationPaths _appPaths;
+ private readonly IJsonSerializer _json;
+ private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
+
private bool _hasExited;
private Stream _logFileStream;
private string _targetPath;
private Process _process;
- private readonly IJsonSerializer _json;
- private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
- private readonly IServerConfigurationManager _config;
public EncodedRecorder(
ILogger logger,
IMediaEncoder mediaEncoder,
IServerApplicationPaths appPaths,
- IJsonSerializer json,
- IServerConfigurationManager config)
+ IJsonSerializer json)
{
_logger = logger;
_mediaEncoder = mediaEncoder;
_appPaths = appPaths;
_json = json;
- _config = config;
}
private static bool CopySubtitles => false;
@@ -58,19 +53,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public async Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
// The media source is infinite so we need to handle stopping ourselves
- var durationToken = new CancellationTokenSource(duration);
- cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
+ using var durationToken = new CancellationTokenSource(duration);
+ using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token);
- await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationToken).ConfigureAwait(false);
+ await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationTokenSource.Token).ConfigureAwait(false);
_logger.LogInformation("Recording completed to file {0}", targetFile);
}
- private EncodingOptions GetEncodingOptions()
- {
- return _config.GetConfiguration<EncodingOptions>("encoding");
- }
-
private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
_targetPath = targetFile;
@@ -108,7 +98,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
StartInfo = processStartInfo,
EnableRaisingEvents = true
};
- _process.Exited += (sender, args) => OnFfMpegProcessExited(_process, inputFile);
+ _process.Exited += (sender, args) => OnFfMpegProcessExited(_process);
_process.Start();
@@ -117,7 +107,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
onStarted();
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
- StartStreamingLog(_process.StandardError.BaseStream, _logFileStream);
+ _ = StartStreamingLog(_process.StandardError.BaseStream, _logFileStream);
_logger.LogInformation("ffmpeg recording process started for {0}", _targetPath);
@@ -183,7 +173,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var subtitleArgs = CopySubtitles ? " -codec:s copy" : " -sn";
- //var outputParam = string.Equals(Path.GetExtension(targetFile), ".mp4", StringComparison.OrdinalIgnoreCase) ?
+ // var outputParam = string.Equals(Path.GetExtension(targetFile), ".mp4", StringComparison.OrdinalIgnoreCase) ?
// " -f mp4 -movflags frag_keyframe+empty_moov" :
// string.Empty;
@@ -206,13 +196,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
return "-codec:a:0 copy";
- //var audioChannels = 2;
- //var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
- //if (audioStream != null)
+ // var audioChannels = 2;
+ // var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
+ // if (audioStream != null)
//{
// audioChannels = audioStream.Channels ?? audioChannels;
//}
- //return "-codec:a:0 aac -strict experimental -ab 320000";
+ // return "-codec:a:0 aac -strict experimental -ab 320000";
}
private static bool EncodeVideo(MediaSourceInfo mediaSource)
@@ -221,20 +211,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
protected string GetOutputSizeParam()
- {
- var filters = new List<string>();
-
- filters.Add("yadif=0:-1:0");
-
- var output = string.Empty;
-
- if (filters.Count > 0)
- {
- output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray()));
- }
-
- return output;
- }
+ => "-vf \"yadif=0:-1:0\"";
private void Stop()
{
@@ -291,7 +268,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
/// <summary>
/// Processes the exited.
/// </summary>
- private void OnFfMpegProcessExited(Process process, string inputFile)
+ private void OnFfMpegProcessExited(Process process)
{
using (process)
{
@@ -321,7 +298,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- private async void StartStreamingLog(Stream source, Stream target)
+ private async Task StartStreamingLog(Stream source, Stream target)
{
try
{
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs
index 69a9cb78a..a2ec2df37 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs
@@ -5,7 +5,7 @@ using MediaBrowser.Controller.Plugins;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
- public class EntryPoint : IServerEntryPoint
+ public sealed class EntryPoint : IServerEntryPoint
{
/// <inheritdoc />
public Task RunAsync()
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
index 0b0ff6cb3..142c59542 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
@@ -56,7 +56,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
name += " " + info.EpisodeTitle;
}
}
-
else if (info.IsMovie && info.ProductionYear != null)
{
name += " (" + info.ProductionYear + ")";
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
index 7ebb043d8..dd479b7d1 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
@@ -5,8 +5,8 @@ using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Threading;
+using Jellyfin.Data.Events;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
@@ -109,7 +109,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
if (startDate < now)
{
- TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo> { Argument = item });
+ TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo>(item));
return;
}
@@ -151,7 +151,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase));
if (timer != null)
{
- TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo> { Argument = timer });
+ TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo>(timer));
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 89b81fd96..28aabc159 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -8,6 +8,8 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Mime;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
@@ -24,23 +26,23 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
public class SchedulesDirect : IListingsProvider
{
- private readonly ILogger _logger;
+ private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
+
+ private readonly ILogger<SchedulesDirect> _logger;
private readonly IJsonSerializer _jsonSerializer;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
private readonly IApplicationHost _appHost;
- private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
-
public SchedulesDirect(
ILogger<SchedulesDirect> logger,
IJsonSerializer jsonSerializer,
- IHttpClient httpClient,
+ IHttpClientFactory httpClientFactory,
IApplicationHost appHost)
{
_logger = logger;
_jsonSerializer = jsonSerializer;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_appHost = appHost;
}
@@ -61,7 +63,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
while (start <= end)
{
- dates.Add(start.ToString("yyyy-MM-dd"));
+ dates.Add(start.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
start = start.AddDays(1);
}
@@ -102,95 +104,78 @@ namespace Emby.Server.Implementations.LiveTv.Listings
var requestString = _jsonSerializer.SerializeToString(requestList);
_logger.LogDebug("Request string for schedules is: {RequestString}", requestString);
- var httpOptions = new HttpRequestOptions()
- {
- Url = ApiUrl + "/schedules",
- UserAgent = UserAgent,
- CancellationToken = cancellationToken,
- LogErrorResponseBody = true,
- RequestContent = requestString
- };
+ using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/schedules");
+ options.Content = new StringContent(requestString, Encoding.UTF8, MediaTypeNames.Application.Json);
+ options.Headers.TryAddWithoutValidation("token", token);
+ using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
+ await using var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(responseStream).ConfigureAwait(false);
+ _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
- httpOptions.RequestHeaders["token"] = token;
+ using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs");
+ programRequestOptions.Headers.TryAddWithoutValidation("token", token);
- using (var response = await Post(httpOptions, true, info).ConfigureAwait(false))
- {
- var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(response.Content).ConfigureAwait(false);
- _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
+ var programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct();
+ programRequestOptions.Content = new StringContent("[\"" + string.Join("\", \"", programsID) + "\"]", Encoding.UTF8, MediaTypeNames.Application.Json);
- httpOptions = new HttpRequestOptions()
- {
- Url = ApiUrl + "/programs",
- UserAgent = UserAgent,
- CancellationToken = cancellationToken,
- LogErrorResponseBody = true
- };
-
- httpOptions.RequestHeaders["token"] = token;
+ using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
+ await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream).ConfigureAwait(false);
+ var programDict = programDetails.ToDictionary(p => p.programID, y => y);
- var programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct();
- httpOptions.RequestContent = "[\"" + string.Join("\", \"", programsID) + "\"]";
-
- using (var innerResponse = await Post(httpOptions, true, info).ConfigureAwait(false))
- {
- var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponse.Content).ConfigureAwait(false);
- var programDict = programDetails.ToDictionary(p => p.programID, y => y);
+ var programIdsWithImages =
+ programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
+ .ToList();
- var programIdsWithImages =
- programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
- .ToList();
+ var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
- var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
+ var programsInfo = new List<ProgramInfo>();
+ foreach (ScheduleDirect.Program schedule in dailySchedules.SelectMany(d => d.programs))
+ {
+ // _logger.LogDebug("Proccesing Schedule for statio ID " + stationID +
+ // " which corresponds to channel " + channelNumber + " and program id " +
+ // schedule.programID + " which says it has images? " +
+ // programDict[schedule.programID].hasImageArtwork);
- var programsInfo = new List<ProgramInfo>();
- foreach (ScheduleDirect.Program schedule in dailySchedules.SelectMany(d => d.programs))
+ if (images != null)
+ {
+ var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10));
+ if (imageIndex > -1)
{
- //_logger.LogDebug("Proccesing Schedule for statio ID " + stationID +
- // " which corresponds to channel " + channelNumber + " and program id " +
- // schedule.programID + " which says it has images? " +
- // programDict[schedule.programID].hasImageArtwork);
+ var programEntry = programDict[schedule.programID];
- if (images != null)
- {
- var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10));
- if (imageIndex > -1)
- {
- var programEntry = programDict[schedule.programID];
-
- var allImages = images[imageIndex].data ?? new List<ScheduleDirect.ImageData>();
- var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase));
- var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase));
-
- const double DesiredAspect = 2.0 / 3;
-
- programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ??
- GetProgramImage(ApiUrl, allImages, true, DesiredAspect);
+ var allImages = images[imageIndex].data ?? new List<ScheduleDirect.ImageData>();
+ var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase));
+ var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase));
- const double WideAspect = 16.0 / 9;
+ const double DesiredAspect = 2.0 / 3;
- programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect);
+ programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ??
+ GetProgramImage(ApiUrl, allImages, true, DesiredAspect);
- // Don't supply the same image twice
- if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal))
- {
- programEntry.thumbImage = null;
- }
+ const double WideAspect = 16.0 / 9;
- programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect);
+ programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect);
- //programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
- // GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
- // GetProgramImage(ApiUrl, data, "Banner-LO", false) ??
- // GetProgramImage(ApiUrl, data, "Banner-LOT", false);
- }
+ // Don't supply the same image twice
+ if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal))
+ {
+ programEntry.thumbImage = null;
}
- programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID]));
- }
+ programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect);
- return programsInfo;
+ // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
+ // GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
+ // GetProgramImage(ApiUrl, data, "Banner-LO", false) ??
+ // GetProgramImage(ApiUrl, data, "Banner-LOT", false);
+ }
}
+
+ programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID]));
}
+
+ return programsInfo;
}
private static int GetSizeOrder(ScheduleDirect.ImageData image)
@@ -212,6 +197,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
channelNumber = map.channel;
}
+
if (string.IsNullOrWhiteSpace(channelNumber))
{
channelNumber = map.atscMajor + "." + map.atscMinor;
@@ -276,7 +262,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
CommunityRating = null,
EpisodeTitle = episodeTitle,
Audio = audioType,
- //IsNew = programInfo.@new ?? false,
+ // IsNew = programInfo.@new ?? false,
IsRepeat = programInfo.@new == null,
IsSeries = string.Equals(details.entityType, "episode", StringComparison.OrdinalIgnoreCase),
ImageUrl = details.primaryImage,
@@ -342,7 +328,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
info.SeriesId = programId.Substring(0, 10);
- info.SeriesProviderIds[MetadataProviders.Zap2It.ToString()] = info.SeriesId;
+ info.SeriesProviderIds[MetadataProvider.Zap2It.ToString()] = info.SeriesId;
if (details.metadata != null)
{
@@ -366,13 +352,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
if (!string.IsNullOrWhiteSpace(details.originalAirDate))
{
- info.OriginalAirDate = DateTime.Parse(details.originalAirDate);
+ info.OriginalAirDate = DateTime.Parse(details.originalAirDate, CultureInfo.InvariantCulture);
info.ProductionYear = info.OriginalAirDate.Value.Year;
}
if (details.movie != null)
{
- if (!string.IsNullOrEmpty(details.movie.year) && int.TryParse(details.movie.year, out int year))
+ if (!string.IsNullOrEmpty(details.movie.year)
+ && int.TryParse(details.movie.year, out int year))
{
info.ProductionYear = year;
}
@@ -400,6 +387,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
date = DateTime.SpecifyKind(date, DateTimeKind.Utc);
}
+
return date;
}
@@ -459,7 +447,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms(
ListingsProviderInfo info,
List<string> programIds,
- CancellationToken cancellationToken)
+ CancellationToken cancellationToken)
{
if (programIds.Count == 0)
{
@@ -472,7 +460,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
var imageId = i.Substring(0, 10);
- if (!imageIdString.Contains(imageId))
+ if (!imageIdString.Contains(imageId, StringComparison.Ordinal))
{
imageIdString += "\"" + imageId + "\",";
}
@@ -480,22 +468,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings
imageIdString = imageIdString.TrimEnd(',') + "]";
- var httpOptions = new HttpRequestOptions()
+ using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs")
{
- Url = ApiUrl + "/metadata/programs",
- UserAgent = UserAgent,
- CancellationToken = cancellationToken,
- RequestContent = imageIdString,
- LogErrorResponseBody = true,
+ Content = new StringContent(imageIdString, Encoding.UTF8, MediaTypeNames.Application.Json)
};
try
{
- using (var innerResponse2 = await Post(httpOptions, true, info).ConfigureAwait(false))
- {
- return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>(
- innerResponse2.Content).ConfigureAwait(false);
- }
+ using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
+ await using var response = await innerResponse2.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>(
+ response).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -516,41 +499,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return lineups;
}
- var options = new HttpRequestOptions()
- {
- Url = ApiUrl + "/headends?country=" + country + "&postalcode=" + location,
- UserAgent = UserAgent,
- CancellationToken = cancellationToken,
- LogErrorResponseBody = true
- };
-
- options.RequestHeaders["token"] = token;
+ using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/headends?country=" + country + "&postalcode=" + location);
+ options.Headers.TryAddWithoutValidation("token", token);
try
{
- using (var httpResponse = await Get(options, false, info).ConfigureAwait(false))
- using (Stream responce = httpResponse.Content)
- {
- var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(responce).ConfigureAwait(false);
+ using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
+ await using var response = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+
+ var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(response).ConfigureAwait(false);
- if (root != null)
+ if (root != null)
+ {
+ foreach (ScheduleDirect.Headends headend in root)
{
- foreach (ScheduleDirect.Headends headend in root)
+ foreach (ScheduleDirect.Lineup lineup in headend.lineups)
{
- foreach (ScheduleDirect.Lineup lineup in headend.lineups)
+ lineups.Add(new NameIdPair
{
- lineups.Add(new NameIdPair
- {
- Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name,
- Id = lineup.uri.Substring(18)
- });
- }
+ Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name,
+ Id = lineup.uri.Substring(18)
+ });
}
}
- else
- {
- _logger.LogInformation("No lineups available");
- }
+ }
+ else
+ {
+ _logger.LogInformation("No lineups available");
}
}
catch (Exception ex)
@@ -585,7 +560,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return null;
}
- NameValuePair savedToken = null;
+ NameValuePair savedToken;
if (!_tokens.TryGetValue(username, out savedToken))
{
savedToken = new NameValuePair();
@@ -622,6 +597,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
_lastErrorResponse = DateTime.UtcNow;
}
}
+
throw;
}
finally
@@ -630,46 +606,16 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
}
- private async Task<HttpResponseInfo> Post(HttpRequestOptions options,
- bool enableRetry,
- ListingsProviderInfo providerInfo)
- {
- // Schedules direct requires that the client support compression and will return a 400 response without it
- options.DecompressionMethod = CompressionMethods.Deflate;
-
- try
- {
- return await _httpClient.Post(options).ConfigureAwait(false);
- }
- catch (HttpException ex)
- {
- _tokens.Clear();
-
- if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500)
- {
- enableRetry = false;
- }
-
- if (!enableRetry)
- {
- throw;
- }
- }
-
- options.RequestHeaders["token"] = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false);
- return await Post(options, false, providerInfo).ConfigureAwait(false);
- }
-
- private async Task<HttpResponseInfo> Get(HttpRequestOptions options,
+ private async Task<HttpResponseMessage> Send(
+ HttpRequestMessage options,
bool enableRetry,
- ListingsProviderInfo providerInfo)
+ ListingsProviderInfo providerInfo,
+ CancellationToken cancellationToken,
+ HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
- // Schedules direct requires that the client support compression and will return a 400 response without it
- options.DecompressionMethod = CompressionMethods.Deflate;
-
try
{
- return await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false);
+ return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
}
catch (HttpException ex)
{
@@ -686,35 +632,28 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
}
- options.RequestHeaders["token"] = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false);
- return await Get(options, false, providerInfo).ConfigureAwait(false);
+ options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false));
+ return await Send(options, false, providerInfo, cancellationToken).ConfigureAwait(false);
}
- private async Task<string> GetTokenInternal(string username, string password,
+ private async Task<string> GetTokenInternal(
+ string username,
+ string password,
CancellationToken cancellationToken)
{
- var httpOptions = new HttpRequestOptions()
- {
- Url = ApiUrl + "/token",
- UserAgent = UserAgent,
- RequestContent = "{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}",
- CancellationToken = cancellationToken,
- LogErrorResponseBody = true
- };
- //_logger.LogInformation("Obtaining token from Schedules Direct from addres: " + httpOptions.Url + " with body " +
- // httpOptions.RequestContent);
+ using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
+ options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
- using (var response = await Post(httpOptions, false, null).ConfigureAwait(false))
+ using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false);
+ if (root.message == "OK")
{
- var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(response.Content).ConfigureAwait(false);
- if (root.message == "OK")
- {
- _logger.LogInformation("Authenticated with Schedules Direct token: " + root.token);
- return root.token;
- }
-
- throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message);
+ _logger.LogInformation("Authenticated with Schedules Direct token: " + root.token);
+ return root.token;
}
+
+ throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message);
}
private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken)
@@ -733,20 +672,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
_logger.LogInformation("Adding new LineUp ");
- var httpOptions = new HttpRequestOptions()
- {
- Url = ApiUrl + "/lineups/" + info.ListingsId,
- UserAgent = UserAgent,
- CancellationToken = cancellationToken,
- LogErrorResponseBody = true,
- BufferContent = false
- };
-
- httpOptions.RequestHeaders["token"] = token;
-
- using (await _httpClient.SendAsync(httpOptions, HttpMethod.Put).ConfigureAwait(false))
- {
- }
+ using var options = new HttpRequestMessage(HttpMethod.Put, ApiUrl + "/lineups/" + info.ListingsId);
+ options.Headers.TryAddWithoutValidation("token", token);
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
}
private async Task<bool> HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken)
@@ -765,25 +693,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings
_logger.LogInformation("Headends on account ");
- var options = new HttpRequestOptions()
- {
- Url = ApiUrl + "/lineups",
- UserAgent = UserAgent,
- CancellationToken = cancellationToken,
- LogErrorResponseBody = true
- };
-
- options.RequestHeaders["token"] = token;
+ using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups");
+ options.Headers.TryAddWithoutValidation("token", token);
try
{
- using (var httpResponse = await Get(options, false, null).ConfigureAwait(false))
- using (var response = httpResponse.Content)
- {
- var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(response).ConfigureAwait(false);
+ using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
+ await using var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ using var response = httpResponse.Content;
+ var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false);
- return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
- }
+ return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
}
catch (HttpException ex)
{
@@ -805,11 +725,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
throw new ArgumentException("Username is required");
}
+
if (string.IsNullOrEmpty(info.Password))
{
throw new ArgumentException("Password is required");
}
}
+
if (validateListings)
{
if (string.IsNullOrEmpty(info.ListingsId))
@@ -846,55 +768,43 @@ namespace Emby.Server.Implementations.LiveTv.Listings
throw new Exception("token required");
}
- var httpOptions = new HttpRequestOptions()
- {
- Url = ApiUrl + "/lineups/" + listingsId,
- UserAgent = UserAgent,
- CancellationToken = cancellationToken,
- LogErrorResponseBody = true,
- };
-
- httpOptions.RequestHeaders["token"] = token;
+ using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId);
+ options.Headers.TryAddWithoutValidation("token", token);
var list = new List<ChannelInfo>();
- using (var httpResponse = await Get(httpOptions, true, info).ConfigureAwait(false))
- using (var response = httpResponse.Content)
- {
- var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(response).ConfigureAwait(false);
- _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
- _logger.LogInformation("Mapping Stations to Channel");
+ using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
+ await using var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(stream).ConfigureAwait(false);
+ _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
+ _logger.LogInformation("Mapping Stations to Channel");
- var allStations = root.stations ?? Enumerable.Empty<ScheduleDirect.Station>();
+ var allStations = root.stations ?? Enumerable.Empty<ScheduleDirect.Station>();
- foreach (ScheduleDirect.Map map in root.map)
- {
- var channelNumber = GetChannelNumber(map);
-
- var station = allStations.FirstOrDefault(item => string.Equals(item.stationID, map.stationID, StringComparison.OrdinalIgnoreCase));
- if (station == null)
- {
- station = new ScheduleDirect.Station
- {
- stationID = map.stationID
- };
- }
+ foreach (ScheduleDirect.Map map in root.map)
+ {
+ var channelNumber = GetChannelNumber(map);
- var channelInfo = new ChannelInfo
- {
- Id = station.stationID,
- CallSign = station.callsign,
- Number = channelNumber,
- Name = string.IsNullOrWhiteSpace(station.name) ? channelNumber : station.name
- };
+ var station = allStations.FirstOrDefault(item => string.Equals(item.stationID, map.stationID, StringComparison.OrdinalIgnoreCase));
+ if (station == null)
+ {
+ station = new ScheduleDirect.Station { stationID = map.stationID };
+ }
- if (station.logo != null)
- {
- channelInfo.ImageUrl = station.logo.URL;
- }
+ var channelInfo = new ChannelInfo
+ {
+ Id = station.stationID,
+ CallSign = station.callsign,
+ Number = channelNumber,
+ Name = string.IsNullOrWhiteSpace(station.name) ? channelNumber : station.name
+ };
- list.Add(channelInfo);
+ if (station.logo != null)
+ {
+ channelInfo.ImageUrl = station.logo.URL;
}
+
+ list.Add(channelInfo);
}
return list;
@@ -924,7 +834,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private static string NormalizeName(string value)
{
- return value.Replace(" ", string.Empty).Replace("-", string.Empty);
+ return value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("-", string.Empty, StringComparison.Ordinal);
}
public class ScheduleDirect
@@ -932,144 +842,194 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public class Token
{
public int code { get; set; }
+
public string message { get; set; }
+
public string serverID { get; set; }
+
public string token { get; set; }
}
+
public class Lineup
{
public string lineup { get; set; }
+
public string name { get; set; }
+
public string transport { get; set; }
+
public string location { get; set; }
+
public string uri { get; set; }
}
public class Lineups
{
public int code { get; set; }
+
public string serverID { get; set; }
+
public string datetime { get; set; }
+
public List<Lineup> lineups { get; set; }
}
-
public class Headends
{
public string headend { get; set; }
+
public string transport { get; set; }
+
public string location { get; set; }
+
public List<Lineup> lineups { get; set; }
}
-
-
public class Map
{
public string stationID { get; set; }
+
public string channel { get; set; }
+
public string logicalChannelNumber { get; set; }
+
public int uhfVhf { get; set; }
+
public int atscMajor { get; set; }
+
public int atscMinor { get; set; }
}
public class Broadcaster
{
public string city { get; set; }
+
public string state { get; set; }
+
public string postalcode { get; set; }
+
public string country { get; set; }
}
public class Logo
{
public string URL { get; set; }
+
public int height { get; set; }
+
public int width { get; set; }
+
public string md5 { get; set; }
}
public class Station
{
public string stationID { get; set; }
+
public string name { get; set; }
+
public string callsign { get; set; }
+
public List<string> broadcastLanguage { get; set; }
+
public List<string> descriptionLanguage { get; set; }
+
public Broadcaster broadcaster { get; set; }
+
public string affiliate { get; set; }
+
public Logo logo { get; set; }
+
public bool? isCommercialFree { get; set; }
}
public class Metadata
{
public string lineup { get; set; }
+
public string modified { get; set; }
+
public string transport { get; set; }
}
public class Channel
{
public List<Map> map { get; set; }
+
public List<Station> stations { get; set; }
+
public Metadata metadata { get; set; }
}
public class RequestScheduleForChannel
{
public string stationID { get; set; }
+
public List<string> date { get; set; }
}
-
-
-
public class Rating
{
public string body { get; set; }
+
public string code { get; set; }
}
public class Multipart
{
public int partNumber { get; set; }
+
public int totalParts { get; set; }
}
public class Program
{
public string programID { get; set; }
+
public string airDateTime { get; set; }
+
public int duration { get; set; }
+
public string md5 { get; set; }
+
public List<string> audioProperties { get; set; }
+
public List<string> videoProperties { get; set; }
+
public List<Rating> ratings { get; set; }
+
public bool? @new { get; set; }
+
public Multipart multipart { get; set; }
+
public string liveTapeDelay { get; set; }
+
public bool premiere { get; set; }
+
public bool repeat { get; set; }
+
public string isPremiereOrFinale { get; set; }
}
-
-
public class MetadataSchedule
{
public string modified { get; set; }
+
public string md5 { get; set; }
+
public string startDate { get; set; }
+
public string endDate { get; set; }
+
public int days { get; set; }
}
public class Day
{
public string stationID { get; set; }
+
public List<Program> programs { get; set; }
+
public MetadataSchedule metadata { get; set; }
public Day()
@@ -1092,24 +1052,28 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public class Description100
{
public string descriptionLanguage { get; set; }
+
public string description { get; set; }
}
public class Description1000
{
public string descriptionLanguage { get; set; }
+
public string description { get; set; }
}
public class DescriptionsProgram
{
public List<Description100> description100 { get; set; }
+
public List<Description1000> description1000 { get; set; }
}
public class Gracenote
{
public int season { get; set; }
+
public int episode { get; set; }
}
@@ -1121,104 +1085,154 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public class ContentRating
{
public string body { get; set; }
+
public string code { get; set; }
}
public class Cast
{
public string billingOrder { get; set; }
+
public string role { get; set; }
+
public string nameId { get; set; }
+
public string personId { get; set; }
+
public string name { get; set; }
+
public string characterName { get; set; }
}
public class Crew
{
public string billingOrder { get; set; }
+
public string role { get; set; }
+
public string nameId { get; set; }
+
public string personId { get; set; }
+
public string name { get; set; }
}
public class QualityRating
{
public string ratingsBody { get; set; }
+
public string rating { get; set; }
+
public string minRating { get; set; }
+
public string maxRating { get; set; }
+
public string increment { get; set; }
}
public class Movie
{
public string year { get; set; }
+
public int duration { get; set; }
+
public List<QualityRating> qualityRating { get; set; }
}
public class Recommendation
{
public string programID { get; set; }
+
public string title120 { get; set; }
}
public class ProgramDetails
{
public string audience { get; set; }
+
public string programID { get; set; }
+
public List<Title> titles { get; set; }
+
public EventDetails eventDetails { get; set; }
+
public DescriptionsProgram descriptions { get; set; }
+
public string originalAirDate { get; set; }
+
public List<string> genres { get; set; }
+
public string episodeTitle150 { get; set; }
+
public List<MetadataPrograms> metadata { get; set; }
+
public List<ContentRating> contentRating { get; set; }
+
public List<Cast> cast { get; set; }
+
public List<Crew> crew { get; set; }
+
public string entityType { get; set; }
+
public string showType { get; set; }
+
public bool hasImageArtwork { get; set; }
+
public string primaryImage { get; set; }
+
public string thumbImage { get; set; }
+
public string backdropImage { get; set; }
+
public string bannerImage { get; set; }
+
public string imageID { get; set; }
+
public string md5 { get; set; }
+
public List<string> contentAdvisory { get; set; }
+
public Movie movie { get; set; }
+
public List<Recommendation> recommendations { get; set; }
}
public class Caption
{
public string content { get; set; }
+
public string lang { get; set; }
}
public class ImageData
{
public string width { get; set; }
+
public string height { get; set; }
+
public string uri { get; set; }
+
public string size { get; set; }
+
public string aspect { get; set; }
+
public string category { get; set; }
+
public string text { get; set; }
+
public string primary { get; set; }
+
public string tier { get; set; }
+
public Caption caption { get; set; }
}
public class ShowImages
{
public string programID { get; set; }
+
public List<ImageData> data { get; set; }
}
-
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index 07f8539c5..2d6f453bd 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -25,20 +25,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public class XmlTvListingsProvider : IListingsProvider
{
private readonly IServerConfigurationManager _config;
- private readonly IHttpClient _httpClient;
- private readonly ILogger _logger;
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly ILogger<XmlTvListingsProvider> _logger;
private readonly IFileSystem _fileSystem;
private readonly IZipClient _zipClient;
public XmlTvListingsProvider(
IServerConfigurationManager config,
- IHttpClient httpClient,
+ IHttpClientFactory httpClientFactory,
ILogger<XmlTvListingsProvider> logger,
IFileSystem fileSystem,
IZipClient zipClient)
{
_config = config;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_logger = logger;
_fileSystem = fileSystem;
_zipClient = zipClient;
@@ -78,28 +78,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
- using (var res = await _httpClient.SendAsync(
- new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = path,
- DecompressionMethod = CompressionMethods.Gzip,
- },
- HttpMethod.Get).ConfigureAwait(false))
- using (var stream = res.Content)
- using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew))
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(path, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew))
{
- if (res.ContentHeaders.ContentEncoding.Contains("gzip"))
- {
- using (var gzStream = new GZipStream(stream, CompressionMode.Decompress))
- {
- await gzStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
- }
- }
- else
- {
- await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
- }
+ await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
return UnzipIfNeeded(path, cacheFile);
@@ -224,6 +207,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
uniqueString = "-" + programInfo.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture);
}
+
if (programInfo.EpisodeNumber.HasValue)
{
uniqueString = "-" + programInfo.EpisodeNumber.Value.ToString(CultureInfo.InvariantCulture);
@@ -236,7 +220,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
&& !programInfo.IsRepeat
&& (programInfo.EpisodeNumber ?? 0) == 0)
{
- programInfo.ShowId = programInfo.ShowId + programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture);
+ programInfo.ShowId += programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture);
}
}
else
@@ -245,7 +229,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
// Construct an id from the channel and start date
- programInfo.Id = string.Format("{0}_{1:O}", program.ChannelId, program.StartDate);
+ programInfo.Id = string.Format(CultureInfo.InvariantCulture, "{0}_{1:O}", program.ChannelId, program.StartDate);
if (programInfo.IsMovie)
{
@@ -295,7 +279,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
Name = c.DisplayName,
ImageUrl = c.Icon != null && !string.IsNullOrEmpty(c.Icon.Source) ? c.Icon.Source : null,
Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number
-
}).ToList();
}
}
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
index a59c1090e..49ad73af3 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.LiveTv
private const string ServiceName = "Emby";
- private readonly ILogger _logger;
+ private readonly ILogger<LiveTvDtoService> _logger;
private readonly IImageProcessor _imageProcessor;
private readonly IDtoService _dtoService;
private readonly IApplicationHost _appHost;
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index 1b10f2d27..5bdd1c23c 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -4,20 +4,20 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
-using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
@@ -25,14 +25,14 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
namespace Emby.Server.Implementations.LiveTv
{
@@ -41,12 +41,13 @@ namespace Emby.Server.Implementations.LiveTv
/// </summary>
public class LiveTvManager : ILiveTvManager, IDisposable
{
+ private const int MaxGuideDays = 14;
private const string ExternalServiceTag = "ExternalServiceId";
private const string EtagKey = "ProgramEtag";
private readonly IServerConfigurationManager _config;
- private readonly ILogger _logger;
+ private readonly ILogger<LiveTvManager> _logger;
private readonly IItemRepository _itemRepo;
private readonly IUserManager _userManager;
private readonly IDtoService _dtoService;
@@ -54,7 +55,6 @@ namespace Emby.Server.Implementations.LiveTv
private readonly ILibraryManager _libraryManager;
private readonly ITaskManager _taskManager;
private readonly ILocalizationManager _localization;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
private readonly IChannelManager _channelManager;
private readonly LiveTvDtoService _tvDtoService;
@@ -73,7 +73,6 @@ namespace Emby.Server.Implementations.LiveTv
ILibraryManager libraryManager,
ITaskManager taskManager,
ILocalizationManager localization,
- IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
IChannelManager channelManager,
LiveTvDtoService liveTvDtoService)
@@ -85,7 +84,6 @@ namespace Emby.Server.Implementations.LiveTv
_libraryManager = libraryManager;
_taskManager = taskManager;
_localization = localization;
- _jsonSerializer = jsonSerializer;
_fileSystem = fileSystem;
_dtoService = dtoService;
_userDataManager = userDataManager;
@@ -148,27 +146,18 @@ namespace Emby.Server.Implementations.LiveTv
{
var timerId = e.Argument;
- TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>
- {
- Argument = new TimerEventInfo
- {
- Id = timerId
- }
- });
+ TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(timerId)));
}
private void OnEmbyTvTimerCreated(object sender, GenericEventArgs<TimerInfo> e)
{
var timer = e.Argument;
- TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>
- {
- Argument = new TimerEventInfo
+ TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>(
+ new TimerEventInfo(timer.Id)
{
- ProgramId = _tvDtoService.GetInternalProgramId(timer.ProgramId),
- Id = timer.Id
- }
- });
+ ProgramId = _tvDtoService.GetInternalProgramId(timer.ProgramId)
+ }));
}
public List<NameIdPair> GetTunerHostTypes()
@@ -415,8 +404,8 @@ namespace Emby.Server.Implementations.LiveTv
if (!(service is EmbyTV.EmbyTV))
{
// We can't trust that we'll be able to direct stream it through emby server, no matter what the provider says
- //mediaSource.SupportsDirectPlay = false;
- //mediaSource.SupportsDirectStream = false;
+ // mediaSource.SupportsDirectPlay = false;
+ // mediaSource.SupportsDirectStream = false;
mediaSource.SupportsTranscoding = true;
foreach (var stream in mediaSource.MediaStreams)
{
@@ -433,7 +422,7 @@ namespace Emby.Server.Implementations.LiveTv
}
}
- private LiveTvChannel GetChannel(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken)
+ private async Task<LiveTvChannel> GetChannelAsync(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken)
{
var parentFolderId = parentFolder.Id;
var isNew = false;
@@ -523,7 +512,7 @@ namespace Emby.Server.Implementations.LiveTv
}
else if (forceUpdate)
{
- _libraryManager.UpdateItem(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken);
+ await _libraryManager.UpdateItemAsync(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
}
return item;
@@ -565,13 +554,14 @@ namespace Emby.Server.Implementations.LiveTv
{
forceUpdate = true;
}
+
item.ParentId = channel.Id;
- //item.ChannelType = channelType;
+ // item.ChannelType = channelType;
item.Audio = info.Audio;
item.ChannelId = channel.Id;
- item.CommunityRating = item.CommunityRating ?? info.CommunityRating;
+ item.CommunityRating ??= info.CommunityRating;
if ((item.CommunityRating ?? 0).Equals(0))
{
item.CommunityRating = null;
@@ -584,6 +574,7 @@ namespace Emby.Server.Implementations.LiveTv
{
forceUpdate = true;
}
+
item.ExternalSeriesId = seriesId;
var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle);
@@ -598,30 +589,37 @@ namespace Emby.Server.Implementations.LiveTv
{
tags.Add("Live");
}
+
if (info.IsPremiere)
{
tags.Add("Premiere");
}
+
if (info.IsNews)
{
tags.Add("News");
}
+
if (info.IsSports)
{
tags.Add("Sports");
}
+
if (info.IsKids)
{
tags.Add("Kids");
}
+
if (info.IsRepeat)
{
tags.Add("Repeat");
}
+
if (info.IsMovie)
{
tags.Add("Movie");
}
+
if (isSeries)
{
tags.Add("Series");
@@ -644,11 +642,12 @@ namespace Emby.Server.Implementations.LiveTv
{
forceUpdate = true;
}
+
item.IsSeries = isSeries;
item.Name = info.Name;
- item.OfficialRating = item.OfficialRating ?? info.OfficialRating;
- item.Overview = item.Overview ?? info.Overview;
+ item.OfficialRating ??= info.OfficialRating;
+ item.Overview ??= info.Overview;
item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
item.ProviderIds = info.ProviderIds;
@@ -661,12 +660,14 @@ namespace Emby.Server.Implementations.LiveTv
{
forceUpdate = true;
}
+
item.StartDate = info.StartDate;
if (item.EndDate != info.EndDate)
{
forceUpdate = true;
}
+
item.EndDate = info.EndDate;
item.ProductionYear = info.ProductionYear;
@@ -683,19 +684,23 @@ namespace Emby.Server.Implementations.LiveTv
{
if (!string.IsNullOrWhiteSpace(info.ImagePath))
{
- item.SetImage(new ItemImageInfo
- {
- Path = info.ImagePath,
- Type = ImageType.Primary
- }, 0);
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = info.ImagePath,
+ Type = ImageType.Primary
+ },
+ 0);
}
else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
{
- item.SetImage(new ItemImageInfo
- {
- Path = info.ImageUrl,
- Type = ImageType.Primary
- }, 0);
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = info.ImageUrl,
+ Type = ImageType.Primary
+ },
+ 0);
}
}
@@ -703,12 +708,13 @@ namespace Emby.Server.Implementations.LiveTv
{
if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
{
- item.SetImage(new ItemImageInfo
- {
- Path = info.ThumbImageUrl,
- Type = ImageType.Thumb
-
- }, 0);
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = info.ThumbImageUrl,
+ Type = ImageType.Thumb
+ },
+ 0);
}
}
@@ -716,12 +722,13 @@ namespace Emby.Server.Implementations.LiveTv
{
if (!string.IsNullOrWhiteSpace(info.LogoImageUrl))
{
- item.SetImage(new ItemImageInfo
- {
- Path = info.LogoImageUrl,
- Type = ImageType.Logo
-
- }, 0);
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = info.LogoImageUrl,
+ Type = ImageType.Logo
+ },
+ 0);
}
}
@@ -729,12 +736,13 @@ namespace Emby.Server.Implementations.LiveTv
{
if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl))
{
- item.SetImage(new ItemImageInfo
- {
- Path = info.BackdropImageUrl,
- Type = ImageType.Backdrop
-
- }, 0);
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = info.BackdropImageUrl,
+ Type = ImageType.Backdrop
+ },
+ 0);
}
}
@@ -771,7 +779,8 @@ namespace Emby.Server.Implementations.LiveTv
var dto = _dtoService.GetBaseItemDto(program, new DtoOptions(), user);
- var list = new List<Tuple<BaseItemDto, string, string>>() {
+ var list = new List<Tuple<BaseItemDto, string, string>>
+ {
new Tuple<BaseItemDto, string, string>(dto, program.ExternalId, program.ExternalSeriesId)
};
@@ -788,22 +797,11 @@ namespace Emby.Server.Implementations.LiveTv
if (query.OrderBy.Count == 0)
{
- if (query.IsAiring ?? false)
- {
- // Unless something else was specified, order by start date to take advantage of a specialized index
- query.OrderBy = new[]
- {
- (ItemSortBy.StartDate, SortOrder.Ascending)
- };
- }
- else
+ // Unless something else was specified, order by start date to take advantage of a specialized index
+ query.OrderBy = new[]
{
- // Unless something else was specified, order by start date to take advantage of a specialized index
- query.OrderBy = new[]
- {
- (ItemSortBy.StartDate, SortOrder.Ascending)
- };
- }
+ (ItemSortBy.StartDate, SortOrder.Ascending)
+ };
}
RemoveFields(options);
@@ -836,7 +834,7 @@ namespace Emby.Server.Implementations.LiveTv
if (!string.IsNullOrWhiteSpace(query.SeriesTimerId))
{
- var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery { }, cancellationToken).ConfigureAwait(false);
+ var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false);
var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.Id).ToString("N", CultureInfo.InvariantCulture), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase));
if (seriesTimer != null)
{
@@ -859,13 +857,11 @@ namespace Emby.Server.Implementations.LiveTv
var returnArray = _dtoService.GetBaseItemDtos(queryResult.Items, options, user);
- var result = new QueryResult<BaseItemDto>
+ return new QueryResult<BaseItemDto>
{
Items = returnArray,
TotalRecordCount = queryResult.TotalRecordCount
};
-
- return result;
}
public QueryResult<BaseItem> GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken)
@@ -1133,7 +1129,7 @@ namespace Emby.Server.Implementations.LiveTv
try
{
- var item = GetChannel(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken);
+ var item = await GetChannelAsync(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken).ConfigureAwait(false);
list.Add(item);
}
@@ -1150,7 +1146,7 @@ namespace Emby.Server.Implementations.LiveTv
double percent = numComplete;
percent /= allChannelsList.Count;
- progress.Report(5 * percent + 10);
+ progress.Report((5 * percent) + 10);
}
progress.Report(15);
@@ -1185,11 +1181,9 @@ namespace Emby.Server.Implementations.LiveTv
var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
{
-
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
ChannelIds = new Guid[] { currentChannel.Id },
DtoOptions = new DtoOptions(true)
-
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
var newPrograms = new List<LiveTvProgram>();
@@ -1227,7 +1221,11 @@ namespace Emby.Server.Implementations.LiveTv
if (updatedPrograms.Count > 0)
{
- _libraryManager.UpdateItems(updatedPrograms, currentChannel, ItemUpdateType.MetadataImport, cancellationToken);
+ await _libraryManager.UpdateItemsAsync(
+ updatedPrograms,
+ currentChannel,
+ ItemUpdateType.MetadataImport,
+ cancellationToken).ConfigureAwait(false);
}
currentChannel.IsMovie = isMovie;
@@ -1240,7 +1238,7 @@ namespace Emby.Server.Implementations.LiveTv
currentChannel.AddTag("Kids");
}
- currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken);
+ await currentChannel.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
await currentChannel.RefreshMetadata(
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
@@ -1311,8 +1309,6 @@ namespace Emby.Server.Implementations.LiveTv
}
}
- private const int MaxGuideDays = 14;
-
private double GetGuideDays()
{
var config = GetConfiguration();
@@ -1389,10 +1385,10 @@ namespace Emby.Server.Implementations.LiveTv
// limit = (query.Limit ?? 10) * 2;
limit = null;
- //var allActivePaths = EmbyTV.EmbyTV.Current.GetAllActiveRecordings().Select(i => i.Path).ToArray();
- //var items = allActivePaths.Select(i => _libraryManager.FindByPath(i, false)).Where(i => i != null).ToArray();
+ // var allActivePaths = EmbyTV.EmbyTV.Current.GetAllActiveRecordings().Select(i => i.Path).ToArray();
+ // var items = allActivePaths.Select(i => _libraryManager.FindByPath(i, false)).Where(i => i != null).ToArray();
- //return new QueryResult<BaseItem>
+ // return new QueryResult<BaseItem>
//{
// Items = items,
// TotalRecordCount = items.Length
@@ -1725,7 +1721,7 @@ namespace Emby.Server.Implementations.LiveTv
if (timer == null)
{
- throw new ResourceNotFoundException(string.Format("Timer with Id {0} not found", id));
+ throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "Timer with Id {0} not found", id));
}
var service = GetService(timer.ServiceName);
@@ -1734,13 +1730,7 @@ namespace Emby.Server.Implementations.LiveTv
if (!(service is EmbyTV.EmbyTV))
{
- TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>
- {
- Argument = new TimerEventInfo
- {
- Id = id
- }
- });
+ TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(id)));
}
}
@@ -1750,29 +1740,24 @@ namespace Emby.Server.Implementations.LiveTv
if (timer == null)
{
- throw new ResourceNotFoundException(string.Format("SeriesTimer with Id {0} not found", id));
+ throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "SeriesTimer with Id {0} not found", id));
}
var service = GetService(timer.ServiceName);
await service.CancelSeriesTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false);
- SeriesTimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>
- {
- Argument = new TimerEventInfo
- {
- Id = id
- }
- });
+ SeriesTimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(id)));
}
public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken)
{
- var results = await GetTimers(new TimerQuery
- {
- Id = id
-
- }, cancellationToken).ConfigureAwait(false);
+ var results = await GetTimers(
+ new TimerQuery
+ {
+ Id = id
+ },
+ cancellationToken).ConfigureAwait(false);
return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
}
@@ -1820,11 +1805,7 @@ namespace Emby.Server.Implementations.LiveTv
}
var returnArray = timers
- .Select(i =>
- {
- return i.Item1;
-
- })
+ .Select(i => i.Item1)
.ToArray();
return new QueryResult<SeriesTimerInfo>
@@ -1878,7 +1859,6 @@ namespace Emby.Server.Implementations.LiveTv
}
return _tvDtoService.GetSeriesTimerInfoDto(i.Item1, i.Item2, channelName);
-
})
.ToArray();
@@ -1911,7 +1891,6 @@ namespace Emby.Server.Implementations.LiveTv
OrderBy = new[] { (ItemSortBy.StartDate, SortOrder.Ascending) },
TopParentIds = new[] { GetInternalLiveTvFolder(CancellationToken.None).Id },
DtoOptions = options
-
}) : new List<BaseItem>();
RemoveFields(options);
@@ -1989,7 +1968,7 @@ namespace Emby.Server.Implementations.LiveTv
OriginalAirDate = program.PremiereDate,
Overview = program.Overview,
StartDate = program.StartDate,
- //ImagePath = program.ExternalImagePath,
+ // ImagePath = program.ExternalImagePath,
Name = program.Name,
OfficialRating = program.OfficialRating
};
@@ -1997,7 +1976,7 @@ namespace Emby.Server.Implementations.LiveTv
if (service == null)
{
- service = _services.First();
+ service = _services[0];
}
var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false);
@@ -2023,9 +2002,7 @@ namespace Emby.Server.Implementations.LiveTv
{
var info = await GetNewTimerDefaultsInternal(cancellationToken).ConfigureAwait(false);
- var obj = _tvDtoService.GetSeriesTimerInfoDto(info.Item1, info.Item2, null);
-
- return obj;
+ return _tvDtoService.GetSeriesTimerInfoDto(info.Item1, info.Item2, null);
}
public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(string programId, CancellationToken cancellationToken)
@@ -2082,14 +2059,11 @@ namespace Emby.Server.Implementations.LiveTv
if (!(service is EmbyTV.EmbyTV))
{
- TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>
- {
- Argument = new TimerEventInfo
+ TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>(
+ new TimerEventInfo(newTimerId)
{
- ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId),
- Id = newTimerId
- }
- });
+ ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId)
+ }));
}
}
@@ -2114,14 +2088,11 @@ namespace Emby.Server.Implementations.LiveTv
await service.CreateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false);
}
- SeriesTimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>
- {
- Argument = new TimerEventInfo
+ SeriesTimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>(
+ new TimerEventInfo(newTimerId)
{
- ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId),
- Id = newTimerId
- }
- });
+ ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId)
+ }));
}
public async Task UpdateTimer(TimerInfoDto timer, CancellationToken cancellationToken)
@@ -2160,9 +2131,11 @@ namespace Emby.Server.Implementations.LiveTv
public void Dispose()
{
Dispose(true);
+ GC.SuppressFinalize(this);
}
private bool _disposed = false;
+
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
@@ -2206,20 +2179,19 @@ namespace Emby.Server.Implementations.LiveTv
var info = new LiveTvInfo
{
Services = services,
- IsEnabled = services.Length > 0
+ IsEnabled = services.Length > 0,
+ EnabledUsers = _userManager.Users
+ .Where(IsLiveTvEnabled)
+ .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
+ .ToArray()
};
- info.EnabledUsers = _userManager.Users
- .Where(IsLiveTvEnabled)
- .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
- .ToArray();
-
return info;
}
private bool IsLiveTvEnabled(User user)
{
- return user.Policy.EnableLiveTvAccess && (Services.Count > 1 || GetConfiguration().TunerHosts.Length > 0);
+ return user.HasPermission(PermissionKind.EnableLiveTvAccess) && (Services.Count > 1 || GetConfiguration().TunerHosts.Length > 0);
}
public IEnumerable<User> GetEnabledUsers()
@@ -2267,7 +2239,7 @@ namespace Emby.Server.Implementations.LiveTv
public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
{
- info = _jsonSerializer.DeserializeFromString<TunerHostInfo>(_jsonSerializer.SerializeToString(info));
+ info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.Serialize(info));
var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
@@ -2311,7 +2283,7 @@ namespace Emby.Server.Implementations.LiveTv
{
// Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
// ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider
- info = _jsonSerializer.DeserializeFromString<ListingsProviderInfo>(_jsonSerializer.SerializeToString(info));
+ info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.Serialize(info));
var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
@@ -2483,8 +2455,7 @@ namespace Emby.Server.Implementations.LiveTv
.SelectMany(i => i.Locations)
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(i => _libraryManager.FindByPath(i, true))
- .Where(i => i != null)
- .Where(i => i.IsVisibleStandalone(user))
+ .Where(i => i != null && i.IsVisibleStandalone(user))
.SelectMany(i => _libraryManager.GetCollectionFolders(i))
.GroupBy(x => x.Id)
.Select(x => x.First())
@@ -2496,7 +2467,6 @@ namespace Emby.Server.Implementations.LiveTv
UserId = user.Id,
IsRecordingsFolder = true,
RefreshLatestChannelItems = refreshChannels
-
}).Items);
return folders.Cast<BaseItem>().ToList();
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
index 7f63991d0..8a0c0043a 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
@@ -19,11 +19,10 @@ namespace Emby.Server.Implementations.LiveTv
public class LiveTvMediaSourceProvider : IMediaSourceProvider
{
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
- private const char StreamIdDelimeter = '_';
- private const string StreamIdDelimeterString = "_";
+ private const char StreamIdDelimiter = '_';
private readonly ILiveTvManager _liveTvManager;
- private readonly ILogger _logger;
+ private readonly ILogger<LiveTvMediaSourceProvider> _logger;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerApplicationHost _appHost;
@@ -47,7 +46,7 @@ namespace Emby.Server.Implementations.LiveTv
}
}
- return Task.FromResult<IEnumerable<MediaSourceInfo>>(Array.Empty<MediaSourceInfo>());
+ return Task.FromResult(Enumerable.Empty<MediaSourceInfo>());
}
private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(BaseItem item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
@@ -98,7 +97,7 @@ namespace Emby.Server.Implementations.LiveTv
source.Id ?? string.Empty
};
- source.OpenToken = string.Join(StreamIdDelimeterString, openKeys);
+ source.OpenToken = string.Join(StreamIdDelimiter, openKeys);
}
// Dummy this up so that direct play checks can still run
@@ -116,7 +115,7 @@ namespace Emby.Server.Implementations.LiveTv
/// <inheritdoc />
public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
- var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);
+ var keys = openToken.Split(StreamIdDelimiter, 3);
var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
index 1056a33b9..582b64923 100644
--- a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
+++ b/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -33,7 +35,7 @@ namespace Emby.Server.Implementations.LiveTv
}
/// <summary>
- /// Creates the triggers that define when the task will run
+ /// Creates the triggers that define when the task will run.
/// </summary>
/// <returns>IEnumerable{BaseTaskTrigger}.</returns>
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
@@ -41,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv
return new[]
{
// Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
};
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index 80ee1ee33..fbcd4ef37 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -1,10 +1,10 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -14,7 +14,7 @@ using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
-using MediaBrowser.Model.Serialization;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts
@@ -22,18 +22,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public abstract class BaseTunerHost
{
protected readonly IServerConfigurationManager Config;
- protected readonly ILogger Logger;
- protected IJsonSerializer JsonSerializer;
+ protected readonly ILogger<BaseTunerHost> Logger;
protected readonly IFileSystem FileSystem;
- private readonly ConcurrentDictionary<string, ChannelCache> _channelCache =
- new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase);
+ private readonly IMemoryCache _memoryCache;
- protected BaseTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem)
+ protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache)
{
Config = config;
Logger = logger;
- JsonSerializer = jsonSerializer;
+ _memoryCache = memoryCache;
FileSystem = fileSystem;
}
@@ -44,23 +42,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)
{
- ChannelCache cache = null;
var key = tuner.Id;
- if (enableCache && !string.IsNullOrEmpty(key) && _channelCache.TryGetValue(key, out cache))
+ if (enableCache && !string.IsNullOrEmpty(key) && _memoryCache.TryGetValue(key, out List<ChannelInfo> cache))
{
- return cache.Channels.ToList();
+ return cache;
}
- var result = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false);
- var list = result.ToList();
- //logger.LogInformation("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list));
+ var list = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false);
+ // logger.LogInformation("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list));
if (!string.IsNullOrEmpty(key) && list.Count > 0)
{
- cache = cache ?? new ChannelCache();
- cache.Channels = list;
- _channelCache.AddOrUpdate(key, cache, (k, v) => cache);
+ _memoryCache.Set(key, list);
}
return list;
@@ -95,11 +89,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
try
{
Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile));
- JsonSerializer.SerializeToFile(channels, channelCacheFile);
+ await using var writeStream = File.OpenWrite(channelCacheFile);
+ await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false);
}
catch (IOException)
{
-
}
}
}
@@ -111,12 +105,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
try
{
- var channels = JsonSerializer.DeserializeFromFile<List<ChannelInfo>>(channelCacheFile);
+ await using var readStream = File.OpenRead(channelCacheFile);
+ var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
list.AddRange(channels);
}
catch (IOException)
{
-
}
}
}
@@ -235,10 +230,5 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
return Config.GetConfiguration<LiveTvOptions>("livetv");
}
-
- private class ChannelCache
- {
- public List<ChannelInfo> Channels;
- }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 25b2c674c..2f4c60117 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -23,32 +24,34 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager;
private readonly IStreamHelper _streamHelper;
+ private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
+
public HdHomerunHost(
IServerConfigurationManager config,
ILogger<HdHomerunHost> logger,
- IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
- IHttpClient httpClient,
+ IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
ISocketFactory socketFactory,
INetworkManager networkManager,
- IStreamHelper streamHelper)
- : base(config, logger, jsonSerializer, fileSystem)
+ IStreamHelper streamHelper,
+ IMemoryCache memoryCache)
+ : base(config, logger, fileSystem, memoryCache)
{
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_appHost = appHost;
_socketFactory = socketFactory;
_networkManager = networkManager;
@@ -68,25 +71,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
- var options = new HttpRequestOptions
- {
- Url = model.LineupURL,
- CancellationToken = cancellationToken,
- BufferContent = false
- };
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken)
+ .ConfigureAwait(false) ?? new List<Channels>();
- using (var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false))
- using (var stream = response.Content)
+ if (info.ImportFavoritesOnly)
{
- var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>();
-
- if (info.ImportFavoritesOnly)
- {
- lineup = lineup.Where(i => i.Favorite).ToList();
- }
-
- return lineup.Where(i => !i.DRM).ToList();
+ lineup = lineup.Where(i => i.Favorite).ToList();
}
+
+ return lineup.Where(i => !i.DRM).ToList();
}
private class HdHomerunChannelInfo : ChannelInfo
@@ -111,11 +106,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
ChannelType = ChannelType.TV,
IsLegacyTuner = (i.URL ?? string.Empty).StartsWith("hdhomerun", StringComparison.OrdinalIgnoreCase),
Path = i.URL
-
}).Cast<ChannelInfo>().ToList();
}
- private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
private async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
{
var cacheKey = info.Id;
@@ -133,35 +126,31 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
try
{
- using (var response = await _httpClient.SendAsync(new HttpRequestOptions()
- {
- Url = string.Format("{0}/discover.json", GetApiUrl(info)),
- CancellationToken = cancellationToken,
- BufferContent = false
- }, HttpMethod.Get).ConfigureAwait(false))
- using (var stream = response.Content)
- {
- var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false);
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
+ .ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
- if (!string.IsNullOrEmpty(cacheKey))
+ if (!string.IsNullOrEmpty(cacheKey))
+ {
+ lock (_modelCache)
{
- lock (_modelCache)
- {
- _modelCache[cacheKey] = discoverResponse;
- }
+ _modelCache[cacheKey] = discoverResponse;
}
-
- return discoverResponse;
}
+
+ return discoverResponse;
}
catch (HttpException ex)
{
- if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
+ if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
{
- var defaultValue = "HDHR";
+ const string DefaultValue = "HDHR";
var response = new DiscoverResponse
{
- ModelNumber = defaultValue
+ ModelNumber = DefaultValue
};
if (!string.IsNullOrEmpty(cacheKey))
{
@@ -171,6 +160,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_modelCache[cacheKey] = response;
}
}
+
return response;
}
@@ -182,42 +172,50 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
- using (var response = await _httpClient.SendAsync(new HttpRequestOptions()
- {
- Url = string.Format("{0}/tuners.html", GetApiUrl(info)),
- CancellationToken = cancellationToken,
- BufferContent = false
- }, HttpMethod.Get).ConfigureAwait(false))
- using (var stream = response.Content)
- using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8))
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
+ .ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
+ var tuners = new List<LiveTvTunerInfo>();
+ while (!sr.EndOfStream)
{
- var tuners = new List<LiveTvTunerInfo>();
- while (!sr.EndOfStream)
+ string line = StripXML(sr.ReadLine());
+ if (line.Contains("Channel", StringComparison.Ordinal))
{
- string line = StripXML(sr.ReadLine());
- if (line.Contains("Channel"))
+ LiveTvTunerStatus status;
+ var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
+ var name = line.Substring(0, index - 1);
+ var currentChannel = line.Substring(index + 7);
+ if (currentChannel != "none")
{
- LiveTvTunerStatus status;
- var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
- var name = line.Substring(0, index - 1);
- var currentChannel = line.Substring(index + 7);
- if (currentChannel != "none") { status = LiveTvTunerStatus.LiveTv; } else { status = LiveTvTunerStatus.Available; }
- tuners.Add(new LiveTvTunerInfo
- {
- Name = name,
- SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
- ProgramName = currentChannel,
- Status = status
- });
+ status = LiveTvTunerStatus.LiveTv;
+ }
+ else
+ {
+ status = LiveTvTunerStatus.Available;
}
- }
- return tuners;
+ tuners.Add(new LiveTvTunerInfo
+ {
+ Name = name,
+ SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
+ ProgramName = currentChannel,
+ Status = status
+ });
+ }
}
+
+ return tuners;
}
private static string StripXML(string source)
{
+ if (string.IsNullOrEmpty(source))
+ {
+ return string.Empty;
+ }
+
char[] buffer = new char[source.Length];
int bufferIndex = 0;
bool inside = false;
@@ -230,11 +228,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
inside = true;
continue;
}
+
if (let == '>')
{
inside = false;
continue;
}
+
if (!inside)
{
buffer[bufferIndex] = let;
@@ -260,7 +260,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
for (int i = 0; i < model.TunerCount; ++i)
{
- var name = string.Format("Tuner {0}", i + 1);
+ var name = string.Format(CultureInfo.InvariantCulture, "Tuner {0}", i + 1);
var currentChannel = "none"; // @todo Get current channel and map back to Station Id
var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false);
var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv;
@@ -332,12 +332,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private class Channels
{
public string GuideNumber { get; set; }
+
public string GuideName { get; set; }
+
public string VideoCodec { get; set; }
+
public string AudioCodec { get; set; }
+
public string URL { get; set; }
+
public bool Favorite { get; set; }
+
public bool DRM { get; set; }
+
public int HD { get; set; }
}
@@ -481,7 +488,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Height = height,
BitRate = videoBitrate,
NalLengthSize = nal
-
},
new MediaStream
{
@@ -502,8 +508,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
SupportsTranscoding = true,
IsInfiniteStream = true,
IgnoreDts = true,
- //IgnoreIndex = true,
- //ReadAtNativeFramerate = true
+ // IgnoreIndex = true,
+ // ReadAtNativeFramerate = true
};
mediaSource.InferTotalBitrate();
@@ -557,6 +563,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
+ var tunerCount = info.TunerCount;
+
+ if (tunerCount > 0)
+ {
+ var tunerHostId = info.Id;
+ var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase));
+
+ if (liveStreams.Count() >= tunerCount)
+ {
+ throw new LiveTvConflictException("HDHomeRun simultaneous stream limit has been reached.");
+ }
+ }
+
var profile = streamId.Split('_')[0];
Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelInfo.Id, streamId, profile);
@@ -610,7 +629,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
info,
streamId,
FileSystem,
- _httpClient,
+ _httpClientFactory,
Logger,
Config,
_appHost,
@@ -659,13 +678,21 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
public class DiscoverResponse
{
public string FriendlyName { get; set; }
+
public string ModelNumber { get; set; }
+
public string FirmwareName { get; set; }
+
public string FirmwareVersion { get; set; }
+
public string DeviceID { get; set; }
+
public string DeviceAuth { get; set; }
+
public string BaseURL { get; set; }
+
public string LineupURL { get; set; }
+
public int TunerCount { get; set; }
public bool SupportsTranscoding
@@ -674,7 +701,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = ModelNumber ?? string.Empty;
- if ((model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1))
+ if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
{
return true;
}
@@ -701,7 +728,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
// Need a way to set the Receive timeout on the socket otherwise this might never timeout?
try
{
- await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken);
+ await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken).ConfigureAwait(false);
var receiveBuffer = new byte[8192];
while (!cancellationToken.IsCancellationRequested)
@@ -722,7 +749,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
}
-
}
catch (OperationCanceledException)
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index 57c5b7500..d4a88e299 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
- public class HdHomerunManager : IDisposable
+ public sealed class HdHomerunManager : IDisposable
{
public const int HdHomeRunPort = 65001;
@@ -105,6 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
StopStreaming(socket).GetAwaiter().GetResult();
}
}
+
+ GC.SuppressFinalize(this);
}
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
@@ -162,7 +164,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
_activeTuner = i;
- var lockKeyString = string.Format("{0:d}", lockKeyValue);
+ var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
@@ -173,8 +175,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
continue;
}
- var commandList = commands.GetCommands();
- foreach (var command in commandList)
+ foreach (var command in commands.GetCommands())
{
var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
@@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
- var targetValue = string.Format("rtp://{0}:{1}", localIp, localPort);
+ var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index 82b1f3cf1..6730751d5 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -117,17 +117,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
taskCompletionSource,
LiveStreamCancellationTokenSource.Token).ConfigureAwait(false);
- //OpenedMediaSource.Protocol = MediaProtocol.File;
- //OpenedMediaSource.Path = tempFile;
- //OpenedMediaSource.ReadAtNativeFramerate = true;
+ // OpenedMediaSource.Protocol = MediaProtocol.File;
+ // OpenedMediaSource.Path = tempFile;
+ // OpenedMediaSource.ReadAtNativeFramerate = true;
MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
MediaSource.Protocol = MediaProtocol.Http;
- //OpenedMediaSource.SupportsDirectPlay = false;
- //OpenedMediaSource.SupportsDirectStream = true;
- //OpenedMediaSource.SupportsTranscoding = true;
+ // OpenedMediaSource.SupportsDirectPlay = false;
+ // OpenedMediaSource.SupportsDirectStream = true;
+ // OpenedMediaSource.SupportsTranscoding = true;
- //await Task.Delay(5000).ConfigureAwait(false);
+ // await Task.Delay(5000).ConfigureAwait(false);
await taskCompletionSource.Task.ConfigureAwait(false);
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
index 4e4f1d7f6..0333e723b 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
@@ -58,12 +58,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
protected virtual int EmptyReadLimit => 1000;
public MediaSourceInfo OriginalMediaSource { get; set; }
+
public MediaSourceInfo MediaSource { get; set; }
public int ConsumerCount { get; set; }
public string OriginalStreamId { get; set; }
+
public bool EnableStreamSharing { get; set; }
+
public string UniqueId { get; }
public string TunerHostId { get; }
@@ -220,11 +223,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
catch (IOException)
{
-
}
catch (ArgumentException)
{
-
}
catch (Exception ex)
{
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index f7c9c736e..8107bc427 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
@@ -18,7 +19,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Serialization;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
@@ -26,7 +27,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
private readonly INetworkManager _networkManager;
private readonly IMediaSourceManager _mediaSourceManager;
@@ -36,15 +37,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
IServerConfigurationManager config,
IMediaSourceManager mediaSourceManager,
ILogger<M3UTunerHost> logger,
- IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
- IHttpClient httpClient,
+ IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
INetworkManager networkManager,
- IStreamHelper streamHelper)
- : base(config, logger, jsonSerializer, fileSystem)
+ IStreamHelper streamHelper,
+ IMemoryCache memoryCache)
+ : base(config, logger, fileSystem, memoryCache)
{
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_appHost = appHost;
_networkManager = networkManager;
_mediaSourceManager = mediaSourceManager;
@@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var channelIdPrefix = GetFullChannelIdPrefix(info);
- return await new M3uParser(Logger, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
+ return await new M3uParser(Logger, _httpClientFactory, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
}
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
@@ -116,7 +117,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
- return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config, _appHost, _streamHelper);
+ return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
}
}
@@ -125,9 +126,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task Validate(TunerHostInfo info)
{
- using (var stream = await new M3uParser(Logger, _httpClient, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
+ using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
{
-
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index 59451fccd..f066a749e 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
@@ -19,13 +20,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public class M3uParser
{
private readonly ILogger _logger;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
- public M3uParser(ILogger logger, IHttpClient httpClient, IServerApplicationHost appHost)
+ public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory, IServerApplicationHost appHost)
{
_logger = logger;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_appHost = appHost;
}
@@ -51,13 +52,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
- return _httpClient.Get(new HttpRequestOptions
- {
- Url = url,
- CancellationToken = cancellationToken,
- // Some data providers will require a user agent
- UserAgent = _appHost.ApplicationUserAgent
- });
+ return _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetStreamAsync(url);
}
return Task.FromResult((Stream)File.OpenRead(url));
@@ -158,15 +154,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl)
{
var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- var nameInExtInf = nameParts.Length > 1 ? nameParts[nameParts.Length - 1].Trim() : null;
+ var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty;
string numberString = null;
string attributeValue;
- double doubleValue;
if (attributes.TryGetValue("tvg-chno", out attributeValue))
{
- if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue))
+ if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{
numberString = attributeValue;
}
@@ -176,41 +171,40 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
if (attributes.TryGetValue("tvg-id", out attributeValue))
{
- if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue))
+ if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{
numberString = attributeValue;
}
else if (attributes.TryGetValue("channel-id", out attributeValue))
{
- if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue))
+ if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{
numberString = attributeValue;
}
}
}
- if (String.IsNullOrWhiteSpace(numberString))
+ if (string.IsNullOrWhiteSpace(numberString))
{
// Using this as a fallback now as this leads to Problems with channels like "5 USA"
// where 5 isnt ment to be the channel number
// Check for channel number with the format from SatIp
// #EXTINF:0,84. VOX Schweiz
// #EXTINF:0,84.0 - VOX Schweiz
- if (!string.IsNullOrWhiteSpace(nameInExtInf))
+ if (!nameInExtInf.IsEmpty && !nameInExtInf.IsWhiteSpace())
{
var numberIndex = nameInExtInf.IndexOf(' ');
if (numberIndex > 0)
{
- var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
+ var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' });
- if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out var number))
+ if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{
- numberString = numberPart;
+ numberString = numberPart.ToString();
}
}
}
}
-
}
if (!IsValidChannelNumber(numberString))
@@ -232,7 +226,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
try
{
- numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/').Last());
+ numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/')[^1]);
if (!IsValidChannelNumber(numberString))
{
@@ -259,7 +253,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return false;
}
- if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
+ if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{
return false;
}
@@ -282,9 +276,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
- if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out var number))
+ if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
{
- //channel.Number = number.ToString();
+ // channel.Number = number.ToString();
nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' });
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index 322fbbbaa..6c10fca8c 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class SharedHttpStream : LiveStream, IDirectStreamProvider
{
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
public SharedHttpStream(
@@ -29,14 +29,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
TunerHostInfo tunerHostInfo,
string originalStreamId,
IFileSystem fileSystem,
- IHttpClient httpClient,
+ IHttpClientFactory httpClientFactory,
ILogger logger,
IConfigurationManager configurationManager,
IServerApplicationHost appHost,
IStreamHelper streamHelper)
: base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper)
{
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_appHost = appHost;
OriginalStreamId = originalStreamId;
EnableStreamSharing = true;
@@ -55,25 +55,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var typeName = GetType().Name;
Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
- var httpRequestOptions = new HttpRequestOptions
- {
- Url = url,
- CancellationToken = CancellationToken.None,
- BufferContent = false,
- DecompressionMethod = CompressionMethods.None
- };
-
- foreach (var header in mediaSource.RequiredHttpHeaders)
- {
- httpRequestOptions.RequestHeaders[header.Key] = header.Value;
- }
-
- var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false);
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
+ .ConfigureAwait(false);
var extension = "ts";
var requiresRemux = false;
- var contentType = response.ContentType ?? string.Empty;
+ var contentType = response.Content.Headers.ContentType.ToString();
if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1)
{
requiresRemux = true;
@@ -103,21 +92,21 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
_ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
- //OpenedMediaSource.Protocol = MediaProtocol.File;
- //OpenedMediaSource.Path = tempFile;
- //OpenedMediaSource.ReadAtNativeFramerate = true;
+ // OpenedMediaSource.Protocol = MediaProtocol.File;
+ // OpenedMediaSource.Path = tempFile;
+ // OpenedMediaSource.ReadAtNativeFramerate = true;
MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
MediaSource.Protocol = MediaProtocol.Http;
- //OpenedMediaSource.Path = TempFilePath;
- //OpenedMediaSource.Protocol = MediaProtocol.File;
+ // OpenedMediaSource.Path = TempFilePath;
+ // OpenedMediaSource.Protocol = MediaProtocol.File;
- //OpenedMediaSource.Path = _tempFilePath;
- //OpenedMediaSource.Protocol = MediaProtocol.File;
- //OpenedMediaSource.SupportsDirectPlay = false;
- //OpenedMediaSource.SupportsDirectStream = true;
- //OpenedMediaSource.SupportsTranscoding = true;
+ // OpenedMediaSource.Path = _tempFilePath;
+ // OpenedMediaSource.Protocol = MediaProtocol.File;
+ // OpenedMediaSource.SupportsDirectPlay = false;
+ // OpenedMediaSource.SupportsDirectStream = true;
+ // OpenedMediaSource.SupportsTranscoding = true;
await taskCompletionSource.Task.ConfigureAwait(false);
if (taskCompletionSource.Task.Exception != null)
{
@@ -132,24 +121,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
}
- private Task StartStreaming(HttpResponseInfo response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
+ private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
return Task.Run(async () =>
{
try
{
Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
- using (response)
- using (var stream = response.Content)
- using (var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read))
- {
- await StreamHelper.CopyToAsync(
- stream,
- fileStream,
- IODefaults.CopyToBufferSize,
- () => Resolve(openTaskCompletionSource),
- cancellationToken).ConfigureAwait(false);
- }
+ using var message = response;
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read);
+ await StreamHelper.CopyToAsync(
+ stream,
+ fileStream,
+ IODefaults.CopyToBufferSize,
+ () => Resolve(openTaskCompletionSource),
+ cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException ex)
{
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index 20447347b..977a1c2d7 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -1,29 +1,29 @@
{
"Artists": "Kunstenare",
"Channels": "Kanale",
- "Folders": "Fouers",
- "Favorites": "Gunstelinge",
+ "Folders": "Lêergidse",
+ "Favorites": "Gunstellinge",
"HeaderFavoriteShows": "Gunsteling Vertonings",
"ValueSpecialEpisodeName": "Spesiale - {0}",
"HeaderAlbumArtists": "Album Kunstenaars",
"Books": "Boeke",
"HeaderNextUp": "Volgende",
- "Movies": "Rolprente",
- "Shows": "Program",
- "HeaderContinueWatching": "Hou Aan Kyk",
+ "Movies": "Flieks",
+ "Shows": "Televisie Reekse",
+ "HeaderContinueWatching": "Kyk Verder",
"HeaderFavoriteEpisodes": "Gunsteling Episodes",
"Photos": "Fotos",
- "Playlists": "Speellysse",
+ "Playlists": "Snitlyste",
"HeaderFavoriteArtists": "Gunsteling Kunstenaars",
"HeaderFavoriteAlbums": "Gunsteling Albums",
"Sync": "Sinkroniseer",
"HeaderFavoriteSongs": "Gunsteling Liedjies",
"Songs": "Liedjies",
- "DeviceOnlineWithName": "{0} is verbind",
- "DeviceOfflineWithName": "{0} het afgesluit",
+ "DeviceOnlineWithName": "{0} gekoppel is",
+ "DeviceOfflineWithName": "{0} is ontkoppel",
"Collections": "Versamelings",
"Inherit": "Ontvang",
- "HeaderLiveTV": "Live TV",
+ "HeaderLiveTV": "Lewendige TV",
"Application": "Program",
"AppDeviceValues": "App: {0}, Toestel: {1}",
"VersionNumber": "Weergawe {0}",
@@ -85,11 +85,32 @@
"ItemAddedWithName": "{0} is in die versameling",
"HomeVideos": "Tuis opnames",
"HeaderRecordingGroups": "Groep Opnames",
- "HeaderCameraUploads": "Kamera Oplaai",
"Genres": "Genres",
"FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}",
"ChapterNameValue": "Hoofstuk",
"CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}",
"AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
- "Albums": "Albums"
+ "Albums": "Albums",
+ "TasksChannelsCategory": "Internet kanale",
+ "TasksApplicationCategory": "aansoek",
+ "TasksLibraryCategory": "biblioteek",
+ "TasksMaintenanceCategory": "onderhoud",
+ "TaskCleanCacheDescription": "Vee kasregister lêers uit wat nie meer deur die stelsel benodig word nie.",
+ "TaskCleanCache": "Reinig Kasgeheue Lêergids",
+ "TaskDownloadMissingSubtitlesDescription": "Soek aanlyn vir vermiste onderskrifte gebasseer op metadata verstellings.",
+ "TaskDownloadMissingSubtitles": "Laai vermiste onderskrifte af",
+ "TaskRefreshChannelsDescription": "Vervris internet kanaal inligting.",
+ "TaskRefreshChannels": "Vervris Kanale",
+ "TaskCleanTranscodeDescription": "Vee transkodering lêers uit wat ouer is as een dag.",
+ "TaskCleanTranscode": "Reinig Transkoderings Leêrbinder",
+ "TaskUpdatePluginsDescription": "Laai opgedateerde inprop-sagteware af en installeer inprop-sagteware wat verstel is om outomaties op te dateer.",
+ "TaskUpdatePlugins": "Dateer Inprop-Sagteware Op",
+ "TaskRefreshPeopleDescription": "Vervris metadata oor akteurs en regisseurs in u media versameling.",
+ "TaskRefreshPeople": "Vervris Mense",
+ "TaskCleanLogsDescription": "Vee loglêers wat ouer as {0} dae is uit.",
+ "TaskCleanLogs": "Reinig Loglêer Lêervouer",
+ "TaskRefreshLibraryDescription": "Skandeer u media versameling vir nuwe lêers en verfris metadata.",
+ "TaskRefreshLibrary": "Skandeer Media Versameling",
+ "TaskRefreshChapterImagesDescription": "Maak kleinkiekeis (fotos) vir films wat hoofstukke het.",
+ "TaskRefreshChapterImages": "Verkry Hoofstuk Beelde"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index d68928fce..4b898e6fe 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -1,5 +1,5 @@
{
- "Albums": "ألبومات",
+ "Albums": "البومات",
"AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
"Application": "تطبيق",
"Artists": "الفنانين",
@@ -14,9 +14,8 @@
"FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
"Favorites": "المفضلة",
"Folders": "المجلدات",
- "Genres": "الأنواع",
+ "Genres": "التضنيفات",
"HeaderAlbumArtists": "فناني الألبومات",
- "HeaderCameraUploads": "تحميلات الكاميرا",
"HeaderContinueWatching": "استئناف",
"HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون",
@@ -50,7 +49,7 @@
"NotificationOptionAudioPlayback": "بدأ تشغيل المقطع الصوتي",
"NotificationOptionAudioPlaybackStopped": "تم إيقاف تشغيل المقطع الصوتي",
"NotificationOptionCameraImageUploaded": "تم رفع صورة الكاميرا",
- "NotificationOptionInstallationFailed": "فشل في التثبيت",
+ "NotificationOptionInstallationFailed": "فشل التثبيت",
"NotificationOptionNewLibraryContent": "تم إضافة محتوى جديد",
"NotificationOptionPluginError": "فشل في البرنامج المضاف",
"NotificationOptionPluginInstalled": "تم تثبيت الملحق",
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index 3fc7c7dc0..1fed83276 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -16,7 +16,6 @@
"Folders": "Папки",
"Genres": "Жанрове",
"HeaderAlbumArtists": "Изпълнители на албуми",
- "HeaderCameraUploads": "Качени от камера",
"HeaderContinueWatching": "Продължаване на гледането",
"HeaderFavoriteAlbums": "Любими албуми",
"HeaderFavoriteArtists": "Любими изпълнители",
diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index 4949b10e6..5667bf337 100644
--- a/Emby.Server.Implementations/Localization/Core/bn.json
+++ b/Emby.Server.Implementations/Localization/Core/bn.json
@@ -1,26 +1,25 @@
{
"DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে",
"DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
- "Collections": "সংকলন",
+ "Collections": "কলেক্শন",
"ChapterNameValue": "অধ্যায় {0}",
"Channels": "চ্যানেল",
- "CameraImageUploadedFrom": "একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে {0} থেকে",
+ "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
"Books": "বই",
- "AuthenticationSucceededWithUserName": "{0} যাচাই সফল",
- "Artists": "শিল্পী",
+ "AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
+ "Artists": "শিল্পীরা",
"Application": "অ্যাপ্লিকেশন",
"Albums": "অ্যালবামগুলো",
"HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন",
- "HeaderCameraUploads": "ক্যামেরার আপলোডগুলো",
- "HeaderAlbumArtists": "এলবামের শিল্পী",
- "Genres": "ঘরানা",
+ "HeaderAlbumArtists": "এলবাম শিল্পী",
+ "Genres": "জেনার",
"Folders": "ফোল্ডারগুলো",
- "Favorites": "ফেভারিটগুলো",
- "FailedLoginAttemptWithUserName": "{0} থেকে লগিন করতে ব্যর্থ",
- "AppDeviceValues": "এপ: {0}, ডিভাইস: {0}",
+ "Favorites": "পছন্দসমূহ",
+ "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
+ "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
"VersionNumber": "সংস্করণ {0}",
"ValueSpecialEpisodeName": "বিশেষ - {0}",
"ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
@@ -62,36 +61,56 @@
"NotificationOptionPluginInstalled": "প্লাগিন ইন্সটল করা হয়েছে",
"NotificationOptionPluginError": "প্লাগিন ব্যর্থ",
"NotificationOptionNewLibraryContent": "নতুন কন্টেন্ট যোগ করা হয়েছে",
- "NotificationOptionInstallationFailed": "ইন্সটল ব্যর্থ",
+ "NotificationOptionInstallationFailed": "ইন্সটল ব্যর্থ হয়েছে",
"NotificationOptionCameraImageUploaded": "ক্যামেরার ছবি আপলোড হয়েছে",
"NotificationOptionAudioPlaybackStopped": "গান বাজা বন্ধ হয়েছে",
"NotificationOptionAudioPlayback": "গান বাজা শুরু হয়েছে",
"NotificationOptionApplicationUpdateInstalled": "এপ্লিকেশনের আপডেট ইনস্টল করা হয়েছে",
"NotificationOptionApplicationUpdateAvailable": "এপ্লিকেশনের আপডেট রয়েছে",
- "NewVersionIsAvailable": "জেলিফিন সার্ভারের একটি নতুন ভার্শন ডাউনলোডের জন্য তৈরী",
+ "NewVersionIsAvailable": "জেলিফিন সার্ভারের একটি নতুন ভার্শন ডাউনলোডের জন্য তৈরী।",
"NameSeasonUnknown": "সিজন অজানা",
"NameSeasonNumber": "সিজন {0}",
"NameInstallFailed": "{0} ইন্সটল ব্যর্থ",
"MusicVideos": "গানের ভিডিও",
"Music": "গান",
- "Movies": "সিনেমা",
+ "Movies": "চলচ্চিত্র",
"MixedContent": "মিশ্র কন্টেন্ট",
- "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন হালনাগাদ করা হয়েছে",
- "HeaderRecordingGroups": "রেকর্ডিং গ্রুপ",
- "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসন অংশ আপডেট করা হয়েছে",
- "MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে হালনাগাদ করা হয়েছে",
- "MessageApplicationUpdated": "জেলিফিন সার্ভার হালনাগাদ করা হয়েছে",
- "Latest": "একদম নতুন",
+ "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
+ "HeaderRecordingGroups": "রেকর্ডিং দল",
+ "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসনের অংশ আপডেট করা হয়েছে",
+ "MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে",
+ "MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে",
+ "Latest": "সর্বশেষ",
"LabelRunningTimeValue": "চলার সময়: {0}",
- "LabelIpAddressValue": "আইপি ঠিকানা: {0}",
+ "LabelIpAddressValue": "আইপি এড্রেস: {0}",
"ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
"ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
"Inherit": "থেকে পাওয়া",
- "HomeVideos": "বাসার ভিডিও",
+ "HomeVideos": "হোম ভিডিও",
"HeaderNextUp": "এরপরে আসছে",
"HeaderLiveTV": "লাইভ টিভি",
"HeaderFavoriteSongs": "প্রিয় গানগুলো",
"HeaderFavoriteShows": "প্রিয় শোগুলো",
"TasksLibraryCategory": "গ্রন্থাগার",
- "TasksMaintenanceCategory": "রক্ষণাবেক্ষণ"
+ "TasksMaintenanceCategory": "রক্ষণাবেক্ষণ",
+ "TaskRefreshLibrary": "স্ক্যান মিডিয়া লাইব্রেরি",
+ "TaskRefreshChapterImagesDescription": "অধ্যায়গুলিতে থাকা ভিডিওগুলির জন্য থাম্বনেইল তৈরি ।",
+ "TaskRefreshChapterImages": "অধ্যায়ের চিত্রগুলি বের করুন",
+ "TaskCleanCacheDescription": "সিস্টেমে আর প্রয়োজন নেই ক্যাশ, ফাইলগুলি মুছে ফেলুন।",
+ "TaskCleanCache": "ক্লিন ক্যাশ ডিরেক্টরি",
+ "TasksChannelsCategory": "ইন্টারনেট চ্যানেল",
+ "TasksApplicationCategory": "আবেদন",
+ "TaskDownloadMissingSubtitlesDescription": "মেটাডেটা কনফিগারেশনের উপর ভিত্তি করে অনুপস্থিত সাবটাইটেলগুলির জন্য ইন্টারনেট অনুসন্ধান করে।",
+ "TaskDownloadMissingSubtitles": "অনুপস্থিত সাবটাইটেলগুলি ডাউনলোড করুন",
+ "TaskRefreshChannelsDescription": "ইন্টারনেট চ্যানেল তথ্য রিফ্রেশ করুন।",
+ "TaskRefreshChannels": "চ্যানেল রিফ্রেশ করুন",
+ "TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলুন।",
+ "TaskCleanTranscode": "ট্রান্সকোড ডিরেক্টরি ক্লিন করুন",
+ "TaskUpdatePluginsDescription": "স্বয়ংক্রিয়ভাবে আপডেট কনফিগার করা প্লাগইনগুলির জন্য আপডেট ডাউনলোড এবং ইনস্টল করুন।",
+ "TaskUpdatePlugins": "প্লাগইন আপডেট করুন",
+ "TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করুন।",
+ "TaskRefreshPeople": "পিপল রিফ্রেশ করুন",
+ "TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলুন।",
+ "TaskCleanLogs": "লগ ডিরেক্টরি ক্লিন করুন",
+ "TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করুন।"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index 7464ac1c0..b7852eccb 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -16,7 +16,6 @@
"Folders": "Carpetes",
"Genres": "Gèneres",
"HeaderAlbumArtists": "Artistes del Àlbum",
- "HeaderCameraUploads": "Pujades de Càmera",
"HeaderContinueWatching": "Continua Veient",
"HeaderFavoriteAlbums": "Àlbums Preferits",
"HeaderFavoriteArtists": "Artistes Preferits",
@@ -92,5 +91,27 @@
"UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}",
"ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva llibreria",
"ValueSpecialEpisodeName": "Especial - {0}",
- "VersionNumber": "Versió {0}"
+ "VersionNumber": "Versió {0}",
+ "TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
+ "TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin",
+ "TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'internet.",
+ "TaskRefreshChannels": "Actualitza Canals",
+ "TaskCleanTranscodeDescription": "Elimina els arxius temporals de transcodificacions que tinguin més d'un dia.",
+ "TaskCleanTranscode": "Neteja les transcodificacions",
+ "TaskUpdatePluginsDescription": "Actualitza les extensions que estan configurades per actualitzar-se automàticament.",
+ "TaskUpdatePlugins": "Actualitza les extensions",
+ "TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva mediateca.",
+ "TaskRefreshPeople": "Actualitza Persones",
+ "TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.",
+ "TaskCleanLogs": "Neteja els registres",
+ "TaskRefreshLibraryDescription": "Escaneja la mediateca buscant fitxers nous i refresca les metadades.",
+ "TaskRefreshLibrary": "Escaneja la biblioteca de mitjans",
+ "TaskRefreshChapterImagesDescription": "Crea les miniatures dels vídeos que tinguin capítols.",
+ "TaskRefreshChapterImages": "Extreure les imatges dels capítols",
+ "TaskCleanCacheDescription": "Elimina els arxius temporals que ja no són necessaris per al servidor.",
+ "TaskCleanCache": "Elimina arxius temporals",
+ "TasksChannelsCategory": "Canals d'internet",
+ "TasksApplicationCategory": "Aplicació",
+ "TasksLibraryCategory": "Biblioteca",
+ "TasksMaintenanceCategory": "Manteniment"
}
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 464ca28ca..b34fad066 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -16,7 +16,6 @@
"Folders": "Složky",
"Genres": "Žánry",
"HeaderAlbumArtists": "Umělci alba",
- "HeaderCameraUploads": "Nahrané fotografie",
"HeaderContinueWatching": "Pokračovat ve sledování",
"HeaderFavoriteAlbums": "Oblíbená alba",
"HeaderFavoriteArtists": "Oblíbení interpreti",
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index f5397b62c..b29ad94ef 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -16,7 +16,6 @@
"Folders": "Mapper",
"Genres": "Genrer",
"HeaderAlbumArtists": "Albumkunstnere",
- "HeaderCameraUploads": "Kamera Uploads",
"HeaderContinueWatching": "Fortsæt Afspilning",
"HeaderFavoriteAlbums": "Favoritalbummer",
"HeaderFavoriteArtists": "Favoritkunstnere",
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 82df43be1..97ad1694a 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -3,9 +3,9 @@
"AppDeviceValues": "App: {0}, Gerät: {1}",
"Application": "Anwendung",
"Artists": "Interpreten",
- "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert",
+ "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
"Books": "Bücher",
- "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
+ "CameraImageUploadedFrom": "Ein neues Kamerafoto wurde von {0} hochgeladen",
"Channels": "Kanäle",
"ChapterNameValue": "Kapitel {0}",
"Collections": "Sammlungen",
@@ -16,7 +16,6 @@
"Folders": "Verzeichnisse",
"Genres": "Genres",
"HeaderAlbumArtists": "Album-Interpreten",
- "HeaderCameraUploads": "Kamera-Uploads",
"HeaderContinueWatching": "Fortsetzen",
"HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblings-Interpreten",
@@ -101,12 +100,12 @@
"TaskCleanTranscode": "Lösche Transkodier Pfad",
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
"TaskUpdatePlugins": "Update Plugins",
- "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.",
- "TaskRefreshPeople": "Erneuere Schausteller",
+ "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
+ "TaskRefreshPeople": "Erneuere Schauspieler",
"TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
"TaskCleanLogs": "Lösche Log Pfad",
"TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
- "TaskRefreshLibrary": "Scanne alle Bibliotheken",
+ "TaskRefreshLibrary": "Scanne Medien-Bibliothek",
"TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.",
"TaskRefreshChapterImages": "Extrahiert Kapitel-Bilder",
"TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.",
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index 0753ea39d..c45cc11cb 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -16,7 +16,6 @@
"Folders": "Φάκελοι",
"Genres": "Είδη",
"HeaderAlbumArtists": "Καλλιτέχνες του Άλμπουμ",
- "HeaderCameraUploads": "Μεταφορτώσεις Κάμερας",
"HeaderContinueWatching": "Συνεχίστε την παρακολούθηση",
"HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
"HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index 544c38cfa..57ff13219 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -16,7 +16,6 @@
"Folders": "Folders",
"Genres": "Genres",
"HeaderAlbumArtists": "Album Artists",
- "HeaderCameraUploads": "Camera Uploads",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favourite Albums",
"HeaderFavoriteArtists": "Favourite Artists",
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index 97a843160..92c54fb0e 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -16,7 +16,6 @@
"Folders": "Folders",
"Genres": "Genres",
"HeaderAlbumArtists": "Album Artists",
- "HeaderCameraUploads": "Camera Uploads",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favorite Albums",
"HeaderFavoriteArtists": "Favorite Artists",
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index fc9a10f27..390074cdd 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -16,11 +16,10 @@
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas de álbum",
- "HeaderCameraUploads": "Subidas de cámara",
"HeaderContinueWatching": "Seguir viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
- "HeaderFavoriteEpisodes": "Episodios favoritos",
+ "HeaderFavoriteEpisodes": "Capítulos favoritos",
"HeaderFavoriteShows": "Programas favoritos",
"HeaderFavoriteSongs": "Canciones favoritas",
"HeaderLiveTV": "TV en vivo",
diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json
index 20b37ec9f..ab54c0ea6 100644
--- a/Emby.Server.Implementations/Localization/Core/es-MX.json
+++ b/Emby.Server.Implementations/Localization/Core/es-MX.json
@@ -16,7 +16,6 @@
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del álbum",
- "HeaderCameraUploads": "Subidas desde la cámara",
"HeaderContinueWatching": "Continuar viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
@@ -31,7 +30,7 @@
"ItemAddedWithName": "{0} fue agregado a la biblioteca",
"ItemRemovedWithName": "{0} fue removido de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}",
- "LabelRunningTimeValue": "Duración: {0}",
+ "LabelRunningTimeValue": "Tiempo de reproducción: {0}",
"Latest": "Recientes",
"MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
"MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index e7bd3959b..d4e0b299d 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -16,7 +16,6 @@
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del álbum",
- "HeaderCameraUploads": "Subidas desde la cámara",
"HeaderContinueWatching": "Continuar viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
new file mode 100644
index 000000000..dcd30694f
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/es_419.json
@@ -0,0 +1,116 @@
+{
+ "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 del á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.",
+ "TaskRefreshChannels": "Actualizar canales",
+ "TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.",
+ "TaskCleanTranscode": "Limpiar directorio de transcodificado",
+ "TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.",
+ "TaskUpdatePlugins": "Actualizar complementos",
+ "TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.",
+ "TaskRefreshPeople": "Actualizar personas",
+ "TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad.",
+ "TaskCleanLogs": "Limpiar directorio de registros",
+ "TaskRefreshLibraryDescription": "Escanea tu biblioteca de medios por archivos nuevos y actualiza los metadatos.",
+ "TaskRefreshLibrary": "Escanear biblioteca de medios",
+ "TaskRefreshChapterImagesDescription": "Crea miniaturas para videos que tienen capítulos.",
+ "TaskRefreshChapterImages": "Extraer imágenes de los capítulos",
+ "TaskCleanCacheDescription": "Elimina archivos caché que ya no son necesarios para el sistema.",
+ "TaskCleanCache": "Limpiar directorio caché",
+ "TasksChannelsCategory": "Canales de Internet",
+ "TasksApplicationCategory": "Aplicación",
+ "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}",
+ "UserLockedOutWithName": "El usuario {0} ha sido bloqueado",
+ "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",
+ "NotificationOptionTaskFailed": "Falla de tarea programada",
+ "NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor",
+ "NotificationOptionPluginUpdateInstalled": "Actualización de complemento instalada",
+ "NotificationOptionPluginUninstalled": "Complemento desinstalado",
+ "NotificationOptionPluginInstalled": "Complemento instalado",
+ "NotificationOptionPluginError": "Falla de complemento",
+ "NotificationOptionNewLibraryContent": "Nuevo contenido agregado",
+ "NotificationOptionInstallationFailed": "Falla de instalación",
+ "NotificationOptionCameraImageUploaded": "Imagen de la cámara subida",
+ "NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida",
+ "NotificationOptionAudioPlayback": "Reproducción de audio iniciada",
+ "NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada",
+ "NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible",
+ "NewVersionIsAvailable": "Una nueva versión del Servidor Jellyfin está disponible para descargar.",
+ "NameSeasonUnknown": "Temporada desconocida",
+ "NameSeasonNumber": "Temporada {0}",
+ "NameInstallFailed": "Falló la instalación de {0}",
+ "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 fallido de inicio de sesión 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": "App: {0}, Dispositivo: {1}"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/es_DO.json b/Emby.Server.Implementations/Localization/Core/es_DO.json
index 0ef16542f..b64ffbfbb 100644
--- a/Emby.Server.Implementations/Localization/Core/es_DO.json
+++ b/Emby.Server.Implementations/Localization/Core/es_DO.json
@@ -12,10 +12,12 @@
"Application": "Aplicación",
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
"HeaderContinueWatching": "Continuar Viendo",
- "HeaderCameraUploads": "Subidas de Cámara",
"HeaderAlbumArtists": "Artistas del Álbum",
"Genres": "Géneros",
"Folders": "Carpetas",
"Favorites": "Favoritos",
- "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}"
+ "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}",
+ "HeaderFavoriteSongs": "Canciones Favoritas",
+ "HeaderFavoriteEpisodes": "Episodios Favoritos",
+ "HeaderFavoriteArtists": "Artistas Favoritos"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index 500c29217..1986decf0 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -16,7 +16,6 @@
"Folders": "پوشه‌ها",
"Genres": "ژانرها",
"HeaderAlbumArtists": "هنرمندان آلبوم",
- "HeaderCameraUploads": "آپلودهای دوربین",
"HeaderContinueWatching": "ادامه تماشا",
"HeaderFavoriteAlbums": "آلبوم‌های مورد علاقه",
"HeaderFavoriteArtists": "هنرمندان مورد علاقه",
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index f8d6e0e09..aae22583a 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -24,7 +24,6 @@
"HeaderFavoriteSongs": "Lempikappaleet",
"HeaderFavoriteShows": "Lempisarjat",
"HeaderFavoriteEpisodes": "Lempijaksot",
- "HeaderCameraUploads": "Kamerasta Lähetetyt",
"HeaderFavoriteArtists": "Lempiartistit",
"HeaderFavoriteAlbums": "Lempialbumit",
"HeaderContinueWatching": "Jatka katsomista",
diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json
index 47daf2044..1a3e18832 100644
--- a/Emby.Server.Implementations/Localization/Core/fil.json
+++ b/Emby.Server.Implementations/Localization/Core/fil.json
@@ -73,7 +73,6 @@
"HeaderFavoriteArtists": "Paboritong Artista",
"HeaderFavoriteAlbums": "Paboritong Albums",
"HeaderContinueWatching": "Ituloy Manood",
- "HeaderCameraUploads": "Camera Uploads",
"HeaderAlbumArtists": "Artista ng Album",
"Genres": "Kategorya",
"Folders": "Folders",
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index 3dcfa6844..3d7592e3c 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -16,7 +16,6 @@
"Folders": "Dossiers",
"Genres": "Genres",
"HeaderAlbumArtists": "Artistes de l'album",
- "HeaderCameraUploads": "Photos transférées",
"HeaderContinueWatching": "Continuer à regarder",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes favoris",
@@ -109,9 +108,10 @@
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
"TaskRefreshLibraryDescription": "Analyse votre bibliothèque média pour trouver de nouveaux fichiers et rafraîchit les métadonnées.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
- "TaskRefreshChapterImagesDescription": "Créer des vignettes pour les vidéos qui ont des chapitres",
+ "TaskRefreshChapterImagesDescription": "Créer des vignettes pour les vidéos qui ont des chapitres.",
"TaskRefreshLibrary": "Analyser la bibliothèque de médias",
"TaskCleanCache": "Nettoyer le répertoire des fichiers temporaires",
"TasksApplicationCategory": "Application",
- "TaskCleanCacheDescription": "Supprime les fichiers temporaires qui ne sont plus nécessaire pour le système."
+ "TaskCleanCacheDescription": "Supprime les fichiers temporaires qui ne sont plus nécessaire pour le système.",
+ "TasksChannelsCategory": "Canaux Internet"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 150952d8b..f4ca8575a 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -5,7 +5,7 @@
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
"Books": "Livres",
- "CameraImageUploadedFrom": "Une nouvelle photographie a été chargée depuis {0}",
+ "CameraImageUploadedFrom": "Une photo a été chargée depuis {0}",
"Channels": "Chaînes",
"ChapterNameValue": "Chapitre {0}",
"Collections": "Collections",
@@ -15,8 +15,7 @@
"Favorites": "Favoris",
"Folders": "Dossiers",
"Genres": "Genres",
- "HeaderAlbumArtists": "Artistes de l'album",
- "HeaderCameraUploads": "Photos transférées",
+ "HeaderAlbumArtists": "Artistes",
"HeaderContinueWatching": "Continuer à regarder",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes préférés",
@@ -107,7 +106,7 @@
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
"TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
- "TaskRefreshLibrary": "Scanner toute les Bibliothèques",
+ "TaskRefreshLibrary": "Scanner toutes les Bibliothèques",
"TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json
index 94034962d..faee2519a 100644
--- a/Emby.Server.Implementations/Localization/Core/gl.json
+++ b/Emby.Server.Implementations/Localization/Core/gl.json
@@ -1,3 +1,11 @@
{
- "Albums": "Álbumes"
+ "Albums": "Álbumes",
+ "Collections": "Colecións",
+ "ChapterNameValue": "Capítulos {0}",
+ "Channels": "Canles",
+ "CameraImageUploadedFrom": "Cargouse unha nova imaxe da cámara desde {0}",
+ "Books": "Libros",
+ "AuthenticationSucceededWithUserName": "{0} autenticouse correctamente",
+ "Artists": "Artistas",
+ "Application": "Aplicativo"
}
diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json
index 8780a884b..ee1f8775e 100644
--- a/Emby.Server.Implementations/Localization/Core/gsw.json
+++ b/Emby.Server.Implementations/Localization/Core/gsw.json
@@ -16,7 +16,6 @@
"Folders": "Ordner",
"Genres": "Genres",
"HeaderAlbumArtists": "Album-Künstler",
- "HeaderCameraUploads": "Kamera-Uploads",
"HeaderContinueWatching": "weiter schauen",
"HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblings-Künstler",
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 682f5325b..f906d6e11 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -16,15 +16,14 @@
"Folders": "תיקיות",
"Genres": "ז'אנרים",
"HeaderAlbumArtists": "אמני האלבום",
- "HeaderCameraUploads": "העלאות ממצלמה",
"HeaderContinueWatching": "המשך לצפות",
- "HeaderFavoriteAlbums": "אלבומים שאהבתי",
+ "HeaderFavoriteAlbums": "אלבומים מועדפים",
"HeaderFavoriteArtists": "אמנים מועדפים",
"HeaderFavoriteEpisodes": "פרקים מועדפים",
- "HeaderFavoriteShows": "סדרות מועדפות",
+ "HeaderFavoriteShows": "תוכניות מועדפות",
"HeaderFavoriteSongs": "שירים מועדפים",
"HeaderLiveTV": "שידורים חיים",
- "HeaderNextUp": "הבא",
+ "HeaderNextUp": "הבא בתור",
"HeaderRecordingGroups": "קבוצות הקלטה",
"HomeVideos": "סרטונים בייתים",
"Inherit": "הורש",
@@ -45,37 +44,37 @@
"NameSeasonNumber": "עונה {0}",
"NameSeasonUnknown": "עונה לא ידועה",
"NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
- "NotificationOptionApplicationUpdateAvailable": "Application update available",
- "NotificationOptionApplicationUpdateInstalled": "Application update installed",
- "NotificationOptionAudioPlayback": "Audio playback started",
- "NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
- "NotificationOptionCameraImageUploaded": "Camera image uploaded",
+ "NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום",
+ "NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן",
+ "NotificationOptionAudioPlayback": "ניגון שמע החל",
+ "NotificationOptionAudioPlaybackStopped": "ניגון שמע הופסק",
+ "NotificationOptionCameraImageUploaded": "תמונת מצלמה הועלתה",
"NotificationOptionInstallationFailed": "התקנה נכשלה",
- "NotificationOptionNewLibraryContent": "New content added",
- "NotificationOptionPluginError": "Plugin failure",
+ "NotificationOptionNewLibraryContent": "תוכן חדש הוסף",
+ "NotificationOptionPluginError": "כשלון בתוסף",
"NotificationOptionPluginInstalled": "התוסף הותקן",
"NotificationOptionPluginUninstalled": "התוסף הוסר",
"NotificationOptionPluginUpdateInstalled": "העדכון לתוסף הותקן",
"NotificationOptionServerRestartRequired": "יש לאתחל את השרת",
- "NotificationOptionTaskFailed": "Scheduled task failure",
- "NotificationOptionUserLockedOut": "User locked out",
- "NotificationOptionVideoPlayback": "Video playback started",
- "NotificationOptionVideoPlaybackStopped": "Video playback stopped",
+ "NotificationOptionTaskFailed": "משימה מתוזמנת נכשלה",
+ "NotificationOptionUserLockedOut": "משתמש ננעל",
+ "NotificationOptionVideoPlayback": "ניגון וידאו החל",
+ "NotificationOptionVideoPlaybackStopped": "ניגון וידאו הופסק",
"Photos": "תמונות",
"Playlists": "רשימות הפעלה",
"Plugin": "Plugin",
- "PluginInstalledWithName": "{0} was installed",
- "PluginUninstalledWithName": "{0} was uninstalled",
- "PluginUpdatedWithName": "{0} was updated",
+ "PluginInstalledWithName": "{0} הותקן",
+ "PluginUninstalledWithName": "{0} הוסר",
+ "PluginUpdatedWithName": "{0} עודכן",
"ProviderValue": "Provider: {0}",
- "ScheduledTaskFailedWithName": "{0} failed",
- "ScheduledTaskStartedWithName": "{0} started",
- "ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
+ "ScheduledTaskFailedWithName": "{0} נכשל",
+ "ScheduledTaskStartedWithName": "{0} החל",
+ "ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
"Shows": "סדרות",
"Songs": "שירים",
"StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
- "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
+ "SubtitleDownloadFailureFromForItem": "הורדת כתוביות נכשלה מ-{0} עבור {1}",
"Sync": "סנכרן",
"System": "System",
"TvShows": "סדרות טלוויזיה",
@@ -83,14 +82,14 @@
"UserCreatedWithName": "המשתמש {0} נוצר",
"UserDeletedWithName": "המשתמש {0} הוסר",
"UserDownloadingItemWithValues": "{0} מוריד את {1}",
- "UserLockedOutWithName": "User {0} has been locked out",
- "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}",
+ "UserLockedOutWithName": "המשתמש {0} ננעל",
+ "UserOfflineFromDevice": "{0} התנתק מ-{1}",
+ "UserOnlineFromDevice": "{0} מחובר מ-{1}",
+ "UserPasswordChangedWithName": "הסיסמה שונתה עבור המשתמש {0}",
+ "UserPolicyUpdatedWithName": "מדיניות המשתמש {0} עודכנה",
"UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}",
"UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}",
- "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
+ "ValueHasBeenAddedToLibrary": "{0} התווסף לספריית המדיה שלך",
"ValueSpecialEpisodeName": "מיוחד- {0}",
"VersionNumber": "Version {0}",
"TaskRefreshLibrary": "סרוק ספריית מדיה",
@@ -109,7 +108,7 @@
"TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
"TasksChannelsCategory": "ערוצי אינטרנט",
"TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.",
- "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות.",
+ "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות",
"TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
"TaskRefreshChannels": "רענן ערוץ",
"TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.",
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index c169a35e7..a3c936240 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -5,23 +5,22 @@
"Artists": "Izvođači",
"AuthenticationSucceededWithUserName": "{0} uspješno ovjerena",
"Books": "Knjige",
- "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
+ "CameraImageUploadedFrom": "Nova fotografija sa kamere je uploadana iz {0}",
"Channels": "Kanali",
"ChapterNameValue": "Poglavlje {0}",
"Collections": "Kolekcije",
"DeviceOfflineWithName": "{0} se odspojilo",
"DeviceOnlineWithName": "{0} je spojeno",
"FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave za {0}",
- "Favorites": "Omiljeni",
+ "Favorites": "Favoriti",
"Folders": "Mape",
"Genres": "Žanrovi",
- "HeaderAlbumArtists": "Izvođači albuma",
- "HeaderCameraUploads": "Camera Uploads",
- "HeaderContinueWatching": "Continue Watching",
+ "HeaderAlbumArtists": "Izvođači na albumu",
+ "HeaderContinueWatching": "Nastavi gledati",
"HeaderFavoriteAlbums": "Omiljeni albumi",
"HeaderFavoriteArtists": "Omiljeni izvođači",
"HeaderFavoriteEpisodes": "Omiljene epizode",
- "HeaderFavoriteShows": "Omiljene emisije",
+ "HeaderFavoriteShows": "Omiljene serije",
"HeaderFavoriteSongs": "Omiljene pjesme",
"HeaderLiveTV": "TV uživo",
"HeaderNextUp": "Sljedeće je",
@@ -34,23 +33,23 @@
"LabelRunningTimeValue": "Vrijeme rada: {0}",
"Latest": "Najnovije",
"MessageApplicationUpdated": "Jellyfin Server je ažuriran",
- "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
+ "MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Odjeljak postavka servera {0} je ažuriran",
"MessageServerConfigurationUpdated": "Postavke servera su ažurirane",
"MixedContent": "Miješani sadržaj",
"Movies": "Filmovi",
"Music": "Glazba",
"MusicVideos": "Glazbeni spotovi",
- "NameInstallFailed": "{0} installation failed",
+ "NameInstallFailed": "{0} neuspješnih instalacija",
"NameSeasonNumber": "Sezona {0}",
- "NameSeasonUnknown": "Season Unknown",
- "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
+ "NameSeasonUnknown": "Nepoznata sezona",
+ "NewVersionIsAvailable": "Nova verzija Jellyfin servera je dostupna za preuzimanje.",
"NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije",
"NotificationOptionApplicationUpdateInstalled": "Instalirano ažuriranje aplikacije",
"NotificationOptionAudioPlayback": "Reprodukcija glazbe započeta",
"NotificationOptionAudioPlaybackStopped": "Reprodukcija audiozapisa je zaustavljena",
"NotificationOptionCameraImageUploaded": "Slike kamere preuzete",
- "NotificationOptionInstallationFailed": "Instalacija nije izvršena",
+ "NotificationOptionInstallationFailed": "Instalacija neuspješna",
"NotificationOptionNewLibraryContent": "Novi sadržaj je dodan",
"NotificationOptionPluginError": "Dodatak otkazao",
"NotificationOptionPluginInstalled": "Dodatak instaliran",
@@ -62,7 +61,7 @@
"NotificationOptionVideoPlayback": "Reprodukcija videa započeta",
"NotificationOptionVideoPlaybackStopped": "Reprodukcija videozapisa je zaustavljena",
"Photos": "Slike",
- "Playlists": "Popisi",
+ "Playlists": "Popis za reprodukciju",
"Plugin": "Dodatak",
"PluginInstalledWithName": "{0} je instalirano",
"PluginUninstalledWithName": "{0} je deinstalirano",
@@ -70,15 +69,15 @@
"ProviderValue": "Pružitelj: {0}",
"ScheduledTaskFailedWithName": "{0} neuspjelo",
"ScheduledTaskStartedWithName": "{0} pokrenuto",
- "ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
- "Shows": "Shows",
+ "ServerNameNeedsToBeRestarted": "{0} treba biti ponovno pokrenuto",
+ "Shows": "Serije",
"Songs": "Pjesme",
"StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Pokušajte ponovo kasnije.",
"SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}",
- "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
+ "SubtitleDownloadFailureFromForItem": "Prijevodi nisu uspješno preuzeti {0} od {1}",
"Sync": "Sink.",
"System": "Sistem",
- "TvShows": "TV Shows",
+ "TvShows": "Serije",
"User": "Korisnik",
"UserCreatedWithName": "Korisnik {0} je stvoren",
"UserDeletedWithName": "Korisnik {0} je obrisan",
@@ -87,10 +86,10 @@
"UserOfflineFromDevice": "{0} se odspojilo od {1}",
"UserOnlineFromDevice": "{0} je online od {1}",
"UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}",
- "UserPolicyUpdatedWithName": "User policy has been updated for {0}",
+ "UserPolicyUpdatedWithName": "Pravila za korisnika su ažurirana za {0}",
"UserStartedPlayingItemWithValues": "{0} je pokrenuo {1}",
"UserStoppedPlayingItemWithValues": "{0} je zaustavio {1}",
- "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
+ "ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku",
"ValueSpecialEpisodeName": "Specijal - {0}",
"VersionNumber": "Verzija {0}",
"TaskRefreshLibraryDescription": "Skenira vašu medijsku knjižnicu sa novim datotekama i osvježuje metapodatke.",
@@ -100,5 +99,19 @@
"TaskCleanCacheDescription": "Briše priručne datoteke nepotrebne za sistem.",
"TaskCleanCache": "Očisti priručnu memoriju",
"TasksApplicationCategory": "Aplikacija",
- "TasksMaintenanceCategory": "Održavanje"
+ "TasksMaintenanceCategory": "Održavanje",
+ "TaskDownloadMissingSubtitlesDescription": "Pretraživanje interneta za prijevodima koji nedostaju bazirano na konfiguraciji meta podataka.",
+ "TaskDownloadMissingSubtitles": "Preuzimanje prijevoda koji nedostaju",
+ "TaskRefreshChannelsDescription": "Osvježava informacije o internet kanalima.",
+ "TaskRefreshChannels": "Osvježi kanale",
+ "TaskCleanTranscodeDescription": "Briše transkodirane fajlove starije od jednog dana.",
+ "TaskCleanTranscode": "Očisti direktorij za transkodiranje",
+ "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su podešeni da se ažuriraju automatski.",
+ "TaskUpdatePlugins": "Ažuriraj dodatke",
+ "TaskRefreshPeopleDescription": "Ažurira meta podatke za glumce i redatelje u vašoj medijskoj biblioteci.",
+ "TaskRefreshPeople": "Osvježi ljude",
+ "TaskCleanLogsDescription": "Briši logove koji su stariji od {0} dana.",
+ "TaskCleanLogs": "Očisti direktorij sa logovima",
+ "TasksChannelsCategory": "Internet kanali",
+ "TasksLibraryCategory": "Biblioteka"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index c5c3844e3..343d213d4 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -16,7 +16,6 @@
"Folders": "Könyvtárak",
"Genres": "Műfajok",
"HeaderAlbumArtists": "Album előadók",
- "HeaderCameraUploads": "Kamera feltöltések",
"HeaderContinueWatching": "Megtekintés folytatása",
"HeaderFavoriteAlbums": "Kedvenc albumok",
"HeaderFavoriteArtists": "Kedvenc előadók",
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index eabdb9138..ef3ed2580 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -1,14 +1,14 @@
{
"Albums": "Album",
"AuthenticationSucceededWithUserName": "{0} berhasil diautentikasi",
- "AppDeviceValues": "Aplikasi: {0}, Alat: {1}",
+ "AppDeviceValues": "Aplikasi : {0}, Alat : {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 dikeluarkan dari perpustakaan",
- "ItemAddedWithName": "{0} sudah dimasukkan ke dalam perpustakaan",
+ "ItemRemovedWithName": "{0} sudah dikeluarkan dari pustaka",
+ "ItemAddedWithName": "{0} telah dimasukkan ke dalam pustaka",
"Inherit": "Warisan",
"HomeVideos": "Video Rumah",
"HeaderRecordingGroups": "Grup Rekaman",
@@ -19,10 +19,9 @@
"HeaderFavoriteEpisodes": "Episode Favorit",
"HeaderFavoriteArtists": "Artis Favorit",
"HeaderFavoriteAlbums": "Album Favorit",
- "HeaderContinueWatching": "Masih Melihat",
- "HeaderCameraUploads": "Uplod Kamera",
+ "HeaderContinueWatching": "Lanjut Menonton",
"HeaderAlbumArtists": "Album Artis",
- "Genres": "Genre",
+ "Genres": "Aliran",
"Folders": "Folder",
"Favorites": "Favorit",
"Collections": "Koleksi",
@@ -32,11 +31,11 @@
"ChapterNameValue": "Bagian {0}",
"Channels": "Saluran",
"TvShows": "Seri TV",
- "SubtitleDownloadFailureFromForItem": "Talop gagal diunduh dari {0} untuk {1}",
- "StartupEmbyServerIsLoading": "Peladen Jellyfin sedang dimuat. Silakan coba kembali beberapa saat lagi.",
+ "SubtitleDownloadFailureFromForItem": "Subtitel gagal diunduh dari {0} untuk {1}",
+ "StartupEmbyServerIsLoading": "Server Jellyfin sedang dimuat. Silakan coba lagi nanti.",
"Songs": "Lagu",
"Playlists": "Daftar putar",
- "NotificationOptionPluginUninstalled": "Plugin dilepas",
+ "NotificationOptionPluginUninstalled": "Plugin dihapus",
"MusicVideos": "Video musik",
"VersionNumber": "Versi {0}",
"ValueSpecialEpisodeName": "Spesial - {0}",
@@ -65,7 +64,7 @@
"Photos": "Foto",
"NotificationOptionUserLockedOut": "Pengguna terkunci",
"NotificationOptionTaskFailed": "Kegagalan tugas terjadwal",
- "NotificationOptionServerRestartRequired": "Restart peladen dibutuhkan",
+ "NotificationOptionServerRestartRequired": "Muat ulang server dibutuhkan",
"NotificationOptionPluginUpdateInstalled": "Pembaruan plugin terpasang",
"NotificationOptionPluginInstalled": "Plugin terpasang",
"NotificationOptionPluginError": "Kegagalan plugin",
@@ -74,14 +73,14 @@
"NotificationOptionCameraImageUploaded": "Gambar kamera terunggah",
"NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang",
"NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia",
- "NewVersionIsAvailable": "Sebuah versi baru dari Peladen Jellyfin tersedia untuk diunduh.",
+ "NewVersionIsAvailable": "Versi baru dari Jellyfin Server tersedia untuk diunduh.",
"NameSeasonUnknown": "Musim tak diketahui",
"NameSeasonNumber": "Musim {0}",
- "NameInstallFailed": "{0} instalasi gagal",
+ "NameInstallFailed": "{0} penginstalan gagal",
"Music": "Musik",
"Movies": "Film",
- "MessageServerConfigurationUpdated": "Konfigurasi peladen telah diperbarui",
- "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi peladen bagian {0} telah diperbarui",
+ "MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
"FailedLoginAttemptWithUserName": "Percobaan login gagal dari {0}",
"CameraImageUploadedFrom": "Sebuah gambar baru telah diunggah dari {0}",
"DeviceOfflineWithName": "{0} telah terputus",
@@ -90,6 +89,28 @@
"NotificationOptionVideoPlayback": "Pemutaran video dimulai",
"NotificationOptionAudioPlaybackStopped": "Pemutaran audio berhenti",
"NotificationOptionAudioPlayback": "Pemutaran audio dimulai",
- "MixedContent": "Konten campur",
- "PluginUninstalledWithName": "{0} telah dihapus"
+ "MixedContent": "Konten campuran",
+ "PluginUninstalledWithName": "{0} telah dihapus",
+ "TaskRefreshChapterImagesDescription": "Membuat gambar mini untuk video yang memiliki bagian.",
+ "TaskRefreshChapterImages": "Ekstrak Gambar Bagian",
+ "TaskCleanCacheDescription": "Menghapus file cache yang tidak lagi dibutuhkan oleh sistem.",
+ "TaskCleanCache": "Bersihkan Cache Direktori",
+ "TasksLibraryCategory": "Pustaka",
+ "TasksMaintenanceCategory": "Perbaikan",
+ "TasksApplicationCategory": "Aplikasi",
+ "TaskRefreshPeopleDescription": "Memperbarui metadata untuk aktor dan sutradara di pustaka media Anda.",
+ "TaskRefreshLibraryDescription": "Memindai Pustaka media Anda untuk mencari file baru dan memperbarui metadata.",
+ "TasksChannelsCategory": "Saluran Online",
+ "TaskDownloadMissingSubtitlesDescription": "Mencari di internet untuk subtitle yang hilang berdasarkan konfigurasi metadata.",
+ "TaskDownloadMissingSubtitles": "Unduh subtitle yang hilang",
+ "TaskRefreshChannelsDescription": "Segarkan informasi saluran internet.",
+ "TaskRefreshChannels": "Segarkan Saluran",
+ "TaskCleanTranscodeDescription": "Menghapus file transcode yang berumur lebih dari satu hari.",
+ "TaskCleanTranscode": "Bersihkan Direktori Transcode",
+ "TaskUpdatePluginsDescription": "Unduh dan instal pembaruan untuk plugin yang dikonfigurasi untuk memperbarui secara otomatis.",
+ "TaskUpdatePlugins": "Perbarui Plugin",
+ "TaskRefreshPeople": "Muat ulang Orang",
+ "TaskCleanLogsDescription": "Menghapus file log yang lebih dari {0} hari.",
+ "TaskCleanLogs": "Bersihkan Log Direktori",
+ "TaskRefreshLibrary": "Pindai Pustaka Media"
}
diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json
index 0f0f9130b..0f769eaad 100644
--- a/Emby.Server.Implementations/Localization/Core/is.json
+++ b/Emby.Server.Implementations/Localization/Core/is.json
@@ -13,7 +13,6 @@
"HeaderFavoriteArtists": "Uppáhalds Listamenn",
"HeaderFavoriteAlbums": "Uppáhalds Plötur",
"HeaderContinueWatching": "Halda áfram að horfa",
- "HeaderCameraUploads": "Myndavéla upphal",
"HeaderAlbumArtists": "Höfundur plötu",
"Genres": "Tegundir",
"Folders": "Möppur",
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 7f5a56e86..0a6238578 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -16,7 +16,6 @@
"Folders": "Cartelle",
"Genres": "Generi",
"HeaderAlbumArtists": "Artisti degli Album",
- "HeaderCameraUploads": "Caricamenti Fotocamera",
"HeaderContinueWatching": "Continua a guardare",
"HeaderFavoriteAlbums": "Album Preferiti",
"HeaderFavoriteArtists": "Artisti Preferiti",
@@ -84,8 +83,8 @@
"UserDeletedWithName": "L'utente {0} è stato rimosso",
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
"UserLockedOutWithName": "L'utente {0} è stato bloccato",
- "UserOfflineFromDevice": "{0} è stato disconnesso da {1}",
- "UserOnlineFromDevice": "{0} è online da {1}",
+ "UserOfflineFromDevice": "{0} si è disconnesso su {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}",
@@ -102,11 +101,11 @@
"TaskUpdatePluginsDescription": "Scarica e installa gli aggiornamenti per i plugin che sono stati configurati per essere aggiornati contemporaneamente.",
"TaskUpdatePlugins": "Aggiorna i Plugin",
"TaskRefreshPeopleDescription": "Aggiorna i metadati per gli attori e registi nella tua libreria multimediale.",
- "TaskRefreshPeople": "Aggiorna persone",
+ "TaskRefreshPeople": "Aggiornamento Persone",
"TaskCleanLogsDescription": "Rimuovi i file di log più vecchi di {0} giorni.",
"TaskCleanLogs": "Pulisci la cartella dei log",
"TaskRefreshLibraryDescription": "Analizza la tua libreria multimediale per nuovi file e rinnova i metadati.",
- "TaskRefreshLibrary": "Analizza la libreria dei contenuti multimediali",
+ "TaskRefreshLibrary": "Scan Librerie",
"TaskRefreshChapterImagesDescription": "Crea le thumbnail per i video che hanno capitoli.",
"TaskRefreshChapterImages": "Estrai immagini capitolo",
"TaskCleanCacheDescription": "Cancella i file di cache non più necessari al sistema.",
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index a4d9f9ef6..35004f0eb 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -16,7 +16,6 @@
"Folders": "フォルダー",
"Genres": "ジャンル",
"HeaderAlbumArtists": "アルバムアーティスト",
- "HeaderCameraUploads": "カメラアップロード",
"HeaderContinueWatching": "視聴を続ける",
"HeaderFavoriteAlbums": "お気に入りのアルバム",
"HeaderFavoriteArtists": "お気に入りのアーティスト",
diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json
index 5618ff4a8..91c1fb15b 100644
--- a/Emby.Server.Implementations/Localization/Core/kk.json
+++ b/Emby.Server.Implementations/Localization/Core/kk.json
@@ -16,7 +16,6 @@
"Folders": "Qaltalar",
"Genres": "Janrlar",
"HeaderAlbumArtists": "Álbom oryndaýshylary",
- "HeaderCameraUploads": "Kameradan júktelgender",
"HeaderContinueWatching": "Qaraýdy jalǵastyrý",
"HeaderFavoriteAlbums": "Tańdaýly álbomdar",
"HeaderFavoriteArtists": "Tańdaýly oryndaýshylar",
diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json
index 9e3ecd5a8..fb01e4645 100644
--- a/Emby.Server.Implementations/Localization/Core/ko.json
+++ b/Emby.Server.Implementations/Localization/Core/ko.json
@@ -16,7 +16,6 @@
"Folders": "폴더",
"Genres": "장르",
"HeaderAlbumArtists": "앨범 아티스트",
- "HeaderCameraUploads": "카메라 업로드",
"HeaderContinueWatching": "계속 시청하기",
"HeaderFavoriteAlbums": "즐겨찾는 앨범",
"HeaderFavoriteArtists": "즐겨찾는 아티스트",
@@ -84,8 +83,8 @@
"UserDeletedWithName": "사용자 {0} 삭제됨",
"UserDownloadingItemWithValues": "{0}이(가) {1}을 다운로드 중입니다",
"UserLockedOutWithName": "유저 {0} 은(는) 잠금처리 되었습니다",
- "UserOfflineFromDevice": "{1}로부터 {0}의 연결이 끊겼습니다",
- "UserOnlineFromDevice": "{0}은 {1}에서 온라인 상태입니다",
+ "UserOfflineFromDevice": "{1}에서 {0}의 연결이 끊킴",
+ "UserOnlineFromDevice": "{0}이 {1}으로 접속",
"UserPasswordChangedWithName": "사용자 {0}의 비밀번호가 변경되었습니다",
"UserPolicyUpdatedWithName": "{0}의 사용자 정책이 업데이트되었습니다",
"UserStartedPlayingItemWithValues": "{2}에서 {0}이 {1} 재생 중",
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index 35053766b..d4cb592ef 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -16,7 +16,6 @@
"Folders": "Katalogai",
"Genres": "Žanrai",
"HeaderAlbumArtists": "Albumo atlikėjai",
- "HeaderCameraUploads": "Kameros",
"HeaderContinueWatching": "Žiūrėti toliau",
"HeaderFavoriteAlbums": "Mėgstami Albumai",
"HeaderFavoriteArtists": "Mėgstami Atlikėjai",
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index dbcf17287..5e3acfbe9 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -72,7 +72,6 @@
"ItemAddedWithName": "{0} tika pievienots bibliotēkai",
"HeaderLiveTV": "Tiešraides TV",
"HeaderContinueWatching": "Turpināt Skatīšanos",
- "HeaderCameraUploads": "Kameras augšupielādes",
"HeaderAlbumArtists": "Albumu Izpildītāji",
"Genres": "Žanri",
"Folders": "Mapes",
diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json
index bbdf99aba..b780ef498 100644
--- a/Emby.Server.Implementations/Localization/Core/mk.json
+++ b/Emby.Server.Implementations/Localization/Core/mk.json
@@ -51,7 +51,6 @@
"HeaderFavoriteArtists": "Омилени Изведувачи",
"HeaderFavoriteAlbums": "Омилени Албуми",
"HeaderContinueWatching": "Продолжи со гледање",
- "HeaderCameraUploads": "Поставувања од камера",
"HeaderAlbumArtists": "Изведувачи од Албуми",
"Genres": "Жанрови",
"Folders": "Папки",
diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json
index 50b6360d8..fdb4171b5 100644
--- a/Emby.Server.Implementations/Localization/Core/mr.json
+++ b/Emby.Server.Implementations/Localization/Core/mr.json
@@ -54,8 +54,9 @@
"ItemAddedWithName": "{0} हे संग्रहालयात जोडले गेले",
"HomeVideos": "घरचे व्हिडीयो",
"HeaderRecordingGroups": "रेकॉर्डिंग गट",
- "HeaderCameraUploads": "कॅमेरा अपलोड",
"CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे",
"Application": "अ‍ॅप्लिकेशन",
- "AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}"
+ "AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}",
+ "Collections": "संग्रह",
+ "ChapterNameValue": "धडा {0}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index 79d078d4a..5e3d095ff 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -5,47 +5,46 @@
"Artists": "Artis",
"AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
"Books": "Buku-buku",
- "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
+ "CameraImageUploadedFrom": "Ada gambar dari kamera yang baru dimuat naik melalui {0}",
"Channels": "Saluran",
- "ChapterNameValue": "Chapter {0}",
+ "ChapterNameValue": "Bab {0}",
"Collections": "Koleksi",
- "DeviceOfflineWithName": "{0} has disconnected",
- "DeviceOnlineWithName": "{0} is connected",
+ "DeviceOfflineWithName": "{0} telah diputuskan sambungan",
+ "DeviceOnlineWithName": "{0} telah disambung",
"FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}",
- "Favorites": "Favorites",
- "Folders": "Folders",
+ "Favorites": "Kegemaran",
+ "Folders": "Fail-fail",
"Genres": "Genre-genre",
- "HeaderAlbumArtists": "Album Artists",
- "HeaderCameraUploads": "Muatnaik Kamera",
+ "HeaderAlbumArtists": "Album Artis-artis",
"HeaderContinueWatching": "Terus Menonton",
- "HeaderFavoriteAlbums": "Favorite Albums",
- "HeaderFavoriteArtists": "Favorite Artists",
- "HeaderFavoriteEpisodes": "Favorite Episodes",
- "HeaderFavoriteShows": "Favorite Shows",
- "HeaderFavoriteSongs": "Favorite 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",
+ "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 Personal",
+ "Inherit": "Mewarisi",
+ "ItemAddedWithName": "{0} telah ditambahkan ke dalam pustaka",
+ "ItemRemovedWithName": "{0} telah dibuang daripada pustaka",
"LabelIpAddressValue": "Alamat IP: {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",
+ "LabelRunningTimeValue": "Masa berjalan: {0}",
+ "Latest": "Terbaru",
+ "MessageApplicationUpdated": "Jellyfin Server telah dikemas kini",
+ "MessageApplicationUpdatedTo": "Jellyfin Server telah dikemas kini ke {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini",
+ "MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini",
+ "MixedContent": "Kandungan campuran",
+ "Movies": "Filem",
"Music": "Muzik",
"MusicVideos": "Video muzik",
- "NameInstallFailed": "{0} installation failed",
- "NameSeasonNumber": "Season {0}",
- "NameSeasonUnknown": "Season Unknown",
- "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
- "NotificationOptionApplicationUpdateAvailable": "Application update available",
+ "NameInstallFailed": "{0} pemasangan gagal",
+ "NameSeasonNumber": "Musim {0}",
+ "NameSeasonUnknown": "Musim Tidak Diketahui",
+ "NewVersionIsAvailable": "Versi terbaru Jellyfin Server bersedia untuk dimuat turunkan.",
+ "NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah sedia",
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
"NotificationOptionAudioPlayback": "Audio playback started",
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index 5637ce346..245c3cd63 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -16,7 +16,6 @@
"Folders": "Mapper",
"Genres": "Sjangre",
"HeaderAlbumArtists": "Albumartister",
- "HeaderCameraUploads": "Kameraopplastinger",
"HeaderContinueWatching": "Fortsett å se",
"HeaderFavoriteAlbums": "Favorittalbum",
"HeaderFavoriteArtists": "Favorittartister",
@@ -45,12 +44,12 @@
"NameSeasonNumber": "Sesong {0}",
"NameSeasonUnknown": "Sesong ukjent",
"NewVersionIsAvailable": "En ny versjon av Jellyfin Server er tilgjengelig for nedlasting.",
- "NotificationOptionApplicationUpdateAvailable": "Programvareoppdatering er tilgjengelig",
+ "NotificationOptionApplicationUpdateAvailable": "En programvareoppdatering er tilgjengelig",
"NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering installert",
"NotificationOptionAudioPlayback": "Lydavspilling startet",
"NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppet",
"NotificationOptionCameraImageUploaded": "Kamerabilde lastet opp",
- "NotificationOptionInstallationFailed": "Installasjonsfeil",
+ "NotificationOptionInstallationFailed": "Installasjonen feilet",
"NotificationOptionNewLibraryContent": "Nytt innhold lagt til",
"NotificationOptionPluginError": "Pluginfeil",
"NotificationOptionPluginInstalled": "Plugin installert",
@@ -71,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} mislykkes",
"ScheduledTaskStartedWithName": "{0} startet",
"ServerNameNeedsToBeRestarted": "{0} må startes på nytt",
- "Shows": "Programmer",
+ "Shows": "Program",
"Songs": "Sanger",
"StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.",
"SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for {0}",
@@ -88,7 +87,7 @@
"UserOnlineFromDevice": "{0} er tilkoblet fra {1}",
"UserPasswordChangedWithName": "Passordet for {0} er oppdatert",
"UserPolicyUpdatedWithName": "Brukerpolicyen har blitt oppdatert for {0}",
- "UserStartedPlayingItemWithValues": "{0} har startet avspilling {1}",
+ "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}",
@@ -101,5 +100,18 @@
"TaskRefreshLibrary": "Skann mediebibliotek",
"TaskRefreshChapterImagesDescription": "Lager forhåndsvisningsbilder for videoer som har kapitler.",
"TaskRefreshChapterImages": "Trekk ut Kapittelbilder",
- "TaskCleanCacheDescription": "Sletter mellomlagrede filer som ikke lengre trengs av systemet."
+ "TaskCleanCacheDescription": "Sletter mellomlagrede filer som ikke lengre trengs av systemet.",
+ "TaskDownloadMissingSubtitlesDescription": "Søker etter manglende underteksting på nett basert på metadatakonfigurasjon.",
+ "TaskDownloadMissingSubtitles": "Last ned manglende underteksting",
+ "TaskRefreshChannelsDescription": "Frisker opp internettkanalinformasjon.",
+ "TaskRefreshChannels": "Oppfrisk kanaler",
+ "TaskCleanTranscodeDescription": "Sletter omkodede filer som er mer enn én dag gamle.",
+ "TaskCleanTranscode": "Tøm transkodingmappe",
+ "TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringer for utvidelser som er stilt inn til å oppdatere automatisk.",
+ "TaskUpdatePlugins": "Oppdater utvidelser",
+ "TaskRefreshPeopleDescription": "Oppdaterer metadata for skuespillere og regissører i mediebiblioteket ditt.",
+ "TaskRefreshPeople": "Oppfrisk personer",
+ "TaskCleanLogsDescription": "Sletter loggfiler som er eldre enn {0} dager gamle.",
+ "TaskCleanLogs": "Tøm loggmappe",
+ "TaskRefreshLibraryDescription": "Skanner mediebibliotekene dine for nye filer og oppdaterer metadata."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ne.json b/Emby.Server.Implementations/Localization/Core/ne.json
new file mode 100644
index 000000000..8e820d40c
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/ne.json
@@ -0,0 +1,85 @@
+{
+ "NotificationOptionUserLockedOut": "प्रयोगकर्ता प्रतिबन्धित",
+ "NotificationOptionTaskFailed": "निर्धारित कार्य विफलता",
+ "NotificationOptionServerRestartRequired": "सर्भर रिस्टार्ट आवाश्यक छ",
+ "NotificationOptionPluginUpdateInstalled": "प्लगइन अद्यावधिक स्थापना भयो",
+ "NotificationOptionPluginUninstalled": "प्लगइन विस्थापित",
+ "NotificationOptionPluginInstalled": "प्लगइन स्थापना भयो",
+ "NotificationOptionPluginError": "प्लगइन असफलता",
+ "NotificationOptionNewLibraryContent": "नयाँ सामग्री थपियो",
+ "NotificationOptionInstallationFailed": "स्थापना असफलता",
+ "NotificationOptionCameraImageUploaded": "क्यामेरा फोटो अपलोड गरियो",
+ "NotificationOptionAudioPlaybackStopped": "ध्वनि प्रक्षेपण रोकियो",
+ "NotificationOptionAudioPlayback": "ध्वनि प्रक्षेपण शुरू भयो",
+ "NotificationOptionApplicationUpdateInstalled": "अनुप्रयोग अद्यावधिक स्थापना भयो",
+ "NotificationOptionApplicationUpdateAvailable": "अनुप्रयोग अपडेट उपलब्ध छ",
+ "NewVersionIsAvailable": "जेलीफिन सर्भर को नयाँ संस्करण डाउनलोड को लागी उपलब्ध छ।",
+ "NameSeasonUnknown": "अज्ञात श्रृंखला",
+ "NameSeasonNumber": "श्रृंखला {0}",
+ "NameInstallFailed": "{0} स्थापना असफल भयो",
+ "MusicVideos": "सांगीतिक भिडियोहरू",
+ "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": "",
+ "StartupEmbyServerIsLoading": "Jellyfin सर्भर लोड हुँदैछ। कृपया छिट्टै फेरि प्रयास गर्नुहोस्।",
+ "Songs": "गीतहरू",
+ "Shows": "शोहरू",
+ "ServerNameNeedsToBeRestarted": "{0} लाई पुन: सुरु गर्नु पर्छ",
+ "ScheduledTaskStartedWithName": "{0} सुरु भयो",
+ "ScheduledTaskFailedWithName": "{0} असफल",
+ "ProviderValue": "प्रदायक: {0}",
+ "Plugin": "प्लगइनहरू",
+ "Playlists": "प्लेलिस्टहरू",
+ "Photos": "तस्बिरहरु",
+ "NotificationOptionVideoPlaybackStopped": "भिडियो प्लेब्याक रोकियो",
+ "NotificationOptionVideoPlayback": "भिडियो प्लेब्याक सुरु भयो"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 41c74d54d..e102b92b9 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -16,7 +16,6 @@
"Folders": "Mappen",
"Genres": "Genres",
"HeaderAlbumArtists": "Albumartiesten",
- "HeaderCameraUploads": "Camera-uploads",
"HeaderContinueWatching": "Kijken hervatten",
"HeaderFavoriteAlbums": "Favoriete albums",
"HeaderFavoriteArtists": "Favoriete artiesten",
diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json
index 281cadac5..6236515b2 100644
--- a/Emby.Server.Implementations/Localization/Core/nn.json
+++ b/Emby.Server.Implementations/Localization/Core/nn.json
@@ -19,7 +19,6 @@
"HeaderFavoriteArtists": "Favoritt Artistar",
"HeaderFavoriteAlbums": "Favoritt Album",
"HeaderContinueWatching": "Fortsett å sjå",
- "HeaderCameraUploads": "Kamera Opplastingar",
"HeaderAlbumArtists": "Album Artist",
"Genres": "Sjangrar",
"Folders": "Mapper",
@@ -35,7 +34,7 @@
"AuthenticationSucceededWithUserName": "{0} Har logga inn",
"Artists": "Artistar",
"Application": "Program",
- "AppDeviceValues": "App: {0}, Einheit: {1}",
+ "AppDeviceValues": "App: {0}, Eining: {1}",
"Albums": "Album",
"NotificationOptionServerRestartRequired": "Tenaren krev omstart",
"NotificationOptionPluginUpdateInstalled": "Tilleggsprogram-oppdatering vart installert",
@@ -43,7 +42,7 @@
"NotificationOptionPluginInstalled": "Tilleggsprogram installert",
"NotificationOptionPluginError": "Tilleggsprogram feila",
"NotificationOptionNewLibraryContent": "Nytt innhald er lagt til",
- "NotificationOptionInstallationFailed": "Installasjonen feila",
+ "NotificationOptionInstallationFailed": "Installasjonsfeil",
"NotificationOptionCameraImageUploaded": "Kamerabilde vart lasta opp",
"NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppa",
"NotificationOptionAudioPlayback": "Lydavspilling påbyrja",
@@ -56,5 +55,62 @@
"MusicVideos": "Musikkvideoar",
"Music": "Musikk",
"Movies": "Filmar",
- "MixedContent": "Blanda innhald"
+ "MixedContent": "Blanda innhald",
+ "Sync": "Synkronisera",
+ "TaskDownloadMissingSubtitlesDescription": "Søk Internettet for manglande undertekstar basert på metadatainnstillingar.",
+ "TaskDownloadMissingSubtitles": "Last ned manglande undertekstar",
+ "TaskRefreshChannelsDescription": "Oppdater internettkanalinformasjon.",
+ "TaskRefreshChannels": "Oppdater kanalar",
+ "TaskCleanTranscodeDescription": "Slett transkodefiler som er meir enn ein dag gamal.",
+ "TaskCleanTranscode": "Reins transkodemappe",
+ "TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringar for programtillegg som er sette opp til å oppdaterast automatisk.",
+ "TaskUpdatePlugins": "Oppdaterer programtillegg",
+ "TaskRefreshPeopleDescription": "Oppdaterer metadata for skodespelarar og regissørar i mediebiblioteket ditt.",
+ "TaskRefreshPeople": "Oppdater personar",
+ "TaskCleanLogsDescription": "Slett loggfiler som er meir enn {0} dagar gamle.",
+ "TaskCleanLogs": "Reins loggmappe",
+ "TaskRefreshLibraryDescription": "Skannar mediebiblioteket ditt for nye filer og oppdaterer metadata.",
+ "TaskRefreshLibrary": "Skann mediebibliotek",
+ "TaskRefreshChapterImagesDescription": "Lager miniatyrbilete for videoar som har kapittel.",
+ "TaskRefreshChapterImages": "Trekk ut kapittelbilete",
+ "TaskCleanCacheDescription": "Slettar mellomlagra filer som ikkje lengre trengst av systemet.",
+ "TaskCleanCache": "Rens mappe for hurtiglager",
+ "TasksChannelsCategory": "Internettkanalar",
+ "TasksApplicationCategory": "Applikasjon",
+ "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}",
+ "UserLockedOutWithName": "Brukar {0} har blitt utestengd",
+ "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": "Jellyfintenaren laster. Prøv igjen om litt.",
+ "Songs": "Songar",
+ "Shows": "Program",
+ "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": "Programtillegg",
+ "Playlists": "Speleliste",
+ "Photos": "Foto",
+ "NotificationOptionVideoPlaybackStopped": "Videoavspeling stoppa",
+ "NotificationOptionVideoPlayback": "Videoavspeling starta",
+ "NotificationOptionUserLockedOut": "Brukar er utestengd",
+ "NotificationOptionTaskFailed": "Planlagt oppgåve feila"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index bdc0d0169..003e591b3 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -16,7 +16,6 @@
"Folders": "Foldery",
"Genres": "Gatunki",
"HeaderAlbumArtists": "Wykonawcy albumów",
- "HeaderCameraUploads": "Przekazane obrazy",
"HeaderContinueWatching": "Kontynuuj odtwarzanie",
"HeaderFavoriteAlbums": "Ulubione albumy",
"HeaderFavoriteArtists": "Ulubieni wykonawcy",
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index 275195640..5e49ca702 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -16,7 +16,6 @@
"Folders": "Pastas",
"Genres": "Gêneros",
"HeaderAlbumArtists": "Artistas do Álbum",
- "HeaderCameraUploads": "Envios da Câmera",
"HeaderContinueWatching": "Continuar Assistindo",
"HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index c1fb65743..90a4941c5 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -16,7 +16,6 @@
"Folders": "Pastas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas do Álbum",
- "HeaderCameraUploads": "Envios a partir da câmara",
"HeaderContinueWatching": "Continuar a Ver",
"HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos",
@@ -26,7 +25,7 @@
"HeaderLiveTV": "TV em Direto",
"HeaderNextUp": "A Seguir",
"HeaderRecordingGroups": "Grupos de Gravação",
- "HomeVideos": "Videos caseiros",
+ "HomeVideos": "Vídeos Caseiros",
"Inherit": "Herdar",
"ItemAddedWithName": "{0} foi adicionado à biblioteca",
"ItemRemovedWithName": "{0} foi removido da biblioteca",
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 25c5b9053..2079940cd 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -83,7 +83,6 @@
"Playlists": "Listas de Reprodução",
"Photos": "Fotografias",
"Movies": "Filmes",
- "HeaderCameraUploads": "Carregamentos a partir da câmara",
"FailedLoginAttemptWithUserName": "Tentativa de ligação falhada a partir de {0}",
"DeviceOnlineWithName": "{0} está connectado",
"DeviceOfflineWithName": "{0} desconectou-se",
@@ -101,7 +100,17 @@
"TaskCleanLogsDescription": "Deletar arquivos de log que existe a mais de {0} dias.",
"TaskCleanLogs": "Limpar diretório de log",
"TaskRefreshLibrary": "Escanear biblioteca de mídias",
- "TaskRefreshChapterImagesDescription": "Criar miniaturas para videos que tem capítulos.",
- "TaskCleanCacheDescription": "Deletar arquivos de cache que não são mais usados pelo sistema.",
- "TasksChannelsCategory": "Canais de Internet"
+ "TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
+ "TaskCleanCacheDescription": "Apaga ficheiros em cache que já não são usados pelo sistema.",
+ "TasksChannelsCategory": "Canais de Internet",
+ "TaskRefreshChapterImages": "Extrair Imagens do Capítulo",
+ "TaskDownloadMissingSubtitlesDescription": "Pesquisa na Internet as legendas em falta com base na configuração de metadados.",
+ "TaskDownloadMissingSubtitles": "Download das legendas em falta",
+ "TaskRefreshChannelsDescription": "Atualiza as informações do canal da Internet.",
+ "TaskCleanTranscodeDescription": "Apagar os ficheiros com mais de um dia, de Transcode.",
+ "TaskCleanTranscode": "Limpar o diretório de Transcode",
+ "TaskUpdatePluginsDescription": "Download e instala as atualizações para plug-ins configurados para atualização automática.",
+ "TaskRefreshPeopleDescription": "Atualiza os metadados para atores e diretores na tua biblioteca de media.",
+ "TaskRefreshPeople": "Atualizar pessoas",
+ "TaskRefreshLibraryDescription": "Pesquisa a tua biblioteca de media por novos ficheiros e atualiza os metadados."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index 699dd26da..bc008df3b 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -74,7 +74,6 @@
"HeaderFavoriteArtists": "Artiști Favoriți",
"HeaderFavoriteAlbums": "Albume Favorite",
"HeaderContinueWatching": "Vizionează în continuare",
- "HeaderCameraUploads": "Incărcări Cameră Foto",
"HeaderAlbumArtists": "Album Artiști",
"Genres": "Genuri",
"Folders": "Dosare",
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 71ee6446c..95b93afb8 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -16,12 +16,11 @@
"Folders": "Папки",
"Genres": "Жанры",
"HeaderAlbumArtists": "Исполнители альбома",
- "HeaderCameraUploads": "Камеры",
"HeaderContinueWatching": "Продолжение просмотра",
"HeaderFavoriteAlbums": "Избранные альбомы",
"HeaderFavoriteArtists": "Избранные исполнители",
"HeaderFavoriteEpisodes": "Избранные эпизоды",
- "HeaderFavoriteShows": "Избранные передачи",
+ "HeaderFavoriteShows": "Избранные сериалы",
"HeaderFavoriteSongs": "Избранные композиции",
"HeaderLiveTV": "Эфир",
"HeaderNextUp": "Очередное",
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index 0ee652637..8e5026944 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -16,7 +16,6 @@
"Folders": "Priečinky",
"Genres": "Žánre",
"HeaderAlbumArtists": "Umelci albumu",
- "HeaderCameraUploads": "Nahrané fotografie",
"HeaderContinueWatching": "Pokračovať v pozeraní",
"HeaderFavoriteAlbums": "Obľúbené albumy",
"HeaderFavoriteArtists": "Obľúbení umelci",
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index 60c58d472..ff4b9e84f 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -16,7 +16,6 @@
"Folders": "Mape",
"Genres": "Zvrsti",
"HeaderAlbumArtists": "Izvajalci albuma",
- "HeaderCameraUploads": "Posnetki kamere",
"HeaderContinueWatching": "Nadaljuj gledanje",
"HeaderFavoriteAlbums": "Priljubljeni albumi",
"HeaderFavoriteArtists": "Priljubljeni izvajalci",
@@ -113,5 +112,6 @@
"TasksChannelsCategory": "Spletni kanali",
"TasksApplicationCategory": "Aplikacija",
"TasksLibraryCategory": "Knjižnica",
- "TasksMaintenanceCategory": "Vzdrževanje"
+ "TasksMaintenanceCategory": "Vzdrževanje",
+ "TaskDownloadMissingSubtitlesDescription": "Na podlagi nastavitev metapodatkov poišče manjkajoče podnapise na internetu."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json
new file mode 100644
index 000000000..ecca5af4c
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/sq.json
@@ -0,0 +1,116 @@
+{
+ "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ë",
+ "TaskRefreshChannelsDescription": "Rifreskon informacionin e kanaleve të internetit.",
+ "TaskRefreshChannels": "Rifresko Kanalet",
+ "TaskCleanTranscodeDescription": "Fshin skedarët e transkodimit që janë më të vjetër se një ditë.",
+ "TaskCleanTranscode": "Fshi dosjen e transkodimit",
+ "TaskUpdatePluginsDescription": "Shkarkon dhe instalon përditësimi për plugin që janë konfiguruar të përditësohen automatikisht.",
+ "TaskUpdatePlugins": "Përditëso Plugin",
+ "TaskRefreshPeopleDescription": "Përditëson metadata të aktorëve dhe regjizorëve në librarinë tuaj.",
+ "TaskRefreshPeople": "Rifresko aktorët",
+ "TaskCleanLogsDescription": "Fshin skëdarët log që janë më të vjetër se {0} ditë.",
+ "TaskCleanLogs": "Fshi dosjen Log",
+ "TaskRefreshLibraryDescription": "Skanon librarinë media për skedarë të rinj dhe rifreskon metadata.",
+ "TaskRefreshLibrary": "Skano librarinë media",
+ "TaskRefreshChapterImagesDescription": "Krijon imazh për videot që kanë kapituj.",
+ "TaskRefreshChapterImages": "Ekstrakto Imazhet e Kapitullit",
+ "TaskCleanCacheDescription": "Fshi skedarët e cache-s që nuk i duhen më sistemit.",
+ "TaskCleanCache": "Pastro memorjen cache",
+ "TasksChannelsCategory": "Kanalet nga interneti",
+ "TasksApplicationCategory": "Aplikacioni",
+ "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}",
+ "UserLockedOutWithName": "Përdoruesi {0} u përjashtua",
+ "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ë",
+ "Shows": "Seriale",
+ "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",
+ "NotificationOptionUserLockedOut": "Përdoruesi u përjashtua",
+ "NotificationOptionTaskFailed": "Ushtrimi i planifikuar dështoi",
+ "NotificationOptionServerRestartRequired": "Kërkohet ristartim i serverit",
+ "NotificationOptionPluginUpdateInstalled": "Përditësimi i plugin u instalua",
+ "NotificationOptionPluginUninstalled": "Plugin u çinstalua",
+ "NotificationOptionPluginInstalled": "Plugin u instalua",
+ "NotificationOptionPluginError": "Plugin dështoi",
+ "NotificationOptionNewLibraryContent": "Një përmbajtje e re u shtua",
+ "NotificationOptionInstallationFailed": "Instalimi dështoi",
+ "NotificationOptionCameraImageUploaded": "Fotoja nga kamera u ngarkua",
+ "NotificationOptionAudioPlaybackStopped": "Luajtja e audios ndaloi",
+ "NotificationOptionAudioPlayback": "Luajtja e audios filloi",
+ "NotificationOptionApplicationUpdateInstalled": "Përditësimi i aplikacionit u instalua",
+ "NotificationOptionApplicationUpdateAvailable": "Një perditësim i aplikacionit është gati",
+ "NewVersionIsAvailable": "Një version i ri i Jellyfin është gati për tu shkarkuar.",
+ "NameSeasonUnknown": "Sezon i panjohur",
+ "NameSeasonNumber": "Sezoni {0}",
+ "NameInstallFailed": "Instalimi i {0} dështoi",
+ "MusicVideos": "Video muzikore",
+ "Music": "Muzikë",
+ "Movies": "Filma",
+ "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": "Zhanre",
+ "Folders": "Dosje",
+ "Favorites": "Të preferuara",
+ "FailedLoginAttemptWithUserName": "Përpjekja për hyrje dështoi nga {0}",
+ "DeviceOnlineWithName": "{0} u lidh",
+ "DeviceOfflineWithName": "{0} u shkëput",
+ "Collections": "Koleksione",
+ "ChapterNameValue": "Kapituj",
+ "Channels": "Kanale",
+ "CameraImageUploadedFrom": "Një foto e re nga kamera u ngarkua nga {0}",
+ "Books": "Libra",
+ "AuthenticationSucceededWithUserName": "{0} u identifikua me sukses",
+ "Artists": "Artistë",
+ "Application": "Aplikacioni",
+ "AppDeviceValues": "Aplikacioni: {0}, Pajisja: {1}",
+ "Albums": "Albumet"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json
index 5f3cbb1c8..2b1eccfaf 100644
--- a/Emby.Server.Implementations/Localization/Core/sr.json
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -74,7 +74,6 @@
"HeaderFavoriteArtists": "Омиљени извођачи",
"HeaderFavoriteAlbums": "Омиљени албуми",
"HeaderContinueWatching": "Настави гледање",
- "HeaderCameraUploads": "Слања са камере",
"HeaderAlbumArtists": "Извођачи албума",
"Genres": "Жанрови",
"Folders": "Фасцикле",
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index c8662b2ca..bea294ba2 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -9,14 +9,13 @@
"Channels": "Kanaler",
"ChapterNameValue": "Kapitel {0}",
"Collections": "Samlingar",
- "DeviceOfflineWithName": "{0} har kopplat från",
+ "DeviceOfflineWithName": "{0} har kopplat ner",
"DeviceOnlineWithName": "{0} är ansluten",
"FailedLoginAttemptWithUserName": "Misslyckat inloggningsförsök från {0}",
"Favorites": "Favoriter",
"Folders": "Mappar",
"Genres": "Genrer",
"HeaderAlbumArtists": "Albumartister",
- "HeaderCameraUploads": "Kamerauppladdningar",
"HeaderContinueWatching": "Fortsätt kolla",
"HeaderFavoriteAlbums": "Favoritalbum",
"HeaderFavoriteArtists": "Favoritartister",
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
new file mode 100644
index 000000000..ae38f45e1
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -0,0 +1,116 @@
+{
+ "VersionNumber": "பதிப்பு {0}",
+ "ValueSpecialEpisodeName": "சிறப்பு - {0}",
+ "TasksMaintenanceCategory": "பராமரிப்பு",
+ "TaskCleanCache": "தற்காலிக சேமிப்பு கோப்பகத்தை சுத்தம் செய்யவும்",
+ "TaskRefreshChapterImages": "அத்தியாயப் படங்களை பிரித்தெடுக்கவும்",
+ "TaskRefreshPeople": "மக்களைப் புதுப்பிக்கவும்",
+ "TaskCleanTranscode": "டிரான்ஸ்கோட் கோப்பகத்தை சுத்தம் செய்யவும்",
+ "TaskRefreshChannelsDescription": "இணையச் சேனல் தகவல்களைப் புதுப்பிக்கிறது.",
+ "System": "ஒருங்கியம்",
+ "NotificationOptionTaskFailed": "திட்டமிடப்பட்ட பணி தோல்வியடைந்தது",
+ "NotificationOptionPluginUpdateInstalled": "உட்செருகி புதுப்பிக்கப்பட்டது",
+ "NotificationOptionPluginUninstalled": "உட்செருகி நீக்கப்பட்டது",
+ "NotificationOptionPluginInstalled": "உட்செருகி நிறுவப்பட்டது",
+ "NotificationOptionPluginError": "உட்செருகி செயலிழந்தது",
+ "NotificationOptionCameraImageUploaded": "புகைப்படம் பதிவேற்றப்பட்டது",
+ "MixedContent": "கலப்பு உள்ளடக்கங்கள்",
+ "MessageServerConfigurationUpdated": "சேவையக அமைப்புகள் புதுப்பிக்கப்பட்டன",
+ "MessageApplicationUpdatedTo": "ஜெல்லிஃபின் சேவையகம் {0} இற்கு புதுப்பிக்கப்பட்டது",
+ "MessageApplicationUpdated": "ஜெல்லிஃபின் சேவையகம் புதுப்பிக்கப்பட்டது",
+ "Inherit": "மரபுரிமையாகப் பெறு",
+ "HeaderRecordingGroups": "பதிவு குழுக்கள்",
+ "Folders": "கோப்புறைகள்",
+ "FailedLoginAttemptWithUserName": "{0} இலிருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது",
+ "DeviceOnlineWithName": "{0} இணைக்கப்பட்டது",
+ "DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது",
+ "Collections": "தொகுப்புகள்",
+ "CameraImageUploadedFrom": "{0} இல் இருந்து புதிய புகைப்படம் பதிவேற்றப்பட்டது",
+ "AppDeviceValues": "செயலி: {0}, சாதனம்: {1}",
+ "TaskDownloadMissingSubtitles": "விடுபட்டுபோன வசன வரிகளைப் பதிவிறக்கு",
+ "TaskRefreshChannels": "சேனல்களை புதுப்பி",
+ "TaskUpdatePlugins": "உட்செருகிகளை புதுப்பி",
+ "TaskRefreshLibrary": "ஊடக நூலகத்தை ஆராய்",
+ "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": "நிகழ்பட ஒளிபரப்பு துவங்கியது",
+ "NotificationOptionUserLockedOut": "பயனர் கணக்கு முடக்கப்பட்டது",
+ "NotificationOptionServerRestartRequired": "சேவையக மறுதொடக்கம் தேவை",
+ "NotificationOptionNewLibraryContent": "புதிய உள்ளடக்கங்கள் சேர்க்கப்பட்டன",
+ "NotificationOptionInstallationFailed": "நிறுவல் தோல்வியடைந்தது",
+ "NotificationOptionAudioPlaybackStopped": "ஒலி இசைத்தல் நிறுத்தப்பட்டது",
+ "NotificationOptionAudioPlayback": "ஒலி இசைக்கத் துவங்கியுள்ளது",
+ "NotificationOptionApplicationUpdateInstalled": "செயலி புதுப்பிக்கப்பட்டது",
+ "NotificationOptionApplicationUpdateAvailable": "செயலியினை புதுப்பிக்கலாம்",
+ "NameSeasonUnknown": "அறியப்படாத பருவம்",
+ "NameSeasonNumber": "பருவம் {0}",
+ "NameInstallFailed": "{0} நிறுவல் தோல்வியடைந்தது",
+ "MusicVideos": "இசைப்படங்கள்",
+ "Music": "இசை",
+ "Movies": "திரைப்படங்கள்",
+ "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} க்கு பதிவிறக்கத் தவறிவிட்டன",
+ "TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
+ "TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.",
+ "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
+ "TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.",
+ "TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",
+ "TaskCleanLogs": "பதிவு அடைவை சுத்தம் செய்யுங்கள்",
+ "TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் ஊடக நூலகத்தை ஆராய்ந்து மீத்தரவை புதுப்பிக்கும்.",
+ "TaskRefreshChapterImagesDescription": "அத்தியாயங்களைக் கொண்ட வீடியோக்களுக்கான சிறு உருவங்களை உருவாக்குகிறது.",
+ "ValueHasBeenAddedToLibrary": "உங்கள் மீடியா நூலகத்தில் {0} சேர்க்கப்பட்டது",
+ "UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்",
+ "HomeVideos": "முகப்பு வீடியோக்கள்",
+ "UserStoppedPlayingItemWithValues": "{0} {2} இல் {1} முடித்துவிட்டது",
+ "UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json
index 32538ac03..71dd2c7a3 100644
--- a/Emby.Server.Implementations/Localization/Core/th.json
+++ b/Emby.Server.Implementations/Localization/Core/th.json
@@ -1,71 +1,116 @@
{
"ProviderValue": "ผู้ให้บริการ: {0}",
- "PluginUpdatedWithName": "{0} ได้รับการ update แล้ว",
- "PluginUninstalledWithName": "ถอนการติดตั้ง {0}",
- "PluginInstalledWithName": "{0} ได้รับการติดตั้ง",
- "Plugin": "Plugin",
- "Playlists": "รายการ",
+ "PluginUpdatedWithName": "อัปเดต {0} แล้ว",
+ "PluginUninstalledWithName": "ถอนการติดตั้ง {0} แล้ว",
+ "PluginInstalledWithName": "ติดตั้ง {0} แล้ว",
+ "Plugin": "ปลั๊กอิน",
+ "Playlists": "เพลย์ลิสต์",
"Photos": "รูปภาพ",
- "NotificationOptionVideoPlaybackStopped": "หยุดการเล่น Video",
- "NotificationOptionVideoPlayback": "เริ่มแสดง Video",
- "NotificationOptionUserLockedOut": "ผู้ใช้ Locked Out",
- "NotificationOptionTaskFailed": "ตารางการทำงานล้มเหลว",
- "NotificationOptionServerRestartRequired": "ควร Restart Server",
- "NotificationOptionPluginUpdateInstalled": "Update Plugin แล้ว",
- "NotificationOptionPluginUninstalled": "ถอด Plugin",
- "NotificationOptionPluginInstalled": "ติดตั้ง Plugin แล้ว",
- "NotificationOptionPluginError": "Plugin ล้มเหลว",
- "NotificationOptionNewLibraryContent": "เพิ่มข้อมูลใหม่แล้ว",
- "NotificationOptionInstallationFailed": "ติดตั้งล้มเหลว",
- "NotificationOptionCameraImageUploaded": "รูปภาพถูก upload",
- "NotificationOptionAudioPlaybackStopped": "หยุดการเล่นเสียง",
+ "NotificationOptionVideoPlaybackStopped": "หยุดเล่นวิดีโอ",
+ "NotificationOptionVideoPlayback": "เริ่มเล่นวิดีโอ",
+ "NotificationOptionUserLockedOut": "ผู้ใช้ถูกล็อก",
+ "NotificationOptionTaskFailed": "งานตามกำหนดการล้มเหลว",
+ "NotificationOptionServerRestartRequired": "จำเป็นต้องรีสตาร์ทเซิร์ฟเวอร์",
+ "NotificationOptionPluginUpdateInstalled": "ติดตั้งการอัปเดตปลั๊กอินแล้ว",
+ "NotificationOptionPluginUninstalled": "ถอนการติดตั้งปลั๊กอินแล้ว",
+ "NotificationOptionPluginInstalled": "ติดตั้งปลั๊กอินแล้ว",
+ "NotificationOptionPluginError": "ปลั๊กอินล้มเหลว",
+ "NotificationOptionNewLibraryContent": "เพิ่มเนื้อหาใหม่แล้ว",
+ "NotificationOptionInstallationFailed": "การติดตั้งล้มเหลว",
+ "NotificationOptionCameraImageUploaded": "อัปโหลดภาพถ่ายแล้ว",
+ "NotificationOptionAudioPlaybackStopped": "หยุดเล่นเสียง",
"NotificationOptionAudioPlayback": "เริ่มเล่นเสียง",
- "NotificationOptionApplicationUpdateInstalled": "Update ระบบแล้ว",
- "NotificationOptionApplicationUpdateAvailable": "ระบบ update สามารถใช้ได้แล้ว",
- "NewVersionIsAvailable": "ตรวจพบ Jellyfin เวอร์ชั่นใหม่",
- "NameSeasonUnknown": "ไม่ทราบปี",
- "NameSeasonNumber": "ปี {0}",
- "NameInstallFailed": "{0} ติดตั้งไม่สำเร็จ",
- "MusicVideos": "MV",
- "Music": "เพลง",
- "Movies": "ภาพยนต์",
- "MixedContent": "รายการแบบผสม",
- "MessageServerConfigurationUpdated": "การตั้งค่า update แล้ว",
- "MessageNamedServerConfigurationUpdatedWithValue": "รายการตั้งค่า {0} ได้รับการ update แล้ว",
- "MessageApplicationUpdatedTo": "Jellyfin Server จะ update ไปที่ {0}",
- "MessageApplicationUpdated": "Jellyfin Server update แล้ว",
+ "NotificationOptionApplicationUpdateInstalled": "ติดตั้งการอัปเดตแอปพลิเคชันแล้ว",
+ "NotificationOptionApplicationUpdateAvailable": "มีการอัปเดตแอปพลิเคชัน",
+ "NewVersionIsAvailable": "เวอร์ชันใหม่ของเซิร์ฟเวอร์ Jellyfin พร้อมให้ดาวน์โหลดแล้ว",
+ "NameSeasonUnknown": "ไม่ทราบซีซัน",
+ "NameSeasonNumber": "ซีซัน {0}",
+ "NameInstallFailed": "การติดตั้ง {0} ล้มเหลว",
+ "MusicVideos": "มิวสิควิดีโอ",
+ "Music": "ดนตรี",
+ "Movies": "ภาพยนตร์",
+ "MixedContent": "เนื้อหาผสม",
+ "MessageServerConfigurationUpdated": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์แล้ว",
+ "MessageNamedServerConfigurationUpdatedWithValue": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์ในส่วน {0} แล้ว",
+ "MessageApplicationUpdatedTo": "เซิร์ฟเวอร์ Jellyfin ได้รับการอัปเดตเป็น {0}",
+ "MessageApplicationUpdated": "อัพเดตเซิร์ฟเวอร์ Jellyfin แล้ว",
"Latest": "ล่าสุด",
- "LabelRunningTimeValue": "เวลาที่เล่น : {0}",
- "LabelIpAddressValue": "IP address: {0}",
- "ItemRemovedWithName": "{0} ถูกลบจากรายการ",
- "ItemAddedWithName": "{0} ถูกเพิ่มในรายการ",
- "Inherit": "การสืบทอด",
- "HomeVideos": "วีดีโอส่วนตัว",
- "HeaderRecordingGroups": "ค่ายบันทึก",
+ "LabelRunningTimeValue": "ผ่านไปแล้ว: {0}",
+ "LabelIpAddressValue": "ที่อยู่ IP: {0}",
+ "ItemRemovedWithName": "{0} ถูกลบออกจากไลบรารี",
+ "ItemAddedWithName": "{0} ถูกเพิ่มลงในไลบรารีแล้ว",
+ "Inherit": "สืบทอด",
+ "HomeVideos": "โฮมวิดีโอ",
+ "HeaderRecordingGroups": "กลุ่มการบันทึก",
"HeaderNextUp": "ถัดไป",
- "HeaderLiveTV": "รายการสด",
- "HeaderFavoriteSongs": "เพลงโปรด",
- "HeaderFavoriteShows": "รายการโชว์โปรด",
- "HeaderFavoriteEpisodes": "ฉากโปรด",
- "HeaderFavoriteArtists": "นักแสดงโปรด",
- "HeaderFavoriteAlbums": "อัมบั้มโปรด",
- "HeaderContinueWatching": "ชมต่อจากเดิม",
- "HeaderCameraUploads": "Upload รูปภาพ",
- "HeaderAlbumArtists": "อัลบั้มนักแสดง",
+ "HeaderLiveTV": "ทีวีสด",
+ "HeaderFavoriteSongs": "เพลงที่ชื่นชอบ",
+ "HeaderFavoriteShows": "รายการที่ชื่นชอบ",
+ "HeaderFavoriteEpisodes": "ตอนที่ชื่นชอบ",
+ "HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ",
+ "HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ",
+ "HeaderContinueWatching": "ดูต่อ",
+ "HeaderAlbumArtists": "อัลบั้มศิลปิน",
"Genres": "ประเภท",
"Folders": "โฟลเดอร์",
"Favorites": "รายการโปรด",
- "FailedLoginAttemptWithUserName": "การเชื่อมต่อล้มเหลวจาก {0}",
- "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จ",
- "DeviceOfflineWithName": "{0} ตัดการเชื่อมต่อ",
- "Collections": "ชุด",
- "ChapterNameValue": "บทที่ {0}",
- "Channels": "ชาแนล",
- "CameraImageUploadedFrom": "รูปภาพถูก upload จาก {0}",
+ "FailedLoginAttemptWithUserName": "ความพยายามในการเข้าสู่ระบบล้มเหลวจาก {0}",
+ "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จแล้ว",
+ "DeviceOfflineWithName": "{0} ยกเลิกการเชื่อมต่อแล้ว",
+ "Collections": "คอลเลกชัน",
+ "ChapterNameValue": "บท {0}",
+ "Channels": "ช่อง",
+ "CameraImageUploadedFrom": "ภาพถ่ายใหม่ได้ถูกอัปโหลดมาจาก {0}",
"Books": "หนังสือ",
- "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จ",
- "Artists": "นักแสดง",
- "Application": "แอปพลิเคชั่น",
- "AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
- "Albums": "อัลบั้ม"
+ "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จแล้ว",
+ "Artists": "ศิลปิน",
+ "Application": "แอปพลิเคชัน",
+ "AppDeviceValues": "แอป: {0}, อุปกรณ์: {1}",
+ "Albums": "อัลบั้ม",
+ "ScheduledTaskStartedWithName": "{0} เริ่มต้น",
+ "ScheduledTaskFailedWithName": "{0} ล้มเหลว",
+ "Songs": "เพลง",
+ "Shows": "รายการ",
+ "ServerNameNeedsToBeRestarted": "{0} ต้องการการรีสตาร์ท",
+ "TaskDownloadMissingSubtitlesDescription": "ค้นหาคำบรรยายที่หายไปในอินเทอร์เน็ตตามค่ากำหนดในข้อมูลเมตา",
+ "TaskDownloadMissingSubtitles": "ดาวน์โหลดคำบรรยายที่ขาดหายไป",
+ "TaskRefreshChannelsDescription": "รีเฟรชข้อมูลช่องอินเทอร์เน็ต",
+ "TaskRefreshChannels": "รีเฟรชช่อง",
+ "TaskCleanTranscodeDescription": "ลบไฟล์ทรานส์โค้ดที่มีอายุมากกว่าหนึ่งวัน",
+ "TaskCleanTranscode": "ล้างไดเรกทอรีทรานส์โค้ด",
+ "TaskUpdatePluginsDescription": "ดาวน์โหลดและติดตั้งโปรแกรมปรับปรุงให้กับปลั๊กอินที่กำหนดค่าให้อัปเดตโดยอัตโนมัติ",
+ "TaskUpdatePlugins": "อัปเดตปลั๊กอิน",
+ "TaskRefreshPeopleDescription": "อัปเดตข้อมูลเมตานักแสดงและผู้กำกับในไลบรารีสื่อ",
+ "TaskRefreshPeople": "รีเฟรชบุคคล",
+ "TaskCleanLogsDescription": "ลบไฟล์บันทึกที่เก่ากว่า {0} วัน",
+ "TaskCleanLogs": "ล้างไดเรกทอรีบันทึก",
+ "TaskRefreshLibraryDescription": "สแกนไลบรารีสื่อของคุณเพื่อหาไฟล์ใหม่และรีเฟรชข้อมูลเมตา",
+ "TaskRefreshLibrary": "สแกนไลบรารีสื่อ",
+ "TaskRefreshChapterImagesDescription": "สร้างภาพขนาดย่อสำหรับวิดีโอที่มีบท",
+ "TaskRefreshChapterImages": "แตกรูปภาพบท",
+ "TaskCleanCacheDescription": "ลบไฟล์แคชที่ระบบไม่ต้องการ",
+ "TaskCleanCache": "ล้างไดเรกทอรีแคช",
+ "TasksChannelsCategory": "ช่องอินเทอร์เน็ต",
+ "TasksApplicationCategory": "แอปพลิเคชัน",
+ "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}",
+ "UserLockedOutWithName": "ผู้ใช้ {0} ถูกล็อก",
+ "UserDownloadingItemWithValues": "{0} กำลังดาวน์โหลด {1}",
+ "UserDeletedWithName": "ลบผู้ใช้ {0} แล้ว",
+ "UserCreatedWithName": "สร้างผู้ใช้ {0} แล้ว",
+ "User": "ผู้ใช้งาน",
+ "TvShows": "รายการทีวี",
+ "System": "ระบบ",
+ "Sync": "ซิงค์",
+ "SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้",
+ "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่"
}
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index 3cf3482eb..1e5f2cf19 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -16,7 +16,6 @@
"Folders": "Klasörler",
"Genres": "Türler",
"HeaderAlbumArtists": "Albüm Sanatçıları",
- "HeaderCameraUploads": "Kamera Yüklemeleri",
"HeaderContinueWatching": "İzlemeye Devam Et",
"HeaderFavoriteAlbums": "Favori Albümler",
"HeaderFavoriteArtists": "Favori Sanatçılar",
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index b2e0b66fe..06cc5f633 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -1,13 +1,13 @@
{
- "MusicVideos": "Музичні відео",
+ "MusicVideos": "Музичні кліпи",
"Music": "Музика",
"Movies": "Фільми",
- "MessageApplicationUpdatedTo": "Jellyfin Server був оновлений до версії {0}",
- "MessageApplicationUpdated": "Jellyfin Server був оновлений",
+ "MessageApplicationUpdatedTo": "Jellyfin Server оновлено до версії {0}",
+ "MessageApplicationUpdated": "Jellyfin Server оновлено",
"Latest": "Останні",
- "LabelIpAddressValue": "IP-адреси: {0}",
- "ItemRemovedWithName": "{0} видалено з бібліотеки",
- "ItemAddedWithName": "{0} додано до бібліотеки",
+ "LabelIpAddressValue": "IP-адреса: {0}",
+ "ItemRemovedWithName": "{0} видалено з медіатеки",
+ "ItemAddedWithName": "{0} додано до медіатеки",
"HeaderNextUp": "Наступний",
"HeaderLiveTV": "Ефірне ТБ",
"HeaderFavoriteSongs": "Улюблені пісні",
@@ -16,21 +16,101 @@
"HeaderFavoriteArtists": "Улюблені виконавці",
"HeaderFavoriteAlbums": "Улюблені альбоми",
"HeaderContinueWatching": "Продовжити перегляд",
- "HeaderCameraUploads": "Завантажено з камери",
- "HeaderAlbumArtists": "Виконавці альбомів",
+ "HeaderAlbumArtists": "Виконавці альбому",
"Genres": "Жанри",
- "Folders": "Директорії",
+ "Folders": "Каталоги",
"Favorites": "Улюблені",
- "DeviceOnlineWithName": "{0} під'єднано",
- "DeviceOfflineWithName": "{0} від'єднано",
+ "DeviceOnlineWithName": "Пристрій {0} підключився",
+ "DeviceOfflineWithName": "Пристрій {0} відключився",
"Collections": "Колекції",
- "ChapterNameValue": "Глава {0}",
+ "ChapterNameValue": "Розділ {0}",
"Channels": "Канали",
"CameraImageUploadedFrom": "Нова фотографія завантажена з {0}",
"Books": "Книги",
- "AuthenticationSucceededWithUserName": "{0} успішно авторизовані",
+ "AuthenticationSucceededWithUserName": "{0} успішно авторизований",
"Artists": "Виконавці",
"Application": "Додаток",
"AppDeviceValues": "Додаток: {0}, Пристрій: {1}",
- "Albums": "Альбоми"
+ "Albums": "Альбоми",
+ "NotificationOptionServerRestartRequired": "Необхідно перезапустити сервер",
+ "NotificationOptionPluginUpdateInstalled": "Встановлено оновлення плагіна",
+ "NotificationOptionPluginUninstalled": "Плагін видалено",
+ "NotificationOptionPluginInstalled": "Плагін встановлено",
+ "NotificationOptionPluginError": "Помилка плагіна",
+ "NotificationOptionNewLibraryContent": "Додано новий контент",
+ "HomeVideos": "Домашнє відео",
+ "FailedLoginAttemptWithUserName": "Невдала спроба входу від {0}",
+ "LabelRunningTimeValue": "Тривалість: {0}",
+ "TaskDownloadMissingSubtitlesDescription": "Шукає в Інтернеті відсутні субтитри на основі конфігурації метаданих.",
+ "TaskDownloadMissingSubtitles": "Завантажити відсутні субтитри",
+ "TaskRefreshChannelsDescription": "Оновлення інформації про Інтернет-канали.",
+ "TaskRefreshChannels": "Оновити канали",
+ "TaskCleanTranscodeDescription": "Вилучає файли для перекодування старше одного дня.",
+ "TaskCleanTranscode": "Очистити каталог перекодування",
+ "TaskUpdatePluginsDescription": "Завантажує та встановлює оновлення для плагінів, налаштованих на автоматичне оновлення.",
+ "TaskUpdatePlugins": "Оновити плагіни",
+ "TaskRefreshPeopleDescription": "Оновлення метаданих для акторів та режисерів у вашій медіатеці.",
+ "TaskRefreshPeople": "Оновити людей",
+ "TaskCleanLogsDescription": "Видаляє файли журналу, яким більше {0} днів.",
+ "TaskCleanLogs": "Очистити журнали",
+ "TaskRefreshLibraryDescription": "Сканує медіатеку на нові файли та оновлює метадані.",
+ "TaskRefreshLibrary": "Сканувати медіатеку",
+ "TaskRefreshChapterImagesDescription": "Створює ескізи для відео, які мають розділи.",
+ "TaskRefreshChapterImages": "Створити ескізи розділів",
+ "TaskCleanCacheDescription": "Видаляє файли кешу, які більше не потрібні системі.",
+ "TaskCleanCache": "Очистити кеш",
+ "TasksChannelsCategory": "Інтернет-канали",
+ "TasksApplicationCategory": "Додаток",
+ "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}",
+ "UserLockedOutWithName": "Користувача {0} заблоковано",
+ "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": "Розпочато відтворення відео",
+ "NotificationOptionUserLockedOut": "Користувача заблоковано",
+ "NotificationOptionTaskFailed": "Помилка запланованого завдання",
+ "NotificationOptionInstallationFailed": "Помилка встановлення",
+ "NotificationOptionCameraImageUploaded": "Фотографію завантажено",
+ "NotificationOptionAudioPlaybackStopped": "Відтворення аудіо зупинено",
+ "NotificationOptionAudioPlayback": "Розпочато відтворення аудіо",
+ "NotificationOptionApplicationUpdateInstalled": "Встановлено оновлення додатка",
+ "NotificationOptionApplicationUpdateAvailable": "Доступне оновлення додатка",
+ "NewVersionIsAvailable": "Для завантаження доступна нова версія Jellyfin Server.",
+ "NameSeasonUnknown": "Сезон Невідомий",
+ "NameSeasonNumber": "Сезон {0}",
+ "NameInstallFailed": "Не вдалося встановити {0}",
+ "MixedContent": "Змішаний контент",
+ "MessageServerConfigurationUpdated": "Конфігурація сервера оновлена",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Розділ конфігурації сервера {0} оновлено",
+ "Inherit": "Успадкувати",
+ "HeaderRecordingGroups": "Групи запису"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ur_PK.json b/Emby.Server.Implementations/Localization/Core/ur_PK.json
index 9a5874e29..fa7b2d4d0 100644
--- a/Emby.Server.Implementations/Localization/Core/ur_PK.json
+++ b/Emby.Server.Implementations/Localization/Core/ur_PK.json
@@ -105,7 +105,6 @@
"Inherit": "وراثت میں",
"HomeVideos": "ہوم ویڈیو",
"HeaderRecordingGroups": "ریکارڈنگ گروپس",
- "HeaderCameraUploads": "کیمرہ اپلوڈز",
"FailedLoginAttemptWithUserName": "لاگن کئ کوشش ناکام {0}",
"DeviceOnlineWithName": "{0} متصل ھو چکا ھے",
"DeviceOfflineWithName": "{0} منقطع ھو چکا ھے",
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
new file mode 100644
index 000000000..ac74deff8
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -0,0 +1,116 @@
+{
+ "Collections": "Bộ Sưu Tập",
+ "Favorites": "Yêu Thích",
+ "Folders": "Thư Mục",
+ "Genres": "Thể Loại",
+ "HeaderAlbumArtists": "Bộ Sưu Tập 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": "Các Bài Hát",
+ "Sync": "Đồng Bộ",
+ "ValueSpecialEpisodeName": "Đặc Biệt - {0}",
+ "Albums": "Albums",
+ "Artists": "Các Nghệ 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",
+ "TaskRefreshChannelsDescription": "Làm mới thông tin kênh internet.",
+ "TaskRefreshChannels": "Làm Mới Kênh",
+ "TaskCleanTranscodeDescription": "Xóa các tệp chuyển mã cũ hơn một ngày.",
+ "TaskCleanTranscode": "Làm Sạch Thư Mục Chuyển Mã",
+ "TaskUpdatePluginsDescription": "Tải xuống và cài đặt các bản cập nhật cho các plugin được định cấu hình để cập nhật tự động.",
+ "TaskUpdatePlugins": "Cập Nhật Plugins",
+ "TaskRefreshPeopleDescription": "Cập nhật thông tin chi tiết cho diễn viên và đạo diễn trong thư viện phương tiện của bạn.",
+ "TaskRefreshPeople": "Làm mới Người dùng",
+ "TaskCleanLogsDescription": "Xóa tập tin nhật ký cũ hơn {0} ngày.",
+ "TaskCleanLogs": "Làm sạch nhật ký",
+ "TaskRefreshLibraryDescription": "Quét thư viện phương tiện của bạn để tìm các tệp mới và làm mới thông tin chi tiết.",
+ "TaskRefreshLibrary": "Quét Thư viện Phương tiện",
+ "TaskRefreshChapterImagesDescription": "Tạo hình thu nhỏ cho video có các phân cảnh.",
+ "TaskRefreshChapterImages": "Trích Xuất Ảnh Phân Cảnh",
+ "TaskCleanCacheDescription": "Xóa các tệp cache không còn cần thiết của hệ thống.",
+ "TaskCleanCache": "Làm Sạch Thư Mục Cache",
+ "TasksChannelsCategory": "Kênh Internet",
+ "TasksApplicationCategory": "Ứng Dụng",
+ "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} đã phát xong {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}",
+ "UserLockedOutWithName": "User {0} đã bị khóa",
+ "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": "Phát lại video đã dừng",
+ "NotificationOptionVideoPlayback": "Đã bắt đầu phát lại video",
+ "NotificationOptionUserLockedOut": "Người dùng bị khóa",
+ "NotificationOptionTaskFailed": "Lỗi tác vụ đã lên lịch",
+ "NotificationOptionServerRestartRequired": "Yêu cầu khởi động lại Server",
+ "NotificationOptionPluginUpdateInstalled": "Cập nhật Plugin đã được cài đặt",
+ "NotificationOptionPluginUninstalled": "Đã gỡ bỏ Plugin",
+ "NotificationOptionPluginInstalled": "Đã cài đặt Plugin",
+ "NotificationOptionPluginError": "Thất bại Plugin",
+ "NotificationOptionNewLibraryContent": "Nội dung mới được thêm vào",
+ "NotificationOptionInstallationFailed": "Cài đặt thất bại",
+ "NotificationOptionCameraImageUploaded": "Đã tải lên hình ảnh máy ảnh",
+ "NotificationOptionAudioPlaybackStopped": "Phát lại âm thanh đã dừng",
+ "NotificationOptionAudioPlayback": "Phát lại âm thanh đã bắt đầu",
+ "NotificationOptionApplicationUpdateInstalled": "Bản cập nhật ứng dụng đã được cài đặt",
+ "NotificationOptionApplicationUpdateAvailable": "Bản cập nhật ứng dụng hiện sẵn có",
+ "NewVersionIsAvailable": "Một phiên bản mới của Jellyfin Server sẵn có để tải.",
+ "NameSeasonUnknown": "Không Rõ Mùa",
+ "NameSeasonNumber": "Mùa {0}",
+ "NameInstallFailed": "{0} cài đặt thất bại",
+ "MusicVideos": "Video 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 thất bại từ {0}",
+ "DeviceOnlineWithName": "{0} đã kết nối",
+ "DeviceOfflineWithName": "{0} đã ngắt kết nối",
+ "ChapterNameValue": "Phân Cảnh {0}",
+ "Channels": "Các 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}"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index 6b563a9b1..53a902de2 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -16,7 +16,6 @@
"Folders": "文件夹",
"Genres": "风格",
"HeaderAlbumArtists": "专辑作家",
- "HeaderCameraUploads": "相机上传",
"HeaderContinueWatching": "继续观影",
"HeaderFavoriteAlbums": "收藏的专辑",
"HeaderFavoriteArtists": "最爱的艺术家",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index 0804fc927..435e294ef 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -1,6 +1,6 @@
{
"Albums": "專輯",
- "AppDeviceValues": "軟件: {0}, 設備: {1}",
+ "AppDeviceValues": "程式: {0}, 設備: {1}",
"Application": "應用程式",
"Artists": "藝人",
"AuthenticationSucceededWithUserName": "{0} 授權成功",
@@ -16,7 +16,6 @@
"Folders": "檔案夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯藝人",
- "HeaderCameraUploads": "相機上載",
"HeaderContinueWatching": "繼續觀看",
"HeaderFavoriteAlbums": "最愛專輯",
"HeaderFavoriteArtists": "最愛的藝人",
@@ -113,5 +112,6 @@
"TaskCleanCacheDescription": "刪除系統不再需要的緩存文件。",
"TaskCleanCache": "清理緩存目錄",
"TasksChannelsCategory": "互聯網頻道",
- "TasksLibraryCategory": "庫"
+ "TasksLibraryCategory": "庫",
+ "TaskRefreshPeople": "刷新人物"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index a22f66df9..30f726630 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -1,6 +1,6 @@
{
"Albums": "專輯",
- "AppDeviceValues": "軟體: {0}, 裝置: {1}",
+ "AppDeviceValues": "軟體:{0},裝置:{1}",
"Application": "應用程式",
"Artists": "演出者",
"AuthenticationSucceededWithUserName": "{0} 成功授權",
@@ -11,12 +11,11 @@
"Collections": "合輯",
"DeviceOfflineWithName": "{0} 已經斷線",
"DeviceOnlineWithName": "{0} 已經連線",
- "FailedLoginAttemptWithUserName": "來自 {0} 的失敗登入嘗試",
+ "FailedLoginAttemptWithUserName": "來自使用者 {0} 的失敗登入",
"Favorites": "我的最愛",
"Folders": "資料夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯演出者",
- "HeaderCameraUploads": "相機上傳",
"HeaderContinueWatching": "繼續觀賞",
"HeaderFavoriteAlbums": "最愛專輯",
"HeaderFavoriteArtists": "最愛演出者",
@@ -28,8 +27,8 @@
"HomeVideos": "自製影片",
"ItemAddedWithName": "{0} 已新增至媒體庫",
"ItemRemovedWithName": "{0} 已從媒體庫移除",
- "LabelIpAddressValue": "IP 位置: {0}",
- "LabelRunningTimeValue": "運行時間: {0}",
+ "LabelIpAddressValue": "IP 位址:{0}",
+ "LabelRunningTimeValue": "運行時間:{0}",
"Latest": "最新",
"MessageApplicationUpdated": "Jellyfin Server 已經更新",
"MessageApplicationUpdatedTo": "Jellyfin Server 已經更新至 {0}",
@@ -42,18 +41,18 @@
"NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季數",
- "NewVersionIsAvailable": "新版本的Jellyfin Server 軟體已經推出可供下載。",
+ "NewVersionIsAvailable": "新版本的 Jellyfin Server 軟體已經可供下載。",
"NotificationOptionApplicationUpdateAvailable": "有可用的應用程式更新",
- "NotificationOptionApplicationUpdateInstalled": "應用程式已更新",
+ "NotificationOptionApplicationUpdateInstalled": "軟體更新已安裝",
"NotificationOptionAudioPlayback": "音樂開始播放",
"NotificationOptionAudioPlaybackStopped": "音樂停止播放",
"NotificationOptionCameraImageUploaded": "相機相片已上傳",
"NotificationOptionInstallationFailed": "安裝失敗",
"NotificationOptionNewLibraryContent": "已新增新內容",
- "NotificationOptionPluginError": "插件安裝錯誤",
- "NotificationOptionPluginInstalled": "插件已安裝",
- "NotificationOptionPluginUninstalled": "插件已移除",
- "NotificationOptionPluginUpdateInstalled": "插件已更新",
+ "NotificationOptionPluginError": "外掛安裝失敗",
+ "NotificationOptionPluginInstalled": "外掛已安裝",
+ "NotificationOptionPluginUninstalled": "外掛已移除",
+ "NotificationOptionPluginUpdateInstalled": "外掛已更新",
"NotificationOptionServerRestartRequired": "伺服器需要重新啟動",
"NotificationOptionTaskFailed": "排程任務失敗",
"NotificationOptionUserLockedOut": "使用者已鎖定",
@@ -61,14 +60,14 @@
"NotificationOptionVideoPlaybackStopped": "影片停止播放",
"Photos": "相片",
"Playlists": "播放清單",
- "Plugin": "插件",
+ "Plugin": "外掛",
"PluginInstalledWithName": "{0} 已安裝",
"PluginUninstalledWithName": "{0} 已移除",
"PluginUpdatedWithName": "{0} 已更新",
"ProviderValue": "提供商: {0}",
- "ScheduledTaskFailedWithName": "{0} 已失敗",
- "ScheduledTaskStartedWithName": "{0} 已開始",
- "ServerNameNeedsToBeRestarted": "{0} 需要重新啟動",
+ "ScheduledTaskFailedWithName": "排程任務 {0} 已失敗",
+ "ScheduledTaskStartedWithName": "排程任務 {0} 已開始",
+ "ServerNameNeedsToBeRestarted": "伺服器 {0} 需要重新啟動",
"Shows": "節目",
"Songs": "歌曲",
"StartupEmbyServerIsLoading": "Jellyfin Server正在啟動,請稍後再試一次。",
@@ -78,10 +77,10 @@
"User": "使用者",
"UserCreatedWithName": "使用者 {0} 已建立",
"UserDeletedWithName": "使用者 {0} 已移除",
- "UserDownloadingItemWithValues": "{0} 正在下載 {1}",
+ "UserDownloadingItemWithValues": "使用者 {0} 正在下載 {1}",
"UserLockedOutWithName": "使用者 {0} 已鎖定",
- "UserOfflineFromDevice": "{0} 已從 {1} 斷線",
- "UserOnlineFromDevice": "{0} 已連線,來自 {1}",
+ "UserOfflineFromDevice": "使用者 {0} 已從 {1} 斷線",
+ "UserOnlineFromDevice": "使用者 {0} 已從 {1} 連線",
"UserPasswordChangedWithName": "使用者 {0} 的密碼已變更",
"UserPolicyUpdatedWithName": "使用者條約已更新為 {0}",
"UserStartedPlayingItemWithValues": "{0}正在使用 {2} 播放 {1}",
@@ -92,26 +91,26 @@
"HeaderRecordingGroups": "錄製組",
"Inherit": "繼承",
"SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
- "TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。",
+ "TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。",
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
"TaskRefreshChannels": "重新整理頻道",
- "TaskUpdatePlugins": "更新插件",
- "TaskRefreshPeople": "重新整理人員",
- "TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔案。",
+ "TaskUpdatePlugins": "更新外掛",
+ "TaskRefreshPeople": "刷新用戶",
+ "TaskCleanLogsDescription": "刪除超過 {0} 天的舊紀錄檔。",
"TaskCleanLogs": "清空紀錄資料夾",
- "TaskRefreshLibraryDescription": "掃描媒體庫內新的檔案並重新整理描述資料。",
- "TaskRefreshLibrary": "掃描媒體庫",
+ "TaskRefreshLibraryDescription": "重新掃描媒體庫的新檔案並更新描述資料。",
+ "TaskRefreshLibrary": "重新掃描媒體庫",
"TaskRefreshChapterImages": "擷取章節圖片",
- "TaskCleanCacheDescription": "刪除系統長時間不需要的快取。",
+ "TaskCleanCacheDescription": "刪除系統已不需要的快取。",
"TaskCleanCache": "清除快取資料夾",
"TasksLibraryCategory": "媒體庫",
- "TaskRefreshChannelsDescription": "重新整理網絡頻道資料。",
+ "TaskRefreshChannelsDescription": "重新整理網路頻道資料。",
"TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。",
"TaskCleanTranscode": "清除轉碼資料夾",
- "TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。",
+ "TaskUpdatePluginsDescription": "為設置自動更新的外掛下載並安裝更新。",
"TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的中繼資料。",
- "TaskRefreshChapterImagesDescription": "為有章節的視頻創建縮圖。",
- "TasksChannelsCategory": "網絡頻道",
+ "TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。",
+ "TasksChannelsCategory": "網路頻道",
"TasksApplicationCategory": "應用程式",
"TasksMaintenanceCategory": "維修"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index e2a634e1a..30aaf3a05 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Localization
private readonly IServerConfigurationManager _configurationManager;
private readonly IJsonSerializer _jsonSerializer;
- private readonly ILogger _logger;
+ private readonly ILogger<LocalizationManager> _logger;
private readonly Dictionary<string, Dictionary<string, ParentalRating>> _allParentalRatings =
new Dictionary<string, Dictionary<string, ParentalRating>>(StringComparer.OrdinalIgnoreCase);
@@ -247,7 +247,7 @@ namespace Emby.Server.Implementations.Localization
}
// Try splitting by : to handle "Germany: FSK 18"
- var index = rating.IndexOf(':');
+ var index = rating.IndexOf(':', StringComparison.Ordinal);
if (index != -1)
{
rating = rating.Substring(index).TrimStart(':').Trim();
@@ -312,12 +312,12 @@ namespace Emby.Server.Implementations.Localization
throw new ArgumentNullException(nameof(culture));
}
- const string prefix = "Core";
- var key = prefix + culture;
+ const string Prefix = "Core";
+ var key = Prefix + culture;
return _dictionaries.GetOrAdd(
key,
- f => GetDictionary(prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult());
+ f => GetDictionary(Prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult());
}
private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename)
@@ -413,6 +413,7 @@ namespace Emby.Server.Implementations.Localization
yield return new LocalizationOption("Swedish", "sv");
yield return new LocalizationOption("Swiss German", "gsw");
yield return new LocalizationOption("Turkish", "tr");
+ yield return new LocalizationOption("Tiếng Việt", "vi");
}
}
}
diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
index 677d68b4c..438bbe24a 100644
--- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
+++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -21,7 +23,7 @@ namespace Emby.Server.Implementations.MediaEncoder
{
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IFileSystem _fileSystem;
- private readonly ILogger _logger;
+ private readonly ILogger<EncodingManager> _logger;
private readonly IMediaEncoder _encoder;
private readonly IChapterManager _chapterManager;
private readonly ILibraryManager _libraryManager;
diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs
index e42ff8496..0781a0e33 100644
--- a/Emby.Server.Implementations/Net/SocketFactory.cs
+++ b/Emby.Server.Implementations/Net/SocketFactory.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Net;
using System.Net.Sockets;
@@ -17,6 +19,7 @@ namespace Emby.Server.Implementations.Net
var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp);
try
{
+ retVal.EnableBroadcast = true;
retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
@@ -44,6 +47,7 @@ namespace Emby.Server.Implementations.Net
var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp);
try
{
+ retVal.EnableBroadcast = true;
retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4);
@@ -96,7 +100,6 @@ namespace Emby.Server.Implementations.Net
}
catch (SocketException)
{
-
}
try
@@ -107,12 +110,12 @@ namespace Emby.Server.Implementations.Net
}
catch (SocketException)
{
-
}
try
{
- //retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true);
+ retVal.EnableBroadcast = true;
+ // retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true);
retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
var localIp = IPAddress.Any;
diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs
index 211ca6784..4e25768cf 100644
--- a/Emby.Server.Implementations/Net/UdpSocket.cs
+++ b/Emby.Server.Implementations/Net/UdpSocket.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Net;
using System.Net.Sockets;
@@ -13,13 +15,11 @@ namespace Emby.Server.Implementations.Net
public sealed class UdpSocket : ISocket, IDisposable
{
private Socket _socket;
- private int _localPort;
+ private readonly int _localPort;
private bool _disposed = false;
public Socket Socket => _socket;
- public IPAddress LocalIPAddress { get; }
-
private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
{
SocketFlags = SocketFlags.None
@@ -35,7 +35,10 @@ namespace Emby.Server.Implementations.Net
public UdpSocket(Socket socket, int localPort, IPAddress ip)
{
- if (socket == null) throw new ArgumentNullException(nameof(socket));
+ if (socket == null)
+ {
+ throw new ArgumentNullException(nameof(socket));
+ }
_socket = socket;
_localPort = localPort;
@@ -46,18 +49,33 @@ namespace Emby.Server.Implementations.Net
InitReceiveSocketAsyncEventArgs();
}
+ public UdpSocket(Socket socket, IPEndPoint endPoint)
+ {
+ if (socket == null)
+ {
+ throw new ArgumentNullException(nameof(socket));
+ }
+
+ _socket = socket;
+ _socket.Connect(endPoint);
+
+ InitReceiveSocketAsyncEventArgs();
+ }
+
+ public IPAddress LocalIPAddress { get; }
+
private void InitReceiveSocketAsyncEventArgs()
{
var receiveBuffer = new byte[8192];
_receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
- _receiveSocketAsyncEventArgs.Completed += _receiveSocketAsyncEventArgs_Completed;
+ _receiveSocketAsyncEventArgs.Completed += OnReceiveSocketAsyncEventArgsCompleted;
var sendBuffer = new byte[8192];
_sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length);
- _sendSocketAsyncEventArgs.Completed += _sendSocketAsyncEventArgs_Completed;
+ _sendSocketAsyncEventArgs.Completed += OnSendSocketAsyncEventArgsCompleted;
}
- private void _receiveSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
+ private void OnReceiveSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
{
var tcs = _currentReceiveTaskCompletionSource;
if (tcs != null)
@@ -81,7 +99,7 @@ namespace Emby.Server.Implementations.Net
}
}
- private void _sendSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
+ private void OnSendSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
{
var tcs = _currentSendTaskCompletionSource;
if (tcs != null)
@@ -99,16 +117,6 @@ namespace Emby.Server.Implementations.Net
}
}
- public UdpSocket(Socket socket, IPEndPoint endPoint)
- {
- if (socket == null) throw new ArgumentNullException(nameof(socket));
-
- _socket = socket;
- _socket.Connect(endPoint);
-
- InitReceiveSocketAsyncEventArgs();
- }
-
public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback)
{
ThrowIfDisposed();
@@ -239,6 +247,7 @@ namespace Emby.Server.Implementations.Net
}
}
+ /// <inheritdoc />
public void Dispose()
{
if (_disposed)
@@ -247,6 +256,8 @@ namespace Emby.Server.Implementations.Net
}
_socket?.Dispose();
+ _receiveSocketAsyncEventArgs.Dispose();
+ _sendSocketAsyncEventArgs.Dispose();
_currentReceiveTaskCompletionSource?.TrySetCanceled();
_currentSendTaskCompletionSource?.TrySetCanceled();
diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs
index b3e88b667..089ec30e6 100644
--- a/Emby.Server.Implementations/Networking/NetworkManager.cs
+++ b/Emby.Server.Implementations/Networking/NetworkManager.cs
@@ -1,6 +1,7 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
@@ -11,16 +12,25 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Networking
{
+ /// <summary>
+ /// Class to take care of network interface management.
+ /// </summary>
public class NetworkManager : INetworkManager
{
- private readonly ILogger _logger;
+ private readonly ILogger<NetworkManager> _logger;
private IPAddress[] _localIpAddresses;
private readonly object _localIpAddressSyncLock = new object();
private readonly object _subnetLookupLock = new object();
- private Dictionary<string, List<string>> _subnetLookup = new Dictionary<string, List<string>>(StringComparer.Ordinal);
+ private readonly Dictionary<string, List<string>> _subnetLookup = new Dictionary<string, List<string>>(StringComparer.Ordinal);
+
+ private List<PhysicalAddress> _macAddresses;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NetworkManager"/> class.
+ /// </summary>
+ /// <param name="logger">Logger to use for messages.</param>
public NetworkManager(ILogger<NetworkManager> logger)
{
_logger = logger;
@@ -29,8 +39,10 @@ namespace Emby.Server.Implementations.Networking
NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
}
+ /// <inheritdoc/>
public event EventHandler NetworkChanged;
+ /// <inheritdoc/>
public Func<string[]> LocalSubnetsFn { get; set; }
private void OnNetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
@@ -56,13 +68,14 @@ namespace Emby.Server.Implementations.Networking
NetworkChanged?.Invoke(this, EventArgs.Empty);
}
- public IPAddress[] GetLocalIpAddresses(bool ignoreVirtualInterface = true)
+ /// <inheritdoc/>
+ public IPAddress[] GetLocalIpAddresses()
{
lock (_localIpAddressSyncLock)
{
if (_localIpAddresses == null)
{
- var addresses = GetLocalIpAddressesInternal(ignoreVirtualInterface).ToArray();
+ var addresses = GetLocalIpAddressesInternal().ToArray();
_localIpAddresses = addresses;
}
@@ -71,42 +84,47 @@ namespace Emby.Server.Implementations.Networking
}
}
- private List<IPAddress> GetLocalIpAddressesInternal(bool ignoreVirtualInterface)
+ private List<IPAddress> GetLocalIpAddressesInternal()
{
- var list = GetIPsDefault(ignoreVirtualInterface).ToList();
+ var list = GetIPsDefault().ToList();
if (list.Count == 0)
{
list = GetLocalIpAddressesFallback().GetAwaiter().GetResult().ToList();
}
- var listClone = list.ToList();
+ var listClone = new List<IPAddress>();
- return list
- .OrderBy(i => i.AddressFamily == AddressFamily.InterNetwork ? 0 : 1)
- .ThenBy(i => listClone.IndexOf(i))
- .Where(FilterIpAddress)
- .GroupBy(i => i.ToString())
- .Select(x => x.First())
- .ToList();
- }
+ var subnets = LocalSubnetsFn();
- private static bool FilterIpAddress(IPAddress address)
- {
- if (address.IsIPv6LinkLocal
- || address.ToString().StartsWith("169.", StringComparison.OrdinalIgnoreCase))
+ foreach (var i in list)
{
- return false;
+ if (i.IsIPv6LinkLocal || i.ToString().StartsWith("169.254.", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ if (Array.IndexOf(subnets, $"[{i}]") == -1)
+ {
+ listClone.Add(i);
+ }
}
- return true;
+ return listClone
+ .OrderBy(i => i.AddressFamily == AddressFamily.InterNetwork ? 0 : 1)
+ // .ThenBy(i => listClone.IndexOf(i))
+ .GroupBy(i => i.ToString())
+ .Select(x => x.First())
+ .ToList();
}
+ /// <inheritdoc/>
public bool IsInPrivateAddressSpace(string endpoint)
{
return IsInPrivateAddressSpace(endpoint, true);
}
+ // Checks if the address in endpoint is an RFC1918, RFC1122, or RFC3927 address
private bool IsInPrivateAddressSpace(string endpoint, bool checkSubnets)
{
if (string.Equals(endpoint, "::1", StringComparison.OrdinalIgnoreCase))
@@ -114,12 +132,12 @@ namespace Emby.Server.Implementations.Networking
return true;
}
- // ipv6
+ // IPV6
if (endpoint.Split('.').Length > 4)
{
// Handle ipv4 mapped to ipv6
var originalEndpoint = endpoint;
- endpoint = endpoint.Replace("::ffff:", string.Empty);
+ endpoint = endpoint.Replace("::ffff:", string.Empty, StringComparison.OrdinalIgnoreCase);
if (string.Equals(endpoint, originalEndpoint, StringComparison.OrdinalIgnoreCase))
{
@@ -128,21 +146,24 @@ namespace Emby.Server.Implementations.Networking
}
// Private address space:
- // http://en.wikipedia.org/wiki/Private_network
- if (endpoint.StartsWith("172.", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(endpoint, "localhost", StringComparison.OrdinalIgnoreCase))
{
- return Is172AddressPrivate(endpoint);
+ return true;
}
- if (endpoint.StartsWith("localhost", StringComparison.OrdinalIgnoreCase) ||
- endpoint.StartsWith("127.", StringComparison.OrdinalIgnoreCase) ||
- endpoint.StartsWith("169.", StringComparison.OrdinalIgnoreCase))
+ if (!IPAddress.TryParse(endpoint, out var ipAddress))
{
- return true;
+ return false;
}
- if (checkSubnets && endpoint.StartsWith("192.168", StringComparison.OrdinalIgnoreCase))
+ byte[] octet = ipAddress.GetAddressBytes();
+
+ if ((octet[0] == 10) ||
+ (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918
+ (octet[0] == 192 && octet[1] == 168) || // RFC1918
+ (octet[0] == 127) || // RFC1122
+ (octet[0] == 169 && octet[1] == 254)) // RFC3927
{
return true;
}
@@ -155,6 +176,7 @@ namespace Emby.Server.Implementations.Networking
return false;
}
+ /// <inheritdoc/>
public bool IsInPrivateAddressSpaceAndLocalSubnet(string endpoint)
{
if (endpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase))
@@ -165,7 +187,7 @@ namespace Emby.Server.Implementations.Networking
foreach (var subnet_Match in subnets)
{
- //logger.LogDebug("subnet_Match:" + subnet_Match);
+ // logger.LogDebug("subnet_Match:" + subnet_Match);
if (endpoint.StartsWith(subnet_Match + ".", StringComparison.OrdinalIgnoreCase))
{
@@ -177,6 +199,7 @@ namespace Emby.Server.Implementations.Networking
return false;
}
+ // Gives a list of possible subnets from the system whose interface ip starts with endpointFirstPart
private List<string> GetSubnets(string endpointFirstPart)
{
lock (_subnetLookupLock)
@@ -222,46 +245,81 @@ namespace Emby.Server.Implementations.Networking
}
}
- private static bool Is172AddressPrivate(string endpoint)
- {
- for (var i = 16; i <= 31; i++)
- {
- if (endpoint.StartsWith("172." + i.ToString(CultureInfo.InvariantCulture) + ".", StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
-
- return false;
- }
-
+ /// <inheritdoc/>
public bool IsInLocalNetwork(string endpoint)
{
return IsInLocalNetworkInternal(endpoint, true);
}
+ /// <inheritdoc/>
public bool IsAddressInSubnets(string addressString, string[] subnets)
{
return IsAddressInSubnets(IPAddress.Parse(addressString), addressString, subnets);
}
+ /// <inheritdoc/>
+ public bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC)
+ {
+ byte[] octet = address.GetAddressBytes();
+
+ if ((octet[0] == 127) || // RFC1122
+ (octet[0] == 169 && octet[1] == 254)) // RFC3927
+ {
+ // don't use on loopback or 169 interfaces
+ return false;
+ }
+
+ string addressString = address.ToString();
+ string excludeAddress = "[" + addressString + "]";
+ var subnets = LocalSubnetsFn();
+
+ // Include any address if LAN subnets aren't specified
+ if (subnets.Length == 0)
+ {
+ return true;
+ }
+
+ // Exclude any addresses if they appear in the LAN list in [ ]
+ if (Array.IndexOf(subnets, excludeAddress) != -1)
+ {
+ return false;
+ }
+
+ return IsAddressInSubnets(address, addressString, subnets);
+ }
+
+ /// <summary>
+ /// Checks if the give address falls within the ranges given in [subnets]. The addresses in subnets can be hosts or subnets in the CIDR format.
+ /// </summary>
+ /// <param name="address">IPAddress version of the address.</param>
+ /// <param name="addressString">The address to check.</param>
+ /// <param name="subnets">If true, check against addresses in the LAN settings which have [] arroud and return true if it matches the address give in address.</param>
+ /// <returns><c>false</c>if the address isn't in the subnets, <c>true</c> otherwise.</returns>
private static bool IsAddressInSubnets(IPAddress address, string addressString, string[] subnets)
{
foreach (var subnet in subnets)
{
var normalizedSubnet = subnet.Trim();
-
+ // Is the subnet a host address and does it match the address being passes?
if (string.Equals(normalizedSubnet, addressString, StringComparison.OrdinalIgnoreCase))
{
return true;
}
+ // Parse CIDR subnets and see if address falls within it.
if (normalizedSubnet.Contains('/', StringComparison.Ordinal))
{
- var ipNetwork = IPNetwork.Parse(normalizedSubnet);
- if (ipNetwork.Contains(address))
+ try
{
- return true;
+ var ipNetwork = IPNetwork.Parse(normalizedSubnet);
+ if (ipNetwork.Contains(address))
+ {
+ return true;
+ }
+ }
+ catch
+ {
+ // Ignoring - invalid subnet passed encountered.
}
}
}
@@ -286,7 +344,7 @@ namespace Emby.Server.Implementations.Networking
var localSubnets = localSubnetsFn();
foreach (var subnet in localSubnets)
{
- // only validate if there's at least one valid entry
+ // Only validate if there's at least one valid entry.
if (!string.IsNullOrWhiteSpace(subnet))
{
return IsAddressInSubnets(address, addressString, localSubnets) || IsInPrivateAddressSpace(addressString, false);
@@ -332,7 +390,7 @@ namespace Emby.Server.Implementations.Networking
var host = uri.DnsSafeHost;
_logger.LogDebug("Resolving host {0}", host);
- address = GetIpAddresses(host).Result.FirstOrDefault();
+ address = GetIpAddresses(host).GetAwaiter().GetResult().FirstOrDefault();
if (address != null)
{
@@ -343,7 +401,7 @@ namespace Emby.Server.Implementations.Networking
}
catch (InvalidOperationException)
{
- // Can happen with reverse proxy or IIS url rewriting
+ // Can happen with reverse proxy or IIS url rewriting?
}
catch (Exception ex)
{
@@ -360,7 +418,7 @@ namespace Emby.Server.Implementations.Networking
return Dns.GetHostAddressesAsync(hostName);
}
- private IEnumerable<IPAddress> GetIPsDefault(bool ignoreVirtualInterface)
+ private IEnumerable<IPAddress> GetIPsDefault()
{
IEnumerable<NetworkInterface> interfaces;
@@ -380,15 +438,7 @@ namespace Emby.Server.Implementations.Networking
{
var ipProperties = network.GetIPProperties();
- // Try to exclude virtual adapters
- // http://stackoverflow.com/questions/8089685/c-sharp-finding-my-machines-local-ip-address-and-not-the-vms
- var addr = ipProperties.GatewayAddresses.FirstOrDefault();
- if (addr == null
- || (ignoreVirtualInterface
- && (addr.Address.Equals(IPAddress.Any) || addr.Address.Equals(IPAddress.IPv6Any))))
- {
- return Enumerable.Empty<IPAddress>();
- }
+ // Exclude any addresses if they appear in the LAN list in [ ]
return ipProperties.UnicastAddresses
.Select(i => i.Address)
@@ -409,7 +459,7 @@ namespace Emby.Server.Implementations.Networking
}
/// <summary>
- /// Gets a random port number that is currently available
+ /// Gets a random port number that is currently available.
/// </summary>
/// <returns>System.Int32.</returns>
public int GetRandomUnusedTcpPort()
@@ -421,33 +471,29 @@ namespace Emby.Server.Implementations.Networking
return port;
}
+ /// <inheritdoc/>
public int GetRandomUnusedUdpPort()
{
var localEndPoint = new IPEndPoint(IPAddress.Any, 0);
using (var udpClient = new UdpClient(localEndPoint))
{
- var port = ((IPEndPoint)udpClient.Client.LocalEndPoint).Port;
- return port;
+ return ((IPEndPoint)udpClient.Client.LocalEndPoint).Port;
}
}
- private List<PhysicalAddress> _macAddresses;
+ /// <inheritdoc/>
public List<PhysicalAddress> GetMacAddresses()
{
- if (_macAddresses == null)
- {
- _macAddresses = GetMacAddressesInternal().ToList();
- }
-
- return _macAddresses;
+ return _macAddresses ??= GetMacAddressesInternal().ToList();
}
private static IEnumerable<PhysicalAddress> GetMacAddressesInternal()
=> NetworkInterface.GetAllNetworkInterfaces()
.Where(i => i.NetworkInterfaceType != NetworkInterfaceType.Loopback)
.Select(x => x.GetPhysicalAddress())
- .Where(x => x != null && x != PhysicalAddress.None);
+ .Where(x => !x.Equals(PhysicalAddress.None));
+ /// <inheritdoc/>
public bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask)
{
IPAddress network1 = GetNetworkAddress(address1, subnetMask);
@@ -474,6 +520,7 @@ namespace Emby.Server.Implementations.Networking
return new IPAddress(broadcastAddress);
}
+ /// <inheritdoc/>
public IPAddress GetLocalIpSubnetMask(IPAddress address)
{
NetworkInterface[] interfaces;
@@ -494,14 +541,11 @@ namespace Emby.Server.Implementations.Networking
foreach (NetworkInterface ni in interfaces)
{
- if (ni.GetIPProperties().GatewayAddresses.FirstOrDefault() != null)
+ foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses)
{
- foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses)
+ if (ip.Address.Equals(address) && ip.IPv4Mask != null)
{
- if (ip.Address.Equals(address) && ip.IPv4Mask != null)
- {
- return ip.IPv4Mask;
- }
+ return ip.IPv4Mask;
}
}
}
diff --git a/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs
index cd9f7946e..358606b0d 100644
--- a/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs
+++ b/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs
@@ -1,6 +1,9 @@
+#pragma warning disable CS1591
+
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Querying;
@@ -42,7 +45,7 @@ namespace Emby.Server.Implementations.Playlists
}
query.Recursive = true;
- query.IncludeItemTypes = new string[] { "Playlist" };
+ query.IncludeItemTypes = new[] { "Playlist" };
query.Parent = null;
return LibraryManager.GetItemsResult(query);
}
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 9b1510ac9..d3b64fb31 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -5,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -19,6 +22,8 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using PlaylistsNET.Content;
using PlaylistsNET.Models;
+using Genre = MediaBrowser.Controller.Entities.Genre;
+using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
namespace Emby.Server.Implementations.Playlists
{
@@ -27,7 +32,7 @@ namespace Emby.Server.Implementations.Playlists
private readonly ILibraryManager _libraryManager;
private readonly IFileSystem _fileSystem;
private readonly ILibraryMonitor _iLibraryMonitor;
- private readonly ILogger _logger;
+ private readonly ILogger<PlaylistManager> _logger;
private readonly IUserManager _userManager;
private readonly IProviderManager _providerManager;
private readonly IConfiguration _appConfig;
@@ -147,16 +152,13 @@ namespace Emby.Server.Implementations.Playlists
if (options.ItemIdList.Length > 0)
{
- AddToPlaylistInternal(playlist.Id.ToString("N", CultureInfo.InvariantCulture), options.ItemIdList, user, new DtoOptions(false)
+ await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false)
{
EnableImages = true
- });
+ }).ConfigureAwait(false);
}
- return new PlaylistCreationResult
- {
- Id = playlist.Id.ToString("N", CultureInfo.InvariantCulture)
- };
+ return new PlaylistCreationResult(playlist.Id.ToString("N", CultureInfo.InvariantCulture));
}
finally
{
@@ -182,17 +184,17 @@ namespace Emby.Server.Implementations.Playlists
return Playlist.GetPlaylistItems(playlistMediaType, items, user, options);
}
- public void AddToPlaylist(string playlistId, ICollection<Guid> itemIds, Guid userId)
+ public Task AddToPlaylistAsync(Guid playlistId, ICollection<Guid> itemIds, Guid userId)
{
var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId);
- AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false)
+ return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false)
{
EnableImages = true
});
}
- private void AddToPlaylistInternal(string playlistId, ICollection<Guid> newItemIds, User user, DtoOptions options)
+ private async Task AddToPlaylistInternal(Guid playlistId, ICollection<Guid> newItemIds, User user, DtoOptions options)
{
// Retrieve the existing playlist
var playlist = _libraryManager.GetItemById(playlistId) as Playlist
@@ -236,7 +238,7 @@ namespace Emby.Server.Implementations.Playlists
// Update the playlist in the repository
playlist.LinkedChildren = newLinkedChildren;
- playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+ await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
// Update the playlist on disk
if (playlist.IsFile)
@@ -254,7 +256,7 @@ namespace Emby.Server.Implementations.Playlists
RefreshPriority.High);
}
- public void RemoveFromPlaylist(string playlistId, IEnumerable<string> entryIds)
+ public async Task RemoveFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds)
{
if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist))
{
@@ -271,7 +273,7 @@ namespace Emby.Server.Implementations.Playlists
.Select(i => i.Item1)
.ToArray();
- playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+ await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
if (playlist.IsFile)
{
@@ -287,7 +289,7 @@ namespace Emby.Server.Implementations.Playlists
RefreshPriority.High);
}
- public void MoveItem(string playlistId, string entryId, int newIndex)
+ public async Task MoveItemAsync(string playlistId, string entryId, int newIndex)
{
if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist))
{
@@ -320,7 +322,7 @@ namespace Emby.Server.Implementations.Playlists
playlist.LinkedChildren = newList.ToArray();
- playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
+ await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
if (playlist.IsFile)
{
@@ -347,16 +349,14 @@ namespace Emby.Server.Implementations.Playlists
AlbumTitle = child.Album
};
- var hasAlbumArtist = child as IHasAlbumArtist;
- if (hasAlbumArtist != null)
+ if (child is IHasAlbumArtist hasAlbumArtist)
{
- entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+ entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
}
- var hasArtist = child as IHasArtist;
- if (hasArtist != null)
+ if (child is IHasArtist hasArtist)
{
- entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
+ entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
}
if (child.RunTimeTicks.HasValue)
@@ -383,22 +383,21 @@ namespace Emby.Server.Implementations.Playlists
AlbumTitle = child.Album
};
- var hasAlbumArtist = child as IHasAlbumArtist;
- if (hasAlbumArtist != null)
+ if (child is IHasAlbumArtist hasAlbumArtist)
{
- entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+ entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
}
- var hasArtist = child as IHasArtist;
- if (hasArtist != null)
+ if (child is IHasArtist hasArtist)
{
- entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
+ entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
}
if (child.RunTimeTicks.HasValue)
{
entry.Duration = TimeSpan.FromTicks(child.RunTimeTicks.Value);
}
+
playlist.PlaylistEntries.Add(entry);
}
@@ -408,8 +407,10 @@ namespace Emby.Server.Implementations.Playlists
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
{
- var playlist = new M3uPlaylist();
- playlist.IsExtended = true;
+ var playlist = new M3uPlaylist
+ {
+ IsExtended = true
+ };
foreach (var child in item.GetLinkedChildren())
{
var entry = new M3uPlaylistEntry()
@@ -419,10 +420,9 @@ namespace Emby.Server.Implementations.Playlists
Album = child.Album
};
- var hasAlbumArtist = child as IHasAlbumArtist;
- if (hasAlbumArtist != null)
+ if (child is IHasAlbumArtist hasAlbumArtist)
{
- entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+ entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
}
if (child.RunTimeTicks.HasValue)
@@ -450,10 +450,9 @@ namespace Emby.Server.Implementations.Playlists
Album = child.Album
};
- var hasAlbumArtist = child as IHasAlbumArtist;
- if (hasAlbumArtist != null)
+ if (child is IHasAlbumArtist hasAlbumArtist)
{
- entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
+ entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
}
if (child.RunTimeTicks.HasValue)
@@ -464,7 +463,7 @@ namespace Emby.Server.Implementations.Playlists
playlist.PlaylistEntries.Add(entry);
}
- string text = new M3u8Content().ToText(playlist);
+ string text = new M3uContent().ToText(playlist);
File.WriteAllText(playlistPath, text);
}
@@ -511,7 +510,7 @@ namespace Emby.Server.Implementations.Playlists
if (!folderPath.EndsWith(Path.DirectorySeparatorChar))
{
- folderPath = folderPath + Path.DirectorySeparatorChar;
+ folderPath += Path.DirectorySeparatorChar;
}
var folderUri = new Uri(folderPath);
@@ -534,24 +533,12 @@ namespace Emby.Server.Implementations.Playlists
return relativePath;
}
- private static string UnEscape(string content)
- {
- if (content == null) return content;
- return content.Replace("&amp;", "&").Replace("&apos;", "'").Replace("&quot;", "\"").Replace("&gt;", ">").Replace("&lt;", "<");
- }
-
- private static string Escape(string content)
- {
- if (content == null) return null;
- return content.Replace("&", "&amp;").Replace("'", "&apos;").Replace("\"", "&quot;").Replace(">", "&gt;").Replace("<", "&lt;");
- }
-
public Folder GetPlaylistsFolder(Guid userId)
{
- var typeName = "PlaylistsFolder";
+ const string TypeName = "PlaylistsFolder";
- return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal)) ??
- _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal));
+ return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)) ??
+ _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal));
}
}
}
diff --git a/Emby.Server.Implementations/Plugins/PluginManifest.cs b/Emby.Server.Implementations/Plugins/PluginManifest.cs
new file mode 100644
index 000000000..33762791b
--- /dev/null
+++ b/Emby.Server.Implementations/Plugins/PluginManifest.cs
@@ -0,0 +1,60 @@
+using System;
+
+namespace Emby.Server.Implementations.Plugins
+{
+ /// <summary>
+ /// Defines a Plugin manifest file.
+ /// </summary>
+ public class PluginManifest
+ {
+ /// <summary>
+ /// Gets or sets the category of the plugin.
+ /// </summary>
+ public string Category { get; set; }
+
+ /// <summary>
+ /// Gets or sets the changelog information.
+ /// </summary>
+ public string Changelog { get; set; }
+
+ /// <summary>
+ /// Gets or sets the description of the plugin.
+ /// </summary>
+ public string Description { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Global Unique Identifier for the plugin.
+ /// </summary>
+ public Guid Guid { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Name of the plugin.
+ /// </summary>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets an overview of the plugin.
+ /// </summary>
+ public string Overview { get; set; }
+
+ /// <summary>
+ /// Gets or sets the owner of the plugin.
+ /// </summary>
+ public string Owner { get; set; }
+
+ /// <summary>
+ /// Gets or sets the compatibility version for the plugin.
+ /// </summary>
+ public string TargetAbi { get; set; }
+
+ /// <summary>
+ /// Gets or sets the timestamp of the plugin.
+ /// </summary>
+ public DateTime Timestamp { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Version number of the plugin.
+ /// </summary>
+ public string Version { get; set; }
+ }
+}
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
new file mode 100644
index 000000000..140a67541
--- /dev/null
+++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
@@ -0,0 +1,285 @@
+using System;
+using System.Collections.Concurrent;
+using System.Globalization;
+using System.Linq;
+using System.Security.Cryptography;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.QuickConnect;
+using MediaBrowser.Controller.Security;
+using MediaBrowser.Model.QuickConnect;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.QuickConnect
+{
+ /// <summary>
+ /// Quick connect implementation.
+ /// </summary>
+ public class QuickConnectManager : IQuickConnect, IDisposable
+ {
+ private readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider();
+ private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ConcurrentDictionary<string, QuickConnectResult>();
+
+ private readonly IServerConfigurationManager _config;
+ private readonly ILogger<QuickConnectManager> _logger;
+ private readonly IAuthenticationRepository _authenticationRepository;
+ private readonly IAuthorizationContext _authContext;
+ private readonly IServerApplicationHost _appHost;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="QuickConnectManager"/> class.
+ /// Should only be called at server startup when a singleton is created.
+ /// </summary>
+ /// <param name="config">Configuration.</param>
+ /// <param name="logger">Logger.</param>
+ /// <param name="appHost">Application host.</param>
+ /// <param name="authContext">Authentication context.</param>
+ /// <param name="authenticationRepository">Authentication repository.</param>
+ public QuickConnectManager(
+ IServerConfigurationManager config,
+ ILogger<QuickConnectManager> logger,
+ IServerApplicationHost appHost,
+ IAuthorizationContext authContext,
+ IAuthenticationRepository authenticationRepository)
+ {
+ _config = config;
+ _logger = logger;
+ _appHost = appHost;
+ _authContext = authContext;
+ _authenticationRepository = authenticationRepository;
+
+ ReloadConfiguration();
+ }
+
+ /// <inheritdoc/>
+ public int CodeLength { get; set; } = 6;
+
+ /// <inheritdoc/>
+ public string TokenName { get; set; } = "QuickConnect";
+
+ /// <inheritdoc/>
+ public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable;
+
+ /// <inheritdoc/>
+ public int Timeout { get; set; } = 5;
+
+ private DateTime DateActivated { get; set; }
+
+ /// <inheritdoc/>
+ public void AssertActive()
+ {
+ if (State != QuickConnectState.Active)
+ {
+ throw new ArgumentException("Quick connect is not active on this server");
+ }
+ }
+
+ /// <inheritdoc/>
+ public void Activate()
+ {
+ DateActivated = DateTime.UtcNow;
+ SetState(QuickConnectState.Active);
+ }
+
+ /// <inheritdoc/>
+ public void SetState(QuickConnectState newState)
+ {
+ _logger.LogDebug("Changed quick connect state from {State} to {newState}", State, newState);
+
+ ExpireRequests(true);
+
+ State = newState;
+ _config.Configuration.QuickConnectAvailable = newState == QuickConnectState.Available || newState == QuickConnectState.Active;
+ _config.SaveConfiguration();
+
+ _logger.LogDebug("Configuration saved");
+ }
+
+ /// <inheritdoc/>
+ public QuickConnectResult TryConnect()
+ {
+ ExpireRequests();
+
+ if (State != QuickConnectState.Active)
+ {
+ _logger.LogDebug("Refusing quick connect initiation request, current state is {State}", State);
+ throw new AuthenticationException("Quick connect is not active on this server");
+ }
+
+ var code = GenerateCode();
+ var result = new QuickConnectResult()
+ {
+ Secret = GenerateSecureRandom(),
+ DateAdded = DateTime.UtcNow,
+ Code = code
+ };
+
+ _currentRequests[code] = result;
+ return result;
+ }
+
+ /// <inheritdoc/>
+ public QuickConnectResult CheckRequestStatus(string secret)
+ {
+ ExpireRequests();
+ AssertActive();
+
+ string code = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Code).DefaultIfEmpty(string.Empty).First();
+
+ if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
+ {
+ throw new ResourceNotFoundException("Unable to find request with provided secret");
+ }
+
+ return result;
+ }
+
+ /// <inheritdoc/>
+ public string GenerateCode()
+ {
+ Span<byte> raw = stackalloc byte[4];
+
+ int min = (int)Math.Pow(10, CodeLength - 1);
+ int max = (int)Math.Pow(10, CodeLength);
+
+ uint scale = uint.MaxValue;
+ while (scale == uint.MaxValue)
+ {
+ _rng.GetBytes(raw);
+ scale = BitConverter.ToUInt32(raw);
+ }
+
+ int code = (int)(min + ((max - min) * (scale / (double)uint.MaxValue)));
+ return code.ToString(CultureInfo.InvariantCulture);
+ }
+
+ /// <inheritdoc/>
+ public bool AuthorizeRequest(Guid userId, string code)
+ {
+ ExpireRequests();
+ AssertActive();
+
+ if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
+ {
+ throw new ResourceNotFoundException("Unable to find request");
+ }
+
+ if (result.Authenticated)
+ {
+ throw new InvalidOperationException("Request is already authorized");
+ }
+
+ result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+
+ // Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated.
+ var added = result.DateAdded ?? DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(Timeout));
+ result.DateAdded = added.Subtract(TimeSpan.FromMinutes(Timeout - 1));
+
+ _authenticationRepository.Create(new AuthenticationInfo
+ {
+ AppName = TokenName,
+ AccessToken = result.Authentication,
+ DateCreated = DateTime.UtcNow,
+ DeviceId = _appHost.SystemId,
+ DeviceName = _appHost.FriendlyName,
+ AppVersion = _appHost.ApplicationVersionString,
+ UserId = userId
+ });
+
+ _logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId);
+
+ return true;
+ }
+
+ /// <inheritdoc/>
+ public int DeleteAllDevices(Guid user)
+ {
+ var raw = _authenticationRepository.Get(new AuthenticationInfoQuery()
+ {
+ DeviceId = _appHost.SystemId,
+ UserId = user
+ });
+
+ var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenName, StringComparison.Ordinal));
+
+ var removed = 0;
+ foreach (var token in tokens)
+ {
+ _authenticationRepository.Delete(token);
+ _logger.LogDebug("Deleted token {AccessToken}", token.AccessToken);
+ removed++;
+ }
+
+ return removed;
+ }
+
+ /// <summary>
+ /// Dispose.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Dispose.
+ /// </summary>
+ /// <param name="disposing">Dispose unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _rng?.Dispose();
+ }
+ }
+
+ private string GenerateSecureRandom(int length = 32)
+ {
+ Span<byte> bytes = stackalloc byte[length];
+ _rng.GetBytes(bytes);
+
+ return Hex.Encode(bytes);
+ }
+
+ /// <inheritdoc/>
+ public void ExpireRequests(bool expireAll = false)
+ {
+ // Check if quick connect should be deactivated
+ if (State == QuickConnectState.Active && DateTime.UtcNow > DateActivated.AddMinutes(Timeout) && !expireAll)
+ {
+ _logger.LogDebug("Quick connect time expired, deactivating");
+ SetState(QuickConnectState.Available);
+ expireAll = true;
+ }
+
+ // Expire stale connection requests
+ var code = string.Empty;
+ var values = _currentRequests.Values.ToList();
+
+ for (int i = 0; i < values.Count; i++)
+ {
+ var added = values[i].DateAdded ?? DateTime.UnixEpoch;
+ if (DateTime.UtcNow > added.AddMinutes(Timeout) || expireAll)
+ {
+ code = values[i].Code;
+ _logger.LogDebug("Removing expired request {code}", code);
+
+ if (!_currentRequests.TryRemove(code, out _))
+ {
+ _logger.LogWarning("Request {code} already expired", code);
+ }
+ }
+ }
+ }
+
+ private void ReloadConfiguration()
+ {
+ State = _config.Configuration.QuickConnectAvailable ? QuickConnectState.Available : QuickConnectState.Unavailable;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/ResourceFileManager.cs b/Emby.Server.Implementations/ResourceFileManager.cs
index 6eda2b503..22fc62293 100644
--- a/Emby.Server.Implementations/ResourceFileManager.cs
+++ b/Emby.Server.Implementations/ResourceFileManager.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.IO;
using MediaBrowser.Controller;
@@ -10,7 +12,7 @@ namespace Emby.Server.Implementations
public class ResourceFileManager : IResourceFileManager
{
private readonly IFileSystem _fileSystem;
- private readonly ILogger _logger;
+ private readonly ILogger<ResourceFileManager> _logger;
public ResourceFileManager(ILogger<ResourceFileManager> logger, IFileSystem fileSystem)
{
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index 5b188d962..bc01f9543 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -1,14 +1,15 @@
+#pragma warning disable CS1591
+
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@@ -16,42 +17,57 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.ScheduledTasks
{
/// <summary>
- /// Class ScheduledTaskWorker
+ /// Class ScheduledTaskWorker.
/// </summary>
public class ScheduledTaskWorker : IScheduledTaskWorker
{
- public event EventHandler<GenericEventArgs<double>> TaskProgress;
-
- /// <summary>
- /// Gets the scheduled task.
- /// </summary>
- /// <value>The scheduled task.</value>
- public IScheduledTask ScheduledTask { get; private set; }
-
/// <summary>
/// Gets or sets the json serializer.
/// </summary>
/// <value>The json serializer.</value>
- private IJsonSerializer JsonSerializer { get; set; }
+ private readonly IJsonSerializer _jsonSerializer;
/// <summary>
/// Gets or sets the application paths.
/// </summary>
/// <value>The application paths.</value>
- private IApplicationPaths ApplicationPaths { get; set; }
+ private readonly IApplicationPaths _applicationPaths;
/// <summary>
- /// Gets the logger.
+ /// Gets or sets the logger.
/// </summary>
/// <value>The logger.</value>
- private ILogger Logger { get; set; }
+ private readonly ILogger _logger;
/// <summary>
- /// Gets the task manager.
+ /// Gets or sets the task manager.
/// </summary>
/// <value>The task manager.</value>
- private ITaskManager TaskManager { get; set; }
- private readonly IFileSystem _fileSystem;
+ private readonly ITaskManager _taskManager;
+
+ /// <summary>
+ /// The _last execution result sync lock.
+ /// </summary>
+ private readonly object _lastExecutionResultSyncLock = new object();
+
+ private bool _readFromFile = false;
+
+ /// <summary>
+ /// The _last execution result.
+ /// </summary>
+ private TaskResult _lastExecutionResult;
+
+ private Task _currentTask;
+
+ /// <summary>
+ /// The _triggers.
+ /// </summary>
+ private Tuple<TaskTriggerInfo, ITaskTrigger>[] _triggers;
+
+ /// <summary>
+ /// The _id.
+ /// </summary>
+ private string _id;
/// <summary>
/// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
@@ -70,50 +86,52 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// or
/// jsonSerializer
/// or
- /// logger
+ /// logger.
/// </exception>
- public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger, IFileSystem fileSystem)
+ public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger)
{
if (scheduledTask == null)
{
throw new ArgumentNullException(nameof(scheduledTask));
}
+
if (applicationPaths == null)
{
throw new ArgumentNullException(nameof(applicationPaths));
}
+
if (taskManager == null)
{
throw new ArgumentNullException(nameof(taskManager));
}
+
if (jsonSerializer == null)
{
throw new ArgumentNullException(nameof(jsonSerializer));
}
+
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
ScheduledTask = scheduledTask;
- ApplicationPaths = applicationPaths;
- TaskManager = taskManager;
- JsonSerializer = jsonSerializer;
- Logger = logger;
- _fileSystem = fileSystem;
+ _applicationPaths = applicationPaths;
+ _taskManager = taskManager;
+ _jsonSerializer = jsonSerializer;
+ _logger = logger;
InitTriggerEvents();
}
- private bool _readFromFile = false;
- /// <summary>
- /// The _last execution result
- /// </summary>
- private TaskResult _lastExecutionResult;
+ public event EventHandler<GenericEventArgs<double>> TaskProgress;
+
/// <summary>
- /// The _last execution result sync lock
+ /// Gets the scheduled task.
/// </summary>
- private readonly object _lastExecutionResultSyncLock = new object();
+ /// <value>The scheduled task.</value>
+ public IScheduledTask ScheduledTask { get; private set; }
+
/// <summary>
/// Gets the last execution result.
/// </summary>
@@ -132,19 +150,21 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
try
{
- _lastExecutionResult = JsonSerializer.DeserializeFromFile<TaskResult>(path);
+ _lastExecutionResult = _jsonSerializer.DeserializeFromFile<TaskResult>(path);
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error deserializing {File}", path);
+ _logger.LogError(ex, "Error deserializing {File}", path);
}
}
+
_readFromFile = true;
}
}
return _lastExecutionResult;
}
+
private set
{
_lastExecutionResult = value;
@@ -154,7 +174,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
lock (_lastExecutionResultSyncLock)
{
- JsonSerializer.SerializeToFile(value, path);
+ _jsonSerializer.SerializeToFile(value, path);
}
}
}
@@ -178,7 +198,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
public string Category => ScheduledTask.Category;
/// <summary>
- /// Gets the current cancellation token
+ /// Gets or sets the current cancellation token.
/// </summary>
/// <value>The current cancellation token source.</value>
private CancellationTokenSource CurrentCancellationTokenSource { get; set; }
@@ -215,12 +235,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
public double? CurrentProgress { get; private set; }
/// <summary>
- /// The _triggers.
- /// </summary>
- private Tuple<TaskTriggerInfo, ITaskTrigger>[] _triggers;
-
- /// <summary>
- /// Gets the triggers that define when the task will run.
+ /// Gets or sets the triggers that define when the task will run.
/// </summary>
/// <value>The triggers.</value>
private Tuple<TaskTriggerInfo, ITaskTrigger>[] InternalTriggers
@@ -249,7 +264,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// Gets the triggers that define when the task will run.
/// </summary>
/// <value>The triggers.</value>
- /// <exception cref="ArgumentNullException">value</exception>
+ /// <exception cref="ArgumentNullException"><c>value</c> is <c>null</c>.</exception>
public TaskTriggerInfo[] Triggers
{
get
@@ -257,6 +272,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
var triggers = InternalTriggers;
return triggers.Select(i => i.Item1).ToArray();
}
+
set
{
if (value == null)
@@ -274,11 +290,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
/// <summary>
- /// The _id
- /// </summary>
- private string _id;
-
- /// <summary>
/// Gets the unique id.
/// </summary>
/// <value>The unique id.</value>
@@ -318,9 +329,9 @@ namespace Emby.Server.Implementations.ScheduledTasks
trigger.Stop();
- trigger.Triggered -= trigger_Triggered;
- trigger.Triggered += trigger_Triggered;
- trigger.Start(LastExecutionResult, Logger, Name, isApplicationStartup);
+ trigger.Triggered -= OnTriggerTriggered;
+ trigger.Triggered += OnTriggerTriggered;
+ trigger.Start(LastExecutionResult, _logger, Name, isApplicationStartup);
}
}
@@ -329,7 +340,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
- async void trigger_Triggered(object sender, EventArgs e)
+ private async void OnTriggerTriggered(object sender, EventArgs e)
{
var trigger = (ITaskTrigger)sender;
@@ -340,21 +351,19 @@ namespace Emby.Server.Implementations.ScheduledTasks
return;
}
- Logger.LogInformation("{0} fired for task: {1}", trigger.GetType().Name, Name);
+ _logger.LogInformation("{0} fired for task: {1}", trigger.GetType().Name, Name);
trigger.Stop();
- TaskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions);
+ _taskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions);
await Task.Delay(1000).ConfigureAwait(false);
- trigger.Start(LastExecutionResult, Logger, Name, false);
+ trigger.Start(LastExecutionResult, _logger, Name, false);
}
- private Task _currentTask;
-
/// <summary>
- /// Executes the task
+ /// Executes the task.
/// </summary>
/// <param name="options">Task options.</param>
/// <returns>Task.</returns>
@@ -388,11 +397,11 @@ namespace Emby.Server.Implementations.ScheduledTasks
CurrentCancellationTokenSource = new CancellationTokenSource();
- Logger.LogInformation("Executing {0}", Name);
+ _logger.LogInformation("Executing {0}", Name);
- ((TaskManager)TaskManager).OnTaskExecuting(this);
+ ((TaskManager)_taskManager).OnTaskExecuting(this);
- progress.ProgressChanged += progress_ProgressChanged;
+ progress.ProgressChanged += OnProgressChanged;
TaskCompletionStatus status;
CurrentExecutionStartTime = DateTime.UtcNow;
@@ -416,7 +425,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error");
+ _logger.LogError(ex, "Error");
failureException = ex;
@@ -426,7 +435,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
var startTime = CurrentExecutionStartTime;
var endTime = DateTime.UtcNow;
- progress.ProgressChanged -= progress_ProgressChanged;
+ progress.ProgressChanged -= OnProgressChanged;
CurrentCancellationTokenSource.Dispose();
CurrentCancellationTokenSource = null;
CurrentProgress = null;
@@ -439,20 +448,17 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The e.</param>
- void progress_ProgressChanged(object sender, double e)
+ private void OnProgressChanged(object sender, double e)
{
e = Math.Min(e, 100);
CurrentProgress = e;
- TaskProgress?.Invoke(this, new GenericEventArgs<double>
- {
- Argument = e
- });
+ TaskProgress?.Invoke(this, new GenericEventArgs<double>(e));
}
/// <summary>
- /// Stops the task if it is currently executing
+ /// Stops the task if it is currently executing.
/// </summary>
/// <exception cref="InvalidOperationException">Cannot cancel a Task unless it is in the Running state.</exception>
public void Cancel()
@@ -472,7 +478,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
if (State == TaskState.Running)
{
- Logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name);
+ _logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name);
CurrentCancellationTokenSource.Cancel();
}
}
@@ -483,7 +489,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <returns>System.String.</returns>
private string GetScheduledTasksConfigurationDirectory()
{
- return Path.Combine(ApplicationPaths.ConfigurationDirectoryPath, "ScheduledTasks");
+ return Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks");
}
/// <summary>
@@ -492,7 +498,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <returns>System.String.</returns>
private string GetScheduledTasksDataDirectory()
{
- return Path.Combine(ApplicationPaths.DataPath, "ScheduledTasks");
+ return Path.Combine(_applicationPaths.DataPath, "ScheduledTasks");
}
/// <summary>
@@ -531,7 +537,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
TaskTriggerInfo[] list = null;
if (File.Exists(path))
{
- list = JsonSerializer.DeserializeFromFile<TaskTriggerInfo[]>(path);
+ list = _jsonSerializer.DeserializeFromFile<TaskTriggerInfo[]>(path);
}
// Return defaults if file doesn't exist.
@@ -567,7 +573,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
Directory.CreateDirectory(Path.GetDirectoryName(path));
- JsonSerializer.SerializeToFile(triggers, path);
+ _jsonSerializer.SerializeToFile(triggers, path);
}
/// <summary>
@@ -576,11 +582,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <param name="startTime">The start time.</param>
/// <param name="endTime">The end time.</param>
/// <param name="status">The status.</param>
+ /// <param name="ex">The exception.</param>
private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex)
{
var elapsedTime = endTime - startTime;
- Logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds);
+ _logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds);
var result = new TaskResult
{
@@ -601,7 +608,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
LastExecutionResult = result;
- ((TaskManager)TaskManager).OnTaskCompleted(this, result);
+ ((TaskManager)_taskManager).OnTaskCompleted(this, result);
}
/// <summary>
@@ -610,6 +617,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
public void Dispose()
{
Dispose(true);
+ GC.SuppressFinalize(this);
}
/// <summary>
@@ -630,34 +638,35 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
try
{
- Logger.LogInformation(Name + ": Cancelling");
+ _logger.LogInformation(Name + ": Cancelling");
token.Cancel();
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error calling CancellationToken.Cancel();");
+ _logger.LogError(ex, "Error calling CancellationToken.Cancel();");
}
}
+
var task = _currentTask;
if (task != null)
{
try
{
- Logger.LogInformation(Name + ": Waiting on Task");
+ _logger.LogInformation(Name + ": Waiting on Task");
var exited = Task.WaitAll(new[] { task }, 2000);
if (exited)
{
- Logger.LogInformation(Name + ": Task exited");
+ _logger.LogInformation(Name + ": Task exited");
}
else
{
- Logger.LogInformation(Name + ": Timed out waiting for task to stop");
+ _logger.LogInformation(Name + ": Timed out waiting for task to stop");
}
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error calling Task.WaitAll();");
+ _logger.LogError(ex, "Error calling Task.WaitAll();");
}
}
@@ -665,14 +674,15 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
try
{
- Logger.LogDebug(Name + ": Disposing CancellationToken");
+ _logger.LogDebug(Name + ": Disposing CancellationToken");
token.Dispose();
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error calling CancellationToken.Dispose();");
+ _logger.LogError(ex, "Error calling CancellationToken.Dispose();");
}
}
+
if (wassRunning)
{
OnTaskCompleted(startTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null);
@@ -681,12 +691,11 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
/// <summary>
- /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger
+ /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger.
/// </summary>
/// <param name="info">The info.</param>
/// <returns>BaseTaskTrigger.</returns>
- /// <exception cref="ArgumentNullException"></exception>
- /// <exception cref="ArgumentException">Invalid trigger type: + info.Type</exception>
+ /// <exception cref="ArgumentException">Invalid trigger type: + info.Type.</exception>
private ITaskTrigger GetTrigger(TaskTriggerInfo info)
{
var options = new TaskOptions
@@ -751,14 +760,14 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
/// <summary>
- /// Disposes each trigger
+ /// Disposes each trigger.
/// </summary>
private void DisposeTriggers()
{
foreach (var triggerInfo in InternalTriggers)
{
var trigger = triggerInfo.Item2;
- trigger.Triggered -= trigger_Triggered;
+ trigger.Triggered -= OnTriggerTriggered;
trigger.Stop();
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
index 6ffa581a9..6f81bf49b 100644
--- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
@@ -1,11 +1,12 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@@ -13,7 +14,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.ScheduledTasks
{
/// <summary>
- /// Class TaskManager
+ /// Class TaskManager.
/// </summary>
public class TaskManager : ITaskManager
{
@@ -21,21 +22,20 @@ namespace Emby.Server.Implementations.ScheduledTasks
public event EventHandler<TaskCompletionEventArgs> TaskCompleted;
/// <summary>
- /// Gets the list of Scheduled Tasks
+ /// Gets the list of Scheduled Tasks.
/// </summary>
/// <value>The scheduled tasks.</value>
public IScheduledTaskWorker[] ScheduledTasks { get; private set; }
/// <summary>
- /// The _task queue
+ /// The _task queue.
/// </summary>
private readonly ConcurrentQueue<Tuple<Type, TaskOptions>> _taskQueue =
new ConcurrentQueue<Tuple<Type, TaskOptions>>();
private readonly IJsonSerializer _jsonSerializer;
private readonly IApplicationPaths _applicationPaths;
- private readonly ILogger _logger;
- private readonly IFileSystem _fileSystem;
+ private readonly ILogger<TaskManager> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="TaskManager" /> class.
@@ -43,17 +43,14 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <param name="applicationPaths">The application paths.</param>
/// <param name="jsonSerializer">The json serializer.</param>
/// <param name="logger">The logger.</param>
- /// <param name="fileSystem">The filesystem manager.</param>
public TaskManager(
IApplicationPaths applicationPaths,
IJsonSerializer jsonSerializer,
- ILogger<TaskManager> logger,
- IFileSystem fileSystem)
+ ILogger<TaskManager> logger)
{
_applicationPaths = applicationPaths;
_jsonSerializer = jsonSerializer;
_logger = logger;
- _fileSystem = fileSystem;
ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
}
@@ -79,7 +76,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
/// <summary>
- /// Cancels if running
+ /// Cancels if running.
/// </summary>
/// <typeparam name="T"></typeparam>
public void CancelIfRunning<T>()
@@ -93,7 +90,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// Queues the scheduled task.
/// </summary>
/// <typeparam name="T"></typeparam>
- /// <param name="options">Task options</param>
+ /// <param name="options">Task options.</param>
public void QueueScheduledTask<T>(TaskOptions options)
where T : IScheduledTask
{
@@ -199,7 +196,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <param name="tasks">The tasks.</param>
public void AddTasks(IEnumerable<IScheduledTask> tasks)
{
- var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _jsonSerializer, _logger, _fileSystem));
+ var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _jsonSerializer, _logger));
ScheduledTasks = ScheduledTasks.Concat(list).ToArray();
}
@@ -210,6 +207,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
public void Dispose()
{
Dispose(true);
+ GC.SuppressFinalize(this);
}
/// <summary>
@@ -240,10 +238,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <param name="task">The task.</param>
internal void OnTaskExecuting(IScheduledTaskWorker task)
{
- TaskExecuting?.Invoke(this, new GenericEventArgs<IScheduledTaskWorker>
- {
- Argument = task
- });
+ TaskExecuting?.Invoke(this, new GenericEventArgs<IScheduledTaskWorker>(task));
}
/// <summary>
@@ -253,11 +248,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <param name="result">The result.</param>
internal void OnTaskCompleted(IScheduledTaskWorker task, TaskResult result)
{
- TaskCompleted?.Invoke(task, new TaskCompletionEventArgs
- {
- Result = result,
- Task = task
- });
+ TaskCompleted?.Invoke(task, new TaskCompletionEventArgs(task, result));
ExecuteQueuedTasks();
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index ea6a70615..8439f8a99 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -14,7 +14,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Logging;
using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks
@@ -25,11 +24,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
public class ChapterImagesTask : IScheduledTask
{
/// <summary>
- /// The _logger.
- /// </summary>
- private readonly ILogger _logger;
-
- /// <summary>
/// The _library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
@@ -46,7 +40,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
/// </summary>
public ChapterImagesTask(
- ILoggerFactory loggerFactory,
ILibraryManager libraryManager,
IItemRepository itemRepo,
IApplicationPaths appPaths,
@@ -54,7 +47,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
IFileSystem fileSystem,
ILocalizationManager localization)
{
- _logger = loggerFactory.CreateLogger(GetType().Name);
_libraryManager = libraryManager;
_itemRepo = itemRepo;
_appPaths = appPaths;
@@ -163,24 +155,31 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
catch (ObjectDisposedException)
{
- //TODO Investigate and properly fix.
+ // TODO Investigate and properly fix.
break;
}
}
}
+ /// <inheritdoc />
public string Name => _localization.GetLocalizedString("TaskRefreshChapterImages");
+ /// <inheritdoc />
public string Description => _localization.GetLocalizedString("TaskRefreshChapterImagesDescription");
+ /// <inheritdoc />
public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+ /// <inheritdoc />
public string Key => "RefreshChapterImages";
+ /// <inheritdoc />
public bool IsHidden => false;
+ /// <inheritdoc />
public bool IsEnabled => true;
+ /// <inheritdoc />
public bool IsLogged => true;
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
index 9df7c538b..692d1667d 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
@@ -5,15 +5,15 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
/// <summary>
- /// Deletes old cache files
+ /// Deletes old cache files.
/// </summary>
public class DeleteCacheFileTask : IScheduledTask, IConfigurableScheduledTask
{
@@ -21,10 +21,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// Gets or sets the application paths.
/// </summary>
/// <value>The application paths.</value>
- private IApplicationPaths ApplicationPaths { get; set; }
-
- private readonly ILogger _logger;
-
+ private readonly IApplicationPaths _applicationPaths;
+ private readonly ILogger<DeleteCacheFileTask> _logger;
private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization;
@@ -37,27 +35,48 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
IFileSystem fileSystem,
ILocalizationManager localization)
{
- ApplicationPaths = appPaths;
+ _applicationPaths = appPaths;
_logger = logger;
_fileSystem = fileSystem;
_localization = localization;
}
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskCleanCache");
+
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription");
+
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+ /// <inheritdoc />
+ public string Key => "DeleteCacheFiles";
+
+ /// <inheritdoc />
+ public bool IsHidden => false;
+
+ /// <inheritdoc />
+ public bool IsEnabled => true;
+
+ /// <inheritdoc />
+ public bool IsLogged => true;
+
/// <summary>
- /// Creates the triggers that define when the task will run
+ /// Creates the triggers that define when the task will run.
/// </summary>
/// <returns>IEnumerable{BaseTaskTrigger}.</returns>
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
- return new[] {
-
+ return new[]
+ {
// Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
};
}
/// <summary>
- /// Returns the task to be executed
+ /// Returns the task to be executed.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="progress">The progress.</param>
@@ -68,7 +87,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
try
{
- DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.CachePath, minDateModified, progress);
+ DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.CachePath, minDateModified, progress);
}
catch (DirectoryNotFoundException)
{
@@ -81,7 +100,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
try
{
- DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, minDateModified, progress);
+ DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.TempDirectory, minDateModified, progress);
}
catch (DirectoryNotFoundException)
{
@@ -91,9 +110,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
return Task.CompletedTask;
}
-
/// <summary>
- /// Deletes the cache files from directory with a last write time less than a given date
+ /// Deletes the cache files from directory with a last write time less than a given date.
/// </summary>
/// <param name="cancellationToken">The task cancellation token.</param>
/// <param name="directory">The directory.</param>
@@ -164,19 +182,5 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
_logger.LogError(ex, "Error deleting file {path}", path);
}
}
-
- public string Name => _localization.GetLocalizedString("TaskCleanCache");
-
- public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription");
-
- public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
-
- public string Key => "DeleteCacheFiles";
-
- public bool IsHidden => false;
-
- public bool IsEnabled => true;
-
- public bool IsLogged => true;
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
index 3140aa489..184d155d4 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
@@ -1,12 +1,13 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
-using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
@@ -15,12 +16,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// </summary>
public class DeleteLogFileTask : IScheduledTask, IConfigurableScheduledTask
{
- /// <summary>
- /// Gets or sets the configuration manager.
- /// </summary>
- /// <value>The configuration manager.</value>
- private IConfigurationManager ConfigurationManager { get; set; }
-
+ private readonly IConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization;
@@ -28,21 +24,48 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
/// Initializes a new instance of the <see cref="DeleteLogFileTask" /> class.
/// </summary>
/// <param name="configurationManager">The configuration manager.</param>
+ /// <param name="fileSystem">The file system.</param>
+ /// <param name="localization">The localization manager.</param>
public DeleteLogFileTask(IConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization)
{
- ConfigurationManager = configurationManager;
+ _configurationManager = configurationManager;
_fileSystem = fileSystem;
_localization = localization;
}
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskCleanLogs");
+
+ /// <inheritdoc />
+ public string Description => string.Format(
+ CultureInfo.InvariantCulture,
+ _localization.GetLocalizedString("TaskCleanLogsDescription"),
+ _configurationManager.CommonConfiguration.LogFileRetentionDays);
+
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+ /// <inheritdoc />
+ public string Key => "CleanLogFiles";
+
+ /// <inheritdoc />
+ public bool IsHidden => false;
+
+ /// <inheritdoc />
+ public bool IsEnabled => true;
+
+ /// <inheritdoc />
+ public bool IsLogged => true;
+
/// <summary>
/// Creates the triggers that define when the task will run.
/// </summary>
/// <returns>IEnumerable{BaseTaskTrigger}.</returns>
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
- return new[] {
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+ return new[]
+ {
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
};
}
@@ -55,10 +78,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
{
// Delete log files more than n days old
- var minDateModified = DateTime.UtcNow.AddDays(-ConfigurationManager.CommonConfiguration.LogFileRetentionDays);
+ var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays);
// Only delete the .txt log files, the *.log files created by serilog get managed by itself
- var filesToDelete = _fileSystem.GetFiles(ConfigurationManager.CommonApplicationPaths.LogDirectoryPath, new[] { ".txt" }, true, true)
+ var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, new[] { ".txt" }, true, true)
.Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
.ToList();
@@ -81,19 +104,5 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
return Task.CompletedTask;
}
-
- public string Name => _localization.GetLocalizedString("TaskCleanLogs");
-
- public string Description => string.Format(_localization.GetLocalizedString("TaskCleanLogsDescription"), ConfigurationManager.CommonConfiguration.LogFileRetentionDays);
-
- public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
-
- public string Key => "CleanLogFiles";
-
- public bool IsHidden => false;
-
- public bool IsEnabled => true;
-
- public bool IsLogged => true;
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
index 1d133dcda..26ef19354 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
@@ -13,11 +13,11 @@ using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
/// <summary>
- /// Deletes all transcoding temp files
+ /// Deletes all transcoding temp files.
/// </summary>
public class DeleteTranscodeFileTask : IScheduledTask, IConfigurableScheduledTask
{
- private readonly ILogger _logger;
+ private readonly ILogger<DeleteTranscodeFileTask> _logger;
private readonly IConfigurationManager _configurationManager;
private readonly IFileSystem _fileSystem;
private readonly ILocalizationManager _localization;
@@ -132,18 +132,25 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
}
}
+ /// <inheritdoc />
public string Name => _localization.GetLocalizedString("TaskCleanTranscode");
+ /// <inheritdoc />
public string Description => _localization.GetLocalizedString("TaskCleanTranscodeDescription");
+ /// <inheritdoc />
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+ /// <inheritdoc />
public string Key => "DeleteTranscodeFiles";
+ /// <inheritdoc />
public bool IsHidden => false;
- public bool IsEnabled => false;
+ /// <inheritdoc />
+ public bool IsEnabled => true;
+ /// <inheritdoc />
public bool IsLogged => true;
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
index 63f867bf6..c384cf4bb 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs
@@ -1,8 +1,9 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Model.Globalization;
@@ -18,19 +19,16 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// The library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
-
- private readonly IServerApplicationHost _appHost;
private readonly ILocalizationManager _localization;
/// <summary>
/// Initializes a new instance of the <see cref="PeopleValidationTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
- /// <param name="appHost">The server application host</param>
- public PeopleValidationTask(ILibraryManager libraryManager, IServerApplicationHost appHost, ILocalizationManager localization)
+ /// <param name="localization">The localization manager.</param>
+ public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization)
{
_libraryManager = libraryManager;
- _appHost = appHost;
_localization = localization;
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
index 6a1afced7..c5af68bce 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.IO;
@@ -20,7 +22,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <summary>
/// The _logger.
/// </summary>
- private readonly ILogger _logger;
+ private readonly ILogger<PluginUpdateTask> _logger;
private readonly IInstallationManager _installationManager;
private readonly ILocalizationManager _localization;
@@ -32,6 +34,27 @@ namespace Emby.Server.Implementations.ScheduledTasks
_localization = localization;
}
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskUpdatePlugins");
+
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription");
+
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksApplicationCategory");
+
+ /// <inheritdoc />
+ public string Key => "PluginUpdates";
+
+ /// <inheritdoc />
+ public bool IsHidden => false;
+
+ /// <inheritdoc />
+ public bool IsEnabled => true;
+
+ /// <inheritdoc />
+ public bool IsLogged => true;
+
/// <summary>
/// Creates the triggers that define when the task will run.
/// </summary>
@@ -80,11 +103,11 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
catch (HttpException ex)
{
- _logger.LogError(ex, "Error downloading {0}", package.name);
+ _logger.LogError(ex, "Error downloading {0}", package.Name);
}
catch (IOException ex)
{
- _logger.LogError(ex, "Error updating {0}", package.name);
+ _logger.LogError(ex, "Error updating {0}", package.Name);
}
// Update progress
@@ -96,26 +119,5 @@ namespace Emby.Server.Implementations.ScheduledTasks
progress.Report(100);
}
-
- /// <inheritdoc />
- public string Name => _localization.GetLocalizedString("TaskUpdatePlugins");
-
- /// <inheritdoc />
- public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription");
-
- /// <inheritdoc />
- public string Category => _localization.GetLocalizedString("TasksApplicationCategory");
-
- /// <inheritdoc />
- public string Key => "PluginUpdates";
-
- /// <inheritdoc />
- public bool IsHidden => false;
-
- /// <inheritdoc />
- public bool IsEnabled => true;
-
- /// <inheritdoc />
- public bool IsLogged => true;
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
index 74cb01444..e470adcf4 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs
@@ -1,9 +1,10 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Model.Globalization;
@@ -16,20 +17,19 @@ namespace Emby.Server.Implementations.ScheduledTasks
public class RefreshMediaLibraryTask : IScheduledTask
{
/// <summary>
- /// The _library manager
+ /// The _library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
- private readonly IServerConfigurationManager _config;
private readonly ILocalizationManager _localization;
/// <summary>
/// Initializes a new instance of the <see cref="RefreshMediaLibraryTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
- public RefreshMediaLibraryTask(ILibraryManager libraryManager, IServerConfigurationManager config, ILocalizationManager localization)
+ /// <param name="localization">The localization manager.</param>
+ public RefreshMediaLibraryTask(ILibraryManager libraryManager, ILocalizationManager localization)
{
_libraryManager = libraryManager;
- _config = config;
_localization = localization;
}
@@ -61,18 +61,25 @@ namespace Emby.Server.Implementations.ScheduledTasks
return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken);
}
+ /// <inheritdoc />
public string Name => _localization.GetLocalizedString("TaskRefreshLibrary");
+ /// <inheritdoc />
public string Description => _localization.GetLocalizedString("TaskRefreshLibraryDescription");
+ /// <inheritdoc />
public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+ /// <inheritdoc />
public string Key => "RefreshLibrary";
+ /// <inheritdoc />
public bool IsHidden => false;
+ /// <inheritdoc />
public bool IsEnabled => true;
+ /// <inheritdoc />
public bool IsLogged => true;
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
index ea278de0d..8b67d37d7 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
@@ -11,7 +11,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
public class DailyTrigger : ITaskTrigger
{
/// <summary>
- /// Get the time of day to trigger the task to run.
+ /// Occurs when [triggered].
+ /// </summary>
+ public event EventHandler<EventArgs> Triggered;
+
+ /// <summary>
+ /// Gets or sets the time of day to trigger the task to run.
/// </summary>
/// <value>The time of day.</value>
public TimeSpan TimeOfDay { get; set; }
@@ -28,9 +33,11 @@ namespace Emby.Server.Implementations.ScheduledTasks
private Timer Timer { get; set; }
/// <summary>
- /// Stars waiting for the trigger action
+ /// Stars waiting for the trigger action.
/// </summary>
/// <param name="lastResult">The last result.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="taskName">The name of the task.</param>
/// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
{
@@ -49,7 +56,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
/// <summary>
- /// Stops waiting for the trigger action
+ /// Stops waiting for the trigger action.
/// </summary>
public void Stop()
{
@@ -68,19 +75,11 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
/// <summary>
- /// Occurs when [triggered].
- /// </summary>
- public event EventHandler<EventArgs> Triggered;
-
- /// <summary>
/// Called when [triggered].
/// </summary>
private void OnTriggered()
{
- if (Triggered != null)
- {
- Triggered(this, EventArgs.Empty);
- }
+ Triggered?.Invoke(this, EventArgs.Empty);
}
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
index 3a34da3af..b04fd7c7e 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs
@@ -7,10 +7,17 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.ScheduledTasks
{
/// <summary>
- /// Represents a task trigger that runs repeatedly on an interval
+ /// Represents a task trigger that runs repeatedly on an interval.
/// </summary>
public class IntervalTrigger : ITaskTrigger
{
+ private DateTime _lastStartDate;
+
+ /// <summary>
+ /// Occurs when [triggered].
+ /// </summary>
+ public event EventHandler<EventArgs> Triggered;
+
/// <summary>
/// Gets or sets the interval.
/// </summary>
@@ -28,12 +35,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// <value>The timer.</value>
private Timer Timer { get; set; }
- private DateTime _lastStartDate;
-
/// <summary>
- /// Stars waiting for the trigger action
+ /// Stars waiting for the trigger action.
/// </summary>
/// <param name="lastResult">The last result.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="taskName">The name of the task.</param>
/// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
{
@@ -68,7 +75,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
/// <summary>
- /// Stops waiting for the trigger action
+ /// Stops waiting for the trigger action.
/// </summary>
public void Stop()
{
@@ -87,11 +94,6 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
/// <summary>
- /// Occurs when [triggered].
- /// </summary>
- public event EventHandler<EventArgs> Triggered;
-
- /// <summary>
/// Called when [triggered].
/// </summary>
private void OnTriggered()
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
index 08ff4f55f..7cd5493da 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Threading.Tasks;
using MediaBrowser.Model.Tasks;
@@ -6,10 +8,15 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.ScheduledTasks
{
/// <summary>
- /// Class StartupTaskTrigger
+ /// Class StartupTaskTrigger.
/// </summary>
public class StartupTrigger : ITaskTrigger
{
+ /// <summary>
+ /// Occurs when [triggered].
+ /// </summary>
+ public event EventHandler<EventArgs> Triggered;
+
public int DelayMs { get; set; }
/// <summary>
@@ -23,9 +30,11 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
/// <summary>
- /// Stars waiting for the trigger action
+ /// Stars waiting for the trigger action.
/// </summary>
/// <param name="lastResult">The last result.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="taskName">The name of the task.</param>
/// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
public async void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
{
@@ -38,26 +47,18 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
/// <summary>
- /// Stops waiting for the trigger action
+ /// Stops waiting for the trigger action.
/// </summary>
public void Stop()
{
}
/// <summary>
- /// Occurs when [triggered].
- /// </summary>
- public event EventHandler<EventArgs> Triggered;
-
- /// <summary>
/// Called when [triggered].
/// </summary>
private void OnTriggered()
{
- if (Triggered != null)
- {
- Triggered(this, EventArgs.Empty);
- }
+ Triggered?.Invoke(this, EventArgs.Empty);
}
}
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
index 2a6a7b13c..0c0ebec08 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs
@@ -6,12 +6,17 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.ScheduledTasks
{
/// <summary>
- /// Represents a task trigger that fires on a weekly basis
+ /// Represents a task trigger that fires on a weekly basis.
/// </summary>
public class WeeklyTrigger : ITaskTrigger
{
/// <summary>
- /// Get the time of day to trigger the task to run
+ /// Occurs when [triggered].
+ /// </summary>
+ public event EventHandler<EventArgs> Triggered;
+
+ /// <summary>
+ /// Gets or sets the time of day to trigger the task to run.
/// </summary>
/// <value>The time of day.</value>
public TimeSpan TimeOfDay { get; set; }
@@ -34,9 +39,11 @@ namespace Emby.Server.Implementations.ScheduledTasks
private Timer Timer { get; set; }
/// <summary>
- /// Stars waiting for the trigger action
+ /// Stars waiting for the trigger action.
/// </summary>
/// <param name="lastResult">The last result.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="taskName">The name of the task.</param>
/// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
{
@@ -75,7 +82,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
/// <summary>
- /// Stops waiting for the trigger action
+ /// Stops waiting for the trigger action.
/// </summary>
public void Stop()
{
@@ -94,19 +101,11 @@ namespace Emby.Server.Implementations.ScheduledTasks
}
/// <summary>
- /// Occurs when [triggered].
- /// </summary>
- public event EventHandler<EventArgs> Triggered;
-
- /// <summary>
/// Called when [triggered].
/// </summary>
private void OnTriggered()
{
- if (Triggered != null)
- {
- Triggered(this, EventArgs.Empty);
- }
+ Triggered?.Invoke(this, EventArgs.Empty);
}
}
}
diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
index 4e4029f06..4bc12f44a 100644
--- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs
+++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -52,14 +54,14 @@ namespace Emby.Server.Implementations.Security
{
if (tableNewlyCreated && TableExists(connection, "AccessTokens"))
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var existingColumnNames = GetColumnNames(db, "AccessTokens");
AddColumn(db, "AccessTokens", "UserName", "TEXT", existingColumnNames);
AddColumn(db, "AccessTokens", "DateLastActivity", "DATETIME", existingColumnNames);
AddColumn(db, "AccessTokens", "AppVersion", "TEXT", existingColumnNames);
-
}, TransactionMode);
connection.RunQueries(new[]
@@ -87,7 +89,8 @@ namespace Emby.Server.Implementations.Security
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
using (var statement = db.PrepareStatement("insert into Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) values (@AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @UserName, @IsActive, @DateCreated, @DateLastActivity)"))
{
@@ -97,7 +100,7 @@ namespace Emby.Server.Implementations.Security
statement.TryBind("@AppName", info.AppName);
statement.TryBind("@AppVersion", info.AppVersion);
statement.TryBind("@DeviceName", info.DeviceName);
- statement.TryBind("@UserId", (info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture)));
+ statement.TryBind("@UserId", info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture));
statement.TryBind("@UserName", info.UserName);
statement.TryBind("@IsActive", true);
statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
@@ -105,7 +108,6 @@ namespace Emby.Server.Implementations.Security
statement.MoveNext();
}
-
}, TransactionMode);
}
}
@@ -119,7 +121,8 @@ namespace Emby.Server.Implementations.Security
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
using (var statement = db.PrepareStatement("Update Tokens set AccessToken=@AccessToken, DeviceId=@DeviceId, AppName=@AppName, AppVersion=@AppVersion, DeviceName=@DeviceName, UserId=@UserId, UserName=@UserName, DateCreated=@DateCreated, DateLastActivity=@DateLastActivity where Id=@Id"))
{
@@ -131,7 +134,7 @@ namespace Emby.Server.Implementations.Security
statement.TryBind("@AppName", info.AppName);
statement.TryBind("@AppVersion", info.AppVersion);
statement.TryBind("@DeviceName", info.DeviceName);
- statement.TryBind("@UserId", (info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture)));
+ statement.TryBind("@UserId", info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture));
statement.TryBind("@UserName", info.UserName);
statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue());
@@ -151,7 +154,8 @@ namespace Emby.Server.Implementations.Security
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
using (var statement = db.PrepareStatement("Delete from Tokens where Id=@Id"))
{
@@ -257,8 +261,7 @@ namespace Emby.Server.Implementations.Security
connection.RunInTransaction(
db =>
{
- var statements = PrepareAll(db, statementTexts)
- .ToList();
+ var statements = PrepareAll(db, statementTexts);
using (var statement = statements[0])
{
@@ -282,7 +285,7 @@ namespace Emby.Server.Implementations.Security
ReadTransactionMode);
}
- result.Items = list.ToArray();
+ result.Items = list;
return result;
}
@@ -347,7 +350,8 @@ namespace Emby.Server.Implementations.Security
{
using (var connection = GetConnection(true))
{
- return connection.RunInTransaction(db =>
+ return connection.RunInTransaction(
+ db =>
{
using (var statement = base.PrepareStatement(db, "select CustomName from Devices where Id=@DeviceId"))
{
@@ -365,7 +369,6 @@ namespace Emby.Server.Implementations.Security
return result;
}
-
}, ReadTransactionMode);
}
}
@@ -379,7 +382,8 @@ namespace Emby.Server.Implementations.Security
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
using (var statement = db.PrepareStatement("replace into devices (Id, CustomName, Capabilities) VALUES (@Id, @CustomName, (Select Capabilities from Devices where Id=@Id))"))
{
@@ -396,7 +400,6 @@ namespace Emby.Server.Implementations.Security
statement.MoveNext();
}
-
}, TransactionMode);
}
}
diff --git a/Emby.Server.Implementations/Serialization/JsonSerializer.cs b/Emby.Server.Implementations/Serialization/JsonSerializer.cs
index bcc814daf..5ec3a735a 100644
--- a/Emby.Server.Implementations/Serialization/JsonSerializer.cs
+++ b/Emby.Server.Implementations/Serialization/JsonSerializer.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Globalization;
using System.IO;
@@ -11,6 +13,9 @@ namespace Emby.Server.Implementations.Serialization
/// </summary>
public class JsonSerializer : IJsonSerializer
{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="JsonSerializer" /> class.
+ /// </summary>
public JsonSerializer()
{
ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.DateHandler.ISO8601;
diff --git a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
index 296822981..27024e4e1 100644
--- a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
+++ b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.Serialization
@@ -53,10 +54,11 @@ namespace Emby.Server.Implementations.Serialization
/// <param name="stream">The stream.</param>
public void SerializeToStream(object obj, Stream stream)
{
- using (var writer = new XmlTextWriter(stream, null))
+ using (var writer = new StreamWriter(stream, null, IODefaults.StreamWriterBufferSize, true))
+ using (var textWriter = new XmlTextWriter(writer))
{
- writer.Formatting = Formatting.Indented;
- SerializeToWriter(obj, writer);
+ textWriter.Formatting = Formatting.Indented;
+ SerializeToWriter(obj, textWriter);
}
}
@@ -95,7 +97,7 @@ namespace Emby.Server.Implementations.Serialization
/// <returns>System.Object.</returns>
public object DeserializeFromBytes(Type type, byte[] buffer)
{
- using (var stream = new MemoryStream(buffer))
+ using (var stream = new MemoryStream(buffer, 0, buffer.Length, false, true))
{
return DeserializeFromStream(type, stream);
}
diff --git a/Emby.Server.Implementations/Services/HttpResult.cs b/Emby.Server.Implementations/Services/HttpResult.cs
deleted file mode 100644
index 095193828..000000000
--- a/Emby.Server.Implementations/Services/HttpResult.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Server.Implementations.Services
-{
- public class HttpResult
- : IHttpResult, IAsyncStreamWriter
- {
- public HttpResult(object response, string contentType, HttpStatusCode statusCode)
- {
- this.Headers = new Dictionary<string, string>();
-
- this.Response = response;
- this.ContentType = contentType;
- this.StatusCode = statusCode;
- }
-
- public object Response { get; set; }
-
- public string ContentType { get; set; }
-
- public IDictionary<string, string> Headers { get; private set; }
-
- public int Status { get; set; }
-
- public HttpStatusCode StatusCode
- {
- get => (HttpStatusCode)Status;
- set => Status = (int)value;
- }
-
- public IRequest RequestContext { get; set; }
-
- public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
- {
- var response = RequestContext?.Response;
-
- if (this.Response is byte[] bytesResponse)
- {
- var contentLength = bytesResponse.Length;
-
- if (response != null)
- {
- response.ContentLength = contentLength;
- }
-
- if (contentLength > 0)
- {
- await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false);
- }
-
- return;
- }
-
- await ResponseHelper.WriteObject(this.RequestContext, this.Response, response).ConfigureAwait(false);
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/RequestHelper.cs b/Emby.Server.Implementations/Services/RequestHelper.cs
deleted file mode 100644
index 2563cac99..000000000
--- a/Emby.Server.Implementations/Services/RequestHelper.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using System;
-using System.IO;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-
-namespace Emby.Server.Implementations.Services
-{
- public class RequestHelper
- {
- public static Func<Type, Stream, Task<object>> GetRequestReader(HttpListenerHost host, string contentType)
- {
- switch (GetContentTypeWithoutEncoding(contentType))
- {
- case "application/xml":
- case "text/xml":
- case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
- return host.DeserializeXml;
-
- case "application/json":
- case "text/json":
- return host.DeserializeJson;
- }
-
- return null;
- }
-
- public static Action<object, Stream> GetResponseWriter(HttpListenerHost host, string contentType)
- {
- switch (GetContentTypeWithoutEncoding(contentType))
- {
- case "application/xml":
- case "text/xml":
- case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
- return host.SerializeToXml;
-
- case "application/json":
- case "text/json":
- return host.SerializeToJson;
- }
-
- return null;
- }
-
- private static string GetContentTypeWithoutEncoding(string contentType)
- {
- return contentType == null
- ? null
- : contentType.Split(';')[0].ToLowerInvariant().Trim();
- }
-
- }
-}
diff --git a/Emby.Server.Implementations/Services/ResponseHelper.cs b/Emby.Server.Implementations/Services/ResponseHelper.cs
deleted file mode 100644
index a566b18dd..000000000
--- a/Emby.Server.Implementations/Services/ResponseHelper.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-using System;
-using System.Globalization;
-using System.IO;
-using System.Net;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-
-namespace Emby.Server.Implementations.Services
-{
- public static class ResponseHelper
- {
- public static Task WriteToResponse(HttpResponse response, IRequest request, object result, CancellationToken cancellationToken)
- {
- if (result == null)
- {
- if (response.StatusCode == (int)HttpStatusCode.OK)
- {
- response.StatusCode = (int)HttpStatusCode.NoContent;
- }
-
- response.ContentLength = 0;
- return Task.CompletedTask;
- }
-
- var httpResult = result as IHttpResult;
- if (httpResult != null)
- {
- httpResult.RequestContext = request;
- request.ResponseContentType = httpResult.ContentType ?? request.ResponseContentType;
- }
-
- var defaultContentType = request.ResponseContentType;
-
- if (httpResult != null)
- {
- if (httpResult.RequestContext == null)
- httpResult.RequestContext = request;
-
- response.StatusCode = httpResult.Status;
- }
-
- var responseOptions = result as IHasHeaders;
- if (responseOptions != null)
- {
- foreach (var responseHeaders in responseOptions.Headers)
- {
- if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase))
- {
- response.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture);
- continue;
- }
-
- response.Headers.Add(responseHeaders.Key, responseHeaders.Value);
- }
- }
-
- //ContentType='text/html' is the default for a HttpResponse
- //Do not override if another has been set
- if (response.ContentType == null || response.ContentType == "text/html")
- {
- response.ContentType = defaultContentType;
- }
-
- if (response.ContentType == "application/json")
- {
- response.ContentType += "; charset=utf-8";
- }
-
- switch (result)
- {
- case IAsyncStreamWriter asyncStreamWriter:
- return asyncStreamWriter.WriteToAsync(response.Body, cancellationToken);
- case IStreamWriter streamWriter:
- streamWriter.WriteTo(response.Body);
- return Task.CompletedTask;
- case FileWriter fileWriter:
- return fileWriter.WriteToAsync(response, cancellationToken);
- case Stream stream:
- return CopyStream(stream, response.Body);
- case byte[] bytes:
- response.ContentType = "application/octet-stream";
- response.ContentLength = bytes.Length;
-
- if (bytes.Length > 0)
- {
- return response.Body.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
- }
-
- return Task.CompletedTask;
- case string responseText:
- var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText);
- response.ContentLength = responseTextAsBytes.Length;
-
- if (responseTextAsBytes.Length > 0)
- {
- return response.Body.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken);
- }
-
- return Task.CompletedTask;
- }
-
- return WriteObject(request, result, response);
- }
-
- private static async Task CopyStream(Stream src, Stream dest)
- {
- using (src)
- {
- await src.CopyToAsync(dest).ConfigureAwait(false);
- }
- }
-
- public static async Task WriteObject(IRequest request, object result, HttpResponse response)
- {
- var contentType = request.ResponseContentType;
- var serializer = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
-
- using (var ms = new MemoryStream())
- {
- serializer(result, ms);
-
- ms.Position = 0;
-
- var contentLength = ms.Length;
- response.ContentLength = contentLength;
-
- if (contentLength > 0)
- {
- await ms.CopyToAsync(response.Body).ConfigureAwait(false);
- }
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs
deleted file mode 100644
index e24a95dbb..000000000
--- a/Emby.Server.Implementations/Services/ServiceController.cs
+++ /dev/null
@@ -1,188 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Services
-{
- public delegate object ActionInvokerFn(object intance, object request);
-
- public delegate void VoidActionInvokerFn(object intance, object request);
-
- public class ServiceController
- {
- private readonly ILogger _logger;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="ServiceController"/> class.
- /// </summary>
- /// <param name="logger">The <see cref="ServiceController"/> logger.</param>
- public ServiceController(ILogger<ServiceController> logger)
- {
- _logger = logger;
- }
-
- public void Init(HttpListenerHost appHost, IEnumerable<Type> serviceTypes)
- {
- foreach (var serviceType in serviceTypes)
- {
- RegisterService(appHost, serviceType);
- }
- }
-
- public void RegisterService(HttpListenerHost appHost, Type serviceType)
- {
- // Make sure the provided type implements IService
- if (!typeof(IService).IsAssignableFrom(serviceType))
- {
- _logger.LogWarning("Tried to register a service that does not implement IService: {ServiceType}", serviceType);
- return;
- }
-
- var processedReqs = new HashSet<Type>();
-
- var actions = ServiceExecGeneral.Reset(serviceType);
-
- foreach (var mi in serviceType.GetActions())
- {
- var requestType = mi.GetParameters()[0].ParameterType;
- if (processedReqs.Contains(requestType))
- {
- continue;
- }
-
- processedReqs.Add(requestType);
-
- ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions);
-
- //var returnMarker = GetTypeWithGenericTypeDefinitionOf(requestType, typeof(IReturn<>));
- //var responseType = returnMarker != null ?
- // GetGenericArguments(returnMarker)[0]
- // : mi.ReturnType != typeof(object) && mi.ReturnType != typeof(void) ?
- // mi.ReturnType
- // : Type.GetType(requestType.FullName + "Response");
-
- RegisterRestPaths(appHost, requestType, serviceType);
-
- appHost.AddServiceInfo(serviceType, requestType);
- }
- }
-
- public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap();
-
- public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType)
- {
- var attrs = appHost.GetRouteAttributes(requestType);
- foreach (var attr in attrs)
- {
- var restPath = new RestPath(appHost.CreateInstance, appHost.GetParseFn, requestType, serviceType, attr.Path, attr.Verbs, attr.IsHidden, attr.Summary, attr.Description);
-
- RegisterRestPath(restPath);
- }
- }
-
- private static readonly char[] InvalidRouteChars = new[] { '?', '&' };
-
- public void RegisterRestPath(RestPath restPath)
- {
- if (restPath.Path[0] != '/')
- {
- throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName()));
- }
-
- if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
- {
- throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName()));
- }
-
- if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
- {
- pathsAtFirstMatch.Add(restPath);
- }
- else
- {
- RestPathMap[restPath.FirstMatchHashKey] = new List<RestPath>() { restPath };
- }
- }
-
- public RestPath GetRestPathForRequest(string httpMethod, string pathInfo)
- {
- var matchUsingPathParts = RestPath.GetPathPartsForMatching(pathInfo);
-
- List<RestPath> firstMatches;
-
- var yieldedHashMatches = RestPath.GetFirstMatchHashKeys(matchUsingPathParts);
- foreach (var potentialHashMatch in yieldedHashMatches)
- {
- if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches))
- {
- continue;
- }
-
- var bestScore = -1;
- RestPath bestMatch = null;
- foreach (var restPath in firstMatches)
- {
- var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
- if (score > bestScore)
- {
- bestScore = score;
- bestMatch = restPath;
- }
- }
-
- if (bestScore > 0 && bestMatch != null)
- {
- return bestMatch;
- }
- }
-
- var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts);
- foreach (var potentialHashMatch in yieldedWildcardMatches)
- {
- if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) continue;
-
- var bestScore = -1;
- RestPath bestMatch = null;
- foreach (var restPath in firstMatches)
- {
- var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
- if (score > bestScore)
- {
- bestScore = score;
- bestMatch = restPath;
- }
- }
-
- if (bestScore > 0 && bestMatch != null)
- {
- return bestMatch;
- }
- }
-
- return null;
- }
-
- public Task<object> Execute(HttpListenerHost httpHost, object requestDto, IRequest req)
- {
- var requestType = requestDto.GetType();
- req.OperationName = requestType.Name;
-
- var serviceType = httpHost.GetServiceTypeByRequest(requestType);
-
- var service = httpHost.CreateInstance(serviceType);
-
- var serviceRequiresContext = service as IRequiresRequest;
- if (serviceRequiresContext != null)
- {
- serviceRequiresContext.Request = req;
- }
-
- //Executes the service and returns the result
- return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName());
- }
- }
-
-}
diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs
deleted file mode 100644
index 9f5f97028..000000000
--- a/Emby.Server.Implementations/Services/ServiceExec.cs
+++ /dev/null
@@ -1,214 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Linq.Expressions;
-using System.Reflection;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Server.Implementations.Services
-{
- public static class ServiceExecExtensions
- {
- public static string[] AllVerbs = new[] {
- "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616
- "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", // RFC 2518
- "VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT",
- "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY", // RFC 3253
- "ORDERPATCH", // RFC 3648
- "ACL", // RFC 3744
- "PATCH", // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/
- "SEARCH", // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/
- "BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY",
- "POLL", "SUBSCRIBE", "UNSUBSCRIBE"
- };
-
- public static List<MethodInfo> GetActions(this Type serviceType)
- {
- var list = new List<MethodInfo>();
-
- foreach (var mi in serviceType.GetRuntimeMethods())
- {
- if (!mi.IsPublic)
- {
- continue;
- }
-
- if (mi.IsStatic)
- {
- continue;
- }
-
- if (mi.GetParameters().Length != 1)
- continue;
-
- var actionName = mi.Name;
- if (!AllVerbs.Contains(actionName, StringComparer.OrdinalIgnoreCase))
- continue;
-
- list.Add(mi);
- }
-
- return list;
- }
- }
-
- internal static class ServiceExecGeneral
- {
- private static Dictionary<string, ServiceMethod> execMap = new Dictionary<string, ServiceMethod>();
-
- public static void CreateServiceRunnersFor(Type requestType, List<ServiceMethod> actions)
- {
- foreach (var actionCtx in actions)
- {
- if (execMap.ContainsKey(actionCtx.Id)) continue;
-
- execMap[actionCtx.Id] = actionCtx;
- }
- }
-
- public static Task<object> Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName)
- {
- var actionName = request.Verb ?? "POST";
-
- if (execMap.TryGetValue(ServiceMethod.Key(serviceType, actionName, requestName), out ServiceMethod actionContext))
- {
- if (actionContext.RequestFilters != null)
- {
- foreach (var requestFilter in actionContext.RequestFilters)
- {
- requestFilter.RequestFilter(request, request.Response, requestDto);
- if (request.Response.HasStarted)
- {
- Task.FromResult<object>(null);
- }
- }
- }
-
- var response = actionContext.ServiceAction(instance, requestDto);
-
- if (response is Task taskResponse)
- {
- return GetTaskResult(taskResponse);
- }
-
- return Task.FromResult(response);
- }
-
- var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant();
- throw new NotImplementedException(string.Format("Could not find method named {1}({0}) or Any({0}) on Service {2}", requestDto.GetType().GetMethodName(), expectedMethodName, serviceType.GetMethodName()));
- }
-
- private static async Task<object> GetTaskResult(Task task)
- {
- try
- {
- if (task is Task<object> taskObject)
- {
- return await taskObject.ConfigureAwait(false);
- }
-
- await task.ConfigureAwait(false);
-
- var type = task.GetType().GetTypeInfo();
- if (!type.IsGenericType)
- {
- return null;
- }
-
- var resultProperty = type.GetDeclaredProperty("Result");
- if (resultProperty == null)
- {
- return null;
- }
-
- var result = resultProperty.GetValue(task);
-
- // hack alert
- if (result.GetType().Name.IndexOf("voidtaskresult", StringComparison.OrdinalIgnoreCase) != -1)
- {
- return null;
- }
-
- return result;
- }
- catch (TypeAccessException)
- {
- return null; // return null for void Task's
- }
- }
-
- public static List<ServiceMethod> Reset(Type serviceType)
- {
- var actions = new List<ServiceMethod>();
-
- foreach (var mi in serviceType.GetActions())
- {
- var actionName = mi.Name;
- var args = mi.GetParameters();
-
- var requestType = args[0].ParameterType;
- var actionCtx = new ServiceMethod
- {
- Id = ServiceMethod.Key(serviceType, actionName, requestType.GetMethodName())
- };
-
- actionCtx.ServiceAction = CreateExecFn(serviceType, requestType, mi);
-
- var reqFilters = new List<IHasRequestFilter>();
-
- foreach (var attr in mi.GetCustomAttributes(true))
- {
- if (attr is IHasRequestFilter hasReqFilter)
- {
- reqFilters.Add(hasReqFilter);
- }
- }
-
- if (reqFilters.Count > 0)
- {
- actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray();
- }
-
- actions.Add(actionCtx);
- }
-
- return actions;
- }
-
- private static ActionInvokerFn CreateExecFn(Type serviceType, Type requestType, MethodInfo mi)
- {
- var serviceParam = Expression.Parameter(typeof(object), "serviceObj");
- var serviceStrong = Expression.Convert(serviceParam, serviceType);
-
- var requestDtoParam = Expression.Parameter(typeof(object), "requestDto");
- var requestDtoStrong = Expression.Convert(requestDtoParam, requestType);
-
- Expression callExecute = Expression.Call(
- serviceStrong, mi, requestDtoStrong);
-
- if (mi.ReturnType != typeof(void))
- {
- var executeFunc = Expression.Lambda<ActionInvokerFn>(
- callExecute,
- serviceParam,
- requestDtoParam).Compile();
-
- return executeFunc;
- }
- else
- {
- var executeFunc = Expression.Lambda<VoidActionInvokerFn>(
- callExecute,
- serviceParam,
- requestDtoParam).Compile();
-
- return (service, request) =>
- {
- executeFunc(service, request);
- return null;
- };
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs
deleted file mode 100644
index 934560de3..000000000
--- a/Emby.Server.Implementations/Services/ServiceHandler.cs
+++ /dev/null
@@ -1,204 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Reflection;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Services
-{
- public class ServiceHandler
- {
- private RestPath _restPath;
-
- private string _responseContentType;
-
- internal ServiceHandler(RestPath restPath, string responseContentType)
- {
- _restPath = restPath;
- _responseContentType = responseContentType;
- }
-
- protected static Task<object> CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType)
- {
- if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0)
- {
- var deserializer = RequestHelper.GetRequestReader(host, contentType);
- if (deserializer != null)
- {
- return deserializer.Invoke(requestType, httpReq.InputStream);
- }
- }
-
- return Task.FromResult(host.CreateInstance(requestType));
- }
-
- public static string GetSanitizedPathInfo(string pathInfo, out string contentType)
- {
- contentType = null;
- var pos = pathInfo.LastIndexOf('.');
- if (pos != -1)
- {
- var format = pathInfo.Substring(pos + 1);
- contentType = GetFormatContentType(format);
- if (contentType != null)
- {
- pathInfo = pathInfo.Substring(0, pos);
- }
- }
-
- return pathInfo;
- }
-
- private static string GetFormatContentType(string format)
- {
- // built-in formats
- switch (format)
- {
- case "json": return "application/json";
- case "xml": return "application/xml";
- default: return null;
- }
- }
-
- public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, ILogger logger, CancellationToken cancellationToken)
- {
- httpReq.Items["__route"] = _restPath;
-
- if (_responseContentType != null)
- {
- httpReq.ResponseContentType = _responseContentType;
- }
-
- var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false);
-
- httpHost.ApplyRequestFilters(httpReq, httpRes, request);
-
- var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
-
- // Apply response filters
- foreach (var responseFilter in httpHost.ResponseFilters)
- {
- responseFilter(httpReq, httpRes, response);
- }
-
- await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false);
- }
-
- public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath, ILogger logger)
- {
- var requestType = restPath.RequestType;
-
- if (RequireqRequestStream(requestType))
- {
- // Used by IRequiresRequestStream
- var requestParams = GetRequestParams(httpReq.Response.HttpContext.Request);
- var request = ServiceHandler.CreateRequest(httpReq, restPath, requestParams, host.CreateInstance(requestType));
-
- var rawReq = (IRequiresRequestStream)request;
- rawReq.RequestStream = httpReq.InputStream;
- return rawReq;
- }
- else
- {
- var requestParams = GetFlattenedRequestParams(httpReq.Response.HttpContext.Request);
-
- var requestDto = await CreateContentTypeRequest(host, httpReq, restPath.RequestType, httpReq.ContentType).ConfigureAwait(false);
-
- return CreateRequest(httpReq, restPath, requestParams, requestDto);
- }
- }
-
- public static bool RequireqRequestStream(Type requestType)
- {
- var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo();
-
- return requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo());
- }
-
- public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams, object requestDto)
- {
- var pathInfo = !restPath.IsWildCardPath
- ? GetSanitizedPathInfo(httpReq.PathInfo, out _)
- : httpReq.PathInfo;
-
- return restPath.CreateRequest(pathInfo, requestParams, requestDto);
- }
-
- /// <summary>
- /// Duplicate Params are given a unique key by appending a #1 suffix
- /// </summary>
- private static Dictionary<string, string> GetRequestParams(HttpRequest request)
- {
- var map = new Dictionary<string, string>();
-
- foreach (var pair in request.Query)
- {
- var values = pair.Value;
- if (values.Count == 1)
- {
- map[pair.Key] = values[0];
- }
- else
- {
- for (var i = 0; i < values.Count; i++)
- {
- map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i];
- }
- }
- }
-
- if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT"))
- && request.HasFormContentType)
- {
- foreach (var pair in request.Form)
- {
- var values = pair.Value;
- if (values.Count == 1)
- {
- map[pair.Key] = values[0];
- }
- else
- {
- for (var i = 0; i < values.Count; i++)
- {
- map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i];
- }
- }
- }
- }
-
- return map;
- }
-
- private static bool IsMethod(string method, string expected)
- => string.Equals(method, expected, StringComparison.OrdinalIgnoreCase);
-
- /// <summary>
- /// Duplicate params have their values joined together in a comma-delimited string
- /// </summary>
- private static Dictionary<string, string> GetFlattenedRequestParams(HttpRequest request)
- {
- var map = new Dictionary<string, string>();
-
- foreach (var pair in request.Query)
- {
- map[pair.Key] = pair.Value;
- }
-
- if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT"))
- && request.HasFormContentType)
- {
- foreach (var pair in request.Form)
- {
- map[pair.Key] = pair.Value;
- }
- }
-
- return map;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/ServiceMethod.cs b/Emby.Server.Implementations/Services/ServiceMethod.cs
deleted file mode 100644
index 5018bf4a2..000000000
--- a/Emby.Server.Implementations/Services/ServiceMethod.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System;
-
-namespace Emby.Server.Implementations.Services
-{
- public class ServiceMethod
- {
- public string Id { get; set; }
-
- public ActionInvokerFn ServiceAction { get; set; }
- public MediaBrowser.Model.Services.IHasRequestFilter[] RequestFilters { get; set; }
-
- public static string Key(Type serviceType, string method, string requestDtoName)
- {
- return serviceType.FullName + " " + method.ToUpperInvariant() + " " + requestDtoName;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs
deleted file mode 100644
index 27c4dcba0..000000000
--- a/Emby.Server.Implementations/Services/ServicePath.cs
+++ /dev/null
@@ -1,538 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using System.Text;
-using System.Text.Json.Serialization;
-
-namespace Emby.Server.Implementations.Services
-{
- public class RestPath
- {
- private const string WildCard = "*";
- private const char WildCardChar = '*';
- private const string PathSeperator = "/";
- private const char PathSeperatorChar = '/';
- private const char ComponentSeperator = '.';
- private const string VariablePrefix = "{";
-
- private readonly bool[] componentsWithSeparators;
-
- private readonly string restPath;
- public bool IsWildCardPath { get; private set; }
-
- private readonly string[] literalsToMatch;
-
- private readonly string[] variablesNames;
-
- private readonly bool[] isWildcard;
- private readonly int wildcardCount = 0;
-
- internal static string[] IgnoreAttributesNamed = new[]
- {
- nameof(JsonIgnoreAttribute)
- };
-
- private static Type _excludeType = typeof(Stream);
-
- public int VariableArgsCount { get; set; }
-
- /// <summary>
- /// The number of segments separated by '/' determinable by path.Split('/').Length
- /// e.g. /path/to/here.ext == 3
- /// </summary>
- public int PathComponentsCount { get; set; }
-
- /// <summary>
- /// Gets or sets the total number of segments after subparts have been exploded ('.')
- /// e.g. /path/to/here.ext == 4.
- /// </summary>
- public int TotalComponentsCount { get; set; }
-
- public string[] Verbs { get; private set; }
-
- public Type RequestType { get; private set; }
-
- public Type ServiceType { get; private set; }
-
- public string Path => this.restPath;
-
- public string Summary { get; private set; }
- public string Description { get; private set; }
- public bool IsHidden { get; private set; }
-
- public static string[] GetPathPartsForMatching(string pathInfo)
- {
- return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
- }
-
- public static List<string> GetFirstMatchHashKeys(string[] pathPartsForMatching)
- {
- var hashPrefix = pathPartsForMatching.Length + PathSeperator;
- return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching);
- }
-
- public static List<string> GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching)
- {
- const string hashPrefix = WildCard + PathSeperator;
- return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching);
- }
-
- private static List<string> GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching)
- {
- var list = new List<string>();
-
- foreach (var part in pathPartsForMatching)
- {
- list.Add(hashPrefix + part);
-
- if (part.IndexOf(ComponentSeperator) == -1)
- {
- continue;
- }
-
- var subParts = part.Split(ComponentSeperator);
- foreach (var subPart in subParts)
- {
- list.Add(hashPrefix + subPart);
- }
- }
-
- return list;
- }
-
- public RestPath(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type requestType, Type serviceType, string path, string verbs, bool isHidden = false, string summary = null, string description = null)
- {
- this.RequestType = requestType;
- this.ServiceType = serviceType;
- this.Summary = summary;
- this.IsHidden = isHidden;
- this.Description = description;
- this.restPath = path;
-
- this.Verbs = string.IsNullOrWhiteSpace(verbs) ? ServiceExecExtensions.AllVerbs : verbs.ToUpperInvariant().Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
-
- var componentsList = new List<string>();
-
- //We only split on '.' if the restPath has them. Allows for /{action}.{type}
- var hasSeparators = new List<bool>();
- foreach (var component in this.restPath.Split(PathSeperatorChar))
- {
- if (string.IsNullOrEmpty(component)) continue;
-
- if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1
- && component.IndexOf(ComponentSeperator) != -1)
- {
- hasSeparators.Add(true);
- componentsList.AddRange(component.Split(ComponentSeperator));
- }
- else
- {
- hasSeparators.Add(false);
- componentsList.Add(component);
- }
- }
-
- var components = componentsList.ToArray();
- this.TotalComponentsCount = components.Length;
-
- this.literalsToMatch = new string[this.TotalComponentsCount];
- this.variablesNames = new string[this.TotalComponentsCount];
- this.isWildcard = new bool[this.TotalComponentsCount];
- this.componentsWithSeparators = hasSeparators.ToArray();
- this.PathComponentsCount = this.componentsWithSeparators.Length;
- string firstLiteralMatch = null;
-
- for (var i = 0; i < components.Length; i++)
- {
- var component = components[i];
-
- if (component.StartsWith(VariablePrefix))
- {
- var variableName = component.Substring(1, component.Length - 2);
- if (variableName[variableName.Length - 1] == WildCardChar)
- {
- this.isWildcard[i] = true;
- variableName = variableName.Substring(0, variableName.Length - 1);
- }
- this.variablesNames[i] = variableName;
- this.VariableArgsCount++;
- }
- else
- {
- this.literalsToMatch[i] = component.ToLowerInvariant();
-
- if (firstLiteralMatch == null)
- {
- firstLiteralMatch = this.literalsToMatch[i];
- }
- }
- }
-
- for (var i = 0; i < components.Length - 1; i++)
- {
- if (!this.isWildcard[i])
- {
- continue;
- }
-
- if (this.literalsToMatch[i + 1] == null)
- {
- throw new ArgumentException(
- "A wildcard path component must be at the end of the path or followed by a literal path component.");
- }
- }
-
- this.wildcardCount = this.isWildcard.Length;
- this.IsWildCardPath = this.wildcardCount > 0;
-
- this.FirstMatchHashKey = !this.IsWildCardPath
- ? this.PathComponentsCount + PathSeperator + firstLiteralMatch
- : WildCardChar + PathSeperator + firstLiteralMatch;
-
- this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType);
-
- _propertyNamesMap = new HashSet<string>(
- GetSerializableProperties(RequestType).Select(x => x.Name),
- StringComparer.OrdinalIgnoreCase);
- }
-
- internal static IEnumerable<PropertyInfo> GetSerializableProperties(Type type)
- {
- foreach (var prop in GetPublicProperties(type))
- {
- if (prop.GetMethod == null
- || _excludeType == prop.PropertyType)
- {
- continue;
- }
-
- var ignored = false;
- foreach (var attr in prop.GetCustomAttributes(true))
- {
- if (IgnoreAttributesNamed.Contains(attr.GetType().Name))
- {
- ignored = true;
- break;
- }
- }
-
- if (!ignored)
- {
- yield return prop;
- }
- }
- }
-
- private static IEnumerable<PropertyInfo> GetPublicProperties(Type type)
- {
- if (type.IsInterface)
- {
- var propertyInfos = new List<PropertyInfo>();
- var considered = new List<Type>()
- {
- type
- };
- var queue = new Queue<Type>();
- queue.Enqueue(type);
-
- while (queue.Count > 0)
- {
- var subType = queue.Dequeue();
- foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces)
- {
- if (considered.Contains(subInterface))
- {
- continue;
- }
-
- considered.Add(subInterface);
- queue.Enqueue(subInterface);
- }
-
- var newPropertyInfos = GetTypesPublicProperties(subType)
- .Where(x => !propertyInfos.Contains(x));
-
- propertyInfos.InsertRange(0, newPropertyInfos);
- }
-
- return propertyInfos;
- }
-
- return GetTypesPublicProperties(type)
- .Where(x => x.GetIndexParameters().Length == 0);
- }
-
- private static IEnumerable<PropertyInfo> GetTypesPublicProperties(Type subType)
- {
- foreach (var pi in subType.GetRuntimeProperties())
- {
- var mi = pi.GetMethod ?? pi.SetMethod;
- if (mi != null && mi.IsStatic)
- {
- continue;
- }
-
- yield return pi;
- }
- }
-
- /// <summary>
- /// Provide for quick lookups based on hashes that can be determined from a request url.
- /// </summary>
- public string FirstMatchHashKey { get; private set; }
-
- private readonly StringMapTypeDeserializer typeDeserializer;
-
- private readonly HashSet<string> _propertyNamesMap;
-
- public int MatchScore(string httpMethod, string[] withPathInfoParts)
- {
- var isMatch = IsMatch(httpMethod, withPathInfoParts, out var wildcardMatchCount);
- if (!isMatch)
- {
- return -1;
- }
-
- //Routes with least wildcard matches get the highest score
- var score = Math.Max((100 - wildcardMatchCount), 1) * 1000
- //Routes with less variable (and more literal) matches
- + Math.Max((10 - VariableArgsCount), 1) * 100;
-
- //Exact verb match is better than ANY
- if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase))
- {
- score += 10;
- }
- else
- {
- score += 1;
- }
-
- return score;
- }
-
- /// <summary>
- /// For performance withPathInfoParts should already be a lower case string
- /// to minimize redundant matching operations.
- /// </summary>
- public bool IsMatch(string httpMethod, string[] withPathInfoParts, out int wildcardMatchCount)
- {
- wildcardMatchCount = 0;
-
- if (withPathInfoParts.Length != this.PathComponentsCount && !this.IsWildCardPath)
- {
- return false;
- }
-
- if (!Verbs.Contains(httpMethod, StringComparer.OrdinalIgnoreCase))
- {
- return false;
- }
-
- if (!ExplodeComponents(ref withPathInfoParts))
- {
- return false;
- }
-
- if (this.TotalComponentsCount != withPathInfoParts.Length && !this.IsWildCardPath)
- {
- return false;
- }
-
- int pathIx = 0;
- for (var i = 0; i < this.TotalComponentsCount; i++)
- {
- if (this.isWildcard[i])
- {
- if (i < this.TotalComponentsCount - 1)
- {
- // Continue to consume up until a match with the next literal
- while (pathIx < withPathInfoParts.Length
- && !string.Equals(withPathInfoParts[pathIx], this.literalsToMatch[i + 1], StringComparison.InvariantCultureIgnoreCase))
- {
- pathIx++;
- wildcardMatchCount++;
- }
-
- // Ensure there are still enough parts left to match the remainder
- if ((withPathInfoParts.Length - pathIx) < (this.TotalComponentsCount - i - 1))
- {
- return false;
- }
- }
- else
- {
- // A wildcard at the end matches the remainder of path
- wildcardMatchCount += withPathInfoParts.Length - pathIx;
- pathIx = withPathInfoParts.Length;
- }
- }
- else
- {
- var literalToMatch = this.literalsToMatch[i];
- if (literalToMatch == null)
- {
- // Matching an ordinary (non-wildcard) variable consumes a single part
- pathIx++;
- continue;
- }
-
- if (withPathInfoParts.Length <= pathIx
- || !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase))
- {
- return false;
- }
-
- pathIx++;
- }
- }
-
- return pathIx == withPathInfoParts.Length;
- }
-
- private bool ExplodeComponents(ref string[] withPathInfoParts)
- {
- var totalComponents = new List<string>();
- for (var i = 0; i < withPathInfoParts.Length; i++)
- {
- var component = withPathInfoParts[i];
- if (string.IsNullOrEmpty(component))
- {
- continue;
- }
-
- if (this.PathComponentsCount != this.TotalComponentsCount
- && this.componentsWithSeparators[i])
- {
- var subComponents = component.Split(ComponentSeperator);
- if (subComponents.Length < 2)
- {
- return false;
- }
-
- totalComponents.AddRange(subComponents);
- }
- else
- {
- totalComponents.Add(component);
- }
- }
-
- withPathInfoParts = totalComponents.ToArray();
- return true;
- }
-
- public object CreateRequest(string pathInfo, Dictionary<string, string> queryStringAndFormData, object fromInstance)
- {
- var requestComponents = pathInfo.Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries);
-
- ExplodeComponents(ref requestComponents);
-
- if (requestComponents.Length != this.TotalComponentsCount)
- {
- var isValidWildCardPath = this.IsWildCardPath
- && requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount;
-
- if (!isValidWildCardPath)
- throw new ArgumentException(
- string.Format(
- CultureInfo.InvariantCulture,
- "Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'",
- pathInfo,
- this.restPath));
- }
-
- var requestKeyValuesMap = new Dictionary<string, string>();
- var pathIx = 0;
- for (var i = 0; i < this.TotalComponentsCount; i++)
- {
- var variableName = this.variablesNames[i];
- if (variableName == null)
- {
- pathIx++;
- continue;
- }
-
- if (!this._propertyNamesMap.Contains(variableName))
- {
- if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
- {
- pathIx++;
- continue;
- }
-
- throw new ArgumentException("Could not find property "
- + variableName + " on " + RequestType.GetMethodName());
- }
-
- var value = requestComponents.Length > pathIx ? requestComponents[pathIx] : null; //wildcard has arg mismatch
- if (value != null && this.isWildcard[i])
- {
- if (i == this.TotalComponentsCount - 1)
- {
- // Wildcard at end of path definition consumes all the rest
- var sb = new StringBuilder();
- sb.Append(value);
- for (var j = pathIx + 1; j < requestComponents.Length; j++)
- {
- sb.Append(PathSeperatorChar + requestComponents[j]);
- }
-
- value = sb.ToString();
- }
- else
- {
- // Wildcard in middle of path definition consumes up until it
- // hits a match for the next element in the definition (which must be a literal)
- // It may consume 0 or more path parts
- var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
- if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
- {
- var sb = new StringBuilder(value);
- pathIx++;
- while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
- {
- sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
- }
-
- value = sb.ToString();
- }
- else
- {
- value = null;
- }
- }
- }
- else
- {
- // Variable consumes single path item
- pathIx++;
- }
-
- requestKeyValuesMap[variableName] = value;
- }
-
- if (queryStringAndFormData != null)
- {
- //Query String and form data can override variable path matches
- //path variables < query string < form data
- foreach (var name in queryStringAndFormData)
- {
- requestKeyValuesMap[name.Key] = name.Value;
- }
- }
-
- return this.typeDeserializer.PopulateFromMap(fromInstance, requestKeyValuesMap);
- }
-
- public class RestPathMap : SortedDictionary<string, List<RestPath>>
- {
- public RestPathMap() : base(StringComparer.OrdinalIgnoreCase)
- {
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
deleted file mode 100644
index 56e23d549..000000000
--- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
+++ /dev/null
@@ -1,114 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Reflection;
-using MediaBrowser.Common.Extensions;
-
-namespace Emby.Server.Implementations.Services
-{
- /// <summary>
- /// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls)
- /// </summary>
- public class StringMapTypeDeserializer
- {
- internal class PropertySerializerEntry
- {
- public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn, Type propertyType)
- {
- PropertySetFn = propertySetFn;
- PropertyParseStringFn = propertyParseStringFn;
- PropertyType = propertyType;
- }
-
- public Action<object, object> PropertySetFn { get; private set; }
- public Func<string, object> PropertyParseStringFn { get; private set; }
- public Type PropertyType { get; private set; }
- }
-
- private readonly Type type;
- private readonly Dictionary<string, PropertySerializerEntry> propertySetterMap
- = new Dictionary<string, PropertySerializerEntry>(StringComparer.OrdinalIgnoreCase);
-
- public Func<string, object> GetParseFn(Type propertyType)
- {
- if (propertyType == typeof(string))
- {
- return s => s;
- }
-
- return _GetParseFn(propertyType);
- }
-
- private readonly Func<Type, object> _CreateInstanceFn;
- private readonly Func<Type, Func<string, object>> _GetParseFn;
-
- public StringMapTypeDeserializer(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type type)
- {
- _CreateInstanceFn = createInstanceFn;
- _GetParseFn = getParseFn;
- this.type = type;
-
- foreach (var propertyInfo in RestPath.GetSerializableProperties(type))
- {
- var propertySetFn = TypeAccessor.GetSetPropertyMethod(propertyInfo);
- var propertyType = propertyInfo.PropertyType;
- var propertyParseStringFn = GetParseFn(propertyType);
- var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType);
-
- propertySetterMap[propertyInfo.Name] = propertySerializer;
- }
- }
-
- public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs)
- {
- PropertySerializerEntry propertySerializerEntry = null;
-
- if (instance == null)
- {
- instance = _CreateInstanceFn(type);
- }
-
- foreach (var pair in keyValuePairs)
- {
- string propertyName = pair.Key;
- string propertyTextValue = pair.Value;
-
- if (propertyTextValue == null
- || !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)
- || propertySerializerEntry.PropertySetFn == null)
- {
- continue;
- }
-
- if (propertySerializerEntry.PropertyType == typeof(bool))
- {
- //InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value
- propertyTextValue = StringExtensions.LeftPart(propertyTextValue, ',').ToString();
- }
-
- var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue);
- if (value == null)
- {
- continue;
- }
-
- propertySerializerEntry.PropertySetFn(instance, value);
- }
-
- return instance;
- }
- }
-
- internal static class TypeAccessor
- {
- public static Action<object, object> GetSetPropertyMethod(PropertyInfo propertyInfo)
- {
- if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0)
- {
- return null;
- }
-
- var setMethodInfo = propertyInfo.SetMethod;
- return (instance, value) => setMethodInfo.Invoke(instance, new[] { value });
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/SwaggerService.cs b/Emby.Server.Implementations/Services/SwaggerService.cs
deleted file mode 100644
index 5177251c3..000000000
--- a/Emby.Server.Implementations/Services/SwaggerService.cs
+++ /dev/null
@@ -1,251 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Emby.Server.Implementations.HttpServer;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Server.Implementations.Services
-{
- [Route("/swagger", "GET", Summary = "Gets the swagger specifications")]
- [Route("/swagger.json", "GET", Summary = "Gets the swagger specifications")]
- public class GetSwaggerSpec : IReturn<SwaggerSpec>
- {
- }
-
- public class SwaggerSpec
- {
- public string swagger { get; set; }
- public string[] schemes { get; set; }
- public SwaggerInfo info { get; set; }
- public string host { get; set; }
- public string basePath { get; set; }
- public SwaggerTag[] tags { get; set; }
- public IDictionary<string, Dictionary<string, SwaggerMethod>> paths { get; set; }
- public Dictionary<string, SwaggerDefinition> definitions { get; set; }
- public SwaggerComponents components { get; set; }
- }
-
- public class SwaggerComponents
- {
- public Dictionary<string, SwaggerSecurityScheme> securitySchemes { get; set; }
- }
-
- public class SwaggerSecurityScheme
- {
- public string name { get; set; }
- public string type { get; set; }
- public string @in { get; set; }
- }
-
- public class SwaggerInfo
- {
- public string description { get; set; }
- public string version { get; set; }
- public string title { get; set; }
- public string termsOfService { get; set; }
-
- public SwaggerConcactInfo contact { get; set; }
- }
-
- public class SwaggerConcactInfo
- {
- public string email { get; set; }
- public string name { get; set; }
- public string url { get; set; }
- }
-
- public class SwaggerTag
- {
- public string description { get; set; }
- public string name { get; set; }
- }
-
- public class SwaggerMethod
- {
- public string summary { get; set; }
- public string description { get; set; }
- public string[] tags { get; set; }
- public string operationId { get; set; }
- public string[] consumes { get; set; }
- public string[] produces { get; set; }
- public SwaggerParam[] parameters { get; set; }
- public Dictionary<string, SwaggerResponse> responses { get; set; }
- public Dictionary<string, string[]>[] security { get; set; }
- }
-
- public class SwaggerParam
- {
- public string @in { get; set; }
- public string name { get; set; }
- public string description { get; set; }
- public bool required { get; set; }
- public string type { get; set; }
- public string collectionFormat { get; set; }
- }
-
- public class SwaggerResponse
- {
- public string description { get; set; }
-
- // ex. "$ref":"#/definitions/Pet"
- public Dictionary<string, string> schema { get; set; }
- }
-
- public class SwaggerDefinition
- {
- public string type { get; set; }
- public Dictionary<string, SwaggerProperty> properties { get; set; }
- }
-
- public class SwaggerProperty
- {
- public string type { get; set; }
- public string format { get; set; }
- public string description { get; set; }
- public string[] @enum { get; set; }
- public string @default { get; set; }
- }
-
- public class SwaggerService : IService, IRequiresRequest
- {
- private readonly IHttpServer _httpServer;
- private SwaggerSpec _spec;
-
- public IRequest Request { get; set; }
-
- public SwaggerService(IHttpServer httpServer)
- {
- _httpServer = httpServer;
- }
-
- public object Get(GetSwaggerSpec request)
- {
- return _spec ?? (_spec = GetSpec());
- }
-
- private SwaggerSpec GetSpec()
- {
- string host = null;
- Uri uri;
- if (Uri.TryCreate(Request.RawUrl, UriKind.Absolute, out uri))
- {
- host = uri.Host;
- }
-
- var securitySchemes = new Dictionary<string, SwaggerSecurityScheme>();
-
- securitySchemes["api_key"] = new SwaggerSecurityScheme
- {
- name = "api_key",
- type = "apiKey",
- @in = "query"
- };
-
- var spec = new SwaggerSpec
- {
- schemes = new[] { "http" },
- tags = GetTags(),
- swagger = "2.0",
- info = new SwaggerInfo
- {
- title = "Jellyfin Server API",
- version = "1.0.0",
- description = "Explore the Jellyfin Server API",
- contact = new SwaggerConcactInfo
- {
- name = "Jellyfin Community",
- url = "https://jellyfin.readthedocs.io/en/latest/user-docs/getting-help/"
- }
- },
- paths = GetPaths(),
- definitions = GetDefinitions(),
- basePath = "/jellyfin",
- host = host,
-
- components = new SwaggerComponents
- {
- securitySchemes = securitySchemes
- }
- };
-
- return spec;
- }
-
-
- private SwaggerTag[] GetTags()
- {
- return Array.Empty<SwaggerTag>();
- }
-
- private Dictionary<string, SwaggerDefinition> GetDefinitions()
- {
- return new Dictionary<string, SwaggerDefinition>();
- }
-
- private IDictionary<string, Dictionary<string, SwaggerMethod>> GetPaths()
- {
- var paths = new SortedDictionary<string, Dictionary<string, SwaggerMethod>>();
-
- // REVIEW: this can be done better
- var all = ((HttpListenerHost)_httpServer).ServiceController.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList();
-
- foreach (var current in all)
- {
- foreach (var info in current.Value)
- {
- if (info.IsHidden)
- {
- continue;
- }
-
- if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase)
- || info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- paths[info.Path] = GetPathInfo(info);
- }
- }
-
- return paths;
- }
-
- private Dictionary<string, SwaggerMethod> GetPathInfo(RestPath info)
- {
- var result = new Dictionary<string, SwaggerMethod>();
-
- foreach (var verb in info.Verbs)
- {
- var responses = new Dictionary<string, SwaggerResponse>
- {
- { "200", new SwaggerResponse { description = "OK" } }
- };
-
- var apiKeySecurity = new Dictionary<string, string[]>
- {
- { "api_key", Array.Empty<string>() }
- };
-
- result[verb.ToLowerInvariant()] = new SwaggerMethod
- {
- summary = info.Summary,
- description = info.Description,
- produces = new[] { "application/json" },
- consumes = new[] { "application/json" },
- operationId = info.RequestType.Name,
- tags = Array.Empty<string>(),
-
- parameters = Array.Empty<SwaggerParam>(),
-
- responses = responses,
-
- security = new[] { apiKeySecurity }
- };
- }
-
- return result;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Services/UrlExtensions.cs b/Emby.Server.Implementations/Services/UrlExtensions.cs
deleted file mode 100644
index 483c63ade..000000000
--- a/Emby.Server.Implementations/Services/UrlExtensions.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using System;
-using MediaBrowser.Common.Extensions;
-
-namespace Emby.Server.Implementations.Services
-{
- /// <summary>
- /// Donated by Ivan Korneliuk from his post:
- /// http://korneliuk.blogspot.com/2012/08/servicestack-reusing-dtos.html
- ///
- /// Modified to only allow using routes matching the supplied HTTP Verb
- /// </summary>
- public static class UrlExtensions
- {
- public static string GetMethodName(this Type type)
- {
- var typeName = type.FullName != null // can be null, e.g. generic types
- ? StringExtensions.LeftPart(type.FullName, "[[", StringComparison.Ordinal).ToString() // Generic Fullname
- .Replace(type.Namespace + ".", string.Empty, StringComparison.Ordinal) // Trim Namespaces
- .Replace("+", ".", StringComparison.Ordinal) // Convert nested into normal type
- : type.Name;
-
- return type.IsGenericParameter ? "'" + typeName : typeName;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 2b09a93ef..607b322f2 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -5,6 +7,9 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@@ -13,7 +18,8 @@ using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Session;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
@@ -21,12 +27,13 @@ using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
namespace Emby.Server.Implementations.Session
{
@@ -35,25 +42,16 @@ namespace Emby.Server.Implementations.Session
/// </summary>
public class SessionManager : ISessionManager, IDisposable
{
- /// <summary>
- /// The user data repository.
- /// </summary>
private readonly IUserDataManager _userDataManager;
-
- /// <summary>
- /// The logger.
- /// </summary>
- private readonly ILogger _logger;
-
+ private readonly ILogger<SessionManager> _logger;
+ private readonly IEventManager _eventManager;
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IMusicManager _musicManager;
private readonly IDtoService _dtoService;
private readonly IImageProcessor _imageProcessor;
private readonly IMediaSourceManager _mediaSourceManager;
-
private readonly IServerApplicationHost _appHost;
-
private readonly IAuthenticationRepository _authRepo;
private readonly IDeviceManager _deviceManager;
@@ -70,6 +68,7 @@ namespace Emby.Server.Implementations.Session
public SessionManager(
ILogger<SessionManager> logger,
+ IEventManager eventManager,
IUserDataManager userDataManager,
ILibraryManager libraryManager,
IUserManager userManager,
@@ -82,6 +81,7 @@ namespace Emby.Server.Implementations.Session
IMediaSourceManager mediaSourceManager)
{
_logger = logger;
+ _eventManager = eventManager;
_userDataManager = userDataManager;
_libraryManager = libraryManager;
_userManager = userManager;
@@ -204,6 +204,8 @@ namespace Emby.Server.Implementations.Session
}
}
+ _eventManager.Publish(new SessionStartedEventArgs(info));
+
EventHelper.QueueEventIfNotNull(
SessionStarted,
this,
@@ -225,6 +227,8 @@ namespace Emby.Server.Implementations.Session
},
_logger);
+ _eventManager.Publish(new SessionEndedEventArgs(info));
+
info.Dispose();
}
@@ -281,11 +285,18 @@ namespace Emby.Server.Implementations.Session
if (user != null)
{
var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue;
- user.LastActivityDate = activityDate;
if ((activityDate - userLastActivityDate).TotalSeconds > 60)
{
- _userManager.UpdateUser(user);
+ try
+ {
+ user.LastActivityDate = activityDate;
+ _userManager.UpdateUser(user);
+ }
+ catch (DbUpdateConcurrencyException e)
+ {
+ _logger.LogDebug(e, "Error updating user's last activity date.");
+ }
}
}
@@ -432,7 +443,13 @@ namespace Emby.Server.Implementations.Session
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
/// <returns>SessionInfo.</returns>
- private SessionInfo GetSessionInfo(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user)
+ private SessionInfo GetSessionInfo(
+ string appName,
+ string appVersion,
+ string deviceId,
+ string deviceName,
+ string remoteEndPoint,
+ User user)
{
CheckDisposed();
@@ -445,14 +462,13 @@ namespace Emby.Server.Implementations.Session
CheckDisposed();
- var sessionInfo = _activeConnections.GetOrAdd(key, k =>
- {
- return CreateSession(k, appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
- });
+ var sessionInfo = _activeConnections.GetOrAdd(
+ key,
+ k => CreateSession(k, appName, appVersion, deviceId, deviceName, remoteEndPoint, user));
- sessionInfo.UserId = user == null ? Guid.Empty : user.Id;
- sessionInfo.UserName = user?.Name;
- sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary);
+ sessionInfo.UserId = user?.Id ?? Guid.Empty;
+ sessionInfo.UserName = user?.Username;
+ sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user);
sessionInfo.RemoteEndPoint = remoteEndPoint;
sessionInfo.Client = appName;
@@ -471,21 +487,29 @@ namespace Emby.Server.Implementations.Session
return sessionInfo;
}
- private SessionInfo CreateSession(string key, string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user)
+ private SessionInfo CreateSession(
+ string key,
+ string appName,
+ string appVersion,
+ string deviceId,
+ string deviceName,
+ string remoteEndPoint,
+ User user)
{
var sessionInfo = new SessionInfo(this, _logger)
{
Client = appName,
DeviceId = deviceId,
ApplicationVersion = appVersion,
- Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture)
+ Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
+ ServerId = _appHost.SystemId
};
- var username = user?.Name;
+ var username = user?.Username;
sessionInfo.UserId = user?.Id ?? Guid.Empty;
sessionInfo.UserName = username;
- sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary);
+ sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user);
sessionInfo.RemoteEndPoint = remoteEndPoint;
if (string.IsNullOrEmpty(deviceName))
@@ -533,10 +557,7 @@ namespace Emby.Server.Implementations.Session
private void StartIdleCheckTimer()
{
- if (_idleTimer == null)
- {
- _idleTimer = new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
- }
+ _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
private void StopIdleCheckTimer()
@@ -645,22 +666,26 @@ namespace Emby.Server.Implementations.Session
}
}
+ var eventArgs = new PlaybackStartEventArgs
+ {
+ Item = libraryItem,
+ Users = users,
+ MediaSourceId = info.MediaSourceId,
+ MediaInfo = info.Item,
+ DeviceName = session.DeviceName,
+ ClientName = session.Client,
+ DeviceId = session.DeviceId,
+ Session = session
+ };
+
+ await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
+
// Nothing to save here
// Fire events to inform plugins
EventHelper.QueueEventIfNotNull(
PlaybackStart,
this,
- new PlaybackProgressEventArgs
- {
- Item = libraryItem,
- Users = users,
- MediaSourceId = info.MediaSourceId,
- MediaInfo = info.Item,
- DeviceName = session.DeviceName,
- ClientName = session.Client,
- DeviceId = session.DeviceId,
- Session = session
- },
+ eventArgs,
_logger);
StartIdleCheckTimer();
@@ -728,23 +753,25 @@ namespace Emby.Server.Implementations.Session
}
}
- PlaybackProgress?.Invoke(
- this,
- new PlaybackProgressEventArgs
- {
- Item = libraryItem,
- Users = users,
- PlaybackPositionTicks = session.PlayState.PositionTicks,
- MediaSourceId = session.PlayState.MediaSourceId,
- MediaInfo = info.Item,
- DeviceName = session.DeviceName,
- ClientName = session.Client,
- DeviceId = session.DeviceId,
- IsPaused = info.IsPaused,
- PlaySessionId = info.PlaySessionId,
- IsAutomated = isAutomated,
- Session = session
- });
+ var eventArgs = new PlaybackProgressEventArgs
+ {
+ Item = libraryItem,
+ Users = users,
+ PlaybackPositionTicks = session.PlayState.PositionTicks,
+ MediaSourceId = session.PlayState.MediaSourceId,
+ MediaInfo = info.Item,
+ DeviceName = session.DeviceName,
+ ClientName = session.Client,
+ DeviceId = session.DeviceId,
+ IsPaused = info.IsPaused,
+ PlaySessionId = info.PlaySessionId,
+ IsAutomated = isAutomated,
+ Session = session
+ };
+
+ await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
+
+ PlaybackProgress?.Invoke(this, eventArgs);
if (!isAutomated)
{
@@ -784,7 +811,7 @@ namespace Emby.Server.Implementations.Session
{
var changed = false;
- if (user.Configuration.RememberAudioSelections)
+ if (user.RememberAudioSelections)
{
if (data.AudioStreamIndex != info.AudioStreamIndex)
{
@@ -801,7 +828,7 @@ namespace Emby.Server.Implementations.Session
}
}
- if (user.Configuration.RememberSubtitleSelections)
+ if (user.RememberSubtitleSelections)
{
if (data.SubtitleStreamIndex != info.SubtitleStreamIndex)
{
@@ -822,12 +849,12 @@ namespace Emby.Server.Implementations.Session
}
/// <summary>
- /// Used to report that playback has ended for an item
+ /// Used to report that playback has ended for an item.
/// </summary>
/// <param name="info">The info.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">info</exception>
- /// <exception cref="ArgumentOutOfRangeException">positionTicks</exception>
+ /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
+ /// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</exception>
public async Task OnPlaybackStopped(PlaybackStopInfo info)
{
CheckDisposed();
@@ -921,23 +948,23 @@ namespace Emby.Server.Implementations.Session
}
}
- EventHelper.QueueEventIfNotNull(
- PlaybackStopped,
- this,
- new PlaybackStopEventArgs
- {
- Item = libraryItem,
- Users = users,
- PlaybackPositionTicks = info.PositionTicks,
- PlayedToCompletion = playedToCompletion,
- MediaSourceId = info.MediaSourceId,
- MediaInfo = info.Item,
- DeviceName = session.DeviceName,
- ClientName = session.Client,
- DeviceId = session.DeviceId,
- Session = session
- },
- _logger);
+ var eventArgs = new PlaybackStopEventArgs
+ {
+ Item = libraryItem,
+ Users = users,
+ PlaybackPositionTicks = info.PositionTicks,
+ PlayedToCompletion = playedToCompletion,
+ MediaSourceId = info.MediaSourceId,
+ MediaInfo = info.Item,
+ DeviceName = session.DeviceName,
+ ClientName = session.Client,
+ DeviceId = session.DeviceId,
+ Session = session
+ };
+
+ await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
+
+ EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger);
}
private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
@@ -1010,7 +1037,7 @@ namespace Emby.Server.Implementations.Session
var generalCommand = new GeneralCommand
{
- Name = GeneralCommandType.DisplayMessage.ToString()
+ Name = GeneralCommandType.DisplayMessage
};
generalCommand.Arguments["Header"] = command.Header;
@@ -1037,10 +1064,10 @@ namespace Emby.Server.Implementations.Session
AssertCanControl(session, controllingSession);
}
- return SendMessageToSession(session, "GeneralCommand", command, cancellationToken);
+ return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken);
}
- private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken)
+ private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, CancellationToken cancellationToken)
{
var controllers = session.SessionControllers;
var messageId = Guid.NewGuid();
@@ -1051,7 +1078,7 @@ namespace Emby.Server.Implementations.Session
}
}
- private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, string name, T data, CancellationToken cancellationToken)
+ private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data, CancellationToken cancellationToken)
{
IEnumerable<Task> GetTasks()
{
@@ -1112,13 +1139,13 @@ namespace Emby.Server.Implementations.Session
if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
{
throw new ArgumentException(
- string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Name));
+ string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username));
}
}
if (user != null
&& command.ItemIds.Length == 1
- && user.Configuration.EnableNextEpisodeAutoPlay
+ && user.EnableNextEpisodeAutoPlay
&& _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode)
{
var series = episode.Series;
@@ -1151,7 +1178,7 @@ namespace Emby.Server.Implementations.Session
}
}
- await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
+ await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -1159,7 +1186,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
var session = GetSessionToRemoteControl(sessionId);
- await SendMessageToSession(session, "SyncPlayCommand", command, cancellationToken).ConfigureAwait(false);
+ await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -1167,7 +1194,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
var session = GetSessionToRemoteControl(sessionId);
- await SendMessageToSession(session, "SyncPlayGroupUpdate", command, cancellationToken).ConfigureAwait(false);
+ await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).ConfigureAwait(false);
}
private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
@@ -1189,7 +1216,7 @@ namespace Emby.Server.Implementations.Session
DtoOptions = new DtoOptions(false)
{
EnableImages = false,
- Fields = new ItemFields[]
+ Fields = new[]
{
ItemFields.SortName
}
@@ -1241,7 +1268,7 @@ namespace Emby.Server.Implementations.Session
{
var generalCommand = new GeneralCommand
{
- Name = GeneralCommandType.DisplayContent.ToString(),
+ Name = GeneralCommandType.DisplayContent,
Arguments =
{
["ItemId"] = command.ItemId,
@@ -1270,7 +1297,7 @@ namespace Emby.Server.Implementations.Session
}
}
- return SendMessageToSession(session, "Playstate", command, cancellationToken);
+ return SendMessageToSession(session, SessionMessageType.PlayState, command, cancellationToken);
}
private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
@@ -1295,7 +1322,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
- return SendMessageToSessions(Sessions, "RestartRequired", string.Empty, cancellationToken);
+ return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
}
/// <summary>
@@ -1307,7 +1334,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
- return SendMessageToSessions(Sessions, "ServerShuttingDown", string.Empty, cancellationToken);
+ return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken);
}
/// <summary>
@@ -1321,7 +1348,7 @@ namespace Emby.Server.Implementations.Session
_logger.LogDebug("Beginning SendServerRestartNotification");
- return SendMessageToSessions(Sessions, "ServerRestarting", string.Empty, cancellationToken);
+ return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken);
}
/// <summary>
@@ -1351,7 +1378,7 @@ namespace Emby.Server.Implementations.Session
list.Add(new SessionUserInfo
{
UserId = userId,
- UserName = user.Name
+ UserName = user.Username
});
session.AdditionalUsers = list.ToArray();
@@ -1402,6 +1429,24 @@ namespace Emby.Server.Implementations.Session
return AuthenticateNewSessionInternal(request, false);
}
+ public Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token)
+ {
+ var result = _authRepo.Get(new AuthenticationInfoQuery()
+ {
+ AccessToken = token,
+ DeviceId = _appHost.SystemId,
+ Limit = 1
+ });
+
+ if (result.TotalRecordCount == 0)
+ {
+ throw new SecurityException("Unknown quick connect token");
+ }
+
+ request.UserId = result.Items[0].UserId;
+ return AuthenticateNewSessionInternal(request, false);
+ }
+
private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)
{
CheckDisposed();
@@ -1439,6 +1484,14 @@ namespace Emby.Server.Implementations.Session
throw new SecurityException("User is not allowed access from this device.");
}
+ int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id));
+ int maxActiveSessions = user.MaxActiveSessions;
+ _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCount, maxActiveSessions);
+ if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions)
+ {
+ throw new SecurityException("User is at their maximum number of sessions.");
+ }
+
var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName);
var session = LogSessionActivity(
@@ -1511,7 +1564,7 @@ namespace Emby.Server.Implementations.Session
DeviceName = deviceName,
UserId = user.Id,
AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
- UserName = user.Name
+ UserName = user.Username
};
_logger.LogInformation("Creating new access token for user {0}", user.Id);
@@ -1708,15 +1761,15 @@ namespace Emby.Server.Implementations.Session
return info;
}
- private string GetImageCacheTag(BaseItem item, ImageType type)
+ private string GetImageCacheTag(User user)
{
try
{
- return _imageProcessor.GetImageCacheTag(item, type);
+ return _imageProcessor.GetImageCacheTag(user);
}
- catch (Exception ex)
+ catch (Exception e)
{
- _logger.LogError(ex, "Error getting image information for {Type}", type);
+ _logger.LogError(e, "Error getting image information for profile image");
return null;
}
}
@@ -1821,17 +1874,20 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
- public Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken)
+ public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
- var adminUserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToList();
+ var adminUserIds = _userManager.Users
+ .Where(i => i.HasPermission(PermissionKind.IsAdministrator))
+ .Select(i => i.Id)
+ .ToList();
return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
}
/// <inheritdoc />
- public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken)
+ public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, CancellationToken cancellationToken)
{
CheckDisposed();
@@ -1846,7 +1902,7 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
- public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken)
+ public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
@@ -1855,7 +1911,7 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
- public Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken)
+ public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index e7b4b0ec3..a5f847953 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -4,17 +4,18 @@ using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Events;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Session
{
/// <summary>
- /// Class SessionWebSocketListener
+ /// Class SessionWebSocketListener.
/// </summary>
public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable
{
@@ -34,17 +35,17 @@ namespace Emby.Server.Implementations.Session
public const float ForceKeepAliveFactor = 0.75f;
/// <summary>
- /// The _session manager
+ /// The _session manager.
/// </summary>
private readonly ISessionManager _sessionManager;
/// <summary>
- /// The _logger
+ /// The _logger.
/// </summary>
- private readonly ILogger _logger;
+ private readonly ILogger<SessionWebSocketListener> _logger;
private readonly ILoggerFactory _loggerFactory;
- private readonly IHttpServer _httpServer;
+ private readonly IWebSocketManager _webSocketManager;
/// <summary>
/// The KeepAlive cancellation token.
@@ -72,19 +73,19 @@ namespace Emby.Server.Implementations.Session
/// <param name="logger">The logger.</param>
/// <param name="sessionManager">The session manager.</param>
/// <param name="loggerFactory">The logger factory.</param>
- /// <param name="httpServer">The HTTP server.</param>
+ /// <param name="webSocketManager">The HTTP server.</param>
public SessionWebSocketListener(
ILogger<SessionWebSocketListener> logger,
ISessionManager sessionManager,
ILoggerFactory loggerFactory,
- IHttpServer httpServer)
+ IWebSocketManager webSocketManager)
{
_logger = logger;
_sessionManager = sessionManager;
_loggerFactory = loggerFactory;
- _httpServer = httpServer;
+ _webSocketManager = webSocketManager;
- httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
+ webSocketManager.WebSocketConnected += OnServerManagerWebSocketConnected;
}
private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
@@ -93,7 +94,7 @@ namespace Emby.Server.Implementations.Session
if (session != null)
{
EnsureController(session, e.Argument);
- await KeepAliveWebSocket(e.Argument);
+ await KeepAliveWebSocket(e.Argument).ConfigureAwait(false);
}
else
{
@@ -121,7 +122,7 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public void Dispose()
{
- _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
+ _webSocketManager.WebSocketConnected -= OnServerManagerWebSocketConnected;
StopKeepAlive();
}
@@ -167,6 +168,7 @@ namespace Emby.Server.Implementations.Session
_logger.LogWarning("Multiple attempts to keep alive single WebSocket {0}", webSocket);
return;
}
+
webSocket.Closed += OnWebSocketClosed;
webSocket.LastKeepAliveDate = DateTime.UtcNow;
@@ -176,7 +178,7 @@ namespace Emby.Server.Implementations.Session
// Notify WebSocket about timeout
try
{
- await SendForceKeepAlive(webSocket);
+ await SendForceKeepAlive(webSocket).ConfigureAwait(false);
}
catch (WebSocketException exception)
{
@@ -232,6 +234,7 @@ namespace Emby.Server.Implementations.Session
if (_keepAliveCancellationToken != null)
{
_keepAliveCancellationToken.Cancel();
+ _keepAliveCancellationToken.Dispose();
_keepAliveCancellationToken = null;
}
}
@@ -267,7 +270,7 @@ namespace Emby.Server.Implementations.Session
lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout).ToList();
}
- if (inactive.Any())
+ if (inactive.Count > 0)
{
_logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
}
@@ -276,7 +279,7 @@ namespace Emby.Server.Implementations.Session
{
try
{
- await SendForceKeepAlive(webSocket);
+ await SendForceKeepAlive(webSocket).ConfigureAwait(false);
}
catch (WebSocketException exception)
{
@@ -287,7 +290,7 @@ namespace Emby.Server.Implementations.Session
lock (_webSocketsLock)
{
- if (lost.Any())
+ if (lost.Count > 0)
{
_logger.LogInformation("Lost {0} WebSockets.", lost.Count);
foreach (var webSocket in lost)
@@ -297,7 +300,7 @@ namespace Emby.Server.Implementations.Session
}
}
- if (!_webSockets.Any())
+ if (_webSockets.Count == 0)
{
StopKeepAlive();
}
@@ -311,11 +314,13 @@ namespace Emby.Server.Implementations.Session
/// <returns>Task.</returns>
private Task SendForceKeepAlive(IWebSocketConnection webSocket)
{
- return webSocket.SendAsync(new WebSocketMessage<int>
- {
- MessageType = "ForceKeepAlive",
- Data = WebSocketLostTimeout
- }, CancellationToken.None);
+ return webSocket.SendAsync(
+ new WebSocketMessage<int>
+ {
+ MessageType = SessionMessageType.ForceKeepAlive,
+ Data = WebSocketLostTimeout
+ },
+ CancellationToken.None);
}
/// <summary>
@@ -329,12 +334,11 @@ namespace Emby.Server.Implementations.Session
{
while (!cancellationToken.IsCancellationRequested)
{
- await callback();
- Task task = Task.Delay(interval, cancellationToken);
+ await callback().ConfigureAwait(false);
try
{
- await task;
+ await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index a0274acd2..b986ffa1c 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -11,13 +11,14 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Session
{
public sealed class WebSocketController : ISessionController, IDisposable
{
- private readonly ILogger _logger;
+ private readonly ILogger<WebSocketController> _logger;
private readonly ISessionManager _sessionManager;
private readonly SessionInfo _session;
@@ -65,7 +66,7 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public Task SendMessage<T>(
- string name,
+ SessionMessageType name,
Guid messageId,
T data,
CancellationToken cancellationToken)
diff --git a/Emby.Server.Implementations/SocketSharp/HttpFile.cs b/Emby.Server.Implementations/SocketSharp/HttpFile.cs
deleted file mode 100644
index 120ac50d9..000000000
--- a/Emby.Server.Implementations/SocketSharp/HttpFile.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.IO;
-using MediaBrowser.Model.Services;
-
-namespace Emby.Server.Implementations.SocketSharp
-{
- public class HttpFile : IHttpFile
- {
- public string Name { get; set; }
-
- public string FileName { get; set; }
-
- public long ContentLength { get; set; }
-
- public string ContentType { get; set; }
-
- public Stream InputStream { get; set; }
- }
-}
diff --git a/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs b/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs
deleted file mode 100644
index 7479d8104..000000000
--- a/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs
+++ /dev/null
@@ -1,198 +0,0 @@
-using System;
-using System.IO;
-
-public sealed class HttpPostedFile : IDisposable
-{
- private string _name;
- private string _contentType;
- private Stream _stream;
- private bool _disposed = false;
-
- internal HttpPostedFile(string name, string content_type, Stream base_stream, long offset, long length)
- {
- _name = name;
- _contentType = content_type;
- _stream = new ReadSubStream(base_stream, offset, length);
- }
-
- public string ContentType => _contentType;
-
- public int ContentLength => (int)_stream.Length;
-
- public string FileName => _name;
-
- public Stream InputStream => _stream;
-
- /// <summary>
- /// Releases the unmanaged resources and disposes of the managed resources used.
- /// </summary>
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- _stream.Dispose();
- _stream = null;
-
- _name = null;
- _contentType = null;
-
- _disposed = true;
- }
-
- private class ReadSubStream : Stream
- {
- private Stream _stream;
- private long _offset;
- private long _end;
- private long _position;
-
- public ReadSubStream(Stream s, long offset, long length)
- {
- _stream = s;
- _offset = offset;
- _end = offset + length;
- _position = offset;
- }
-
- public override bool CanRead => true;
-
- public override bool CanSeek => true;
-
- public override bool CanWrite => false;
-
- public override long Length => _end - _offset;
-
- public override long Position
- {
- get => _position - _offset;
- set
- {
- if (value > Length)
- {
- throw new ArgumentOutOfRangeException(nameof(value));
- }
-
- _position = Seek(value, SeekOrigin.Begin);
- }
- }
-
- public override void Flush()
- {
- }
-
- public override int Read(byte[] buffer, int dest_offset, int count)
- {
- if (buffer == null)
- {
- throw new ArgumentNullException(nameof(buffer));
- }
-
- if (dest_offset < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(dest_offset), "< 0");
- }
-
- if (count < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(count), "< 0");
- }
-
- int len = buffer.Length;
- if (dest_offset > len)
- {
- throw new ArgumentException("destination offset is beyond array size", nameof(dest_offset));
- }
-
- // reordered to avoid possible integer overflow
- if (dest_offset > len - count)
- {
- throw new ArgumentException("Reading would overrun buffer", nameof(count));
- }
-
- if (count > _end - _position)
- {
- count = (int)(_end - _position);
- }
-
- if (count <= 0)
- {
- return 0;
- }
-
- _stream.Position = _position;
- int result = _stream.Read(buffer, dest_offset, count);
- if (result > 0)
- {
- _position += result;
- }
- else
- {
- _position = _end;
- }
-
- return result;
- }
-
- public override int ReadByte()
- {
- if (_position >= _end)
- {
- return -1;
- }
-
- _stream.Position = _position;
- int result = _stream.ReadByte();
- if (result < 0)
- {
- _position = _end;
- }
- else
- {
- _position++;
- }
-
- return result;
- }
-
- public override long Seek(long d, SeekOrigin origin)
- {
- long real;
- switch (origin)
- {
- case SeekOrigin.Begin:
- real = _offset + d;
- break;
- case SeekOrigin.End:
- real = _end + d;
- break;
- case SeekOrigin.Current:
- real = _position + d;
- break;
- default:
- throw new ArgumentException("Unknown SeekOrigin value", nameof(origin));
- }
-
- long virt = real - _offset;
- if (virt < 0 || virt > Length)
- {
- throw new ArgumentException("Invalid position", nameof(d));
- }
-
- _position = _stream.Seek(real, SeekOrigin.Begin);
- return _position;
- }
-
- public override void SetLength(long value)
- {
- throw new NotSupportedException();
- }
-
- public override void Write(byte[] buffer, int offset, int count)
- {
- throw new NotSupportedException();
- }
- }
-}
diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
deleted file mode 100644
index ee5131c1f..000000000
--- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
+++ /dev/null
@@ -1,246 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Net.Mime;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Primitives;
-using Microsoft.Net.Http.Headers;
-using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest;
-
-namespace Emby.Server.Implementations.SocketSharp
-{
- public class WebSocketSharpRequest : IHttpRequest
- {
- private const string FormUrlEncoded = "application/x-www-form-urlencoded";
- private const string MultiPartFormData = "multipart/form-data";
- private const string Soap11 = "text/xml; charset=utf-8";
-
- private string _remoteIp;
- private Dictionary<string, object> _items;
- private string _responseContentType;
-
- public WebSocketSharpRequest(HttpRequest httpRequest, HttpResponse httpResponse, string operationName, ILogger logger)
- {
- this.OperationName = operationName;
- this.Request = httpRequest;
- this.Response = httpResponse;
- }
-
- public string Accept => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Accept]) ? null : Request.Headers[HeaderNames.Accept].ToString();
-
- public string Authorization => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Authorization]) ? null : Request.Headers[HeaderNames.Authorization].ToString();
-
- public HttpRequest Request { get; }
-
- public HttpResponse Response { get; }
-
- public string OperationName { get; set; }
-
- public string RawUrl => Request.GetEncodedPathAndQuery();
-
- public string AbsoluteUri => Request.GetDisplayUrl().TrimEnd('/');
-
- public string RemoteIp
- {
- get
- {
- if (_remoteIp != null)
- {
- return _remoteIp;
- }
-
- IPAddress ip;
-
- // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip
- // (if the server is behind a reverse proxy for example)
- if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XForwardedFor), out ip))
- {
- if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip))
- {
- ip = Request.HttpContext.Connection.RemoteIpAddress;
-
- // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests)
- ip ??= IPAddress.Loopback;
- }
- }
-
- return _remoteIp = NormalizeIp(ip).ToString();
- }
- }
-
- public string[] AcceptTypes => Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
-
- public Dictionary<string, object> Items => _items ?? (_items = new Dictionary<string, object>());
-
- public string ResponseContentType
- {
- get =>
- _responseContentType
- ?? (_responseContentType = GetResponseContentType(Request));
- set => _responseContentType = value;
- }
-
- public string PathInfo => Request.Path.Value;
-
- public string UserAgent => Request.Headers[HeaderNames.UserAgent];
-
- public IHeaderDictionary Headers => Request.Headers;
-
- public IQueryCollection QueryString => Request.Query;
-
- public bool IsLocal =>
- (Request.HttpContext.Connection.LocalIpAddress == null
- && Request.HttpContext.Connection.RemoteIpAddress == null)
- || Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress);
-
- public string HttpMethod => Request.Method;
-
- public string Verb => HttpMethod;
-
- public string ContentType => Request.ContentType;
-
- public Uri UrlReferrer => Request.GetTypedHeaders().Referer;
-
- public Stream InputStream => Request.Body;
-
- public long ContentLength => Request.ContentLength ?? 0;
-
- private string GetHeader(string name) => Request.Headers[name].ToString();
-
- private static IPAddress NormalizeIp(IPAddress ip)
- {
- if (ip.IsIPv4MappedToIPv6)
- {
- return ip.MapToIPv4();
- }
-
- return ip;
- }
-
- public static string GetResponseContentType(HttpRequest httpReq)
- {
- var specifiedContentType = GetQueryStringContentType(httpReq);
- if (!string.IsNullOrEmpty(specifiedContentType))
- {
- return specifiedContentType;
- }
-
- const string ServerDefaultContentType = MediaTypeNames.Application.Json;
-
- var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept);
- string defaultContentType = null;
- if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData))
- {
- defaultContentType = ServerDefaultContentType;
- }
-
- var acceptsAnything = false;
- var hasDefaultContentType = defaultContentType != null;
- if (acceptContentTypes != null)
- {
- foreach (ReadOnlySpan<char> acceptsType in acceptContentTypes)
- {
- ReadOnlySpan<char> contentType = acceptsType;
- var index = contentType.IndexOf(';');
- if (index != -1)
- {
- contentType = contentType.Slice(0, index);
- }
-
- contentType = contentType.Trim();
- acceptsAnything = contentType.Equals("*/*", StringComparison.OrdinalIgnoreCase);
-
- if (acceptsAnything)
- {
- break;
- }
- }
-
- if (acceptsAnything)
- {
- if (hasDefaultContentType)
- {
- return defaultContentType;
- }
- else
- {
- return ServerDefaultContentType;
- }
- }
- }
-
- if (acceptContentTypes == null && httpReq.ContentType == Soap11)
- {
- return Soap11;
- }
-
- // We could also send a '406 Not Acceptable', but this is allowed also
- return ServerDefaultContentType;
- }
-
- public static bool HasAnyOfContentTypes(HttpRequest request, params string[] contentTypes)
- {
- if (contentTypes == null || request.ContentType == null)
- {
- return false;
- }
-
- foreach (var contentType in contentTypes)
- {
- if (IsContentType(request, contentType))
- {
- return true;
- }
- }
-
- return false;
- }
-
- public static bool IsContentType(HttpRequest request, string contentType)
- {
- return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase);
- }
-
- private static string GetQueryStringContentType(HttpRequest httpReq)
- {
- ReadOnlySpan<char> format = httpReq.Query["format"].ToString();
- if (format == null)
- {
- const int FormatMaxLength = 4;
- ReadOnlySpan<char> pi = httpReq.Path.ToString();
- if (pi == null || pi.Length <= FormatMaxLength)
- {
- return null;
- }
-
- if (pi[0] == '/')
- {
- pi = pi.Slice(1);
- }
-
- format = pi.LeftPart('/');
- if (format.Length > FormatMaxLength)
- {
- return null;
- }
- }
-
- format = format.LeftPart('.');
- if (format.Contains("json", StringComparison.OrdinalIgnoreCase))
- {
- return "application/json";
- }
- else if (format.Contains("xml", StringComparison.OrdinalIgnoreCase))
- {
- return "application/xml";
- }
-
- return null;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
index 16507466f..1f68a9c81 100644
--- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
@@ -32,7 +34,7 @@ namespace Emby.Server.Implementations.Sorting
if (val != 0)
{
- //return val;
+ // return val;
}
}
@@ -152,8 +154,8 @@ namespace Emby.Server.Implementations.Sorting
private static int CompareEpisodes(Episode x, Episode y)
{
- var xValue = (x.ParentIndexNumber ?? -1) * 1000 + (x.IndexNumber ?? -1);
- var yValue = (y.ParentIndexNumber ?? -1) * 1000 + (y.IndexNumber ?? -1);
+ var xValue = ((x.ParentIndexNumber ?? -1) * 1000) + (x.IndexNumber ?? -1);
+ var yValue = ((y.ParentIndexNumber ?? -1) * 1000) + (y.IndexNumber ?? -1);
return xValue.CompareTo(yValue);
}
diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
index 0804b01fc..7657cc74e 100644
--- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
@@ -8,7 +8,7 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
/// <summary>
- /// Class AlbumArtistComparer
+ /// Class AlbumArtistComparer.
/// </summary>
public class AlbumArtistComparer : IBaseItemComparer
{
diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
index 3831a0d2d..7dfdd9ecf 100644
--- a/Emby.Server.Implementations/Sorting/AlbumComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
@@ -7,7 +7,7 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
/// <summary>
- /// Class AlbumComparer
+ /// Class AlbumComparer.
/// </summary>
public class AlbumComparer : IBaseItemComparer
{
diff --git a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
index 87d3ae2d6..980954ba0 100644
--- a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
@@ -8,6 +10,12 @@ namespace Emby.Server.Implementations.Sorting
public class CommunityRatingComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.CommunityRating;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -16,18 +24,16 @@ namespace Emby.Server.Implementations.Sorting
public int Compare(BaseItem x, BaseItem y)
{
if (x == null)
+ {
throw new ArgumentNullException(nameof(x));
+ }
if (y == null)
+ {
throw new ArgumentNullException(nameof(y));
+ }
return (x.CommunityRating ?? 0).CompareTo(y.CommunityRating ?? 0);
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.CommunityRating;
}
}
diff --git a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
index adb78dec5..fa136c36d 100644
--- a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
@@ -5,7 +5,7 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
/// <summary>
- /// Class CriticRatingComparer
+ /// Class CriticRatingComparer.
/// </summary>
public class CriticRatingComparer : IBaseItemComparer
{
diff --git a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
index 8501bd9ee..cbca300d2 100644
--- a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
@@ -6,7 +6,7 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
/// <summary>
- /// Class DateCreatedComparer
+ /// Class DateCreatedComparer.
/// </summary>
public class DateCreatedComparer : IBaseItemComparer
{
@@ -19,10 +19,14 @@ namespace Emby.Server.Implementations.Sorting
public int Compare(BaseItem x, BaseItem y)
{
if (x == null)
+ {
throw new ArgumentNullException(nameof(x));
+ }
if (y == null)
+ {
throw new ArgumentNullException(nameof(y));
+ }
return DateTime.Compare(x.DateCreated, y.DateCreated);
}
diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
index 623675157..03ff19d21 100644
--- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
@@ -1,4 +1,7 @@
+#pragma warning disable CS1591
+
using System;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -27,6 +30,12 @@ namespace Emby.Server.Implementations.Sorting
public IUserDataManager UserDataRepository { get; set; }
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.DateLastContentAdded;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -44,9 +53,7 @@ namespace Emby.Server.Implementations.Sorting
/// <returns>DateTime.</returns>
private static DateTime GetDate(BaseItem x)
{
- var folder = x as Folder;
-
- if (folder != null)
+ if (x is Folder folder)
{
if (folder.DateLastMediaAdded.HasValue)
{
@@ -56,11 +63,5 @@ namespace Emby.Server.Implementations.Sorting
return DateTime.MinValue;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.DateLastContentAdded;
}
}
diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
index 73f59f8cd..16bd2aff8 100644
--- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -7,7 +8,7 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
/// <summary>
- /// Class DatePlayedComparer
+ /// Class DatePlayedComparer.
/// </summary>
public class DatePlayedComparer : IUserBaseItemComparer
{
diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
index 66de05a6a..0c4e82d01 100644
--- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -14,6 +17,24 @@ namespace Emby.Server.Implementations.Sorting
public User User { get; set; }
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.IsFavoriteOrLiked;
+
+ /// <summary>
+ /// Gets or sets the user data repository.
+ /// </summary>
+ /// <value>The user data repository.</value>
+ public IUserDataManager UserDataRepository { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user manager.
+ /// </summary>
+ /// <value>The user manager.</value>
+ public IUserManager UserManager { get; set; }
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -33,23 +54,5 @@ namespace Emby.Server.Implementations.Sorting
{
return x.IsFavoriteOrLiked(User) ? 0 : 1;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.IsFavoriteOrLiked;
-
- /// <summary>
- /// Gets or sets the user data repository.
- /// </summary>
- /// <value>The user data repository.</value>
- public IUserDataManager UserDataRepository { get; set; }
-
- /// <summary>
- /// Gets or sets the user manager.
- /// </summary>
- /// <value>The user manager.</value>
- public IUserManager UserManager { get; set; }
}
}
diff --git a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
index dfaa144cd..a35192eff 100644
--- a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -7,6 +9,12 @@ namespace Emby.Server.Implementations.Sorting
public class IsFolderComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.IsFolder;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -26,11 +34,5 @@ namespace Emby.Server.Implementations.Sorting
{
return x.IsFolder ? 0 : 1;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.IsFolder;
}
}
diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
index da3f3dd25..d95948406 100644
--- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -14,6 +17,24 @@ namespace Emby.Server.Implementations.Sorting
public User User { get; set; }
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.IsUnplayed;
+
+ /// <summary>
+ /// Gets or sets the user data repository.
+ /// </summary>
+ /// <value>The user data repository.</value>
+ public IUserDataManager UserDataRepository { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user manager.
+ /// </summary>
+ /// <value>The user manager.</value>
+ public IUserManager UserManager { get; set; }
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -33,23 +54,5 @@ namespace Emby.Server.Implementations.Sorting
{
return x.IsPlayed(User) ? 0 : 1;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.IsUnplayed;
-
- /// <summary>
- /// Gets or sets the user data repository.
- /// </summary>
- /// <value>The user data repository.</value>
- public IUserDataManager UserDataRepository { get; set; }
-
- /// <summary>
- /// Gets or sets the user manager.
- /// </summary>
- /// <value>The user manager.</value>
- public IUserManager UserManager { get; set; }
}
}
diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
index d99d0eff2..1632c5a7a 100644
--- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
@@ -1,3 +1,6 @@
+#pragma warning disable CS1591
+
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -14,6 +17,24 @@ namespace Emby.Server.Implementations.Sorting
public User User { get; set; }
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.IsUnplayed;
+
+ /// <summary>
+ /// Gets or sets the user data repository.
+ /// </summary>
+ /// <value>The user data repository.</value>
+ public IUserDataManager UserDataRepository { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user manager.
+ /// </summary>
+ /// <value>The user manager.</value>
+ public IUserManager UserManager { get; set; }
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -33,23 +54,5 @@ namespace Emby.Server.Implementations.Sorting
{
return x.IsUnplayed(User) ? 0 : 1;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.IsUnplayed;
-
- /// <summary>
- /// Gets or sets the user data repository.
- /// </summary>
- /// <value>The user data repository.</value>
- public IUserDataManager UserDataRepository { get; set; }
-
- /// <summary>
- /// Gets or sets the user manager.
- /// </summary>
- /// <value>The user manager.</value>
- public IUserManager UserManager { get; set; }
}
}
diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs
index 4eb1549f5..da020d8d8 100644
--- a/Emby.Server.Implementations/Sorting/NameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/NameComparer.cs
@@ -6,7 +6,7 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
/// <summary>
- /// Class NameComparer
+ /// Class NameComparer.
/// </summary>
public class NameComparer : IBaseItemComparer
{
diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
index 7afbd9ff7..76bb798b5 100644
--- a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
@@ -16,6 +18,12 @@ namespace Emby.Server.Implementations.Sorting
}
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.OfficialRating;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -38,11 +46,5 @@ namespace Emby.Server.Implementations.Sorting
return levelX.CompareTo(levelY);
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.OfficialRating;
}
}
diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
index eb74ce1bd..5c2830322 100644
--- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
@@ -1,3 +1,4 @@
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -6,7 +7,7 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
/// <summary>
- /// Class PlayCountComparer
+ /// Class PlayCountComparer.
/// </summary>
public class PlayCountComparer : IUserBaseItemComparer
{
diff --git a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
index 0c944a7a0..92ac04dc6 100644
--- a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
@@ -6,7 +6,7 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
/// <summary>
- /// Class PremiereDateComparer
+ /// Class PremiereDateComparer.
/// </summary>
public class PremiereDateComparer : IBaseItemComparer
{
@@ -44,6 +44,7 @@ namespace Emby.Server.Implementations.Sorting
// Don't blow up if the item has a bad ProductionYear, just return MinValue
}
}
+
return DateTime.MinValue;
}
diff --git a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
index 472a07eb3..e2857df0b 100644
--- a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
@@ -5,7 +5,7 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
/// <summary>
- /// Class ProductionYearComparer
+ /// Class ProductionYearComparer.
/// </summary>
public class ProductionYearComparer : IBaseItemComparer
{
diff --git a/Emby.Server.Implementations/Sorting/RandomComparer.cs b/Emby.Server.Implementations/Sorting/RandomComparer.cs
index bde8b4534..7739d0418 100644
--- a/Emby.Server.Implementations/Sorting/RandomComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RandomComparer.cs
@@ -6,7 +6,7 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
/// <summary>
- /// Class RandomComparer
+ /// Class RandomComparer.
/// </summary>
public class RandomComparer : IBaseItemComparer
{
diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
index 1d2bdde26..dde44333d 100644
--- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
@@ -6,7 +6,7 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
/// <summary>
- /// Class RuntimeComparer
+ /// Class RuntimeComparer.
/// </summary>
public class RuntimeComparer : IBaseItemComparer
{
@@ -19,10 +19,14 @@ namespace Emby.Server.Implementations.Sorting
public int Compare(BaseItem x, BaseItem y)
{
if (x == null)
+ {
throw new ArgumentNullException(nameof(x));
+ }
if (y == null)
+ {
throw new ArgumentNullException(nameof(y));
+ }
return (x.RunTimeTicks ?? 0).CompareTo(y.RunTimeTicks ?? 0);
}
diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
index 504b6d283..b9205ee07 100644
--- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
@@ -8,6 +10,12 @@ namespace Emby.Server.Implementations.Sorting
public class SeriesSortNameComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.SeriesSortName;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -18,12 +26,6 @@ namespace Emby.Server.Implementations.Sorting
return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase);
}
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.SeriesSortName;
-
private static string GetValue(BaseItem item)
{
var hasSeries = item as IHasSeries;
diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
index cc0571c78..f745e193b 100644
--- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
@@ -6,7 +6,7 @@ using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
/// <summary>
- /// Class SortNameComparer
+ /// Class SortNameComparer.
/// </summary>
public class SortNameComparer : IBaseItemComparer
{
@@ -19,10 +19,14 @@ namespace Emby.Server.Implementations.Sorting
public int Compare(BaseItem x, BaseItem y)
{
if (x == null)
+ {
throw new ArgumentNullException(nameof(x));
+ }
if (y == null)
+ {
throw new ArgumentNullException(nameof(y));
+ }
return string.Compare(x.SortName, y.SortName, StringComparison.CurrentCultureIgnoreCase);
}
diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
index aa040fa15..558a3d351 100644
--- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.LiveTv;
@@ -9,6 +11,12 @@ namespace Emby.Server.Implementations.Sorting
public class StartDateComparer : IBaseItemComparer
{
/// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name => ItemSortBy.StartDate;
+
+ /// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
@@ -26,19 +34,12 @@ namespace Emby.Server.Implementations.Sorting
/// <returns>DateTime.</returns>
private static DateTime GetDate(BaseItem x)
{
- var hasStartDate = x as LiveTvProgram;
-
- if (hasStartDate != null)
+ if (x is LiveTvProgram hasStartDate)
{
return hasStartDate.StartDate;
}
+
return DateTime.MinValue;
}
-
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
- public string Name => ItemSortBy.StartDate;
}
}
diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs
index c9ac765c1..5766dc542 100644
--- a/Emby.Server.Implementations/Sorting/StudioComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Linq;
using MediaBrowser.Controller.Entities;
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
index d430d4d16..538479512 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -27,14 +28,17 @@ namespace Emby.Server.Implementations.SyncPlay
/// All sessions will receive the message.
/// </summary>
AllGroup = 0,
+
/// <summary>
/// Only the specified session will receive the message.
/// </summary>
CurrentSession = 1,
+
/// <summary>
/// All sessions, except the current one, will receive the message.
/// </summary>
AllExceptCurrentSession = 2,
+
/// <summary>
/// Only sessions that are not buffering will receive the message.
/// </summary>
@@ -56,6 +60,19 @@ namespace Emby.Server.Implementations.SyncPlay
/// </summary>
private readonly GroupInfo _group = new GroupInfo();
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SyncPlayController" /> class.
+ /// </summary>
+ /// <param name="sessionManager">The session manager.</param>
+ /// <param name="syncPlayManager">The SyncPlay manager.</param>
+ public SyncPlayController(
+ ISessionManager sessionManager,
+ ISyncPlayManager syncPlayManager)
+ {
+ _sessionManager = sessionManager;
+ _syncPlayManager = syncPlayManager;
+ }
+
/// <inheritdoc />
public Guid GetGroupId() => _group.GroupId;
@@ -65,14 +82,6 @@ namespace Emby.Server.Implementations.SyncPlay
/// <inheritdoc />
public bool IsGroupEmpty() => _group.IsEmpty();
- public SyncPlayController(
- ISessionManager sessionManager,
- ISyncPlayManager syncPlayManager)
- {
- _sessionManager = sessionManager;
- _syncPlayManager = syncPlayManager;
- }
-
/// <summary>
/// Converts DateTime to UTC string.
/// </summary>
@@ -80,7 +89,7 @@ namespace Emby.Server.Implementations.SyncPlay
/// <value>The UTC string.</value>
private string DateToUTCString(DateTime date)
{
- return date.ToUniversalTime().ToString("o");
+ return date.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture);
}
/// <summary>
@@ -89,28 +98,23 @@ namespace Emby.Server.Implementations.SyncPlay
/// <param name="from">The current session.</param>
/// <param name="type">The filtering type.</param>
/// <value>The array of sessions matching the filter.</value>
- private SessionInfo[] FilterSessions(SessionInfo from, BroadcastType type)
+ private IEnumerable<SessionInfo> FilterSessions(SessionInfo from, BroadcastType type)
{
switch (type)
{
case BroadcastType.CurrentSession:
return new SessionInfo[] { from };
case BroadcastType.AllGroup:
- return _group.Participants.Values.Select(
- session => session.Session
- ).ToArray();
+ return _group.Participants.Values
+ .Select(session => session.Session);
case BroadcastType.AllExceptCurrentSession:
- return _group.Participants.Values.Select(
- session => session.Session
- ).Where(
- session => !session.Id.Equals(from.Id)
- ).ToArray();
+ return _group.Participants.Values
+ .Select(session => session.Session)
+ .Where(session => !session.Id.Equals(from.Id, StringComparison.Ordinal));
case BroadcastType.AllReady:
- return _group.Participants.Values.Where(
- session => !session.IsBuffering
- ).Select(
- session => session.Session
- ).ToArray();
+ return _group.Participants.Values
+ .Where(session => !session.IsBuffering)
+ .Select(session => session.Session);
default:
return Array.Empty<SessionInfo>();
}
@@ -128,10 +132,9 @@ namespace Emby.Server.Implementations.SyncPlay
{
IEnumerable<Task> GetTasks()
{
- SessionInfo[] sessions = FilterSessions(from, type);
- foreach (var session in sessions)
+ foreach (var session in FilterSessions(from, type))
{
- yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), message, cancellationToken);
+ yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id, message, cancellationToken);
}
}
@@ -150,10 +153,9 @@ namespace Emby.Server.Implementations.SyncPlay
{
IEnumerable<Task> GetTasks()
{
- SessionInfo[] sessions = FilterSessions(from, type);
- foreach (var session in sessions)
+ foreach (var session in FilterSessions(from, type))
{
- yield return _sessionManager.SendSyncPlayCommand(session.Id.ToString(), message, cancellationToken);
+ yield return _sessionManager.SendSyncPlayCommand(session.Id, message, cancellationToken);
}
}
@@ -194,26 +196,24 @@ namespace Emby.Server.Implementations.SyncPlay
}
/// <inheritdoc />
- public void InitGroup(SessionInfo session, CancellationToken cancellationToken)
+ public void CreateGroup(SessionInfo session, CancellationToken cancellationToken)
{
_group.AddSession(session);
_syncPlayManager.AddSessionToGroup(session, this);
_group.PlayingItem = session.FullNowPlayingItem;
- _group.IsPaused = true;
+ _group.IsPaused = session.PlayState.IsPaused;
_group.PositionTicks = session.PlayState.PositionTicks ?? 0;
_group.LastActivity = DateTime.UtcNow;
var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow));
SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
- var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause);
- SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken);
}
/// <inheritdoc />
public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
{
- if (session.NowPlayingItem?.Id == _group.PlayingItem.Id && request.PlayingItemId == _group.PlayingItem.Id)
+ if (session.NowPlayingItem?.Id == _group.PlayingItem.Id)
{
_group.AddSession(session);
_syncPlayManager.AddSessionToGroup(session, this);
@@ -224,7 +224,7 @@ namespace Emby.Server.Implementations.SyncPlay
var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
- // Client join and play, syncing will happen client side
+ // Syncing will happen client-side
if (!_group.IsPaused)
{
var playCommand = NewSyncPlayCommand(SendCommandType.Play);
@@ -238,9 +238,11 @@ namespace Emby.Server.Implementations.SyncPlay
}
else
{
- var playRequest = new PlayRequest();
- playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id };
- playRequest.StartPositionTicks = _group.PositionTicks;
+ var playRequest = new PlayRequest
+ {
+ ItemIds = new Guid[] { _group.PlayingItem.Id },
+ StartPositionTicks = _group.PositionTicks
+ };
var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken);
}
@@ -262,10 +264,9 @@ namespace Emby.Server.Implementations.SyncPlay
/// <inheritdoc />
public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
{
- // The server's job is to mantain a consistent state to which clients refer to,
- // as also to notify clients of state changes.
- // The actual syncing of media playback happens client side.
- // Clients are aware of the server's time and use it to sync.
+ // The server's job is to maintain a consistent state for clients to reference
+ // and notify clients of state changes. The actual syncing of media playback
+ // happens client side. Clients are aware of the server's time and use it to sync.
switch (request.Type)
{
case PlaybackRequestType.Play:
@@ -277,13 +278,13 @@ namespace Emby.Server.Implementations.SyncPlay
case PlaybackRequestType.Seek:
HandleSeekRequest(session, request, cancellationToken);
break;
- case PlaybackRequestType.Buffering:
+ case PlaybackRequestType.Buffer:
HandleBufferingRequest(session, request, cancellationToken);
break;
- case PlaybackRequestType.BufferingDone:
+ case PlaybackRequestType.Ready:
HandleBufferingDoneRequest(session, request, cancellationToken);
break;
- case PlaybackRequestType.UpdatePing:
+ case PlaybackRequestType.Ping:
HandlePingUpdateRequest(session, request);
break;
}
@@ -300,8 +301,7 @@ namespace Emby.Server.Implementations.SyncPlay
if (_group.IsPaused)
{
// Pick a suitable time that accounts for latency
- var delay = _group.GetHighestPing() * 2;
- delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
+ var delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing);
// Unpause group and set starting point in future
// Clients will start playback at LastActivity (datetime) from PositionTicks (playback position)
@@ -309,8 +309,7 @@ namespace Emby.Server.Implementations.SyncPlay
// Playback synchronization will mainly happen client side
_group.IsPaused = false;
_group.LastActivity = DateTime.UtcNow.AddMilliseconds(
- delay
- );
+ delay);
var command = NewSyncPlayCommand(SendCommandType.Play);
SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
@@ -338,8 +337,9 @@ namespace Emby.Server.Implementations.SyncPlay
var currentTime = DateTime.UtcNow;
var elapsedTime = currentTime - _group.LastActivity;
_group.LastActivity = currentTime;
+
// Seek only if playback actually started
- // (a pause request may be issued during the delay added to account for latency)
+ // Pause request may be issued during the delay added to account for latency
_group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
var command = NewSyncPlayCommand(SendCommandType.Pause);
@@ -444,20 +444,17 @@ namespace Emby.Server.Implementations.SyncPlay
{
// Client that was buffering is recovering, notifying others to resume
_group.LastActivity = currentTime.AddMilliseconds(
- delay
- );
+ delay);
var command = NewSyncPlayCommand(SendCommandType.Play);
SendCommand(session, BroadcastType.AllExceptCurrentSession, command, cancellationToken);
}
else
{
// Client, that was buffering, resumed playback but did not update others in time
- delay = _group.GetHighestPing() * 2;
- delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
+ delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing);
_group.LastActivity = currentTime.AddMilliseconds(
- delay
- );
+ delay);
var command = NewSyncPlayCommand(SendCommandType.Play);
SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
@@ -498,7 +495,7 @@ namespace Emby.Server.Implementations.SyncPlay
private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request)
{
// Collected pings are used to account for network latency when unpausing playback
- _group.UpdatePing(session, request.Ping ?? _group.DefaulPing);
+ _group.UpdatePing(session, request.Ping ?? GroupInfo.DefaultPing);
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
index 1f76dd4e3..966ed5024 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
@@ -3,13 +3,13 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
-using Microsoft.Extensions.Logging;
-using MediaBrowser.Controller.Entities;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.SyncPlay;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.SyncPlay;
+using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.SyncPlay
{
@@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.SyncPlay
/// <summary>
/// The logger.
/// </summary>
- private readonly ILogger _logger;
+ private readonly ILogger<SyncPlayManager> _logger;
/// <summary>
/// The user manager.
@@ -57,6 +57,13 @@ namespace Emby.Server.Implementations.SyncPlay
private bool _disposed = false;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SyncPlayManager" /> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="userManager">The user manager.</param>
+ /// <param name="sessionManager">The session manager.</param>
+ /// <param name="libraryManager">The library manager.</param>
public SyncPlayManager(
ILogger<SyncPlayManager> logger,
IUserManager userManager,
@@ -102,14 +109,6 @@ namespace Emby.Server.Implementations.SyncPlay
_disposed = true;
}
- private void CheckDisposed()
- {
- if (_disposed)
- {
- throw new ObjectDisposedException(GetType().Name);
- }
- }
-
private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
{
var session = e.SessionInfo;
@@ -142,38 +141,24 @@ namespace Emby.Server.Implementations.SyncPlay
var item = _libraryManager.GetItemById(itemId);
// Check ParentalRating access
- var hasParentalRatingAccess = true;
- if (user.Policy.MaxParentalRating.HasValue)
- {
- hasParentalRatingAccess = item.InheritedParentalRatingValue <= user.Policy.MaxParentalRating;
- }
+ var hasParentalRatingAccess = !user.MaxParentalAgeRating.HasValue
+ || item.InheritedParentalRatingValue <= user.MaxParentalAgeRating;
- if (!user.Policy.EnableAllFolders && hasParentalRatingAccess)
+ if (!user.HasPermission(PermissionKind.EnableAllFolders) && hasParentalRatingAccess)
{
var collections = _libraryManager.GetCollectionFolders(item).Select(
- folder => folder.Id.ToString("N", CultureInfo.InvariantCulture)
- );
- var intersect = collections.Intersect(user.Policy.EnabledFolders);
- return intersect.Any();
- }
- else
- {
- return hasParentalRatingAccess;
+ folder => folder.Id.ToString("N", CultureInfo.InvariantCulture));
+
+ return collections.Intersect(user.GetPreference(PreferenceKind.EnabledFolders)).Any();
}
+
+ return hasParentalRatingAccess;
}
private Guid? GetSessionGroup(SessionInfo session)
{
- ISyncPlayController group;
- _sessionToGroupMap.TryGetValue(session.Id, out group);
- if (group != null)
- {
- return group.GetGroupId();
- }
- else
- {
- return null;
- }
+ _sessionToGroupMap.TryGetValue(session.Id, out var group);
+ return group?.GetGroupId();
}
/// <inheritdoc />
@@ -181,15 +166,16 @@ namespace Emby.Server.Implementations.SyncPlay
{
var user = _userManager.GetUserById(session.UserId);
- if (user.Policy.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups)
+ if (user.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups)
{
_logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id);
- var error = new GroupUpdate<string>()
+ var error = new GroupUpdate<string>
{
Type = GroupUpdateType.CreateGroupDenied
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -203,7 +189,7 @@ namespace Emby.Server.Implementations.SyncPlay
var group = new SyncPlayController(_sessionManager, this);
_groups[group.GetGroupId()] = group;
- group.InitGroup(session, cancellationToken);
+ group.CreateGroup(session, cancellationToken);
}
}
@@ -212,7 +198,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
var user = _userManager.GetUserById(session.UserId);
- if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
+ if (user.SyncPlayAccess == SyncPlayAccess.None)
{
_logger.LogWarning("JoinGroup: {0} does not have access to SyncPlay.", session.Id);
@@ -220,7 +206,8 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.JoinGroupDenied
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -237,7 +224,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.GroupDoesNotExist
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -250,7 +237,7 @@ namespace Emby.Server.Implementations.SyncPlay
GroupId = group.GetGroupId().ToString(),
Type = GroupUpdateType.LibraryAccessDenied
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -274,8 +261,7 @@ namespace Emby.Server.Implementations.SyncPlay
// TODO: determine what happens to users that are in a group and get their permissions revoked
lock (_groupsLock)
{
- ISyncPlayController group;
- _sessionToGroupMap.TryGetValue(session.Id, out group);
+ _sessionToGroupMap.TryGetValue(session.Id, out var group);
if (group == null)
{
@@ -285,7 +271,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.NotInGroup
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -304,7 +290,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
var user = _userManager.GetUserById(session.UserId);
- if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
+ if (user.SyncPlayAccess == SyncPlayAccess.None)
{
return new List<GroupInfoView>();
}
@@ -313,19 +299,15 @@ namespace Emby.Server.Implementations.SyncPlay
if (!filterItemId.Equals(Guid.Empty))
{
return _groups.Values.Where(
- group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId())
- ).Select(
- group => group.GetInfo()
- ).ToList();
+ group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId())).Select(
+ group => group.GetInfo()).ToList();
}
- // Otherwise show all available groups
else
{
+ // Otherwise show all available groups
return _groups.Values.Where(
- group => HasAccessToItem(user, group.GetPlayingItemId())
- ).Select(
- group => group.GetInfo()
- ).ToList();
+ group => HasAccessToItem(user, group.GetPlayingItemId())).Select(
+ group => group.GetInfo()).ToList();
}
}
@@ -334,7 +316,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
var user = _userManager.GetUserById(session.UserId);
- if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
+ if (user.SyncPlayAccess == SyncPlayAccess.None)
{
_logger.LogWarning("HandleRequest: {0} does not have access to SyncPlay.", session.Id);
@@ -342,14 +324,14 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.JoinGroupDenied
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
lock (_groupsLock)
{
- ISyncPlayController group;
- _sessionToGroupMap.TryGetValue(session.Id, out group);
+ _sessionToGroupMap.TryGetValue(session.Id, out var group);
if (group == null)
{
@@ -359,7 +341,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.NotInGroup
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@@ -386,9 +368,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Session not in any group!");
}
- ISyncPlayController tempGroup;
- _sessionToGroupMap.Remove(session.Id, out tempGroup);
-
+ _sessionToGroupMap.Remove(session.Id, out var tempGroup);
if (!tempGroup.GetGroupId().Equals(group.GetGroupId()))
{
throw new InvalidOperationException("Session was in wrong group!");
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 4c2f24e6f..d1818deff 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -1,15 +1,20 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
+using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace Emby.Server.Implementations.TV
{
@@ -18,14 +23,12 @@ namespace Emby.Server.Implementations.TV
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
- private readonly IServerConfigurationManager _config;
- public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager, IServerConfigurationManager config)
+ public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager)
{
_userManager = userManager;
_userDataManager = userDataManager;
_libraryManager = libraryManager;
- _config = config;
}
public QueryResult<BaseItem> GetNextUp(NextUpQuery request, DtoOptions dtoOptions)
@@ -74,7 +77,8 @@ namespace Emby.Server.Implementations.TV
{
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
- .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
+ .Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes)
+ .Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
.ToArray();
}
@@ -113,23 +117,20 @@ namespace Emby.Server.Implementations.TV
limit = limit.Value + 10;
}
- var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
- {
- IncludeItemTypes = new[] { typeof(Episode).Name },
- OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
- SeriesPresentationUniqueKey = presentationUniqueKey,
- Limit = limit,
- DtoOptions = new DtoOptions
- {
- Fields = new ItemFields[]
+ var items = _libraryManager
+ .GetItemList(
+ new InternalItemsQuery(user)
{
- ItemFields.SeriesPresentationUniqueKey
- },
- EnableImages = false
- },
- GroupBySeriesPresentationUniqueKey = true
-
- }, parentsFolders.ToList()).Cast<Episode>().Select(GetUniqueSeriesKey);
+ IncludeItemTypes = new[] { typeof(Episode).Name },
+ OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
+ SeriesPresentationUniqueKey = presentationUniqueKey,
+ Limit = limit,
+ DtoOptions = new DtoOptions { Fields = new[] { ItemFields.SeriesPresentationUniqueKey }, EnableImages = false },
+ GroupBySeriesPresentationUniqueKey = true
+ }, parentsFolders.ToList())
+ .Cast<Episode>()
+ .Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey))
+ .Select(GetUniqueSeriesKey);
// Avoid implicitly captured closure
var episodes = GetNextUpEpisodes(request, user, items, dtoOptions);
@@ -145,7 +146,7 @@ namespace Emby.Server.Implementations.TV
var allNextUp = seriesKeys
.Select(i => GetNextUp(i, currentUser, dtoOptions));
- //allNextUp = allNextUp.OrderByDescending(i => i.Item1);
+ // allNextUp = allNextUp.OrderByDescending(i => i.Item1);
// If viewing all next up for all series, remove first episodes
// But if that returns empty, keep those first episodes (avoid completely empty view)
@@ -192,7 +193,7 @@ namespace Emby.Server.Implementations.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
- IncludeItemTypes = new[] { typeof(Episode).Name },
+ IncludeItemTypes = new[] { nameof(Episode) },
OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Descending) },
IsPlayed = true,
Limit = 1,
@@ -205,7 +206,6 @@ namespace Emby.Server.Implementations.TV
},
EnableImages = false
}
-
}).FirstOrDefault();
Func<Episode> getEpisode = () =>
@@ -220,9 +220,8 @@ namespace Emby.Server.Implementations.TV
IsPlayed = false,
IsVirtualItem = false,
ParentIndexNumberNotEquals = 0,
- MinSortName = lastWatchedEpisode == null ? null : lastWatchedEpisode.SortName,
+ MinSortName = lastWatchedEpisode?.SortName,
DtoOptions = dtoOptions
-
}).Cast<Episode>().FirstOrDefault();
};
@@ -254,6 +253,7 @@ namespace Emby.Server.Implementations.TV
{
items = items.Skip(query.StartIndex.Value);
}
+
if (query.Limit.HasValue)
{
items = items.Take(query.Limit.Value);
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index a26f714b1..b7a59cee2 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -18,7 +18,7 @@ namespace Emby.Server.Implementations.Udp
public sealed class UdpServer : IDisposable
{
/// <summary>
- /// The _logger
+ /// The _logger.
/// </summary>
private readonly ILogger _logger;
private readonly IServerApplicationHost _appHost;
@@ -68,12 +68,6 @@ namespace Emby.Server.Implementations.Udp
{
_logger.LogError(ex, "Error sending response message");
}
-
- var parts = messageText.Split('|');
- if (parts.Length > 1)
- {
- _appHost.EnableLoopback(parts[1]);
- }
}
else
{
@@ -101,11 +95,18 @@ namespace Emby.Server.Implementations.Udp
{
while (!cancellationToken.IsCancellationRequested)
{
+ var infiniteTask = Task.Delay(-1, cancellationToken);
try
{
- var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint).ConfigureAwait(false);
+ var task = _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint);
+ await Task.WhenAny(task, infiniteTask).ConfigureAwait(false);
+
+ if (!task.IsCompleted)
+ {
+ return;
+ }
- cancellationToken.ThrowIfCancellationRequested();
+ var result = task.Result;
var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 0b2309889..003cf3c74 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -1,11 +1,11 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Net;
using System.Net.Http;
-using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Security.Cryptography;
using System.Threading;
@@ -15,13 +15,14 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
+using MediaBrowser.Common.System;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Updates;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
+using MediaBrowser.Model.System;
namespace Emby.Server.Implementations.Updates
{
@@ -31,16 +32,11 @@ namespace Emby.Server.Implementations.Updates
public class InstallationManager : IInstallationManager
{
/// <summary>
- /// The key for a setting that specifies a URL for the plugin repository JSON manifest.
- /// </summary>
- public const string PluginManifestUrlKey = "InstallationManager:PluginManifestUrl";
-
- /// <summary>
/// The logger.
/// </summary>
- private readonly ILogger _logger;
+ private readonly ILogger<InstallationManager> _logger;
private readonly IApplicationPaths _appPaths;
- private readonly IHttpClient _httpClient;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly IJsonSerializer _jsonSerializer;
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
@@ -52,7 +48,6 @@ namespace Emby.Server.Implementations.Updates
private readonly IApplicationHost _applicationHost;
private readonly IZipClient _zipClient;
- private readonly IConfiguration _appConfig;
private readonly object _currentInstallationsLock = new object();
@@ -70,12 +65,11 @@ namespace Emby.Server.Implementations.Updates
ILogger<InstallationManager> logger,
IApplicationHost appHost,
IApplicationPaths appPaths,
- IHttpClient httpClient,
+ IHttpClientFactory httpClientFactory,
IJsonSerializer jsonSerializer,
IServerConfigurationManager config,
IFileSystem fileSystem,
- IZipClient zipClient,
- IConfiguration appConfig)
+ IZipClient zipClient)
{
if (logger == null)
{
@@ -88,82 +82,91 @@ namespace Emby.Server.Implementations.Updates
_logger = logger;
_applicationHost = appHost;
_appPaths = appPaths;
- _httpClient = httpClient;
+ _httpClientFactory = httpClientFactory;
_jsonSerializer = jsonSerializer;
_config = config;
_fileSystem = fileSystem;
_zipClient = zipClient;
- _appConfig = appConfig;
}
/// <inheritdoc />
- public event EventHandler<InstallationEventArgs> PackageInstalling;
+ public event EventHandler<InstallationInfo> PackageInstalling;
/// <inheritdoc />
- public event EventHandler<InstallationEventArgs> PackageInstallationCompleted;
+ public event EventHandler<InstallationInfo> PackageInstallationCompleted;
/// <inheritdoc />
public event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed;
/// <inheritdoc />
- public event EventHandler<InstallationEventArgs> PackageInstallationCancelled;
+ public event EventHandler<InstallationInfo> PackageInstallationCancelled;
/// <inheritdoc />
- public event EventHandler<GenericEventArgs<IPlugin>> PluginUninstalled;
+ public event EventHandler<IPlugin> PluginUninstalled;
/// <inheritdoc />
- public event EventHandler<GenericEventArgs<(IPlugin, VersionInfo)>> PluginUpdated;
+ public event EventHandler<InstallationInfo> PluginUpdated;
/// <inheritdoc />
- public event EventHandler<GenericEventArgs<VersionInfo>> PluginInstalled;
+ public event EventHandler<InstallationInfo> PluginInstalled;
/// <inheritdoc />
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
/// <inheritdoc />
- public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
+ public async Task<IReadOnlyList<PackageInfo>> GetPackages(string manifest, CancellationToken cancellationToken = default)
{
- var manifestUrl = _appConfig.GetValue<string>(PluginManifestUrlKey);
-
try
{
- using (var response = await _httpClient.SendAsync(
- new HttpRequestOptions
- {
- Url = manifestUrl,
- CancellationToken = cancellationToken,
- CacheMode = CacheMode.Unconditional,
- CacheLength = TimeSpan.FromMinutes(3)
- },
- HttpMethod.Get).ConfigureAwait(false))
- using (Stream stream = response.Content)
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetAsync(manifest, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+
+ try
{
- try
- {
- return await _jsonSerializer.DeserializeFromStreamAsync<IReadOnlyList<PackageInfo>>(stream).ConfigureAwait(false);
- }
- catch (SerializationException ex)
- {
- const string LogTemplate =
- "Failed to deserialize the plugin manifest retrieved from {PluginManifestUrl}. If you " +
- "have specified a custom plugin repository manifest URL with --plugin-manifest-url or " +
- PluginManifestUrlKey + ", please ensure that it is correct.";
- _logger.LogError(ex, LogTemplate, manifestUrl);
- throw;
- }
+ return await _jsonSerializer.DeserializeFromStreamAsync<IReadOnlyList<PackageInfo>>(stream).ConfigureAwait(false);
+ }
+ catch (SerializationException ex)
+ {
+ _logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
+ return Array.Empty<PackageInfo>();
}
}
catch (UriFormatException ex)
{
- const string LogTemplate =
- "The URL configured for the plugin repository manifest URL is not valid: {PluginManifestUrl}. " +
- "Please check the URL configured by --plugin-manifest-url or " + PluginManifestUrlKey;
- _logger.LogError(ex, LogTemplate, manifestUrl);
- throw;
+ _logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest);
+ return Array.Empty<PackageInfo>();
+ }
+ catch (HttpException ex)
+ {
+ _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
+ return Array.Empty<PackageInfo>();
+ }
+ catch (HttpRequestException ex)
+ {
+ _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
+ return Array.Empty<PackageInfo>();
}
}
/// <inheritdoc />
+ public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
+ {
+ var result = new List<PackageInfo>();
+ foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
+ {
+ foreach (var package in await GetPackages(repository.Url, cancellationToken).ConfigureAwait(true))
+ {
+ package.repositoryName = repository.Name;
+ package.repositoryUrl = repository.Url;
+ result.Add(package);
+ }
+ }
+
+ return result;
+ }
+
+ /// <inheritdoc />
public IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages,
string name = null,
@@ -183,56 +186,62 @@ namespace Emby.Server.Implementations.Updates
}
/// <inheritdoc />
- public IEnumerable<VersionInfo> GetCompatibleVersions(
- IEnumerable<VersionInfo> availableVersions,
- Version minVersion = null)
- {
- var appVer = _applicationHost.ApplicationVersion;
- availableVersions = availableVersions
- .Where(x => Version.Parse(x.targetAbi) <= appVer);
-
- if (minVersion != null)
- {
- availableVersions = availableVersions.Where(x => x.version >= minVersion);
- }
-
- return availableVersions.OrderByDescending(x => x.version);
- }
-
- /// <inheritdoc />
- public IEnumerable<VersionInfo> GetCompatibleVersions(
+ public IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
string name = null,
Guid guid = default,
- Version minVersion = null)
+ Version minVersion = null,
+ Version specificVersion = null)
{
var package = FilterPackages(availablePackages, name, guid).FirstOrDefault();
// Package not found in repository
if (package == null)
{
- return Enumerable.Empty<VersionInfo>();
+ yield break;
}
- return GetCompatibleVersions(
- package.versions,
- minVersion);
+ var appVer = _applicationHost.ApplicationVersion;
+ var availableVersions = package.versions
+ .Where(x => Version.Parse(x.targetAbi) <= appVer);
+
+ if (specificVersion != null)
+ {
+ availableVersions = availableVersions.Where(x => new Version(x.version) == specificVersion);
+ }
+ else if (minVersion != null)
+ {
+ availableVersions = availableVersions.Where(x => new Version(x.version) >= minVersion);
+ }
+
+ foreach (var v in availableVersions.OrderByDescending(x => x.version))
+ {
+ yield return new InstallationInfo
+ {
+ Changelog = v.changelog,
+ Guid = new Guid(package.guid),
+ Name = package.name,
+ Version = new Version(v.version),
+ SourceUrl = v.sourceUrl,
+ Checksum = v.checksum
+ };
+ }
}
/// <inheritdoc />
- public async Task<IEnumerable<VersionInfo>> GetAvailablePluginUpdates(CancellationToken cancellationToken = default)
+ public async Task<IEnumerable<InstallationInfo>> GetAvailablePluginUpdates(CancellationToken cancellationToken = default)
{
var catalog = await GetAvailablePackages(cancellationToken).ConfigureAwait(false);
return GetAvailablePluginUpdates(catalog);
}
- private IEnumerable<VersionInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
+ private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
{
foreach (var plugin in _applicationHost.Plugins)
{
- var compatibleversions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, plugin.Version);
- var version = compatibleversions.FirstOrDefault(y => y.version > plugin.Version);
- if (version != null && !CompletedInstallations.Any(x => string.Equals(x.Guid, version.guid, StringComparison.OrdinalIgnoreCase)))
+ var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
+ var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
+ if (version != null && CompletedInstallations.All(x => x.Guid != version.Guid))
{
yield return version;
}
@@ -240,23 +249,16 @@ namespace Emby.Server.Implementations.Updates
}
/// <inheritdoc />
- public async Task InstallPackage(VersionInfo package, CancellationToken cancellationToken)
+ public async Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken)
{
if (package == null)
{
throw new ArgumentNullException(nameof(package));
}
- var installationInfo = new InstallationInfo
- {
- Guid = package.guid,
- Name = package.name,
- Version = package.version.ToString()
- };
-
var innerCancellationTokenSource = new CancellationTokenSource();
- var tuple = (installationInfo, innerCancellationTokenSource);
+ var tuple = (package, innerCancellationTokenSource);
// Add it to the in-progress list
lock (_currentInstallationsLock)
@@ -266,13 +268,7 @@ namespace Emby.Server.Implementations.Updates
var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token;
- var installationEventArgs = new InstallationEventArgs
- {
- InstallationInfo = installationInfo,
- VersionInfo = package
- };
-
- PackageInstalling?.Invoke(this, installationEventArgs);
+ PackageInstalling?.Invoke(this, package);
try
{
@@ -283,9 +279,9 @@ namespace Emby.Server.Implementations.Updates
_currentInstallations.Remove(tuple);
}
- _completedInstallationsInternal.Add(installationInfo);
+ _completedInstallationsInternal.Add(package);
- PackageInstallationCompleted?.Invoke(this, installationEventArgs);
+ PackageInstallationCompleted?.Invoke(this, package);
}
catch (OperationCanceledException)
{
@@ -294,9 +290,9 @@ namespace Emby.Server.Implementations.Updates
_currentInstallations.Remove(tuple);
}
- _logger.LogInformation("Package installation cancelled: {0} {1}", package.name, package.version);
+ _logger.LogInformation("Package installation cancelled: {0} {1}", package.Name, package.Version);
- PackageInstallationCancelled?.Invoke(this, installationEventArgs);
+ PackageInstallationCancelled?.Invoke(this, package);
throw;
}
@@ -311,7 +307,7 @@ namespace Emby.Server.Implementations.Updates
PackageInstallationFailed?.Invoke(this, new InstallationFailedEventArgs
{
- InstallationInfo = installationInfo,
+ InstallationInfo = package,
Exception = ex
});
@@ -330,11 +326,11 @@ namespace Emby.Server.Implementations.Updates
/// <param name="package">The package.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns><see cref="Task" />.</returns>
- private async Task InstallPackageInternal(VersionInfo package, CancellationToken cancellationToken)
+ private async Task InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
{
// Set last update time if we were installed before
- IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => string.Equals(p.Id.ToString(), package.guid, StringComparison.OrdinalIgnoreCase))
- ?? _applicationHost.Plugins.FirstOrDefault(p => p.Name.Equals(package.name, StringComparison.OrdinalIgnoreCase));
+ IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => p.Id == package.Guid)
+ ?? _applicationHost.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase));
// Do the install
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
@@ -342,68 +338,70 @@ namespace Emby.Server.Implementations.Updates
// Do plugin-specific processing
if (plugin == null)
{
- _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.version);
+ _logger.LogInformation("New plugin installed: {0} {1}", package.Name, package.Version);
- PluginInstalled?.Invoke(this, new GenericEventArgs<VersionInfo>(package));
+ PluginInstalled?.Invoke(this, package);
}
else
{
- _logger.LogInformation("Plugin updated: {0} {1} {2}", package.name, package.version);
+ _logger.LogInformation("Plugin updated: {0} {1}", package.Name, package.Version);
- PluginUpdated?.Invoke(this, new GenericEventArgs<(IPlugin, VersionInfo)>((plugin, package)));
+ PluginUpdated?.Invoke(this, package);
}
_applicationHost.NotifyPendingRestart();
}
- private async Task PerformPackageInstallation(VersionInfo package, CancellationToken cancellationToken)
+ private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
{
- var extension = Path.GetExtension(package.filename);
+ var extension = Path.GetExtension(package.SourceUrl);
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
{
- _logger.LogError("Only zip packages are supported. {Filename} is not a zip archive.", package.filename);
+ _logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
return;
}
// Always override the passed-in target (which is a file) and figure it out again
- string targetDir = Path.Combine(_appPaths.PluginsPath, package.name);
+ string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name);
+
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetAsync(package.SourceUrl, cancellationToken).ConfigureAwait(false);
+ await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
// CA5351: Do Not Use Broken Cryptographic Algorithms
#pragma warning disable CA5351
- using (var res = await _httpClient.SendAsync(
- new HttpRequestOptions
- {
- Url = package.sourceUrl,
- CancellationToken = cancellationToken,
- // We need it to be buffered for setting the position
- BufferContent = true
- },
- HttpMethod.Get).ConfigureAwait(false))
- using (var stream = res.Content)
- using (var md5 = MD5.Create())
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- var hash = Hex.Encode(md5.ComputeHash(stream));
- 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.");
- }
+ using var md5 = MD5.Create();
+ cancellationToken.ThrowIfCancellationRequested();
- if (Directory.Exists(targetDir))
+ var hash = Hex.Encode(md5.ComputeHash(stream));
+ 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;
+
+ if (Directory.Exists(targetDir))
+ {
+ try
{
Directory.Delete(targetDir, true);
}
-
- stream.Position = 0;
- _zipClient.ExtractAllFromZip(stream, targetDir, true);
+ catch
+ {
+ // Ignore any exceptions.
+ }
}
+ stream.Position = 0;
+ _zipClient.ExtractAllFromZip(stream, targetDir, true);
+
#pragma warning restore CA5351
}
@@ -413,6 +411,12 @@ namespace Emby.Server.Implementations.Updates
/// <param name="plugin">The plugin.</param>
public void UninstallPlugin(IPlugin plugin)
{
+ if (!plugin.CanUninstall)
+ {
+ _logger.LogWarning("Attempt to delete non removable plugin {0}, ignoring request", plugin.Name);
+ return;
+ }
+
plugin.OnUninstalling();
// Remove it the quick way for now
@@ -436,15 +440,22 @@ namespace Emby.Server.Implementations.Updates
path = file;
}
- if (isDirectory)
+ try
{
- _logger.LogInformation("Deleting plugin directory {0}", path);
- Directory.Delete(path, true);
+ if (isDirectory)
+ {
+ _logger.LogInformation("Deleting plugin directory {0}", path);
+ Directory.Delete(path, true);
+ }
+ else
+ {
+ _logger.LogInformation("Deleting plugin file {0}", path);
+ _fileSystem.DeleteFile(path);
+ }
}
- else
+ catch
{
- _logger.LogInformation("Deleting plugin file {0}", path);
- _fileSystem.DeleteFile(path);
+ // Ignore file errors.
}
var list = _config.Configuration.UninstalledPlugins.ToList();
@@ -456,7 +467,7 @@ namespace Emby.Server.Implementations.Updates
_config.SaveConfiguration();
}
- PluginUninstalled?.Invoke(this, new GenericEventArgs<IPlugin> { Argument = plugin });
+ PluginUninstalled?.Invoke(this, plugin);
_applicationHost.NotifyPendingRestart();
}
@@ -466,7 +477,7 @@ namespace Emby.Server.Implementations.Updates
{
lock (_currentInstallationsLock)
{
- var install = _currentInstallations.Find(x => x.info.Guid == id.ToString());
+ var install = _currentInstallations.Find(x => x.info.Guid == id);
if (install == default((InstallationInfo, CancellationTokenSource)))
{
return false;
@@ -486,9 +497,9 @@ namespace Emby.Server.Implementations.Updates
}
/// <summary>
- /// Releases unmanaged and - optionally - managed resources.
+ /// Releases unmanaged and optionally managed resources.
/// </summary>
- /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources or <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool dispose)
{
if (dispose)