aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Notifications/Notifications.cs2
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs12
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs4
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs6
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs4
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs4
-rw-r--r--Emby.Server.Implementations/Library/ConnectManager.cs46
-rw-r--r--Emby.Server.Implementations/Library/UserManager.cs4
-rw-r--r--MediaBrowser.Api/PackageService.cs40
-rw-r--r--MediaBrowser.Api/Playback/StreamState.cs5
-rw-r--r--MediaBrowser.Api/SearchService.cs2
-rw-r--r--MediaBrowser.Api/StartupWizardService.cs4
-rw-r--r--MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs18
-rw-r--r--MediaBrowser.Common/Configuration/IApplicationPaths.cs84
-rw-r--r--MediaBrowser.Common/Configuration/IConfigurationFactory.cs22
-rw-r--r--MediaBrowser.Common/Configuration/IConfigurationManager.cs82
-rw-r--r--MediaBrowser.Common/Events/EventHelper.cs108
-rw-r--r--MediaBrowser.Common/Extensions/BaseExtensions.cs58
-rw-r--r--MediaBrowser.Common/Extensions/ResourceNotFoundException.cs63
-rw-r--r--MediaBrowser.Common/IApplicationHost.cs147
-rw-r--r--MediaBrowser.Common/MediaBrowser.Common.csproj16
-rw-r--r--MediaBrowser.Common/Net/HttpRequestOptions.cs157
-rw-r--r--MediaBrowser.Common/Net/HttpResponseInfo.cs75
-rw-r--r--MediaBrowser.Common/Net/IHttpClient.cs59
-rw-r--r--MediaBrowser.Common/Net/INetworkManager.cs66
-rw-r--r--MediaBrowser.Common/Plugins/BasePlugin.cs276
-rw-r--r--MediaBrowser.Common/Plugins/IPlugin.cs83
-rw-r--r--MediaBrowser.Common/Progress/ActionableProgress.cs54
-rw-r--r--MediaBrowser.Common/Properties/AssemblyInfo.cs27
-rw-r--r--MediaBrowser.Common/Security/IRequiresRegistration.cs15
-rw-r--r--MediaBrowser.Common/Security/ISecurityManager.cs32
-rw-r--r--MediaBrowser.Common/Security/PaymentRequiredException.cs8
-rw-r--r--MediaBrowser.Common/Updates/GithubUpdater.cs278
-rw-r--r--MediaBrowser.Common/Updates/IInstallationManager.cs121
-rw-r--r--MediaBrowser.Common/Updates/InstallationEventArgs.cs11
-rw-r--r--MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs9
-rw-r--r--MediaBrowser.Controller/Authentication/AuthenticationResult.cs14
-rw-r--r--MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs35
-rw-r--r--MediaBrowser.Controller/Channels/Channel.cs94
-rw-r--r--MediaBrowser.Controller/Channels/ChannelItemInfo.cs82
-rw-r--r--MediaBrowser.Controller/Channels/ChannelItemResult.cs16
-rw-r--r--MediaBrowser.Controller/Channels/ChannelItemType.cs9
-rw-r--r--MediaBrowser.Controller/Channels/ChannelParentalRating.cs15
-rw-r--r--MediaBrowser.Controller/Channels/ChannelSearchInfo.cs14
-rw-r--r--MediaBrowser.Controller/Channels/IChannel.cs76
-rw-r--r--MediaBrowser.Controller/Channels/IChannelManager.cs89
-rw-r--r--MediaBrowser.Controller/Channels/IHasCacheKey.cs13
-rw-r--r--MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs15
-rw-r--r--MediaBrowser.Controller/Channels/ISearchableChannel.cs50
-rw-r--r--MediaBrowser.Controller/Channels/InternalChannelFeatures.cs61
-rw-r--r--MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs21
-rw-r--r--MediaBrowser.Controller/Chapters/IChapterManager.cs23
-rw-r--r--MediaBrowser.Controller/Collections/CollectionCreationOptions.cs27
-rw-r--r--MediaBrowser.Controller/Collections/CollectionEvents.cs37
-rw-r--r--MediaBrowser.Controller/Collections/ICollectionManager.cs57
-rw-r--r--MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs25
-rw-r--r--MediaBrowser.Controller/Connect/IConnectManager.cs45
-rw-r--r--MediaBrowser.Controller/Connect/UserLinkResult.cs10
-rw-r--r--MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs10
-rw-r--r--MediaBrowser.Controller/Devices/IDeviceManager.cs73
-rw-r--r--MediaBrowser.Controller/Dlna/IDlnaManager.cs76
-rw-r--r--MediaBrowser.Controller/Drawing/IImageEncoder.cs49
-rw-r--r--MediaBrowser.Controller/Drawing/IImageProcessor.cs118
-rw-r--r--MediaBrowser.Controller/Drawing/ImageCollageOptions.cs27
-rw-r--r--MediaBrowser.Controller/Drawing/ImageHelper.cs72
-rw-r--r--MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs114
-rw-r--r--MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs25
-rw-r--r--MediaBrowser.Controller/Drawing/ImageStream.cs28
-rw-r--r--MediaBrowser.Controller/Dto/DtoOptions.cs72
-rw-r--r--MediaBrowser.Controller/Dto/IDtoService.cs70
-rw-r--r--MediaBrowser.Controller/Entities/AggregateFolder.cs219
-rw-r--r--MediaBrowser.Controller/Entities/Audio/Audio.cs216
-rw-r--r--MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs15
-rw-r--r--MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs9
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs272
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs275
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicGenre.cs145
-rw-r--r--MediaBrowser.Controller/Entities/AudioBook.cs69
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs2960
-rw-r--r--MediaBrowser.Controller/Entities/BaseItemExtensions.cs65
-rw-r--r--MediaBrowser.Controller/Entities/BasePluginFolder.cs54
-rw-r--r--MediaBrowser.Controller/Entities/Book.cs72
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs405
-rw-r--r--MediaBrowser.Controller/Entities/DayOfWeekHelper.cs71
-rw-r--r--MediaBrowser.Controller/Entities/Extensions.cs46
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs1803
-rw-r--r--MediaBrowser.Controller/Entities/Game.cs129
-rw-r--r--MediaBrowser.Controller/Entities/GameGenre.cs128
-rw-r--r--MediaBrowser.Controller/Entities/GameSystem.cs101
-rw-r--r--MediaBrowser.Controller/Entities/Genre.cs140
-rw-r--r--MediaBrowser.Controller/Entities/ICollectionFolder.cs27
-rw-r--r--MediaBrowser.Controller/Entities/IHasAspectRatio.cs14
-rw-r--r--MediaBrowser.Controller/Entities/IHasDisplayOrder.cs15
-rw-r--r--MediaBrowser.Controller/Entities/IHasMediaSources.cs19
-rw-r--r--MediaBrowser.Controller/Entities/IHasProgramAttributes.cs17
-rw-r--r--MediaBrowser.Controller/Entities/IHasScreenshots.cs10
-rw-r--r--MediaBrowser.Controller/Entities/IHasSeries.cs20
-rw-r--r--MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs13
-rw-r--r--MediaBrowser.Controller/Entities/IHasStartDate.cs9
-rw-r--r--MediaBrowser.Controller/Entities/IHasTrailers.cs39
-rw-r--r--MediaBrowser.Controller/Entities/IItemByName.cs17
-rw-r--r--MediaBrowser.Controller/Entities/IMetadataContainer.cs19
-rw-r--r--MediaBrowser.Controller/Entities/ISupportsBoxSetGrouping.cs12
-rw-r--r--MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs12
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs261
-rw-r--r--MediaBrowser.Controller/Entities/InternalPeopleQuery.cs21
-rw-r--r--MediaBrowser.Controller/Entities/ItemImageInfo.cs46
-rw-r--r--MediaBrowser.Controller/Entities/LinkedChild.cs73
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs263
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs203
-rw-r--r--MediaBrowser.Controller/Entities/MusicVideo.cs79
-rw-r--r--MediaBrowser.Controller/Entities/PeopleHelper.cs119
-rw-r--r--MediaBrowser.Controller/Entities/Person.cs216
-rw-r--r--MediaBrowser.Controller/Entities/Photo.cs104
-rw-r--r--MediaBrowser.Controller/Entities/PhotoAlbum.cs34
-rw-r--r--MediaBrowser.Controller/Entities/Share.cs15
-rw-r--r--MediaBrowser.Controller/Entities/SourceType.cs10
-rw-r--r--MediaBrowser.Controller/Entities/Studio.cs141
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs374
-rw-r--r--MediaBrowser.Controller/Entities/TV/Season.cs273
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs540
-rw-r--r--MediaBrowser.Controller/Entities/TagExtensions.cs35
-rw-r--r--MediaBrowser.Controller/Entities/Trailer.cs111
-rw-r--r--MediaBrowser.Controller/Entities/User.cs342
-rw-r--r--MediaBrowser.Controller/Entities/UserItemData.cs124
-rw-r--r--MediaBrowser.Controller/Entities/UserRootFolder.cs158
-rw-r--r--MediaBrowser.Controller/Entities/UserView.cs210
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs1070
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs626
-rw-r--r--MediaBrowser.Controller/Entities/Year.cs147
-rw-r--r--MediaBrowser.Controller/Extensions/StringExtensions.cs17
-rw-r--r--MediaBrowser.Controller/IO/FileData.cs123
-rw-r--r--MediaBrowser.Controller/IResourceFileManager.cs32
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs100
-rw-r--r--MediaBrowser.Controller/IServerApplicationPaths.cs109
-rw-r--r--MediaBrowser.Controller/Library/DeleteOptions.cs14
-rw-r--r--MediaBrowser.Controller/Library/IIntroProvider.cs32
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs550
-rw-r--r--MediaBrowser.Controller/Library/ILibraryMonitor.cs43
-rw-r--r--MediaBrowser.Controller/Library/ILibraryPostScanTask.cs20
-rw-r--r--MediaBrowser.Controller/Library/ILiveStream.cs21
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceManager.cs100
-rw-r--r--MediaBrowser.Controller/Library/IMediaSourceProvider.cs25
-rw-r--r--MediaBrowser.Controller/Library/IMetadataFileSaver.cs19
-rw-r--r--MediaBrowser.Controller/Library/IMetadataSaver.cs33
-rw-r--r--MediaBrowser.Controller/Library/IMusicManager.cs25
-rw-r--r--MediaBrowser.Controller/Library/ISearchEngine.cs18
-rw-r--r--MediaBrowser.Controller/Library/IUserDataManager.cs67
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs207
-rw-r--r--MediaBrowser.Controller/Library/IUserViewManager.cs19
-rw-r--r--MediaBrowser.Controller/Library/IntroInfo.cs19
-rw-r--r--MediaBrowser.Controller/Library/ItemChangeEventArgs.cs24
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs281
-rw-r--r--MediaBrowser.Controller/Library/ItemUpdateType.cs14
-rw-r--r--MediaBrowser.Controller/Library/LibraryManagerExtensions.cs13
-rw-r--r--MediaBrowser.Controller/Library/MetadataConfigurationStore.cs29
-rw-r--r--MediaBrowser.Controller/Library/NameExtensions.cs26
-rw-r--r--MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs34
-rw-r--r--MediaBrowser.Controller/Library/PlaybackStopEventArgs.cs11
-rw-r--r--MediaBrowser.Controller/Library/Profiler.cs76
-rw-r--r--MediaBrowser.Controller/Library/SearchHintInfo.cs22
-rw-r--r--MediaBrowser.Controller/Library/TVUtils.cs59
-rw-r--r--MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs39
-rw-r--r--MediaBrowser.Controller/LiveTv/ChannelInfo.cs74
-rw-r--r--MediaBrowser.Controller/LiveTv/IListingsProvider.cs19
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvManager.cs293
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvService.cs190
-rw-r--r--MediaBrowser.Controller/LiveTv/ITunerHost.cs62
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvChannel.cs197
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs20
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvProgram.cs349
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs49
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs73
-rw-r--r--MediaBrowser.Controller/LiveTv/ProgramInfo.cs213
-rw-r--r--MediaBrowser.Controller/LiveTv/RecordingInfo.cs205
-rw-r--r--MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs12
-rw-r--r--MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs119
-rw-r--r--MediaBrowser.Controller/LiveTv/TimerEventInfo.cs10
-rw-r--r--MediaBrowser.Controller/LiveTv/TimerInfo.cs175
-rw-r--r--MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs10
-rw-r--r--MediaBrowser.Controller/MediaBrowser.Controller.csproj17
-rw-r--r--MediaBrowser.Controller/MediaEncoding/DroidSansFallback.ttf.REMOVED.git-id1
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs2503
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs775
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs265
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs17
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs124
-rw-r--r--MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs33
-rw-r--r--MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs20
-rw-r--r--MediaBrowser.Controller/MediaEncoding/JobLogger.cs149
-rw-r--r--MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs53
-rw-r--r--MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs24
-rw-r--r--MediaBrowser.Controller/Net/AuthenticatedAttribute.cs69
-rw-r--r--MediaBrowser.Controller/Net/AuthorizationInfo.cs52
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs322
-rw-r--r--MediaBrowser.Controller/Net/IAuthService.cs9
-rw-r--r--MediaBrowser.Controller/Net/IAuthorizationContext.cs21
-rw-r--r--MediaBrowser.Controller/Net/IHasResultFactory.cs17
-rw-r--r--MediaBrowser.Controller/Net/IHttpResultFactory.cs80
-rw-r--r--MediaBrowser.Controller/Net/IHttpServer.cs39
-rw-r--r--MediaBrowser.Controller/Net/ISessionContext.cs16
-rw-r--r--MediaBrowser.Controller/Net/IWebSocketConnection.cs85
-rw-r--r--MediaBrowser.Controller/Net/IWebSocketListener.cs17
-rw-r--r--MediaBrowser.Controller/Net/SecurityException.cs21
-rw-r--r--MediaBrowser.Controller/Net/StaticResultOptions.cs41
-rw-r--r--MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs41
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessageInfo.cs16
-rw-r--r--MediaBrowser.Controller/Notifications/INotificationManager.cs41
-rw-r--r--MediaBrowser.Controller/Notifications/INotificationService.cs30
-rw-r--r--MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs14
-rw-r--r--MediaBrowser.Controller/Notifications/UserNotification.cs21
-rw-r--r--MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs49
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs159
-rw-r--r--MediaBrowser.Controller/Persistence/IRepository.cs16
-rw-r--r--MediaBrowser.Controller/Persistence/IUserDataRepository.cs50
-rw-r--r--MediaBrowser.Controller/Persistence/IUserRepository.cs29
-rw-r--r--MediaBrowser.Controller/Persistence/MediaStreamQuery.cs26
-rw-r--r--MediaBrowser.Controller/Playlists/IPlaylistManager.cs59
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs315
-rw-r--r--MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs20
-rw-r--r--MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs50
-rw-r--r--MediaBrowser.Controller/Plugins/IServerEntryPoint.cs20
-rw-r--r--MediaBrowser.Controller/Properties/AssemblyInfo.cs27
-rw-r--r--MediaBrowser.Controller/Providers/AlbumInfo.cs28
-rw-r--r--MediaBrowser.Controller/Providers/ArtistInfo.cs14
-rw-r--r--MediaBrowser.Controller/Providers/BookInfo.cs7
-rw-r--r--MediaBrowser.Controller/Providers/BoxSetInfo.cs7
-rw-r--r--MediaBrowser.Controller/Providers/DirectoryService.cs106
-rw-r--r--MediaBrowser.Controller/Providers/DynamicImageInfo.cs10
-rw-r--r--MediaBrowser.Controller/Providers/DynamicImageResponse.cs36
-rw-r--r--MediaBrowser.Controller/Providers/EpisodeInfo.cs20
-rw-r--r--MediaBrowser.Controller/Providers/ExtraInfo.cs15
-rw-r--r--MediaBrowser.Controller/Providers/ExtraSource.cs9
-rw-r--r--MediaBrowser.Controller/Providers/GameInfo.cs11
-rw-r--r--MediaBrowser.Controller/Providers/GameSystemInfo.cs11
-rw-r--r--MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs24
-rw-r--r--MediaBrowser.Controller/Providers/IDirectoryService.cs15
-rw-r--r--MediaBrowser.Controller/Providers/IDynamicImageProvider.cs27
-rw-r--r--MediaBrowser.Controller/Providers/IExternalId.cs15
-rw-r--r--MediaBrowser.Controller/Providers/IExtrasProvider.cs20
-rw-r--r--MediaBrowser.Controller/Providers/IForcedProvider.cs10
-rw-r--r--MediaBrowser.Controller/Providers/IHasItemChangeMonitor.cs15
-rw-r--r--MediaBrowser.Controller/Providers/IHasLookupInfo.cs8
-rw-r--r--MediaBrowser.Controller/Providers/IHasOrder.cs7
-rw-r--r--MediaBrowser.Controller/Providers/IImageEnhancer.cs61
-rw-r--r--MediaBrowser.Controller/Providers/IImageProvider.cs23
-rw-r--r--MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs10
-rw-r--r--MediaBrowser.Controller/Providers/ILocalImageProvider.cs9
-rw-r--r--MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs25
-rw-r--r--MediaBrowser.Controller/Providers/IMetadataProvider.cs21
-rw-r--r--MediaBrowser.Controller/Providers/IMetadataService.cs34
-rw-r--r--MediaBrowser.Controller/Providers/IPreRefreshProvider.cs7
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs178
-rw-r--r--MediaBrowser.Controller/Providers/IRemoteImageProvider.cs39
-rw-r--r--MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs25
-rw-r--r--MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs17
-rw-r--r--MediaBrowser.Controller/Providers/ImageRefreshOptions.cs33
-rw-r--r--MediaBrowser.Controller/Providers/ItemInfo.cs32
-rw-r--r--MediaBrowser.Controller/Providers/ItemLookupInfo.cs47
-rw-r--r--MediaBrowser.Controller/Providers/LocalImageInfo.cs13
-rw-r--r--MediaBrowser.Controller/Providers/MetadataProviderPriority.cs40
-rw-r--r--MediaBrowser.Controller/Providers/MetadataRefreshMode.cs25
-rw-r--r--MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs72
-rw-r--r--MediaBrowser.Controller/Providers/MetadataResult.cs77
-rw-r--r--MediaBrowser.Controller/Providers/MovieInfo.cs7
-rw-r--r--MediaBrowser.Controller/Providers/MusicVideoInfo.cs7
-rw-r--r--MediaBrowser.Controller/Providers/PersonLookupInfo.cs7
-rw-r--r--MediaBrowser.Controller/Providers/RemoteSearchQuery.cs21
-rw-r--r--MediaBrowser.Controller/Providers/SeasonInfo.cs15
-rw-r--r--MediaBrowser.Controller/Providers/SeriesInfo.cs6
-rw-r--r--MediaBrowser.Controller/Providers/SongInfo.cs16
-rw-r--r--MediaBrowser.Controller/Providers/TrailerInfo.cs6
-rw-r--r--MediaBrowser.Controller/Providers/VideoContentType.cs19
-rw-r--r--MediaBrowser.Controller/Resolvers/BaseItemResolver.cs61
-rw-r--r--MediaBrowser.Controller/Resolvers/IItemResolver.cs48
-rw-r--r--MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs15
-rw-r--r--MediaBrowser.Controller/Resolvers/ResolverPriority.cs27
-rw-r--r--MediaBrowser.Controller/Security/AuthenticationInfo.cs70
-rw-r--r--MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs49
-rw-r--r--MediaBrowser.Controller/Security/IAuthenticationRepository.cs37
-rw-r--r--MediaBrowser.Controller/Security/IEncryptionManager.cs20
-rw-r--r--MediaBrowser.Controller/Session/AuthenticationRequest.cs19
-rw-r--r--MediaBrowser.Controller/Session/ISessionController.cs25
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs328
-rw-r--r--MediaBrowser.Controller/Session/SessionEventArgs.cs9
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs396
-rw-r--r--MediaBrowser.Controller/Sorting/IBaseItemComparer.cs17
-rw-r--r--MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs29
-rw-r--r--MediaBrowser.Controller/Sorting/SortExtensions.cs143
-rw-r--r--MediaBrowser.Controller/Sorting/SortHelper.cs25
-rw-r--r--MediaBrowser.Controller/Subtitles/ISubtitleManager.cs74
-rw-r--r--MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs40
-rw-r--r--MediaBrowser.Controller/Subtitles/SubtitleDownloadEventArgs.cs27
-rw-r--r--MediaBrowser.Controller/Subtitles/SubtitleResponse.cs12
-rw-r--r--MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs39
-rw-r--r--MediaBrowser.Controller/Sync/IHasDynamicAccess.cs18
-rw-r--r--MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs10
-rw-r--r--MediaBrowser.Controller/Sync/IServerSyncProvider.cs29
-rw-r--r--MediaBrowser.Controller/Sync/ISyncProvider.cs27
-rw-r--r--MediaBrowser.Controller/Sync/SyncedFileInfo.cs35
-rw-r--r--MediaBrowser.Controller/TV/ITVSeriesManager.cs20
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs1
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs6
-rw-r--r--MediaBrowser.Model/Activity/ActivityLogEntry.cs68
-rw-r--r--MediaBrowser.Model/Activity/IActivityManager.cs17
-rw-r--r--MediaBrowser.Model/Activity/IActivityRepository.cs12
-rw-r--r--MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs27
-rw-r--r--MediaBrowser.Model/Branding/BrandingOptions.cs17
-rw-r--r--MediaBrowser.Model/Channels/ChannelFeatures.cs85
-rw-r--r--MediaBrowser.Model/Channels/ChannelFolderType.cs17
-rw-r--r--MediaBrowser.Model/Channels/ChannelInfo.cs30
-rw-r--r--MediaBrowser.Model/Channels/ChannelItemSortField.cs13
-rw-r--r--MediaBrowser.Model/Channels/ChannelMediaContentType.cs23
-rw-r--r--MediaBrowser.Model/Channels/ChannelMediaType.cs11
-rw-r--r--MediaBrowser.Model/Channels/ChannelQuery.cs52
-rw-r--r--MediaBrowser.Model/Collections/CollectionCreationResult.cs9
-rw-r--r--MediaBrowser.Model/Configuration/AccessSchedule.cs22
-rw-r--r--MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs57
-rw-r--r--MediaBrowser.Model/Configuration/DynamicDayOfWeek.cs17
-rw-r--r--MediaBrowser.Model/Configuration/EncodingOptions.cs36
-rw-r--r--MediaBrowser.Model/Configuration/FanartOptions.cs12
-rw-r--r--MediaBrowser.Model/Configuration/ImageOption.cs29
-rw-r--r--MediaBrowser.Model/Configuration/ImageSavingConvention.cs8
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs444
-rw-r--r--MediaBrowser.Model/Configuration/MetadataConfiguration.cs13
-rw-r--r--MediaBrowser.Model/Configuration/MetadataOptions.cs38
-rw-r--r--MediaBrowser.Model/Configuration/MetadataPlugin.cs17
-rw-r--r--MediaBrowser.Model/Configuration/MetadataPluginSummary.cs32
-rw-r--r--MediaBrowser.Model/Configuration/MetadataPluginType.cs16
-rw-r--r--MediaBrowser.Model/Configuration/ServerConfiguration.cs313
-rw-r--r--MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs11
-rw-r--r--MediaBrowser.Model/Configuration/UnratedItem.cs16
-rw-r--r--MediaBrowser.Model/Configuration/UserConfiguration.cs66
-rw-r--r--MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs23
-rw-r--r--MediaBrowser.Model/Connect/ConnectAuthenticationExchangeResult.cs17
-rw-r--r--MediaBrowser.Model/Connect/ConnectAuthenticationResult.cs17
-rw-r--r--MediaBrowser.Model/Connect/ConnectAuthorization.cs21
-rw-r--r--MediaBrowser.Model/Connect/ConnectAuthorizationRequest.cs19
-rw-r--r--MediaBrowser.Model/Connect/ConnectUser.cs12
-rw-r--r--MediaBrowser.Model/Connect/ConnectUserQuery.cs11
-rw-r--r--MediaBrowser.Model/Connect/UserLinkType.cs15
-rw-r--r--MediaBrowser.Model/Cryptography/ICryptoProvider.cs13
-rw-r--r--MediaBrowser.Model/Devices/ContentUploadHistory.cs15
-rw-r--r--MediaBrowser.Model/Devices/DeviceInfo.cs69
-rw-r--r--MediaBrowser.Model/Devices/DeviceQuery.cs22
-rw-r--r--MediaBrowser.Model/Devices/DevicesOptions.cs24
-rw-r--r--MediaBrowser.Model/Devices/LocalFileInfo.cs11
-rw-r--r--MediaBrowser.Model/Diagnostics/IProcess.cs21
-rw-r--r--MediaBrowser.Model/Diagnostics/IProcessFactory.cs24
-rw-r--r--MediaBrowser.Model/Dlna/AudioOptions.cs87
-rw-r--r--MediaBrowser.Model/Dlna/CodecProfile.cs68
-rw-r--r--MediaBrowser.Model/Dlna/CodecType.cs9
-rw-r--r--MediaBrowser.Model/Dlna/ConditionProcessor.cs284
-rw-r--r--MediaBrowser.Model/Dlna/ContainerProfile.cs101
-rw-r--r--MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs236
-rw-r--r--MediaBrowser.Model/Dlna/DeviceIdentification.cs61
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfile.cs327
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfileInfo.cs24
-rw-r--r--MediaBrowser.Model/Dlna/DeviceProfileType.cs8
-rw-r--r--MediaBrowser.Model/Dlna/DirectPlayProfile.cs36
-rw-r--r--MediaBrowser.Model/Dlna/DlnaFlags.cs48
-rw-r--r--MediaBrowser.Model/Dlna/DlnaMaps.cs56
-rw-r--r--MediaBrowser.Model/Dlna/DlnaProfileType.cs9
-rw-r--r--MediaBrowser.Model/Dlna/EncodingContext.cs8
-rw-r--r--MediaBrowser.Model/Dlna/HeaderMatchType.cs9
-rw-r--r--MediaBrowser.Model/Dlna/HttpHeaderInfo.cs17
-rw-r--r--MediaBrowser.Model/Dlna/IDeviceDiscovery.cs11
-rw-r--r--MediaBrowser.Model/Dlna/ITranscoderSupport.cs25
-rw-r--r--MediaBrowser.Model/Dlna/MediaFormatProfile.cs113
-rw-r--r--MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs439
-rw-r--r--MediaBrowser.Model/Dlna/PlaybackErrorCode.cs10
-rw-r--r--MediaBrowser.Model/Dlna/ProfileCondition.cs38
-rw-r--r--MediaBrowser.Model/Dlna/ProfileConditionType.cs11
-rw-r--r--MediaBrowser.Model/Dlna/ProfileConditionValue.cs29
-rw-r--r--MediaBrowser.Model/Dlna/ResolutionConfiguration.cs14
-rw-r--r--MediaBrowser.Model/Dlna/ResolutionNormalizer.cs99
-rw-r--r--MediaBrowser.Model/Dlna/ResolutionOptions.cs8
-rw-r--r--MediaBrowser.Model/Dlna/ResponseProfile.cs49
-rw-r--r--MediaBrowser.Model/Dlna/SearchCriteria.cs75
-rw-r--r--MediaBrowser.Model/Dlna/SearchType.cs12
-rw-r--r--MediaBrowser.Model/Dlna/SortCriteria.cs17
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs1900
-rw-r--r--MediaBrowser.Model/Dlna/StreamInfo.cs1092
-rw-r--r--MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs22
-rw-r--r--MediaBrowser.Model/Dlna/SubtitleProfile.cs46
-rw-r--r--MediaBrowser.Model/Dlna/SubtitleStreamInfo.cs15
-rw-r--r--MediaBrowser.Model/Dlna/TranscodeSeekInfo.cs8
-rw-r--r--MediaBrowser.Model/Dlna/TranscodingProfile.cs59
-rw-r--r--MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs14
-rw-r--r--MediaBrowser.Model/Dlna/VideoOptions.cs11
-rw-r--r--MediaBrowser.Model/Dlna/XmlAttribute.cs13
-rw-r--r--MediaBrowser.Model/Drawing/DrawingUtils.cs91
-rw-r--r--MediaBrowser.Model/Drawing/ImageFormat.cs30
-rw-r--r--MediaBrowser.Model/Drawing/ImageOrientation.cs15
-rw-r--r--MediaBrowser.Model/Drawing/ImageSize.cs93
-rw-r--r--MediaBrowser.Model/Dto/BaseItemDto.cs791
-rw-r--r--MediaBrowser.Model/Dto/BaseItemPerson.cs53
-rw-r--r--MediaBrowser.Model/Dto/ChapterInfoDto.cs38
-rw-r--r--MediaBrowser.Model/Dto/GameSystemSummary.cs49
-rw-r--r--MediaBrowser.Model/Dto/IHasServerId.cs8
-rw-r--r--MediaBrowser.Model/Dto/IItemDto.cs15
-rw-r--r--MediaBrowser.Model/Dto/ImageByNameInfo.cs32
-rw-r--r--MediaBrowser.Model/Dto/ImageInfo.cs51
-rw-r--r--MediaBrowser.Model/Dto/ImageOptions.cs110
-rw-r--r--MediaBrowser.Model/Dto/ItemCounts.cs67
-rw-r--r--MediaBrowser.Model/Dto/ItemIndex.cs21
-rw-r--r--MediaBrowser.Model/Dto/MediaSourceInfo.cs236
-rw-r--r--MediaBrowser.Model/Dto/MediaSourceType.cs9
-rw-r--r--MediaBrowser.Model/Dto/MetadataEditorInfo.cs27
-rw-r--r--MediaBrowser.Model/Dto/NameIdPair.cs24
-rw-r--r--MediaBrowser.Model/Dto/NameValuePair.cs28
-rw-r--r--MediaBrowser.Model/Dto/RatingType.cs8
-rw-r--r--MediaBrowser.Model/Dto/RecommendationDto.cs15
-rw-r--r--MediaBrowser.Model/Dto/RecommendationType.cs17
-rw-r--r--MediaBrowser.Model/Dto/UserDto.cs125
-rw-r--r--MediaBrowser.Model/Dto/UserItemDataDto.cs76
-rw-r--r--MediaBrowser.Model/Entities/ChapterInfo.cs31
-rw-r--r--MediaBrowser.Model/Entities/CollectionType.cs58
-rw-r--r--MediaBrowser.Model/Entities/DisplayPreferences.cs99
-rw-r--r--MediaBrowser.Model/Entities/EmptyRequestResult.cs7
-rw-r--r--MediaBrowser.Model/Entities/ExtraType.cs16
-rw-r--r--MediaBrowser.Model/Entities/IHasProviderIds.cs16
-rw-r--r--MediaBrowser.Model/Entities/ImageType.cs58
-rw-r--r--MediaBrowser.Model/Entities/IsoType.cs17
-rw-r--r--MediaBrowser.Model/Entities/LibraryUpdateInfo.cs62
-rw-r--r--MediaBrowser.Model/Entities/LocationType.cs26
-rw-r--r--MediaBrowser.Model/Entities/MBRegistrationRecord.cs14
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs475
-rw-r--r--MediaBrowser.Model/Entities/MediaStreamType.cs25
-rw-r--r--MediaBrowser.Model/Entities/MediaType.cs30
-rw-r--r--MediaBrowser.Model/Entities/MediaUrl.cs9
-rw-r--r--MediaBrowser.Model/Entities/MetadataFields.cs46
-rw-r--r--MediaBrowser.Model/Entities/MetadataProviders.cs41
-rw-r--r--MediaBrowser.Model/Entities/PackageReviewInfo.cs38
-rw-r--r--MediaBrowser.Model/Entities/ParentalRating.cs32
-rw-r--r--MediaBrowser.Model/Entities/PersonType.cs42
-rw-r--r--MediaBrowser.Model/Entities/PluginSecurityInfo.cs21
-rw-r--r--MediaBrowser.Model/Entities/ProviderIdsExtensions.cs103
-rw-r--r--MediaBrowser.Model/Entities/ScrollDirection.cs17
-rw-r--r--MediaBrowser.Model/Entities/SeriesStatus.cs18
-rw-r--r--MediaBrowser.Model/Entities/SortOrder.cs17
-rw-r--r--MediaBrowser.Model/Entities/TrailerType.cs11
-rw-r--r--MediaBrowser.Model/Entities/UserDataSaveReason.cs34
-rw-r--r--MediaBrowser.Model/Entities/Video3DFormat.cs12
-rw-r--r--MediaBrowser.Model/Entities/VideoType.cs26
-rw-r--r--MediaBrowser.Model/Entities/VirtualFolderInfo.cs55
-rw-r--r--MediaBrowser.Model/Events/GenericEventArgs.cs33
-rw-r--r--MediaBrowser.Model/Extensions/LinqExtensions.cs97
-rw-r--r--MediaBrowser.Model/Extensions/ListHelper.cs24
-rw-r--r--MediaBrowser.Model/Extensions/StringHelper.cs57
-rw-r--r--MediaBrowser.Model/Globalization/CountryInfo.cs33
-rw-r--r--MediaBrowser.Model/Globalization/CultureDto.cs52
-rw-r--r--MediaBrowser.Model/Globalization/ILocalizationManager.cs63
-rw-r--r--MediaBrowser.Model/Globalization/LocalizatonOption.cs8
-rw-r--r--MediaBrowser.Model/IO/FileSystemEntryInfo.cs27
-rw-r--r--MediaBrowser.Model/IO/FileSystemEntryType.cs25
-rw-r--r--MediaBrowser.Model/IO/FileSystemMetadata.cs54
-rw-r--r--MediaBrowser.Model/IO/IFileSystem.cs454
-rw-r--r--MediaBrowser.Model/IO/IIsoManager.cs34
-rw-r--r--MediaBrowser.Model/IO/IIsoMount.cs22
-rw-r--r--MediaBrowser.Model/IO/IIsoMounter.cs32
-rw-r--r--MediaBrowser.Model/IO/IShortcutHandler.cs25
-rw-r--r--MediaBrowser.Model/IO/IStreamHelper.cs19
-rw-r--r--MediaBrowser.Model/IO/IZipClient.cs69
-rw-r--r--MediaBrowser.Model/IO/StreamDefaults.cs19
-rw-r--r--MediaBrowser.Model/Library/PlayAccess.cs9
-rw-r--r--MediaBrowser.Model/Library/UserViewQuery.cs33
-rw-r--r--MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs127
-rw-r--r--MediaBrowser.Model/LiveTv/ChannelType.cs19
-rw-r--r--MediaBrowser.Model/LiveTv/DayPattern.cs9
-rw-r--r--MediaBrowser.Model/LiveTv/GuideInfo.cs19
-rw-r--r--MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs104
-rw-r--r--MediaBrowser.Model/LiveTv/LiveTvInfo.cs32
-rw-r--r--MediaBrowser.Model/LiveTv/LiveTvOptions.cs88
-rw-r--r--MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs58
-rw-r--r--MediaBrowser.Model/LiveTv/LiveTvServiceStatus.cs8
-rw-r--r--MediaBrowser.Model/LiveTv/LiveTvTunerStatus.cs10
-rw-r--r--MediaBrowser.Model/LiveTv/ProgramAudio.cs12
-rw-r--r--MediaBrowser.Model/LiveTv/ProgramQuery.cs117
-rw-r--r--MediaBrowser.Model/LiveTv/RecommendedProgramQuery.cs73
-rw-r--r--MediaBrowser.Model/LiveTv/RecordingQuery.cs82
-rw-r--r--MediaBrowser.Model/LiveTv/RecordingStatus.cs14
-rw-r--r--MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs92
-rw-r--r--MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs19
-rw-r--r--MediaBrowser.Model/LiveTv/TimerInfoDto.cs43
-rw-r--r--MediaBrowser.Model/LiveTv/TimerQuery.cs23
-rw-r--r--MediaBrowser.Model/Logging/IConsoleLogger.cs7
-rw-r--r--MediaBrowser.Model/Logging/ILogManager.cs56
-rw-r--r--MediaBrowser.Model/Logging/ILogger.cs78
-rw-r--r--MediaBrowser.Model/Logging/LogHelper.cs97
-rw-r--r--MediaBrowser.Model/Logging/LogSeverity.cs30
-rw-r--r--MediaBrowser.Model/Logging/NullLogger.cs44
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj12
-rw-r--r--MediaBrowser.Model/MediaInfo/AudioCodec.cs26
-rw-r--r--MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs37
-rw-r--r--MediaBrowser.Model/MediaInfo/Container.cs9
-rw-r--r--MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs16
-rw-r--r--MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs47
-rw-r--r--MediaBrowser.Model/MediaInfo/LiveStreamResponse.cs9
-rw-r--r--MediaBrowser.Model/MediaInfo/MediaInfo.cs68
-rw-r--r--MediaBrowser.Model/MediaInfo/MediaProtocol.cs13
-rw-r--r--MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs50
-rw-r--r--MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs32
-rw-r--r--MediaBrowser.Model/MediaInfo/SubtitleFormat.cs13
-rw-r--r--MediaBrowser.Model/MediaInfo/SubtitleTrackEvent.cs11
-rw-r--r--MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs14
-rw-r--r--MediaBrowser.Model/MediaInfo/TransportStreamTimestamp.cs9
-rw-r--r--MediaBrowser.Model/MediaInfo/VideoCodec.cs14
-rw-r--r--MediaBrowser.Model/Net/EndPointInfo.cs8
-rw-r--r--MediaBrowser.Model/Net/HttpException.cs43
-rw-r--r--MediaBrowser.Model/Net/HttpResponse.cs64
-rw-r--r--MediaBrowser.Model/Net/IAcceptSocket.cs15
-rw-r--r--MediaBrowser.Model/Net/ISocket.cs28
-rw-r--r--MediaBrowser.Model/Net/ISocketFactory.cs49
-rw-r--r--MediaBrowser.Model/Net/IpAddressInfo.cs37
-rw-r--r--MediaBrowser.Model/Net/IpEndPointInfo.cs30
-rw-r--r--MediaBrowser.Model/Net/MimeTypes.cs346
-rw-r--r--MediaBrowser.Model/Net/NetworkShare.cs31
-rw-r--r--MediaBrowser.Model/Net/NetworkShareType.cs30
-rw-r--r--MediaBrowser.Model/Net/SocketReceiveResult.cs25
-rw-r--r--MediaBrowser.Model/Net/WebSocketMessage.cs24
-rw-r--r--MediaBrowser.Model/News/INewsService.cs17
-rw-r--r--MediaBrowser.Model/News/NewsItem.cs14
-rw-r--r--MediaBrowser.Model/News/NewsQuery.cs9
-rw-r--r--MediaBrowser.Model/Notifications/NotificationLevel.cs10
-rw-r--r--MediaBrowser.Model/Notifications/NotificationOption.cs56
-rw-r--r--MediaBrowser.Model/Notifications/NotificationOptions.cs132
-rw-r--r--MediaBrowser.Model/Notifications/NotificationRequest.cs35
-rw-r--r--MediaBrowser.Model/Notifications/NotificationServiceInfo.cs8
-rw-r--r--MediaBrowser.Model/Notifications/NotificationType.cs24
-rw-r--r--MediaBrowser.Model/Notifications/NotificationTypeInfo.cs29
-rw-r--r--MediaBrowser.Model/Notifications/SendToUserType.cs9
-rw-r--r--MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs21
-rw-r--r--MediaBrowser.Model/Playlists/PlaylistCreationResult.cs8
-rw-r--r--MediaBrowser.Model/Playlists/PlaylistItemQuery.cs37
-rw-r--r--MediaBrowser.Model/Plugins/BasePluginConfiguration.cs10
-rw-r--r--MediaBrowser.Model/Plugins/IHasWebPages.cs9
-rw-r--r--MediaBrowser.Model/Plugins/PluginInfo.cs45
-rw-r--r--MediaBrowser.Model/Plugins/PluginPageInfo.cs17
-rw-r--r--MediaBrowser.Model/Properties/AssemblyInfo.cs23
-rw-r--r--MediaBrowser.Model/Providers/ExternalIdInfo.cs24
-rw-r--r--MediaBrowser.Model/Providers/ExternalUrl.cs17
-rw-r--r--MediaBrowser.Model/Providers/ImageProviderInfo.cs24
-rw-r--r--MediaBrowser.Model/Providers/RemoteImageInfo.cs71
-rw-r--r--MediaBrowser.Model/Providers/RemoteImageQuery.cs15
-rw-r--r--MediaBrowser.Model/Providers/RemoteImageResult.cs28
-rw-r--r--MediaBrowser.Model/Providers/RemoteSearchResult.cs46
-rw-r--r--MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs19
-rw-r--r--MediaBrowser.Model/Providers/SubtitleOptions.cs27
-rw-r--r--MediaBrowser.Model/Providers/SubtitleProviderInfo.cs8
-rw-r--r--MediaBrowser.Model/Querying/AllThemeMediaResult.cs20
-rw-r--r--MediaBrowser.Model/Querying/EpisodeQuery.cs62
-rw-r--r--MediaBrowser.Model/Querying/ItemCountsQuery.cs21
-rw-r--r--MediaBrowser.Model/Querying/ItemFields.cs222
-rw-r--r--MediaBrowser.Model/Querying/ItemFilter.cs46
-rw-r--r--MediaBrowser.Model/Querying/ItemSortBy.cs81
-rw-r--r--MediaBrowser.Model/Querying/LatestItemsQuery.cs76
-rw-r--r--MediaBrowser.Model/Querying/MovieRecommendationQuery.cs39
-rw-r--r--MediaBrowser.Model/Querying/NextUpQuery.cs67
-rw-r--r--MediaBrowser.Model/Querying/QueryFilters.cs32
-rw-r--r--MediaBrowser.Model/Querying/QueryResult.cs23
-rw-r--r--MediaBrowser.Model/Querying/SessionQuery.cs14
-rw-r--r--MediaBrowser.Model/Querying/SimilarItemsQuery.cs29
-rw-r--r--MediaBrowser.Model/Querying/ThemeMediaResult.cs17
-rw-r--r--MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs57
-rw-r--r--MediaBrowser.Model/Querying/UserQuery.cs9
-rw-r--r--MediaBrowser.Model/Reflection/IAssemblyInfo.cs14
-rw-r--r--MediaBrowser.Model/Search/SearchHint.cs158
-rw-r--r--MediaBrowser.Model/Search/SearchHintResult.cs21
-rw-r--r--MediaBrowser.Model/Search/SearchQuery.cs65
-rw-r--r--MediaBrowser.Model/Serialization/IJsonSerializer.cs91
-rw-r--r--MediaBrowser.Model/Serialization/IXmlSerializer.cs46
-rw-r--r--MediaBrowser.Model/Serialization/IgnoreDataMemberAttribute.cs12
-rw-r--r--MediaBrowser.Model/Services/ApiMemberAttribute.cs61
-rw-r--r--MediaBrowser.Model/Services/HttpUtility.cs923
-rw-r--r--MediaBrowser.Model/Services/IAsyncStreamWriter.cs11
-rw-r--r--MediaBrowser.Model/Services/IHasHeaders.cs9
-rw-r--r--MediaBrowser.Model/Services/IHasRequestFilter.cs21
-rw-r--r--MediaBrowser.Model/Services/IHttpRequest.cs45
-rw-r--r--MediaBrowser.Model/Services/IHttpResponse.cs24
-rw-r--r--MediaBrowser.Model/Services/IHttpResult.cs36
-rw-r--r--MediaBrowser.Model/Services/IRequest.cs159
-rw-r--r--MediaBrowser.Model/Services/IRequiresRequestStream.cs12
-rw-r--r--MediaBrowser.Model/Services/IService.cs12
-rw-r--r--MediaBrowser.Model/Services/IStreamWriter.cs9
-rw-r--r--MediaBrowser.Model/Services/QueryParamCollection.cs229
-rw-r--r--MediaBrowser.Model/Services/RouteAttribute.cs148
-rw-r--r--MediaBrowser.Model/Session/BrowseRequest.cs27
-rw-r--r--MediaBrowser.Model/Session/ClientCapabilities.cs33
-rw-r--r--MediaBrowser.Model/Session/GeneralCommand.cs19
-rw-r--r--MediaBrowser.Model/Session/GeneralCommandType.cs46
-rw-r--r--MediaBrowser.Model/Session/MessageCommand.cs12
-rw-r--r--MediaBrowser.Model/Session/PlayCommand.cs29
-rw-r--r--MediaBrowser.Model/Session/PlayMethod.cs9
-rw-r--r--MediaBrowser.Model/Session/PlayRequest.cs43
-rw-r--r--MediaBrowser.Model/Session/PlaybackProgressInfo.cs119
-rw-r--r--MediaBrowser.Model/Session/PlaybackStartInfo.cs10
-rw-r--r--MediaBrowser.Model/Session/PlaybackStopInfo.cs57
-rw-r--r--MediaBrowser.Model/Session/PlayerStateInfo.cs65
-rw-r--r--MediaBrowser.Model/Session/PlaystateCommand.cs43
-rw-r--r--MediaBrowser.Model/Session/PlaystateRequest.cs15
-rw-r--r--MediaBrowser.Model/Session/SessionInfoDto.cs117
-rw-r--r--MediaBrowser.Model/Session/SessionUserInfo.cs21
-rw-r--r--MediaBrowser.Model/Session/TranscodingInfo.cs55
-rw-r--r--MediaBrowser.Model/Session/UserDataChangeInfo.cs23
-rw-r--r--MediaBrowser.Model/Sync/SyncCategory.cs19
-rw-r--r--MediaBrowser.Model/Sync/SyncJob.cs114
-rw-r--r--MediaBrowser.Model/Sync/SyncJobStatus.cs14
-rw-r--r--MediaBrowser.Model/Sync/SyncTarget.cs17
-rw-r--r--MediaBrowser.Model/System/Architecture.cs10
-rw-r--r--MediaBrowser.Model/System/IEnvironmentInfo.cs24
-rw-r--r--MediaBrowser.Model/System/IPowerManagement.cs11
-rw-r--r--MediaBrowser.Model/System/ISystemEvents.cs12
-rw-r--r--MediaBrowser.Model/System/LogFile.cs31
-rw-r--r--MediaBrowser.Model/System/PublicSystemInfo.cs41
-rw-r--r--MediaBrowser.Model/System/SystemInfo.cs140
-rw-r--r--MediaBrowser.Model/System/WakeOnLanInfo.cs13
-rw-r--r--MediaBrowser.Model/Tasks/IConfigurableScheduledTask.cs18
-rw-r--r--MediaBrowser.Model/Tasks/IScheduledTask.cs47
-rw-r--r--MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs76
-rw-r--r--MediaBrowser.Model/Tasks/ITaskManager.cs78
-rw-r--r--MediaBrowser.Model/Tasks/ITaskTrigger.cs32
-rw-r--r--MediaBrowser.Model/Tasks/ScheduledTaskHelpers.cs44
-rw-r--r--MediaBrowser.Model/Tasks/SystemEvent.cs14
-rw-r--r--MediaBrowser.Model/Tasks/TaskCompletionEventArgs.cs11
-rw-r--r--MediaBrowser.Model/Tasks/TaskCompletionStatus.cs29
-rw-r--r--MediaBrowser.Model/Tasks/TaskInfo.cs78
-rw-r--r--MediaBrowser.Model/Tasks/TaskOptions.cs8
-rw-r--r--MediaBrowser.Model/Tasks/TaskResult.cs58
-rw-r--r--MediaBrowser.Model/Tasks/TaskState.cs22
-rw-r--r--MediaBrowser.Model/Tasks/TaskTriggerInfo.cs52
-rw-r--r--MediaBrowser.Model/Text/ITextEncoding.cs14
-rw-r--r--MediaBrowser.Model/Threading/ITimer.cs10
-rw-r--r--MediaBrowser.Model/Threading/ITimerFactory.cs10
-rw-r--r--MediaBrowser.Model/Updates/CheckForUpdateResult.cs30
-rw-r--r--MediaBrowser.Model/Updates/InstallationInfo.cs46
-rw-r--r--MediaBrowser.Model/Updates/PackageInfo.cs176
-rw-r--r--MediaBrowser.Model/Updates/PackageTargetSystem.cs21
-rw-r--r--MediaBrowser.Model/Updates/PackageVersionClass.cs21
-rw-r--r--MediaBrowser.Model/Updates/PackageVersionInfo.cs95
-rw-r--r--MediaBrowser.Model/Users/ForgotPasswordAction.cs10
-rw-r--r--MediaBrowser.Model/Users/ForgotPasswordResult.cs23
-rw-r--r--MediaBrowser.Model/Users/PinRedeemResult.cs17
-rw-r--r--MediaBrowser.Model/Users/UserAction.cs15
-rw-r--r--MediaBrowser.Model/Users/UserActionType.cs8
-rw-r--r--MediaBrowser.Model/Users/UserPolicy.cs121
-rw-r--r--MediaBrowser.Model/Xml/IXmlReaderSettingsFactory.cs9
-rw-r--r--MediaBrowser.Server.Mono/MonoAppHost.cs5
648 files changed, 50005 insertions, 123 deletions
diff --git a/Emby.Notifications/Notifications.cs b/Emby.Notifications/Notifications.cs
index 64863eb39..3cadad6c8 100644
--- a/Emby.Notifications/Notifications.cs
+++ b/Emby.Notifications/Notifications.cs
@@ -231,7 +231,7 @@ namespace Emby.Notifications
}
}
- var hasSeries = item as IHasSeriesName;
+ var hasSeries = item as IHasSeries;
if (hasSeries != null)
{
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 011f3a0fb..c6cfe7214 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -318,7 +318,6 @@ namespace Emby.Server.Implementations
private IMediaEncoder MediaEncoder { get; set; }
private ISubtitleEncoder SubtitleEncoder { get; set; }
- private IConnectManager ConnectManager { get; set; }
private ISessionManager SessionManager { get; set; }
private ILiveTvManager LiveTvManager { get; set; }
@@ -839,8 +838,6 @@ namespace Emby.Server.Implementations
}
}
- protected abstract IConnectManager CreateConnectManager();
-
protected virtual IHttpClient CreateHttpClient()
{
return new HttpClientManager.HttpClientManager(ApplicationPaths, LogManager.GetLogger("HttpClient"), FileSystemManager, GetDefaultUserAgent);
@@ -947,7 +944,7 @@ namespace Emby.Server.Implementations
AuthenticationRepository = GetAuthenticationRepository();
RegisterSingleInstance(AuthenticationRepository);
- UserManager = new UserManager(LogManager.GetLogger("UserManager"), ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, () => ConnectManager, this, JsonSerializer, FileSystemManager, CryptographyProvider);
+ UserManager = new UserManager(LogManager.GetLogger("UserManager"), ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager, CryptographyProvider);
RegisterSingleInstance(UserManager);
LibraryManager = new LibraryManager(this, Logger, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager);
@@ -986,9 +983,6 @@ namespace Emby.Server.Implementations
var encryptionManager = new EncryptionManager();
RegisterSingleInstance<IEncryptionManager>(encryptionManager);
- ConnectManager = CreateConnectManager();
- RegisterSingleInstance(ConnectManager);
-
DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager, LogManager.GetLogger("DeviceManager"), NetworkManager);
RegisterSingleInstance(DeviceManager);
@@ -1045,11 +1039,11 @@ namespace Emby.Server.Implementations
RegisterSingleInstance(activityLogRepo);
RegisterSingleInstance<IActivityManager>(new ActivityManager(LogManager.GetLogger("ActivityManager"), activityLogRepo, UserManager));
- var authContext = new AuthorizationContext(AuthenticationRepository, ConnectManager, UserManager);
+ var authContext = new AuthorizationContext(AuthenticationRepository, UserManager);
RegisterSingleInstance<IAuthorizationContext>(authContext);
RegisterSingleInstance<ISessionContext>(new SessionContext(UserManager, authContext, SessionManager));
- AuthService = new AuthService(UserManager, authContext, ServerConfigurationManager, ConnectManager, SessionManager, NetworkManager);
+ AuthService = new AuthService(UserManager, authContext, ServerConfigurationManager, SessionManager, NetworkManager);
RegisterSingleInstance<IAuthService>(AuthService);
SubtitleEncoder = new MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder(LibraryManager, LogManager.GetLogger("SubtitleEncoder"), ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager, ProcessFactory, TextEncoding);
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index e832c7c6f..e61c5e283 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -1086,7 +1086,7 @@ namespace Emby.Server.Implementations.Channels
}
item.ParentId = parentFolderId;
- var hasSeries = item as IHasSeriesName;
+ var hasSeries = item as IHasSeries;
if (hasSeries != null)
{
if (!string.Equals(hasSeries.SeriesName, info.SeriesName, StringComparison.OrdinalIgnoreCase))
@@ -1215,4 +1215,4 @@ namespace Emby.Server.Implementations.Channels
return result;
}
}
-} \ No newline at end of file
+}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index bde28c923..c9495c574 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -942,7 +942,7 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.TryBind("@Album", item.Album);
saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
- var hasSeriesName = item as IHasSeriesName;
+ var hasSeriesName = item as IHasSeries;
if (hasSeriesName != null)
{
saveItemStatement.TryBind("@SeriesName", hasSeriesName.SeriesName);
@@ -1757,7 +1757,7 @@ namespace Emby.Server.Implementations.Data
}
index++;
- var hasSeriesName = item as IHasSeriesName;
+ var hasSeriesName = item as IHasSeries;
if (hasSeriesName != null)
{
if (!reader.IsDBNull(index))
@@ -6441,4 +6441,4 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
}
-} \ No newline at end of file
+}
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index 9dfb8cf03..e153d6f71 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -17,19 +17,17 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
private readonly IServerConfigurationManager _config;
- public AuthService(IUserManager userManager, IAuthorizationContext authorizationContext, IServerConfigurationManager config, IConnectManager connectManager, ISessionManager sessionManager, INetworkManager networkManager)
+ public AuthService(IUserManager userManager, IAuthorizationContext authorizationContext, IServerConfigurationManager config, ISessionManager sessionManager, INetworkManager networkManager)
{
AuthorizationContext = authorizationContext;
_config = config;
SessionManager = sessionManager;
- ConnectManager = connectManager;
UserManager = userManager;
NetworkManager = networkManager;
}
public IUserManager UserManager { get; private set; }
public IAuthorizationContext AuthorizationContext { get; private set; }
- public IConnectManager ConnectManager { get; private set; }
public ISessionManager SessionManager { get; private set; }
public INetworkManager NetworkManager { get; private set; }
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
index 75dfa95d9..c3e2d3170 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
@@ -13,13 +13,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
public class AuthorizationContext : IAuthorizationContext
{
private readonly IAuthenticationRepository _authRepo;
- private readonly IConnectManager _connectManager;
private readonly IUserManager _userManager;
- public AuthorizationContext(IAuthenticationRepository authRepo, IConnectManager connectManager, IUserManager userManager)
+ public AuthorizationContext(IAuthenticationRepository authRepo, IUserManager userManager)
{
_authRepo = authRepo;
- _connectManager = connectManager;
_userManager = userManager;
}
diff --git a/Emby.Server.Implementations/Library/ConnectManager.cs b/Emby.Server.Implementations/Library/ConnectManager.cs
deleted file mode 100644
index 5df45aa44..000000000
--- a/Emby.Server.Implementations/Library/ConnectManager.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using MediaBrowser.Common.Events;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Connect;
-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.Providers;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Connect;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Users;
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Cryptography;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Controller.Plugins;
-
-namespace Emby.Server.Implementations.Library
-{
- public class ConnectManager : IConnectManager
- {
- public ConnectManager()
- {
- }
-
- }
-}
diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs
index 317b64693..da80a4824 100644
--- a/Emby.Server.Implementations/Library/UserManager.cs
+++ b/Emby.Server.Implementations/Library/UserManager.cs
@@ -74,7 +74,6 @@ namespace Emby.Server.Implementations.Library
private readonly Func<IImageProcessor> _imageProcessorFactory;
private readonly Func<IDtoService> _dtoServiceFactory;
- private readonly Func<IConnectManager> _connectFactory;
private readonly IServerApplicationHost _appHost;
private readonly IFileSystem _fileSystem;
private readonly ICryptoProvider _cryptographyProvider;
@@ -82,7 +81,7 @@ namespace Emby.Server.Implementations.Library
private IAuthenticationProvider[] _authenticationProviders;
private DefaultAuthenticationProvider _defaultAuthenticationProvider;
- public UserManager(ILogger logger, IServerConfigurationManager configurationManager, IUserRepository userRepository, IXmlSerializer xmlSerializer, INetworkManager networkManager, Func<IImageProcessor> imageProcessorFactory, Func<IDtoService> dtoServiceFactory, Func<IConnectManager> connectFactory, IServerApplicationHost appHost, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ICryptoProvider cryptographyProvider)
+ public UserManager(ILogger logger, IServerConfigurationManager configurationManager, IUserRepository userRepository, IXmlSerializer xmlSerializer, INetworkManager networkManager, Func<IImageProcessor> imageProcessorFactory, Func<IDtoService> dtoServiceFactory, IServerApplicationHost appHost, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ICryptoProvider cryptographyProvider)
{
_logger = logger;
UserRepository = userRepository;
@@ -90,7 +89,6 @@ namespace Emby.Server.Implementations.Library
_networkManager = networkManager;
_imageProcessorFactory = imageProcessorFactory;
_dtoServiceFactory = dtoServiceFactory;
- _connectFactory = connectFactory;
_appHost = appHost;
_jsonSerializer = jsonSerializer;
_fileSystem = fileSystem;
diff --git a/MediaBrowser.Api/PackageService.cs b/MediaBrowser.Api/PackageService.cs
index 366d1318b..0b1dc083d 100644
--- a/MediaBrowser.Api/PackageService.cs
+++ b/MediaBrowser.Api/PackageService.cs
@@ -62,21 +62,6 @@ namespace MediaBrowser.Api
}
/// <summary>
- /// Class GetPackageVersionUpdates
- /// </summary>
- [Route("/Packages/Updates", "GET", Summary = "Gets available package updates for currently installed packages")]
- [Authenticated(Roles = "Admin")]
- public class GetPackageVersionUpdates : IReturn<PackageVersionInfo[]>
- {
- /// <summary>
- /// Gets or sets the name.
- /// </summary>
- /// <value>The name.</value>
- [ApiMember(Name = "PackageType", Description = "Package type filter (System/UserInstalled)", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string PackageType { get; set; }
- }
-
- /// <summary>
/// Class InstallPackage
/// </summary>
[Route("/Packages/Installed/{Name}", "POST", Summary = "Installs a package")]
@@ -146,30 +131,7 @@ namespace MediaBrowser.Api
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
- public async Task<object> Get(GetPackageVersionUpdates request)
- {
- PackageVersionInfo[] result = null;
-
- if (string.Equals(request.PackageType, "UserInstalled", StringComparison.OrdinalIgnoreCase) || string.Equals(request.PackageType, "All", StringComparison.OrdinalIgnoreCase))
- {
- result = (await _installationManager.GetAvailablePluginUpdates(_appHost.ApplicationVersion, false, CancellationToken.None).ConfigureAwait(false)).ToArray();
- }
-
- else if (string.Equals(request.PackageType, "System", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(request.PackageType, "All", StringComparison.OrdinalIgnoreCase))
- {
- var updateCheckResult = await _appHost
- .CheckForApplicationUpdate(CancellationToken.None, new SimpleProgress<double>()).ConfigureAwait(false);
-
- if (updateCheckResult.IsUpdateAvailable)
- {
- result = new PackageVersionInfo[] { updateCheckResult.Package };
- }
- }
-
- return ToOptimizedResult(result ?? new PackageVersionInfo[] { });
- }
-
+ ///
/// <summary>
/// Gets the specified request.
/// </summary>
diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs
index ac20b5e33..db5d256c4 100644
--- a/MediaBrowser.Api/Playback/StreamState.cs
+++ b/MediaBrowser.Api/Playback/StreamState.cs
@@ -148,7 +148,6 @@ namespace MediaBrowser.Api.Playback
DisposeTranscodingThrottler();
DisposeLiveStream();
DisposeLogStream();
- DisposeIsoMount();
TranscodingJob = null;
}
@@ -251,9 +250,9 @@ namespace MediaBrowser.Api.Playback
public DeviceProfile DeviceProfile { get; set; }
public TranscodingJob TranscodingJob;
- public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
+ public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float framerate, double? percentComplete, long bytesTranscoded, int? bitRate)
{
- ApiEntryPoint.Instance.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate);
+ ApiEntryPoint.Instance.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, 0, percentComplete, 0, bitRate);
}
}
}
diff --git a/MediaBrowser.Api/SearchService.cs b/MediaBrowser.Api/SearchService.cs
index 2e363136b..0473dd924 100644
--- a/MediaBrowser.Api/SearchService.cs
+++ b/MediaBrowser.Api/SearchService.cs
@@ -230,7 +230,7 @@ namespace MediaBrowser.Api
result.StartDate = program.StartDate;
}
- var hasSeries = item as IHasSeriesName;
+ var hasSeries = item as IHasSeries;
if (hasSeries != null)
{
result.Series = hasSeries.SeriesName;
diff --git a/MediaBrowser.Api/StartupWizardService.cs b/MediaBrowser.Api/StartupWizardService.cs
index b4891ba1a..7fccbcc9b 100644
--- a/MediaBrowser.Api/StartupWizardService.cs
+++ b/MediaBrowser.Api/StartupWizardService.cs
@@ -51,16 +51,14 @@ namespace MediaBrowser.Api
private readonly IServerConfigurationManager _config;
private readonly IServerApplicationHost _appHost;
private readonly IUserManager _userManager;
- private readonly IConnectManager _connectManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IHttpClient _httpClient;
- public StartupWizardService(IServerConfigurationManager config, IHttpClient httpClient, IServerApplicationHost appHost, IUserManager userManager, IConnectManager connectManager, IMediaEncoder mediaEncoder)
+ public StartupWizardService(IServerConfigurationManager config, IHttpClient httpClient, IServerApplicationHost appHost, IUserManager userManager, IMediaEncoder mediaEncoder)
{
_config = config;
_appHost = appHost;
_userManager = userManager;
- _connectManager = connectManager;
_mediaEncoder = mediaEncoder;
_httpClient = httpClient;
}
diff --git a/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs b/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs
new file mode 100644
index 000000000..310e2aa63
--- /dev/null
+++ b/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace MediaBrowser.Common.Configuration
+{
+ public class ConfigurationUpdateEventArgs : EventArgs
+ {
+ /// <summary>
+ /// Gets or sets the key.
+ /// </summary>
+ /// <value>The key.</value>
+ public string Key { get; set; }
+ /// <summary>
+ /// Gets or sets the new configuration.
+ /// </summary>
+ /// <value>The new configuration.</value>
+ public object NewConfiguration { get; set; }
+ }
+}
diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs
new file mode 100644
index 000000000..c6256bc5a
--- /dev/null
+++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs
@@ -0,0 +1,84 @@
+
+namespace MediaBrowser.Common.Configuration
+{
+ /// <summary>
+ /// Interface IApplicationPaths
+ /// </summary>
+ public interface IApplicationPaths
+ {
+ /// <summary>
+ /// Gets the path to the program data folder
+ /// </summary>
+ /// <value>The program data path.</value>
+ string ProgramDataPath { get; }
+
+ /// <summary>
+ /// Gets the path to the program system folder
+ /// </summary>
+ /// <value>The program data path.</value>
+ string ProgramSystemPath { get; }
+
+ /// <summary>
+ /// Gets the folder path to the data directory
+ /// </summary>
+ /// <value>The data directory.</value>
+ string DataPath { get; }
+
+ /// <summary>
+ /// Gets the image cache path.
+ /// </summary>
+ /// <value>The image cache path.</value>
+ string ImageCachePath { get; }
+
+ /// <summary>
+ /// Gets the path to the plugin directory
+ /// </summary>
+ /// <value>The plugins path.</value>
+ string PluginsPath { get; }
+
+ /// <summary>
+ /// Gets the path to the plugin configurations directory
+ /// </summary>
+ /// <value>The plugin configurations path.</value>
+ string PluginConfigurationsPath { get; }
+
+ /// <summary>
+ /// Gets the path to where temporary update files will be stored
+ /// </summary>
+ /// <value>The plugin configurations path.</value>
+ string TempUpdatePath { get; }
+
+ /// <summary>
+ /// Gets the path to the log directory
+ /// </summary>
+ /// <value>The log directory path.</value>
+ string LogDirectoryPath { get; }
+
+ /// <summary>
+ /// Gets the path to the application configuration root directory
+ /// </summary>
+ /// <value>The configuration directory path.</value>
+ string ConfigurationDirectoryPath { get; }
+
+ /// <summary>
+ /// Gets the path to the system configuration file
+ /// </summary>
+ /// <value>The system configuration file path.</value>
+ string SystemConfigurationFilePath { get; }
+
+ /// <summary>
+ /// Gets the folder path to the cache directory
+ /// </summary>
+ /// <value>The cache directory.</value>
+ string CachePath { get; }
+
+ /// <summary>
+ /// Gets the folder path to the temp directory within the cache folder
+ /// </summary>
+ /// <value>The temp directory.</value>
+ string TempDirectory { get; }
+
+ string VirtualDataPath { get; }
+ }
+
+}
diff --git a/MediaBrowser.Common/Configuration/IConfigurationFactory.cs b/MediaBrowser.Common/Configuration/IConfigurationFactory.cs
new file mode 100644
index 000000000..6ed638536
--- /dev/null
+++ b/MediaBrowser.Common/Configuration/IConfigurationFactory.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Common.Configuration
+{
+ public interface IConfigurationFactory
+ {
+ IEnumerable<ConfigurationStore> GetConfigurations();
+ }
+
+ public class ConfigurationStore
+ {
+ public string Key { get; set; }
+
+ public Type ConfigurationType { get; set; }
+ }
+
+ public interface IValidatingConfiguration
+ {
+ void Validate(object oldConfig, object newConfig);
+ }
+}
diff --git a/MediaBrowser.Common/Configuration/IConfigurationManager.cs b/MediaBrowser.Common/Configuration/IConfigurationManager.cs
new file mode 100644
index 000000000..d826a3ee7
--- /dev/null
+++ b/MediaBrowser.Common/Configuration/IConfigurationManager.cs
@@ -0,0 +1,82 @@
+using MediaBrowser.Model.Configuration;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Common.Configuration
+{
+ public interface IConfigurationManager
+ {
+ /// <summary>
+ /// Occurs when [configuration updating].
+ /// </summary>
+ event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating;
+
+ /// <summary>
+ /// Occurs when [configuration updated].
+ /// </summary>
+ event EventHandler<EventArgs> ConfigurationUpdated;
+
+ /// <summary>
+ /// Occurs when [named configuration updated].
+ /// </summary>
+ event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated;
+
+ /// <summary>
+ /// Gets or sets the application paths.
+ /// </summary>
+ /// <value>The application paths.</value>
+ IApplicationPaths CommonApplicationPaths { get; }
+
+ /// <summary>
+ /// Gets the configuration.
+ /// </summary>
+ /// <value>The configuration.</value>
+ BaseApplicationConfiguration CommonConfiguration { get; }
+
+ /// <summary>
+ /// Saves the configuration.
+ /// </summary>
+ void SaveConfiguration();
+
+ /// <summary>
+ /// Replaces the configuration.
+ /// </summary>
+ /// <param name="newConfiguration">The new configuration.</param>
+ void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration);
+
+ /// <summary>
+ /// Gets the configuration.
+ /// </summary>
+ /// <param name="key">The key.</param>
+ /// <returns>System.Object.</returns>
+ object GetConfiguration(string key);
+
+ /// <summary>
+ /// Gets the type of the configuration.
+ /// </summary>
+ /// <param name="key">The key.</param>
+ /// <returns>Type.</returns>
+ Type GetConfigurationType(string key);
+
+ /// <summary>
+ /// Saves the configuration.
+ /// </summary>
+ /// <param name="key">The key.</param>
+ /// <param name="configuration">The configuration.</param>
+ void SaveConfiguration(string key, object configuration);
+
+ /// <summary>
+ /// Adds the parts.
+ /// </summary>
+ /// <param name="factories">The factories.</param>
+ void AddParts(IEnumerable<IConfigurationFactory> factories);
+ }
+
+ public static class ConfigurationManagerExtensions
+ {
+ public static T GetConfiguration<T>(this IConfigurationManager manager, string key)
+ {
+ return (T)manager.GetConfiguration(key);
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Events/EventHelper.cs b/MediaBrowser.Common/Events/EventHelper.cs
new file mode 100644
index 000000000..2bb52f0ae
--- /dev/null
+++ b/MediaBrowser.Common/Events/EventHelper.cs
@@ -0,0 +1,108 @@
+using MediaBrowser.Model.Logging;
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Events
+{
+ /// <summary>
+ /// Class EventHelper
+ /// </summary>
+ public static class EventHelper
+ {
+ /// <summary>
+ /// Fires the event.
+ /// </summary>
+ /// <param name="handler">The handler.</param>
+ /// <param name="sender">The sender.</param>
+ /// <param name="args">The <see cref="EventArgs" /> instance containing the event data.</param>
+ /// <param name="logger">The logger.</param>
+ public static void QueueEventIfNotNull(EventHandler handler, object sender, EventArgs args, ILogger logger)
+ {
+ if (handler != null)
+ {
+ Task.Run(() =>
+ {
+ try
+ {
+ handler(sender, args);
+ }
+ catch (Exception ex)
+ {
+ logger.ErrorException("Error in event handler", ex);
+ }
+ });
+ }
+ }
+
+ /// <summary>
+ /// Queues the event.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="handler">The handler.</param>
+ /// <param name="sender">The sender.</param>
+ /// <param name="args">The args.</param>
+ /// <param name="logger">The logger.</param>
+ public static void QueueEventIfNotNull<T>(EventHandler<T> handler, object sender, T args, ILogger logger)
+ {
+ if (handler != null)
+ {
+ Task.Run(() =>
+ {
+ try
+ {
+ handler(sender, args);
+ }
+ catch (Exception ex)
+ {
+ logger.ErrorException("Error in event handler", ex);
+ }
+ });
+ }
+ }
+
+ /// <summary>
+ /// Fires the event.
+ /// </summary>
+ /// <param name="handler">The handler.</param>
+ /// <param name="sender">The sender.</param>
+ /// <param name="args">The <see cref="EventArgs" /> instance containing the event data.</param>
+ /// <param name="logger">The logger.</param>
+ public static void FireEventIfNotNull(EventHandler handler, object sender, EventArgs args, ILogger logger)
+ {
+ if (handler != null)
+ {
+ try
+ {
+ handler(sender, args);
+ }
+ catch (Exception ex)
+ {
+ logger.ErrorException("Error in event handler", ex);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Fires the event.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="handler">The handler.</param>
+ /// <param name="sender">The sender.</param>
+ /// <param name="args">The args.</param>
+ /// <param name="logger">The logger.</param>
+ public static void FireEventIfNotNull<T>(EventHandler<T> handler, object sender, T args, ILogger logger)
+ {
+ if (handler != null)
+ {
+ try
+ {
+ handler(sender, args);
+ }
+ catch (Exception ex)
+ {
+ logger.ErrorException("Error in event handler", ex);
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs
new file mode 100644
index 000000000..d7f4424fa
--- /dev/null
+++ b/MediaBrowser.Common/Extensions/BaseExtensions.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Globalization;
+using System.Text.RegularExpressions;
+using MediaBrowser.Model.Cryptography;
+
+namespace MediaBrowser.Common.Extensions
+{
+ /// <summary>
+ /// Class BaseExtensions
+ /// </summary>
+ public static class BaseExtensions
+ {
+ public static ICryptoProvider CryptographyProvider { get; set; }
+
+ /// <summary>
+ /// Strips the HTML.
+ /// </summary>
+ /// <param name="htmlString">The HTML string.</param>
+ /// <returns>System.String.</returns>
+ public static string StripHtml(this string htmlString)
+ {
+ // http://stackoverflow.com/questions/1349023/how-can-i-strip-html-from-text-in-net
+ const string pattern = @"<(.|\n)*?>";
+
+ return Regex.Replace(htmlString, pattern, string.Empty).Trim();
+ }
+
+ /// <summary>
+ /// Gets the M d5.
+ /// </summary>
+ /// <param name="str">The STR.</param>
+ /// <returns>Guid.</returns>
+ public static Guid GetMD5(this string str)
+ {
+ return CryptographyProvider.GetMD5(str);
+ }
+
+ /// <summary>
+ /// Gets the MB id.
+ /// </summary>
+ /// <param name="str">The STR.</param>
+ /// <param name="type">The type.</param>
+ /// <returns>Guid.</returns>
+ /// <exception cref="System.ArgumentNullException">type</exception>
+ [Obsolete("Use LibraryManager.GetNewItemId")]
+ public static Guid GetMBId(this string str, Type type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ var key = type.FullName + str.ToLower();
+
+ return key.GetMD5();
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs
new file mode 100644
index 000000000..89e20b1b4
--- /dev/null
+++ b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs
@@ -0,0 +1,63 @@
+using System;
+
+namespace MediaBrowser.Common.Extensions
+{
+ /// <summary>
+ /// Class ResourceNotFoundException
+ /// </summary>
+ public class ResourceNotFoundException : Exception
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ResourceNotFoundException" /> class.
+ /// </summary>
+ public ResourceNotFoundException()
+ {
+
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ResourceNotFoundException" /> class.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ public ResourceNotFoundException(string message)
+ : base(message)
+ {
+
+ }
+ }
+
+ public class RemoteServiceUnavailableException : Exception
+ {
+ public RemoteServiceUnavailableException()
+ {
+
+ }
+
+ public RemoteServiceUnavailableException(string message)
+ : base(message)
+ {
+
+ }
+ }
+
+ public class RateLimitExceededException : Exception
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RateLimitExceededException" /> class.
+ /// </summary>
+ public RateLimitExceededException()
+ {
+
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RateLimitExceededException" /> class.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ public RateLimitExceededException(string message)
+ : base(message)
+ {
+
+ }
+ }
+}
diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs
new file mode 100644
index 000000000..32b942b60
--- /dev/null
+++ b/MediaBrowser.Common/IApplicationHost.cs
@@ -0,0 +1,147 @@
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Updates;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common
+{
+ /// <summary>
+ /// An interface to be implemented by the applications hosting a kernel
+ /// </summary>
+ public interface IApplicationHost
+ {
+ /// <summary>
+ /// Gets the display name of the operating system.
+ /// </summary>
+ /// <value>The display name of the operating system.</value>
+ string OperatingSystemDisplayName { get; }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ /// <summary>
+ /// Gets the device identifier.
+ /// </summary>
+ /// <value>The device identifier.</value>
+ string SystemId { get; }
+
+ /// <summary>
+ /// Occurs when [application updated].
+ /// </summary>
+ event EventHandler<GenericEventArgs<PackageVersionInfo>> ApplicationUpdated;
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has pending kernel reload.
+ /// </summary>
+ /// <value><c>true</c> if this instance has pending kernel reload; otherwise, <c>false</c>.</value>
+ bool HasPendingRestart { get; }
+
+ bool IsShuttingDown { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance can self restart.
+ /// </summary>
+ /// <value><c>true</c> if this instance can self restart; otherwise, <c>false</c>.</value>
+ bool CanSelfRestart { get; }
+
+ /// <summary>
+ /// Occurs when [has pending restart changed].
+ /// </summary>
+ event EventHandler HasPendingRestartChanged;
+
+ /// <summary>
+ /// Notifies the pending restart.
+ /// </summary>
+ void NotifyPendingRestart();
+
+ /// <summary>
+ /// Restarts this instance.
+ /// </summary>
+ void Restart();
+
+ /// <summary>
+ /// Gets the application version.
+ /// </summary>
+ /// <value>The application version.</value>
+ Version ApplicationVersion { get; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance can self update.
+ /// </summary>
+ /// <value><c>true</c> if this instance can self update; otherwise, <c>false</c>.</value>
+ bool CanSelfUpdate { get; }
+
+ /// <summary>
+ /// Gets the exports.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="manageLiftime">if set to <c>true</c> [manage liftime].</param>
+ /// <returns>IEnumerable{``0}.</returns>
+ IEnumerable<T> GetExports<T>(bool manageLiftime = true);
+
+ /// <summary>
+ /// Checks for update.
+ /// </summary>
+ /// <returns>Task{CheckForUpdateResult}.</returns>
+ Task<CheckForUpdateResult> CheckForApplicationUpdate(CancellationToken cancellationToken, IProgress<double> progress);
+
+ /// <summary>
+ /// Updates the application.
+ /// </summary>
+ /// <returns>Task.</returns>
+ Task UpdateApplication(PackageVersionInfo package, CancellationToken cancellationToken, IProgress<double> progress);
+
+ /// <summary>
+ /// Resolves this instance.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <returns>``0.</returns>
+ T Resolve<T>();
+
+ /// <summary>
+ /// Resolves this instance.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <returns>``0.</returns>
+ T TryResolve<T>();
+
+ /// <summary>
+ /// Shuts down.
+ /// </summary>
+ Task Shutdown();
+
+ /// <summary>
+ /// Gets the plugins.
+ /// </summary>
+ /// <value>The plugins.</value>
+ IPlugin[] Plugins { get; }
+
+ /// <summary>
+ /// Removes the plugin.
+ /// </summary>
+ /// <param name="plugin">The plugin.</param>
+ void RemovePlugin(IPlugin plugin);
+
+ /// <summary>
+ /// Inits this instance.
+ /// </summary>
+ void Init();
+
+ /// <summary>
+ /// Creates the instance.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <returns>System.Object.</returns>
+ object CreateInstance(Type type);
+
+ PackageVersionClass SystemUpdateLevel { get; }
+
+ string GetValue(string name);
+ }
+}
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
new file mode 100644
index 000000000..43f3b0be7
--- /dev/null
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <ItemGroup>
+ <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Compile Include="..\SharedVersion.cs"/>
+ </ItemGroup>
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ </PropertyGroup>
+
+</Project>
diff --git a/MediaBrowser.Common/Net/HttpRequestOptions.cs b/MediaBrowser.Common/Net/HttpRequestOptions.cs
new file mode 100644
index 000000000..c61e88c87
--- /dev/null
+++ b/MediaBrowser.Common/Net/HttpRequestOptions.cs
@@ -0,0 +1,157 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Text;
+
+namespace MediaBrowser.Common.Net
+{
+ /// <summary>
+ /// Class HttpRequestOptions
+ /// </summary>
+ public class HttpRequestOptions
+ {
+ /// <summary>
+ /// Gets or sets the URL.
+ /// </summary>
+ /// <value>The URL.</value>
+ public string Url { get; set; }
+
+ public CompressionMethod? DecompressionMethod { get; set; }
+
+ /// <summary>
+ /// Gets or sets the accept header.
+ /// </summary>
+ /// <value>The accept header.</value>
+ public string AcceptHeader
+ {
+ get { return GetHeaderValue("Accept"); }
+ set
+ {
+ RequestHeaders["Accept"] = value;
+ }
+ }
+ /// <summary>
+ /// Gets or sets the cancellation token.
+ /// </summary>
+ /// <value>The cancellation token.</value>
+ public CancellationToken CancellationToken { get; set; }
+
+ /// <summary>
+ /// Gets or sets the resource pool.
+ /// </summary>
+ /// <value>The resource pool.</value>
+ public SemaphoreSlim ResourcePool { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user agent.
+ /// </summary>
+ /// <value>The user agent.</value>
+ public string UserAgent
+ {
+ get { return GetHeaderValue("User-Agent"); }
+ set
+ {
+ RequestHeaders["User-Agent"] = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the referrer.
+ /// </summary>
+ /// <value>The referrer.</value>
+ public string Referer { get; set; }
+
+ /// <summary>
+ /// Gets or sets the host.
+ /// </summary>
+ /// <value>The host.</value>
+ public string Host { get; set; }
+
+ /// <summary>
+ /// Gets or sets the progress.
+ /// </summary>
+ /// <value>The progress.</value>
+ public IProgress<double> Progress { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [enable HTTP compression].
+ /// </summary>
+ /// <value><c>true</c> if [enable HTTP compression]; otherwise, <c>false</c>.</value>
+ public bool EnableHttpCompression { get; set; }
+
+ public Dictionary<string, string> RequestHeaders { get; private set; }
+
+ public string RequestContentType { get; set; }
+
+ public string RequestContent { get; set; }
+ public byte[] RequestContentBytes { get; set; }
+
+ public bool BufferContent { get; set; }
+
+ public bool LogRequest { get; set; }
+ public bool LogRequestAsDebug { get; set; }
+ public bool LogErrors { get; set; }
+ public bool LogResponse { get; set; }
+ public bool LogResponseHeaders { get; set; }
+
+ public bool LogErrorResponseBody { get; set; }
+ public bool EnableKeepAlive { get; set; }
+
+ public CacheMode CacheMode { get; set; }
+ public TimeSpan CacheLength { get; set; }
+
+ public int TimeoutMs { get; set; }
+ public bool EnableDefaultUserAgent { get; set; }
+
+ public bool AppendCharsetToMimeType { get; set; }
+ public string DownloadFilePath { get; set; }
+
+ private string GetHeaderValue(string name)
+ {
+ string value;
+
+ RequestHeaders.TryGetValue(name, out value);
+
+ return value;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpRequestOptions"/> class.
+ /// </summary>
+ public HttpRequestOptions()
+ {
+ EnableHttpCompression = true;
+
+ RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+ LogRequest = true;
+ LogErrors = true;
+ CacheMode = CacheMode.None;
+
+ TimeoutMs = 20000;
+ }
+
+ public void SetPostData(IDictionary<string,string> values)
+ {
+ var strings = values.Keys.Select(key => string.Format("{0}={1}", key, values[key]));
+ var postContent = string.Join("&", strings.ToArray());
+
+ RequestContent = postContent;
+ RequestContentType = "application/x-www-form-urlencoded";
+ }
+ }
+
+ public enum CacheMode
+ {
+ None = 0,
+ Unconditional = 1
+ }
+
+ public enum CompressionMethod
+ {
+ Deflate,
+ Gzip
+ }
+}
diff --git a/MediaBrowser.Common/Net/HttpResponseInfo.cs b/MediaBrowser.Common/Net/HttpResponseInfo.cs
new file mode 100644
index 000000000..ed941a447
--- /dev/null
+++ b/MediaBrowser.Common/Net/HttpResponseInfo.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+
+namespace MediaBrowser.Common.Net
+{
+ /// <summary>
+ /// Class HttpResponseInfo
+ /// </summary>
+ public class HttpResponseInfo : IDisposable
+ {
+ /// <summary>
+ /// Gets or sets the type of the content.
+ /// </summary>
+ /// <value>The type of the content.</value>
+ public string ContentType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the response URL.
+ /// </summary>
+ /// <value>The response URL.</value>
+ public string ResponseUrl { get; set; }
+
+ /// <summary>
+ /// Gets or sets the content.
+ /// </summary>
+ /// <value>The content.</value>
+ public Stream Content { get; set; }
+
+ /// <summary>
+ /// Gets or sets the status code.
+ /// </summary>
+ /// <value>The status code.</value>
+ public HttpStatusCode StatusCode { get; set; }
+
+ /// <summary>
+ /// Gets or sets the temp file path.
+ /// </summary>
+ /// <value>The temp file path.</value>
+ public string TempFilePath { get; set; }
+
+ /// <summary>
+ /// Gets or sets the length of the content.
+ /// </summary>
+ /// <value>The length of the content.</value>
+ public long? ContentLength { get; set; }
+
+ /// <summary>
+ /// Gets or sets the headers.
+ /// </summary>
+ /// <value>The headers.</value>
+ public Dictionary<string,string> Headers { get; set; }
+
+ private readonly IDisposable _disposable;
+
+ public HttpResponseInfo(IDisposable disposable)
+ {
+ _disposable = disposable;
+ Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+ public HttpResponseInfo()
+ {
+ Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ public void Dispose()
+ {
+ if (_disposable != null)
+ {
+ _disposable.Dispose();
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Net/IHttpClient.cs b/MediaBrowser.Common/Net/IHttpClient.cs
new file mode 100644
index 000000000..cf5511965
--- /dev/null
+++ b/MediaBrowser.Common/Net/IHttpClient.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net
+{
+ /// <summary>
+ /// Interface IHttpClient
+ /// </summary>
+ public interface IHttpClient
+ {
+ /// <summary>
+ /// Gets the response.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <returns>Task{HttpResponseInfo}.</returns>
+ Task<HttpResponseInfo> GetResponse(HttpRequestOptions options);
+
+ /// <summary>
+ /// Gets the specified options.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <returns>Task{Stream}.</returns>
+ Task<Stream> Get(HttpRequestOptions options);
+
+ /// <summary>
+ /// Sends the asynchronous.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <param name="httpMethod">The HTTP method.</param>
+ /// <returns>Task{HttpResponseInfo}.</returns>
+ Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod);
+
+ /// <summary>
+ /// Posts the specified options.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <returns>Task{HttpResponseInfo}.</returns>
+ Task<HttpResponseInfo> Post(HttpRequestOptions options);
+
+ /// <summary>
+ /// Downloads the contents of a given url into a temporary location
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <returns>Task{System.String}.</returns>
+ /// <exception cref="System.ArgumentNullException">progress</exception>
+ /// <exception cref="MediaBrowser.Model.Net.HttpException"></exception>
+ Task<string> GetTempFile(HttpRequestOptions options);
+
+ /// <summary>
+ /// Gets the temporary file response.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <returns>Task{HttpResponseInfo}.</returns>
+ Task<HttpResponseInfo> GetTempFileResponse(HttpRequestOptions options);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs
new file mode 100644
index 000000000..b2ff797bc
--- /dev/null
+++ b/MediaBrowser.Common/Net/INetworkManager.cs
@@ -0,0 +1,66 @@
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Net;
+using System.Collections.Generic;
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Net
+{
+ public interface INetworkManager
+ {
+ event EventHandler NetworkChanged;
+
+ /// <summary>
+ /// Gets a random port number that is currently available
+ /// </summary>
+ /// <returns>System.Int32.</returns>
+ int GetRandomUnusedTcpPort();
+
+ int GetRandomUnusedUdpPort();
+
+ Func<string[]> LocalSubnetsFn { get; set; }
+
+ /// <summary>
+ /// Returns MAC Address from first Network Card in Computer
+ /// </summary>
+ /// <returns>[string] MAC Address</returns>
+ List<string> GetMacAddresses();
+
+ /// <summary>
+ /// Determines whether [is in private address space] [the specified endpoint].
+ /// </summary>
+ /// <param name="endpoint">The endpoint.</param>
+ /// <returns><c>true</c> if [is in private address space] [the specified endpoint]; otherwise, <c>false</c>.</returns>
+ bool IsInPrivateAddressSpace(string endpoint);
+
+ /// <summary>
+ /// Gets the network shares.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>IEnumerable{NetworkShare}.</returns>
+ IEnumerable<NetworkShare> GetNetworkShares(string path);
+
+ /// <summary>
+ /// Gets available devices within the domain
+ /// </summary>
+ /// <returns>PC's in the Domain</returns>
+ IEnumerable<FileSystemEntryInfo> GetNetworkDevices();
+
+ /// <summary>
+ /// Determines whether [is in local network] [the specified endpoint].
+ /// </summary>
+ /// <param name="endpoint">The endpoint.</param>
+ /// <returns><c>true</c> if [is in local network] [the specified endpoint]; otherwise, <c>false</c>.</returns>
+ bool IsInLocalNetwork(string endpoint);
+
+ IpAddressInfo[] GetLocalIpAddresses();
+
+ IpAddressInfo ParseIpAddress(string ipAddress);
+
+ bool TryParseIpAddress(string ipAddress, out IpAddressInfo ipAddressInfo);
+
+ Task<IpAddressInfo[]> GetHostAddressesAsync(string host);
+
+ bool IsAddressInSubnets(string addressString, string[] subnets);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs
new file mode 100644
index 000000000..82eb6ba4b
--- /dev/null
+++ b/MediaBrowser.Common/Plugins/BasePlugin.cs
@@ -0,0 +1,276 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.IO;
+
+namespace MediaBrowser.Common.Plugins
+{
+ public abstract class BasePlugin : IPlugin, IPluginAssembly
+ {
+ /// <summary>
+ /// Gets the name of the plugin
+ /// </summary>
+ /// <value>The name.</value>
+ public abstract string Name { get; }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public virtual string Description
+ {
+ get { return string.Empty; }
+ }
+
+ /// <summary>
+ /// Gets the unique id.
+ /// </summary>
+ /// <value>The unique id.</value>
+ public virtual Guid Id { get; private set; }
+
+ /// <summary>
+ /// Gets the plugin version
+ /// </summary>
+ /// <value>The version.</value>
+ public Version Version { get; private set; }
+
+ /// <summary>
+ /// Gets the path to the assembly file
+ /// </summary>
+ /// <value>The assembly file path.</value>
+ public string AssemblyFilePath { get; private set; }
+
+ /// <summary>
+ /// Gets the plugin info.
+ /// </summary>
+ /// <returns>PluginInfo.</returns>
+ public virtual PluginInfo GetPluginInfo()
+ {
+ var info = new PluginInfo
+ {
+ Name = Name,
+ Version = Version.ToString(),
+ Description = Description,
+ Id = Id.ToString()
+ };
+
+ return info;
+ }
+
+ /// <summary>
+ /// Called when just before the plugin is uninstalled from the server.
+ /// </summary>
+ public virtual void OnUninstalling()
+ {
+
+ }
+
+ public void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion)
+ {
+ AssemblyFilePath = assemblyFilePath;
+ DataFolderPath = dataFolderPath;
+ Version = assemblyVersion;
+ }
+
+ public void SetId(Guid assemblyId)
+ {
+ Id = assemblyId;
+ }
+
+ /// <summary>
+ /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed
+ /// </summary>
+ /// <value>The data folder path.</value>
+ public string DataFolderPath { get; private set; }
+ }
+
+ /// <summary>
+ /// Provides a common base class for all plugins
+ /// </summary>
+ /// <typeparam name="TConfigurationType">The type of the T configuration type.</typeparam>
+ public abstract class BasePlugin<TConfigurationType> : BasePlugin, IHasPluginConfiguration
+ where TConfigurationType : BasePluginConfiguration
+ {
+ /// <summary>
+ /// Gets the application paths.
+ /// </summary>
+ /// <value>The application paths.</value>
+ protected IApplicationPaths ApplicationPaths { get; private set; }
+
+ /// <summary>
+ /// Gets the XML serializer.
+ /// </summary>
+ /// <value>The XML serializer.</value>
+ protected IXmlSerializer XmlSerializer { get; private set; }
+
+ /// <summary>
+ /// Gets the type of configuration this plugin uses
+ /// </summary>
+ /// <value>The type of the configuration.</value>
+ public Type ConfigurationType
+ {
+ get { return typeof(TConfigurationType); }
+ }
+
+ private Action<string> _directoryCreateFn;
+ public void SetStartupInfo(Action<string> directoryCreateFn)
+ {
+ // hack alert, until the .net core transition is complete
+ _directoryCreateFn = directoryCreateFn;
+ }
+
+ /// <summary>
+ /// Gets the name the assembly file
+ /// </summary>
+ /// <value>The name of the assembly file.</value>
+ protected string AssemblyFileName
+ {
+ get
+ {
+ return Path.GetFileName(AssemblyFilePath);
+ }
+ }
+
+ /// <summary>
+ /// The _configuration sync lock
+ /// </summary>
+ private readonly object _configurationSyncLock = new object();
+ /// <summary>
+ /// The _configuration
+ /// </summary>
+ private TConfigurationType _configuration;
+ /// <summary>
+ /// Gets the plugin's configuration
+ /// </summary>
+ /// <value>The configuration.</value>
+ public TConfigurationType Configuration
+ {
+ get
+ {
+ // Lazy load
+ if (_configuration == null)
+ {
+ lock (_configurationSyncLock)
+ {
+ if (_configuration == null)
+ {
+ _configuration = LoadConfiguration();
+ }
+ }
+ }
+ return _configuration;
+ }
+ protected set
+ {
+ _configuration = value;
+ }
+ }
+
+ private TConfigurationType LoadConfiguration()
+ {
+ var path = ConfigurationFilePath;
+
+ try
+ {
+ return (TConfigurationType)XmlSerializer.DeserializeFromFile(typeof(TConfigurationType), path);
+ }
+ catch
+ {
+ return (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
+ }
+ }
+
+ /// <summary>
+ /// Gets the name of the configuration file. Subclasses should override
+ /// </summary>
+ /// <value>The name of the configuration file.</value>
+ public virtual string ConfigurationFileName
+ {
+ get { return Path.ChangeExtension(AssemblyFileName, ".xml"); }
+ }
+
+ /// <summary>
+ /// Gets the full path to the configuration file
+ /// </summary>
+ /// <value>The configuration file path.</value>
+ public string ConfigurationFilePath
+ {
+ get
+ {
+ return Path.Combine(ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName);
+ }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BasePlugin{TConfigurationType}" /> class.
+ /// </summary>
+ /// <param name="applicationPaths">The application paths.</param>
+ /// <param name="xmlSerializer">The XML serializer.</param>
+ protected BasePlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ {
+ ApplicationPaths = applicationPaths;
+ XmlSerializer = xmlSerializer;
+ }
+
+ /// <summary>
+ /// The _save lock
+ /// </summary>
+ private readonly object _configurationSaveLock = new object();
+
+ /// <summary>
+ /// Saves the current configuration to the file system
+ /// </summary>
+ public virtual void SaveConfiguration()
+ {
+ lock (_configurationSaveLock)
+ {
+ _directoryCreateFn(Path.GetDirectoryName(ConfigurationFilePath));
+
+ XmlSerializer.SerializeToFile(Configuration, ConfigurationFilePath);
+ }
+ }
+
+ /// <summary>
+ /// Completely overwrites the current configuration with a new copy
+ /// Returns true or false indicating success or failure
+ /// </summary>
+ /// <param name="configuration">The configuration.</param>
+ /// <exception cref="System.ArgumentNullException">configuration</exception>
+ public virtual void UpdateConfiguration(BasePluginConfiguration configuration)
+ {
+ if (configuration == null)
+ {
+ throw new ArgumentNullException("configuration");
+ }
+
+ Configuration = (TConfigurationType)configuration;
+
+ SaveConfiguration();
+ }
+
+ /// <summary>
+ /// Gets the plugin's configuration
+ /// </summary>
+ /// <value>The configuration.</value>
+ BasePluginConfiguration IHasPluginConfiguration.Configuration
+ {
+ get { return Configuration; }
+ }
+
+ public override PluginInfo GetPluginInfo()
+ {
+ var info = base.GetPluginInfo();
+
+ info.ConfigurationFileName = ConfigurationFileName;
+
+ return info;
+ }
+ }
+
+ public interface IPluginAssembly
+ {
+ void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion);
+ void SetId(Guid assemblyId);
+ }
+}
diff --git a/MediaBrowser.Common/Plugins/IPlugin.cs b/MediaBrowser.Common/Plugins/IPlugin.cs
new file mode 100644
index 000000000..bffd21143
--- /dev/null
+++ b/MediaBrowser.Common/Plugins/IPlugin.cs
@@ -0,0 +1,83 @@
+using MediaBrowser.Model.Plugins;
+using System;
+
+namespace MediaBrowser.Common.Plugins
+{
+ /// <summary>
+ /// Interface IPlugin
+ /// </summary>
+ public interface IPlugin
+ {
+ /// <summary>
+ /// Gets the name of the plugin
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ string Description { get; }
+
+ /// <summary>
+ /// Gets the unique id.
+ /// </summary>
+ /// <value>The unique id.</value>
+ Guid Id { get; }
+
+ /// <summary>
+ /// Gets the plugin version
+ /// </summary>
+ /// <value>The version.</value>
+ Version Version { get; }
+
+ /// <summary>
+ /// Gets the path to the assembly file
+ /// </summary>
+ /// <value>The assembly file path.</value>
+ string AssemblyFilePath { get; }
+
+ /// <summary>
+ /// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed
+ /// </summary>
+ /// <value>The data folder path.</value>
+ string DataFolderPath { get; }
+
+ /// <summary>
+ /// Gets the plugin info.
+ /// </summary>
+ /// <returns>PluginInfo.</returns>
+ PluginInfo GetPluginInfo();
+
+ /// <summary>
+ /// Called when just before the plugin is uninstalled from the server.
+ /// </summary>
+ void OnUninstalling();
+ }
+
+ public interface IHasPluginConfiguration
+ {
+ /// <summary>
+ /// Gets the type of configuration this plugin uses
+ /// </summary>
+ /// <value>The type of the configuration.</value>
+ Type ConfigurationType { get; }
+
+ /// <summary>
+ /// Completely overwrites the current configuration with a new copy
+ /// Returns true or false indicating success or failure
+ /// </summary>
+ /// <param name="configuration">The configuration.</param>
+ /// <exception cref="System.ArgumentNullException">configuration</exception>
+ void UpdateConfiguration(BasePluginConfiguration configuration);
+
+ /// <summary>
+ /// Gets the plugin's configuration
+ /// </summary>
+ /// <value>The configuration.</value>
+ BasePluginConfiguration Configuration { get; }
+
+ void SetStartupInfo(Action<string> directoryCreateFn);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Common/Progress/ActionableProgress.cs b/MediaBrowser.Common/Progress/ActionableProgress.cs
new file mode 100644
index 000000000..67347bc15
--- /dev/null
+++ b/MediaBrowser.Common/Progress/ActionableProgress.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Common.Progress
+{
+ /// <summary>
+ /// Class ActionableProgress
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public class ActionableProgress<T> : IProgress<T>
+ {
+ /// <summary>
+ /// The _actions
+ /// </summary>
+ private Action<T> _action;
+ public event EventHandler<T> ProgressChanged;
+
+ /// <summary>
+ /// Registers the action.
+ /// </summary>
+ /// <param name="action">The action.</param>
+ public void RegisterAction(Action<T> action)
+ {
+ _action = action;
+ }
+
+ public void Report(T value)
+ {
+ if (ProgressChanged != null)
+ {
+ ProgressChanged(this, value);
+ }
+
+ var action = _action;
+ if (action != null)
+ {
+ action(value);
+ }
+ }
+ }
+
+ public class SimpleProgress<T> : IProgress<T>
+ {
+ public event EventHandler<T> ProgressChanged;
+
+ public void Report(T value)
+ {
+ if (ProgressChanged != null)
+ {
+ ProgressChanged(this, value);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Properties/AssemblyInfo.cs b/MediaBrowser.Common/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..09fd68f93
--- /dev/null
+++ b/MediaBrowser.Common/Properties/AssemblyInfo.cs
@@ -0,0 +1,27 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("MediaBrowser.Common")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.Common")]
+[assembly: AssemblyCopyright("Copyright © 2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+// \ No newline at end of file
diff --git a/MediaBrowser.Common/Security/IRequiresRegistration.cs b/MediaBrowser.Common/Security/IRequiresRegistration.cs
new file mode 100644
index 000000000..7b1667c2e
--- /dev/null
+++ b/MediaBrowser.Common/Security/IRequiresRegistration.cs
@@ -0,0 +1,15 @@
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Security
+{
+ public interface IRequiresRegistration
+ {
+ /// <summary>
+ /// Load all registration information required for this entity.
+ /// Your class should re-load all MBRegistrationRecords when this is called even if they were
+ /// previously loaded.
+ /// </summary>
+ /// <returns></returns>
+ Task LoadRegistrationInfoAsync();
+ }
+}
diff --git a/MediaBrowser.Common/Security/ISecurityManager.cs b/MediaBrowser.Common/Security/ISecurityManager.cs
new file mode 100644
index 000000000..b63a9efd0
--- /dev/null
+++ b/MediaBrowser.Common/Security/ISecurityManager.cs
@@ -0,0 +1,32 @@
+using MediaBrowser.Model.Entities;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Security
+{
+ public interface ISecurityManager
+ {
+ /// <summary>
+ /// Gets a value indicating whether this instance is MB supporter.
+ /// </summary>
+ /// <value><c>true</c> if this instance is MB supporter; otherwise, <c>false</c>.</value>
+ Task<bool> IsSupporter();
+
+ /// <summary>
+ /// Gets or sets the supporter key.
+ /// </summary>
+ /// <value>The supporter key.</value>
+ string SupporterKey { get; }
+
+ /// <summary>
+ /// Gets the registration status. Overload to support existing plug-ins.
+ /// </summary>
+ Task<MBRegistrationRecord> GetRegistrationStatus(string feature);
+
+ /// <summary>
+ /// Register and app store sale with our back-end
+ /// </summary>
+ /// <param name="parameters">Json parameters to pass to admin server</param>
+ Task RegisterAppStoreSale(string parameters);
+ Task UpdateSupporterKey(string newValue);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Common/Security/PaymentRequiredException.cs b/MediaBrowser.Common/Security/PaymentRequiredException.cs
new file mode 100644
index 000000000..27b3e6961
--- /dev/null
+++ b/MediaBrowser.Common/Security/PaymentRequiredException.cs
@@ -0,0 +1,8 @@
+using System;
+
+namespace MediaBrowser.Common.Security
+{
+ public class PaymentRequiredException : Exception
+ {
+ }
+}
diff --git a/MediaBrowser.Common/Updates/GithubUpdater.cs b/MediaBrowser.Common/Updates/GithubUpdater.cs
new file mode 100644
index 000000000..4275799a9
--- /dev/null
+++ b/MediaBrowser.Common/Updates/GithubUpdater.cs
@@ -0,0 +1,278 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Updates;
+
+namespace MediaBrowser.Common.Updates
+{
+ public class GithubUpdater
+ {
+ private readonly IHttpClient _httpClient;
+ private readonly IJsonSerializer _jsonSerializer;
+
+ public GithubUpdater(IHttpClient httpClient, IJsonSerializer jsonSerializer)
+ {
+ _httpClient = httpClient;
+ _jsonSerializer = jsonSerializer;
+ }
+
+ public async Task<CheckForUpdateResult> CheckForUpdateResult(string organzation, string repository, Version minVersion, PackageVersionClass updateLevel, string assetFilename, string packageName, string targetFilename, TimeSpan cacheLength, CancellationToken cancellationToken)
+ {
+ var url = string.Format("https://api.github.com/repos/{0}/{1}/releases", organzation, repository);
+
+ var options = new HttpRequestOptions
+ {
+ Url = url,
+ EnableKeepAlive = false,
+ CancellationToken = cancellationToken,
+ UserAgent = "Emby/3.0",
+ BufferContent = false
+ };
+
+ if (cacheLength.Ticks > 0)
+ {
+ options.CacheMode = CacheMode.Unconditional;
+ options.CacheLength = cacheLength;
+ }
+
+ using (var response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false))
+ {
+ using (var stream = response.Content)
+ {
+ var obj = _jsonSerializer.DeserializeFromStream<RootObject[]>(stream);
+
+ return CheckForUpdateResult(obj, minVersion, updateLevel, assetFilename, packageName, targetFilename);
+ }
+ }
+ }
+
+ private CheckForUpdateResult CheckForUpdateResult(RootObject[] obj, Version minVersion, PackageVersionClass updateLevel, string assetFilename, string packageName, string targetFilename)
+ {
+ if (updateLevel == PackageVersionClass.Release)
+ {
+ // Technically all we need to do is check that it's not pre-release
+ // But let's addititional checks for -beta and -dev to handle builds that might be temporarily tagged incorrectly.
+ obj = obj.Where(i => !i.prerelease && !i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) && !i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase)).ToArray();
+ }
+ else if (updateLevel == PackageVersionClass.Beta)
+ {
+ obj = obj.Where(i => i.prerelease && i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase)).ToArray();
+ }
+ else if (updateLevel == PackageVersionClass.Dev)
+ {
+ obj = obj.Where(i => !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) || i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase)).ToArray();
+ }
+
+ var availableUpdate = obj
+ .Select(i => CheckForUpdateResult(i, minVersion, assetFilename, packageName, targetFilename))
+ .Where(i => i != null)
+ .OrderByDescending(i => Version.Parse(i.AvailableVersion))
+ .FirstOrDefault();
+
+ return availableUpdate ?? new CheckForUpdateResult
+ {
+ IsUpdateAvailable = false
+ };
+ }
+
+ private bool MatchesUpdateLevel(RootObject i, PackageVersionClass updateLevel)
+ {
+ if (updateLevel == PackageVersionClass.Beta)
+ {
+ return i.prerelease && i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase);
+ }
+ if (updateLevel == PackageVersionClass.Dev)
+ {
+ return !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) ||
+ i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase);
+ }
+
+ // Technically all we need to do is check that it's not pre-release
+ // But let's addititional checks for -beta and -dev to handle builds that might be temporarily tagged incorrectly.
+ return !i.prerelease && !i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) &&
+ !i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase);
+ }
+
+ public async Task<List<RootObject>> GetLatestReleases(string organzation, string repository, string assetFilename, CancellationToken cancellationToken)
+ {
+ var list = new List<RootObject>();
+
+ var url = string.Format("https://api.github.com/repos/{0}/{1}/releases", organzation, repository);
+
+ var options = new HttpRequestOptions
+ {
+ Url = url,
+ EnableKeepAlive = false,
+ CancellationToken = cancellationToken,
+ UserAgent = "Emby/3.0",
+ BufferContent = false
+ };
+
+ using (var response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false))
+ {
+ using (var stream = response.Content)
+ {
+ var obj = _jsonSerializer.DeserializeFromStream<RootObject[]>(stream);
+
+ obj = obj.Where(i => (i.assets ?? new List<Asset>()).Any(a => IsAsset(a, assetFilename, i.tag_name))).ToArray();
+
+ list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Release)).OrderByDescending(GetVersion).Take(1));
+ list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Beta)).OrderByDescending(GetVersion).Take(1));
+ list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Dev)).OrderByDescending(GetVersion).Take(1));
+
+ return list;
+ }
+ }
+ }
+
+ public Version GetVersion(RootObject obj)
+ {
+ Version version;
+ if (!Version.TryParse(obj.tag_name, out version))
+ {
+ return new Version(1, 0);
+ }
+
+ return version;
+ }
+
+ private CheckForUpdateResult CheckForUpdateResult(RootObject obj, Version minVersion, string assetFilename, string packageName, string targetFilename)
+ {
+ Version version;
+ var versionString = obj.tag_name;
+ if (!Version.TryParse(versionString, out version))
+ {
+ return null;
+ }
+
+ if (version < minVersion)
+ {
+ return null;
+ }
+
+ var asset = (obj.assets ?? new List<Asset>()).FirstOrDefault(i => IsAsset(i, assetFilename, versionString));
+
+ if (asset == null)
+ {
+ return null;
+ }
+
+ return new CheckForUpdateResult
+ {
+ AvailableVersion = version.ToString(),
+ IsUpdateAvailable = version > minVersion,
+ Package = new PackageVersionInfo
+ {
+ classification = obj.prerelease ?
+ (obj.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase) ? PackageVersionClass.Dev : PackageVersionClass.Beta) :
+ PackageVersionClass.Release,
+ name = packageName,
+ sourceUrl = asset.browser_download_url,
+ targetFilename = targetFilename,
+ versionStr = version.ToString(),
+ requiredVersionStr = "1.0.0",
+ description = obj.body,
+ infoUrl = obj.html_url
+ }
+ };
+ }
+
+ private bool IsAsset(Asset asset, string assetFilename, string version)
+ {
+ var downloadFilename = Path.GetFileName(asset.browser_download_url) ?? string.Empty;
+
+ assetFilename = assetFilename.Replace("{version}", version);
+
+ if (downloadFilename.IndexOf(assetFilename, StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return true;
+ }
+
+ return string.Equals(assetFilename, downloadFilename, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public class Uploader
+ {
+ public string login { get; set; }
+ public int id { get; set; }
+ public string avatar_url { get; set; }
+ public string gravatar_id { get; set; }
+ public string url { get; set; }
+ public string html_url { get; set; }
+ public string followers_url { get; set; }
+ public string following_url { get; set; }
+ public string gists_url { get; set; }
+ public string starred_url { get; set; }
+ public string subscriptions_url { get; set; }
+ public string organizations_url { get; set; }
+ public string repos_url { get; set; }
+ public string events_url { get; set; }
+ public string received_events_url { get; set; }
+ public string type { get; set; }
+ public bool site_admin { get; set; }
+ }
+
+ public class Asset
+ {
+ public string url { get; set; }
+ public int id { get; set; }
+ public string name { get; set; }
+ public object label { get; set; }
+ public Uploader uploader { get; set; }
+ public string content_type { get; set; }
+ public string state { get; set; }
+ public int size { get; set; }
+ public int download_count { get; set; }
+ public string created_at { get; set; }
+ public string updated_at { get; set; }
+ public string browser_download_url { get; set; }
+ }
+
+ public class Author
+ {
+ public string login { get; set; }
+ public int id { get; set; }
+ public string avatar_url { get; set; }
+ public string gravatar_id { get; set; }
+ public string url { get; set; }
+ public string html_url { get; set; }
+ public string followers_url { get; set; }
+ public string following_url { get; set; }
+ public string gists_url { get; set; }
+ public string starred_url { get; set; }
+ public string subscriptions_url { get; set; }
+ public string organizations_url { get; set; }
+ public string repos_url { get; set; }
+ public string events_url { get; set; }
+ public string received_events_url { get; set; }
+ public string type { get; set; }
+ public bool site_admin { get; set; }
+ }
+
+ public class RootObject
+ {
+ public string url { get; set; }
+ public string assets_url { get; set; }
+ public string upload_url { get; set; }
+ public string html_url { get; set; }
+ public int id { get; set; }
+ public string tag_name { get; set; }
+ public string target_commitish { get; set; }
+ public string name { get; set; }
+ public bool draft { get; set; }
+ public Author author { get; set; }
+ public bool prerelease { get; set; }
+ public string created_at { get; set; }
+ public string published_at { get; set; }
+ public List<Asset> assets { get; set; }
+ public string tarball_url { get; set; }
+ public string zipball_url { get; set; }
+ public string body { get; set; }
+ }
+ }
+}
diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs
new file mode 100644
index 000000000..dab38b27c
--- /dev/null
+++ b/MediaBrowser.Common/Updates/IInstallationManager.cs
@@ -0,0 +1,121 @@
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Updates;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Common.Updates
+{
+ public interface IInstallationManager : IDisposable
+ {
+ event EventHandler<InstallationEventArgs> PackageInstalling;
+ event EventHandler<InstallationEventArgs> PackageInstallationCompleted;
+ event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed;
+ event EventHandler<InstallationEventArgs> PackageInstallationCancelled;
+
+ /// <summary>
+ /// The current installations
+ /// </summary>
+ List<Tuple<InstallationInfo, CancellationTokenSource>> CurrentInstallations { get; set; }
+
+ /// <summary>
+ /// The completed installations
+ /// </summary>
+ IEnumerable<InstallationInfo> CompletedInstallations { get; }
+
+ /// <summary>
+ /// Occurs when [plugin uninstalled].
+ /// </summary>
+ event EventHandler<GenericEventArgs<IPlugin>> PluginUninstalled;
+
+ /// <summary>
+ /// Occurs when [plugin updated].
+ /// </summary>
+ event EventHandler<GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>>> PluginUpdated;
+
+ /// <summary>
+ /// Occurs when [plugin updated].
+ /// </summary>
+ event EventHandler<GenericEventArgs<PackageVersionInfo>> PluginInstalled;
+
+ /// <summary>
+ /// Gets all available packages.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="withRegistration">if set to <c>true</c> [with registration].</param>
+ /// <param name="packageType">Type of the package.</param>
+ /// <param name="applicationVersion">The application version.</param>
+ /// <returns>Task{List{PackageInfo}}.</returns>
+ Task<List<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken,
+ bool withRegistration = true,
+ string packageType = null,
+ Version applicationVersion = null);
+
+ /// <summary>
+ /// Gets all available packages from a static resource.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{List{PackageInfo}}.</returns>
+ Task<List<PackageInfo>> GetAvailablePackagesWithoutRegistrationInfo(CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the package.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="guid">The assembly guid</param>
+ /// <param name="classification">The classification.</param>
+ /// <param name="version">The version.</param>
+ /// <returns>Task{PackageVersionInfo}.</returns>
+ Task<PackageVersionInfo> GetPackage(string name, string guid, PackageVersionClass classification, Version version);
+
+ /// <summary>
+ /// Gets the latest compatible version.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="guid">The assembly guid</param>
+ /// <param name="currentServerVersion">The current server version.</param>
+ /// <param name="classification">The classification.</param>
+ /// <returns>Task{PackageVersionInfo}.</returns>
+ Task<PackageVersionInfo> GetLatestCompatibleVersion(string name, string guid, Version currentServerVersion, PackageVersionClass classification = PackageVersionClass.Release);
+
+ /// <summary>
+ /// Gets the latest compatible version.
+ /// </summary>
+ /// <param name="availablePackages">The available packages.</param>
+ /// <param name="name">The name.</param>
+ /// <param name="guid">The assembly guid</param>
+ /// <param name="currentServerVersion">The current server version.</param>
+ /// <param name="classification">The classification.</param>
+ /// <returns>PackageVersionInfo.</returns>
+ PackageVersionInfo GetLatestCompatibleVersion(IEnumerable<PackageInfo> availablePackages, string name, string guid, Version currentServerVersion, PackageVersionClass classification = PackageVersionClass.Release);
+
+ /// <summary>
+ /// Gets the available plugin updates.
+ /// </summary>
+ /// <param name="applicationVersion">The current server version.</param>
+ /// <param name="withAutoUpdateEnabled">if set to <c>true</c> [with auto update enabled].</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{PackageVersionInfo}}.</returns>
+ Task<IEnumerable<PackageVersionInfo>> GetAvailablePluginUpdates(Version applicationVersion, bool withAutoUpdateEnabled, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Installs the package.
+ /// </summary>
+ /// <param name="package">The package.</param>
+ /// <param name="isPlugin">if set to <c>true</c> [is plugin].</param>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">package</exception>
+ Task InstallPackage(PackageVersionInfo package, bool isPlugin, IProgress<double> progress, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Uninstalls a plugin
+ /// </summary>
+ /// <param name="plugin">The plugin.</param>
+ /// <exception cref="System.ArgumentException"></exception>
+ void UninstallPlugin(IPlugin plugin);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Common/Updates/InstallationEventArgs.cs b/MediaBrowser.Common/Updates/InstallationEventArgs.cs
new file mode 100644
index 000000000..9dc8ead83
--- /dev/null
+++ b/MediaBrowser.Common/Updates/InstallationEventArgs.cs
@@ -0,0 +1,11 @@
+using MediaBrowser.Model.Updates;
+
+namespace MediaBrowser.Common.Updates
+{
+ public class InstallationEventArgs
+ {
+ public InstallationInfo InstallationInfo { get; set; }
+
+ public PackageVersionInfo PackageVersionInfo { get; set; }
+ }
+}
diff --git a/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs b/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs
new file mode 100644
index 000000000..69dc1ee98
--- /dev/null
+++ b/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace MediaBrowser.Common.Updates
+{
+ public class InstallationFailedEventArgs : InstallationEventArgs
+ {
+ public Exception Exception { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs
new file mode 100644
index 000000000..6dd8f02d8
--- /dev/null
+++ b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs
@@ -0,0 +1,14 @@
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dto;
+
+
+namespace MediaBrowser.Controller.Authentication
+{
+ public class AuthenticationResult
+ {
+ public UserDto User { get; set; }
+ public SessionInfo SessionInfo { get; set; }
+ public string AccessToken { get; set; }
+ public string ServerId { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
new file mode 100644
index 000000000..becb3ea62
--- /dev/null
+++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Users;
+
+namespace MediaBrowser.Controller.Authentication
+{
+ public interface IAuthenticationProvider
+ {
+ string Name { get; }
+ bool IsEnabled { get; }
+ Task<ProviderAuthenticationResult> Authenticate(string username, string password);
+ Task<bool> HasPassword(User user);
+ Task ChangePassword(User user, string newPassword);
+ }
+
+ public interface IRequiresResolvedUser
+ {
+ Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser);
+ }
+
+ public interface IHasNewUserPolicy
+ {
+ UserPolicy GetNewUserPolicy();
+ }
+
+ public class ProviderAuthenticationResult
+ {
+ public string Username { get; set; }
+ public string DisplayName { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs
new file mode 100644
index 000000000..9cd50db17
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/Channel.cs
@@ -0,0 +1,94 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Channels;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Linq;
+using MediaBrowser.Model.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Progress;
+
+namespace MediaBrowser.Controller.Channels
+{
+ public class Channel : Folder
+ {
+ public override bool IsVisible(User user)
+ {
+ if (user.Policy.BlockedChannels != null)
+ {
+ if (user.Policy.BlockedChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ }
+
+ return base.IsVisible(user);
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsInheritedParentImages
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override SourceType SourceType
+ {
+ get { return SourceType.Channel; }
+ }
+
+ protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
+ {
+ try
+ {
+ query.Parent = this;
+ query.ChannelIds = new Guid[] { Id };
+
+ // Don't blow up here because it could cause parent screens with other content to fail
+ return ChannelManager.GetChannelItemsInternal(query, new SimpleProgress<double>(), CancellationToken.None).Result;
+ }
+ catch
+ {
+ // Already logged at lower levels
+ return new QueryResult<BaseItem>();
+ }
+ }
+
+ protected override string GetInternalMetadataPath(string basePath)
+ {
+ return GetInternalMetadataPath(basePath, Id);
+ }
+
+ public static string GetInternalMetadataPath(string basePath, Guid id)
+ {
+ return System.IO.Path.Combine(basePath, "channels", id.ToString("N"), "metadata");
+ }
+
+ public override bool CanDelete()
+ {
+ return false;
+ }
+
+ protected override bool IsAllowTagFilterEnforced()
+ {
+ return false;
+ }
+
+ internal static bool IsChannelVisible(BaseItem channelItem, User user)
+ {
+ var channel = ChannelManager.GetChannel(channelItem.ChannelId.ToString(""));
+
+ return channel.IsVisible(user);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Channels/ChannelItemInfo.cs b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs
new file mode 100644
index 000000000..0de2b9a0c
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs
@@ -0,0 +1,82 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Channels;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Controller.Channels
+{
+ public class ChannelItemInfo : IHasProviderIds
+ {
+ public string Name { get; set; }
+
+ public string SeriesName { get; set; }
+
+ public string Id { get; set; }
+
+ public DateTime DateModified { get; set; }
+
+ public ChannelItemType Type { get; set; }
+
+ public string OfficialRating { get; set; }
+
+ public string Overview { get; set; }
+
+ public List<string> Genres { get; set; }
+ public List<string> Studios { get; set; }
+ public List<string> Tags { get; set; }
+
+ public List<PersonInfo> People { get; set; }
+
+ public float? CommunityRating { get; set; }
+
+ public long? RunTimeTicks { get; set; }
+
+ public string ImageUrl { get; set; }
+ public string OriginalTitle { get; set; }
+
+ public ChannelMediaType MediaType { get; set; }
+ public ChannelFolderType FolderType { get; set; }
+
+ public ChannelMediaContentType ContentType { get; set; }
+ public ExtraType ExtraType { get; set; }
+ public List<TrailerType> TrailerTypes { get; set; }
+
+ public Dictionary<string, string> ProviderIds { get; set; }
+
+ public DateTime? PremiereDate { get; set; }
+ public int? ProductionYear { get; set; }
+
+ public DateTime? DateCreated { get; set; }
+
+ public DateTime? StartDate { get; set; }
+ public DateTime? EndDate { get; set; }
+
+ public int? IndexNumber { get; set; }
+ public int? ParentIndexNumber { get; set; }
+
+ public List<MediaSourceInfo> MediaSources { get; set; }
+
+ public string HomePageUrl { get; set; }
+
+ public List<string> Artists { get; set; }
+
+ public List<string> AlbumArtists { get; set; }
+ public bool IsLiveStream { get; set; }
+ public string Etag { get; set; }
+
+ public ChannelItemInfo()
+ {
+ MediaSources = new List<MediaSourceInfo>();
+ TrailerTypes = new List<TrailerType>();
+ Genres = new List<string>();
+ Studios = new List<string>();
+ People = new List<PersonInfo>();
+ Tags = new List<string>();
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ Artists = new List<string>();
+ AlbumArtists = new List<string>();
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Channels/ChannelItemResult.cs b/MediaBrowser.Controller/Channels/ChannelItemResult.cs
new file mode 100644
index 000000000..f88881811
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/ChannelItemResult.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Channels
+{
+ public class ChannelItemResult
+ {
+ public List<ChannelItemInfo> Items { get; set; }
+
+ public int? TotalRecordCount { get; set; }
+
+ public ChannelItemResult()
+ {
+ Items = new List<ChannelItemInfo>();
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Channels/ChannelItemType.cs b/MediaBrowser.Controller/Channels/ChannelItemType.cs
new file mode 100644
index 000000000..184ce8a76
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/ChannelItemType.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Controller.Channels
+{
+ public enum ChannelItemType
+ {
+ Media = 0,
+
+ Folder = 1
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Channels/ChannelParentalRating.cs b/MediaBrowser.Controller/Channels/ChannelParentalRating.cs
new file mode 100644
index 000000000..d9cc521b3
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/ChannelParentalRating.cs
@@ -0,0 +1,15 @@
+namespace MediaBrowser.Controller.Channels
+{
+ public enum ChannelParentalRating
+ {
+ GeneralAudience = 0,
+
+ UsPG = 1,
+
+ UsPG13 = 2,
+
+ UsR = 3,
+
+ Adult = 4
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs b/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs
new file mode 100644
index 000000000..c2a51654c
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs
@@ -0,0 +1,14 @@
+namespace MediaBrowser.Controller.Channels
+{
+ public class ChannelSearchInfo
+ {
+ public string SearchTerm { get; set; }
+
+ public string UserId { get; set; }
+ }
+
+ public class ChannelLatestMediaSearch
+ {
+ public string UserId { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Channels/IChannel.cs b/MediaBrowser.Controller/Channels/IChannel.cs
new file mode 100644
index 000000000..dc1d9b00a
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/IChannel.cs
@@ -0,0 +1,76 @@
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Channels
+{
+ public interface IChannel
+ {
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ string Description { get; }
+
+ /// <summary>
+ /// Gets the data version.
+ /// </summary>
+ /// <value>The data version.</value>
+ string DataVersion { get; }
+
+ /// <summary>
+ /// Gets the home page URL.
+ /// </summary>
+ /// <value>The home page URL.</value>
+ string HomePageUrl { get; }
+
+ /// <summary>
+ /// Gets the parental rating.
+ /// </summary>
+ /// <value>The parental rating.</value>
+ ChannelParentalRating ParentalRating { get; }
+
+ /// <summary>
+ /// Gets the channel information.
+ /// </summary>
+ /// <returns>ChannelFeatures.</returns>
+ InternalChannelFeatures GetChannelFeatures();
+
+ /// <summary>
+ /// Determines whether [is enabled for] [the specified user].
+ /// </summary>
+ /// <param name="userId">The user identifier.</param>
+ /// <returns><c>true</c> if [is enabled for] [the specified user]; otherwise, <c>false</c>.</returns>
+ bool IsEnabledFor(string userId);
+
+ /// <summary>
+ /// Gets the channel items.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{ChannelItem}}.</returns>
+ Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the channel image.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{DynamicImageInfo}.</returns>
+ Task<DynamicImageResponse> GetChannelImage(ImageType type, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the supported channel images.
+ /// </summary>
+ /// <returns>IEnumerable{ImageType}.</returns>
+ IEnumerable<ImageType> GetSupportedChannelImages();
+ }
+}
diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs
new file mode 100644
index 000000000..a9839e1fb
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/IChannelManager.cs
@@ -0,0 +1,89 @@
+using System;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Channels;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Channels
+{
+ public interface IChannelManager
+ {
+ /// <summary>
+ /// Adds the parts.
+ /// </summary>
+ /// <param name="channels">The channels.</param>
+ void AddParts(IEnumerable<IChannel> channels);
+
+ /// <summary>
+ /// Gets the channel features.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <returns>ChannelFeatures.</returns>
+ ChannelFeatures GetChannelFeatures(string id);
+
+ /// <summary>
+ /// Gets all channel features.
+ /// </summary>
+ /// <returns>IEnumerable{ChannelFeatures}.</returns>
+ ChannelFeatures[] GetAllChannelFeatures();
+
+ bool EnableMediaSourceDisplay(BaseItem item);
+ bool CanDelete(BaseItem item);
+
+ Task DeleteItem(BaseItem item);
+
+ /// <summary>
+ /// Gets the channel.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <returns>Channel.</returns>
+ Channel GetChannel(string id);
+
+ /// <summary>
+ /// Gets the channels internal.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ QueryResult<Channel> GetChannelsInternal(ChannelQuery query);
+
+ /// <summary>
+ /// Gets the channels.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ QueryResult<BaseItemDto> GetChannels(ChannelQuery query);
+
+ /// <summary>
+ /// Gets the latest media.
+ /// </summary>
+ Task<QueryResult<BaseItemDto>> GetLatestChannelItems(InternalItemsQuery query, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the latest media.
+ /// </summary>
+ Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(InternalItemsQuery query, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the channel items.
+ /// </summary>
+ Task<QueryResult<BaseItemDto>> GetChannelItems(InternalItemsQuery query, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the channel items internal.
+ /// </summary>
+ Task<QueryResult<BaseItem>> GetChannelItemsInternal(InternalItemsQuery query, IProgress<double> progress, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the channel item media sources.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{MediaSourceInfo}}.</returns>
+ IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken);
+
+ bool EnableMediaProbe(BaseItem item);
+ }
+}
diff --git a/MediaBrowser.Controller/Channels/IHasCacheKey.cs b/MediaBrowser.Controller/Channels/IHasCacheKey.cs
new file mode 100644
index 000000000..6376d2f91
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/IHasCacheKey.cs
@@ -0,0 +1,13 @@
+
+namespace MediaBrowser.Controller.Channels
+{
+ public interface IHasCacheKey
+ {
+ /// <summary>
+ /// Gets the cache key.
+ /// </summary>
+ /// <param name="userId">The user identifier.</param>
+ /// <returns>System.String.</returns>
+ string GetCacheKey(string userId);
+ }
+}
diff --git a/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs b/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs
new file mode 100644
index 000000000..a2c63586b
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Controller.Channels
+{
+ public interface IRequiresMediaInfoCallback
+ {
+ /// <summary>
+ /// Gets the channel item media information.
+ /// </summary>
+ Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaInfo(string id, CancellationToken cancellationToken);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Channels/ISearchableChannel.cs b/MediaBrowser.Controller/Channels/ISearchableChannel.cs
new file mode 100644
index 000000000..bf9842eb4
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/ISearchableChannel.cs
@@ -0,0 +1,50 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Channels
+{
+ public interface ISearchableChannel
+ {
+ /// <summary>
+ /// Searches the specified search term.
+ /// </summary>
+ /// <param name="searchInfo">The search information.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns>
+ Task<IEnumerable<ChannelItemInfo>> Search(ChannelSearchInfo searchInfo, CancellationToken cancellationToken);
+ }
+
+ public interface ISupportsLatestMedia
+ {
+ /// <summary>
+ /// Gets the latest media.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{ChannelItemInfo}}.</returns>
+ Task<IEnumerable<ChannelItemInfo>> GetLatestMedia(ChannelLatestMediaSearch request, CancellationToken cancellationToken);
+ }
+
+ public interface ISupportsDelete
+ {
+ bool CanDelete(BaseItem item);
+ Task DeleteItem(string id, CancellationToken cancellationToken);
+ }
+
+ public interface IDisableMediaSourceDisplay
+ {
+
+ }
+
+ public interface ISupportsMediaProbe
+ {
+
+ }
+
+ public interface IHasFolderAttributes
+ {
+ string[] Attributes { get; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs
new file mode 100644
index 000000000..976808aad
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs
@@ -0,0 +1,61 @@
+using System;
+using MediaBrowser.Model.Channels;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Channels
+{
+ public class InternalChannelFeatures
+ {
+ /// <summary>
+ /// Gets or sets the media types.
+ /// </summary>
+ /// <value>The media types.</value>
+ public List<ChannelMediaType> MediaTypes { get; set; }
+
+ /// <summary>
+ /// Gets or sets the content types.
+ /// </summary>
+ /// <value>The content types.</value>
+ public List<ChannelMediaContentType> ContentTypes { get; set; }
+
+ /// <summary>
+ /// Represents the maximum number of records the channel allows retrieving at a time
+ /// </summary>
+ public int? MaxPageSize { get; set; }
+
+ /// <summary>
+ /// Gets or sets the default sort orders.
+ /// </summary>
+ /// <value>The default sort orders.</value>
+ public List<ChannelItemSortField> DefaultSortFields { get; set; }
+
+ /// <summary>
+ /// Indicates if a sort ascending/descending toggle is supported or not.
+ /// </summary>
+ public bool SupportsSortOrderToggle { get; set; }
+ /// <summary>
+ /// Gets or sets the automatic refresh levels.
+ /// </summary>
+ /// <value>The automatic refresh levels.</value>
+ public int? AutoRefreshLevels { get; set; }
+
+ /// <summary>
+ /// Gets or sets the daily download limit.
+ /// </summary>
+ /// <value>The daily download limit.</value>
+ public int? DailyDownloadLimit { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether [supports downloading].
+ /// </summary>
+ /// <value><c>true</c> if [supports downloading]; otherwise, <c>false</c>.</value>
+ public bool SupportsContentDownloading { get; set; }
+
+ public InternalChannelFeatures()
+ {
+ MediaTypes = new List<ChannelMediaType>();
+ ContentTypes = new List<ChannelMediaContentType>();
+
+ DefaultSortFields = new List<ChannelItemSortField>();
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs b/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs
new file mode 100644
index 000000000..c69a1f6c3
--- /dev/null
+++ b/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs
@@ -0,0 +1,21 @@
+using MediaBrowser.Model.Channels;
+using System;
+
+
+namespace MediaBrowser.Controller.Channels
+{
+ public class InternalChannelItemQuery
+ {
+ public string FolderId { get; set; }
+
+ public Guid UserId { get; set; }
+
+ public int? StartIndex { get; set; }
+
+ public int? Limit { get; set; }
+
+ public ChannelItemSortField? SortBy { get; set; }
+
+ public bool SortDescending { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs
new file mode 100644
index 000000000..2a20eb365
--- /dev/null
+++ b/MediaBrowser.Controller/Chapters/IChapterManager.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Chapters
+{
+ /// <summary>
+ /// Interface IChapterManager
+ /// </summary>
+ public interface IChapterManager
+ {
+ /// <summary>
+ /// Gets the chapters.
+ /// </summary>
+ /// <param name="itemId">The item identifier.</param>
+ /// <returns>List{ChapterInfo}.</returns>
+
+ /// <summary>
+ /// Saves the chapters.
+ /// </summary>
+ void SaveChapters(string itemId, List<ChapterInfo> chapters);
+ }
+}
diff --git a/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs
new file mode 100644
index 000000000..727b487a7
--- /dev/null
+++ b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs
@@ -0,0 +1,27 @@
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Collections
+{
+ public class CollectionCreationOptions : IHasProviderIds
+ {
+ public string Name { get; set; }
+
+ public Guid? ParentId { get; set; }
+
+ public bool IsLocked { get; set; }
+
+ public Dictionary<string, string> ProviderIds { get; set; }
+
+ public string[] ItemIdList { get; set; }
+ public Guid[] UserIds { get; set; }
+
+ public CollectionCreationOptions()
+ {
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ ItemIdList = new string[] {};
+ UserIds = new Guid[] {};
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Collections/CollectionEvents.cs b/MediaBrowser.Controller/Collections/CollectionEvents.cs
new file mode 100644
index 000000000..80f66a444
--- /dev/null
+++ b/MediaBrowser.Controller/Collections/CollectionEvents.cs
@@ -0,0 +1,37 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Collections
+{
+ public class CollectionCreatedEventArgs : EventArgs
+ {
+ /// <summary>
+ /// Gets or sets the collection.
+ /// </summary>
+ /// <value>The collection.</value>
+ public BoxSet Collection { get; set; }
+
+ /// <summary>
+ /// Gets or sets the options.
+ /// </summary>
+ /// <value>The options.</value>
+ public CollectionCreationOptions Options { get; set; }
+ }
+
+ public class CollectionModifiedEventArgs : EventArgs
+ {
+ /// <summary>
+ /// Gets or sets the collection.
+ /// </summary>
+ /// <value>The collection.</value>
+ public BoxSet Collection { get; set; }
+
+ /// <summary>
+ /// Gets or sets the items changed.
+ /// </summary>
+ /// <value>The items changed.</value>
+ public List<BaseItem> ItemsChanged { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs
new file mode 100644
index 000000000..05bc927ba
--- /dev/null
+++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs
@@ -0,0 +1,57 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Collections
+{
+ public interface ICollectionManager
+ {
+ /// <summary>
+ /// Occurs when [collection created].
+ /// </summary>
+ event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
+
+ /// <summary>
+ /// Occurs when [items added to collection].
+ /// </summary>
+ event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
+
+ /// <summary>
+ /// Occurs when [items removed from collection].
+ /// </summary>
+ event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
+
+ /// <summary>
+ /// Creates the collection.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ BoxSet CreateCollection(CollectionCreationOptions options);
+
+ /// <summary>
+ /// Adds to collection.
+ /// </summary>
+ /// <param name="collectionId">The collection identifier.</param>
+ /// <param name="itemIds">The item ids.</param>
+ void AddToCollection(Guid collectionId, IEnumerable<string> itemIds);
+
+ /// <summary>
+ /// Removes from collection.
+ /// </summary>
+ /// <param name="collectionId">The collection identifier.</param>
+ /// <param name="itemIds">The item ids.</param>
+ void RemoveFromCollection(Guid collectionId, IEnumerable<string> itemIds);
+
+ void AddToCollection(Guid collectionId, IEnumerable<Guid> itemIds);
+ void RemoveFromCollection(Guid collectionId, IEnumerable<Guid> itemIds);
+
+ /// <summary>
+ /// Collapses the items within box sets.
+ /// </summary>
+ /// <param name="items">The items.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ IEnumerable<BaseItem> CollapseItemsWithinBoxSets(IEnumerable<BaseItem> items, User user);
+ }
+}
diff --git a/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs b/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs
new file mode 100644
index 000000000..af5714932
--- /dev/null
+++ b/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs
@@ -0,0 +1,25 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Controller.Configuration
+{
+ /// <summary>
+ /// Interface IServerConfigurationManager
+ /// </summary>
+ public interface IServerConfigurationManager : IConfigurationManager
+ {
+ /// <summary>
+ /// Gets the application paths.
+ /// </summary>
+ /// <value>The application paths.</value>
+ IServerApplicationPaths ApplicationPaths { get; }
+
+ /// <summary>
+ /// Gets the configuration.
+ /// </summary>
+ /// <value>The configuration.</value>
+ ServerConfiguration Configuration { get; }
+
+ bool SetOptimalValues();
+ }
+}
diff --git a/MediaBrowser.Controller/Connect/IConnectManager.cs b/MediaBrowser.Controller/Connect/IConnectManager.cs
new file mode 100644
index 000000000..8ac61bf2b
--- /dev/null
+++ b/MediaBrowser.Controller/Connect/IConnectManager.cs
@@ -0,0 +1,45 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Connect;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Connect
+{
+ public interface IConnectManager
+ {
+ /// <summary>
+ /// Gets the wan API address.
+ /// </summary>
+ /// <value>The wan API address.</value>
+ string WanApiAddress { get; }
+
+ /// <summary>
+ /// Links the user.
+ /// </summary>
+ /// <param name="userId">The user identifier.</param>
+ /// <param name="connectUsername">The connect username.</param>
+ /// <returns>Task.</returns>
+ Task<UserLinkResult> LinkUser(string userId, string connectUsername);
+
+ /// <summary>
+ /// Removes the link.
+ /// </summary>
+ /// <param name="userId">The user identifier.</param>
+ /// <returns>Task.</returns>
+ Task RemoveConnect(string userId);
+
+ User GetUserFromExchangeToken(string token);
+
+ /// <summary>
+ /// Authenticates the specified username.
+ /// </summary>
+ Task<ConnectAuthenticationResult> Authenticate(string username, string password, string passwordMd5);
+
+ /// <summary>
+ /// Determines whether [is authorization token valid] [the specified token].
+ /// </summary>
+ /// <param name="token">The token.</param>
+ /// <returns><c>true</c> if [is authorization token valid] [the specified token]; otherwise, <c>false</c>.</returns>
+ bool IsAuthorizationTokenValid(string token);
+ }
+}
diff --git a/MediaBrowser.Controller/Connect/UserLinkResult.cs b/MediaBrowser.Controller/Connect/UserLinkResult.cs
new file mode 100644
index 000000000..16ebfc70a
--- /dev/null
+++ b/MediaBrowser.Controller/Connect/UserLinkResult.cs
@@ -0,0 +1,10 @@
+
+namespace MediaBrowser.Controller.Connect
+{
+ public class UserLinkResult
+ {
+ public bool IsPending { get; set; }
+ public bool IsNewUserInvitation { get; set; }
+ public string GuestDisplayName { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs b/MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs
new file mode 100644
index 000000000..b3f3bb902
--- /dev/null
+++ b/MediaBrowser.Controller/Devices/CameraImageUploadInfo.cs
@@ -0,0 +1,10 @@
+using MediaBrowser.Model.Devices;
+
+namespace MediaBrowser.Controller.Devices
+{
+ public class CameraImageUploadInfo
+ {
+ public LocalFileInfo FileInfo { get; set; }
+ public DeviceInfo Device { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs
new file mode 100644
index 000000000..d29fb8ded
--- /dev/null
+++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs
@@ -0,0 +1,73 @@
+using MediaBrowser.Model.Devices;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Session;
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Devices
+{
+ public interface IDeviceManager
+ {
+ /// <summary>
+ /// Occurs when [camera image uploaded].
+ /// </summary>
+ event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded;
+
+ /// <summary>
+ /// Saves the capabilities.
+ /// </summary>
+ /// <param name="reportedId">The reported identifier.</param>
+ /// <param name="capabilities">The capabilities.</param>
+ /// <returns>Task.</returns>
+ void SaveCapabilities(string reportedId, ClientCapabilities capabilities);
+
+ /// <summary>
+ /// Gets the capabilities.
+ /// </summary>
+ /// <param name="reportedId">The reported identifier.</param>
+ /// <returns>ClientCapabilities.</returns>
+ ClientCapabilities GetCapabilities(string reportedId);
+
+ /// <summary>
+ /// Gets the device information.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <returns>DeviceInfo.</returns>
+ DeviceInfo GetDevice(string id);
+
+ /// <summary>
+ /// Gets the devices.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns>
+ QueryResult<DeviceInfo> GetDevices(DeviceQuery query);
+
+ /// <summary>
+ /// Gets the upload history.
+ /// </summary>
+ /// <param name="deviceId">The device identifier.</param>
+ /// <returns>ContentUploadHistory.</returns>
+ ContentUploadHistory GetCameraUploadHistory(string deviceId);
+
+ /// <summary>
+ /// Accepts the upload.
+ /// </summary>
+ /// <param name="deviceId">The device identifier.</param>
+ /// <param name="stream">The stream.</param>
+ /// <param name="file">The file.</param>
+ /// <returns>Task.</returns>
+ Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file);
+
+ /// <summary>
+ /// Determines whether this instance [can access device] the specified user identifier.
+ /// </summary>
+ bool CanAccessDevice(User user, string deviceId);
+
+ void UpdateDeviceOptions(string deviceId, DeviceOptions options);
+ DeviceOptions GetDeviceOptions(string deviceId);
+ event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
+ }
+}
diff --git a/MediaBrowser.Controller/Dlna/IDlnaManager.cs b/MediaBrowser.Controller/Dlna/IDlnaManager.cs
new file mode 100644
index 000000000..2f64cd194
--- /dev/null
+++ b/MediaBrowser.Controller/Dlna/IDlnaManager.cs
@@ -0,0 +1,76 @@
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Model.Dlna;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Dlna
+{
+ public interface IDlnaManager
+ {
+ /// <summary>
+ /// Gets the profile infos.
+ /// </summary>
+ /// <returns>IEnumerable{DeviceProfileInfo}.</returns>
+ IEnumerable<DeviceProfileInfo> GetProfileInfos();
+
+ /// <summary>
+ /// Gets the profile.
+ /// </summary>
+ /// <param name="headers">The headers.</param>
+ /// <returns>DeviceProfile.</returns>
+ DeviceProfile GetProfile(IDictionary<string,string> headers);
+
+ /// <summary>
+ /// Gets the default profile.
+ /// </summary>
+ /// <returns>DeviceProfile.</returns>
+ DeviceProfile GetDefaultProfile();
+
+ /// <summary>
+ /// Creates the profile.
+ /// </summary>
+ /// <param name="profile">The profile.</param>
+ void CreateProfile(DeviceProfile profile);
+
+ /// <summary>
+ /// Updates the profile.
+ /// </summary>
+ /// <param name="profile">The profile.</param>
+ void UpdateProfile(DeviceProfile profile);
+
+ /// <summary>
+ /// Deletes the profile.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ void DeleteProfile(string id);
+
+ /// <summary>
+ /// Gets the profile.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <returns>DeviceProfile.</returns>
+ DeviceProfile GetProfile(string id);
+
+ /// <summary>
+ /// Gets the profile.
+ /// </summary>
+ /// <param name="deviceInfo">The device information.</param>
+ /// <returns>DeviceProfile.</returns>
+ DeviceProfile GetProfile(DeviceIdentification deviceInfo);
+
+ /// <summary>
+ /// Gets the server description XML.
+ /// </summary>
+ /// <param name="headers">The headers.</param>
+ /// <param name="serverUuId">The server uu identifier.</param>
+ /// <param name="serverAddress">The server address.</param>
+ /// <returns>System.String.</returns>
+ string GetServerDescriptionXml(IDictionary<string, string> headers, string serverUuId, string serverAddress);
+
+ /// <summary>
+ /// Gets the icon.
+ /// </summary>
+ /// <param name="filename">The filename.</param>
+ /// <returns>DlnaIconResponse.</returns>
+ ImageStream GetIcon(string filename);
+ }
+}
diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
new file mode 100644
index 000000000..757448eb2
--- /dev/null
+++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
@@ -0,0 +1,49 @@
+using System;
+using MediaBrowser.Model.Drawing;
+
+namespace MediaBrowser.Controller.Drawing
+{
+ public interface IImageEncoder
+ {
+ /// <summary>
+ /// Gets the supported input formats.
+ /// </summary>
+ /// <value>The supported input formats.</value>
+ string[] SupportedInputFormats { get; }
+ /// <summary>
+ /// Gets the supported output formats.
+ /// </summary>
+ /// <value>The supported output formats.</value>
+ ImageFormat[] SupportedOutputFormats { get; }
+
+ /// <summary>
+ /// Encodes the image.
+ /// </summary>
+ string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat);
+
+ /// <summary>
+ /// Creates the image collage.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ void CreateImageCollage(ImageCollageOptions options);
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether [supports image collage creation].
+ /// </summary>
+ /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value>
+ bool SupportsImageCollageCreation { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether [supports image encoding].
+ /// </summary>
+ /// <value><c>true</c> if [supports image encoding]; otherwise, <c>false</c>.</value>
+ bool SupportsImageEncoding { get; }
+
+ ImageSize GetImageSize(string path);
+ }
+}
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
new file mode 100644
index 000000000..fdf10e223
--- /dev/null
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -0,0 +1,118 @@
+using System;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Drawing
+{
+ /// <summary>
+ /// Interface IImageProcessor
+ /// </summary>
+ public interface IImageProcessor
+ {
+ /// <summary>
+ /// Gets the supported input formats.
+ /// </summary>
+ /// <value>The supported input formats.</value>
+ string[] SupportedInputFormats { get; }
+
+ /// <summary>
+ /// Gets the image enhancers.
+ /// </summary>
+ /// <value>The image enhancers.</value>
+ IImageEnhancer[] ImageEnhancers { get; }
+
+ ImageSize GetImageSize(string path);
+
+ /// <summary>
+ /// Gets the size of the image.
+ /// </summary>
+ /// <param name="info">The information.</param>
+ /// <returns>ImageSize.</returns>
+ ImageSize GetImageSize(BaseItem item, ItemImageInfo info);
+
+ ImageSize GetImageSize(BaseItem item, ItemImageInfo info, bool allowSlowMethods, bool updateItem);
+
+ /// <summary>
+ /// Adds the parts.
+ /// </summary>
+ /// <param name="enhancers">The enhancers.</param>
+ void AddParts(IEnumerable<IImageEnhancer> enhancers);
+
+ /// <summary>
+ /// Gets the supported enhancers.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <returns>IEnumerable{IImageEnhancer}.</returns>
+ IImageEnhancer[] GetSupportedEnhancers(BaseItem item, ImageType imageType);
+
+ /// <summary>
+ /// Gets the image cache tag.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="image">The image.</param>
+ /// <returns>Guid.</returns>
+ string GetImageCacheTag(BaseItem item, ItemImageInfo image);
+ string GetImageCacheTag(BaseItem item, ChapterInfo info);
+
+ /// <summary>
+ /// Gets the image cache tag.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="image">The image.</param>
+ /// <param name="imageEnhancers">The image enhancers.</param>
+ /// <returns>Guid.</returns>
+ string GetImageCacheTag(BaseItem item, ItemImageInfo image, IImageEnhancer[] imageEnhancers);
+
+ /// <summary>
+ /// Processes the image.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <param name="toStream">To stream.</param>
+ /// <returns>Task.</returns>
+ Task ProcessImage(ImageProcessingOptions options, Stream toStream);
+
+ /// <summary>
+ /// Processes the image.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <returns>Task.</returns>
+ Task<Tuple<string, string, DateTime>> ProcessImage(ImageProcessingOptions options);
+
+ /// <summary>
+ /// Gets the enhanced image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <returns>Task{System.String}.</returns>
+ Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex);
+
+ /// <summary>
+ /// Gets the supported image output formats.
+ /// </summary>
+ /// <returns>ImageOutputFormat[].</returns>
+ ImageFormat[] GetSupportedImageOutputFormats();
+
+ /// <summary>
+ /// Creates the image collage.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ void CreateImageCollage(ImageCollageOptions options);
+
+ /// <summary>
+ /// Gets a value indicating whether [supports image collage creation].
+ /// </summary>
+ /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value>
+ bool SupportsImageCollageCreation { get; }
+
+ IImageEncoder ImageEncoder { get; set; }
+
+ bool SupportsTransparency(string path);
+ }
+}
diff --git a/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs
new file mode 100644
index 000000000..92a7f5ac9
--- /dev/null
+++ b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs
@@ -0,0 +1,27 @@
+
+namespace MediaBrowser.Controller.Drawing
+{
+ public class ImageCollageOptions
+ {
+ /// <summary>
+ /// Gets or sets the input paths.
+ /// </summary>
+ /// <value>The input paths.</value>
+ public string[] InputPaths { get; set; }
+ /// <summary>
+ /// Gets or sets the output path.
+ /// </summary>
+ /// <value>The output path.</value>
+ public string OutputPath { get; set; }
+ /// <summary>
+ /// Gets or sets the width.
+ /// </summary>
+ /// <value>The width.</value>
+ public int Width { get; set; }
+ /// <summary>
+ /// Gets or sets the height.
+ /// </summary>
+ /// <value>The height.</value>
+ public int Height { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Drawing/ImageHelper.cs b/MediaBrowser.Controller/Drawing/ImageHelper.cs
new file mode 100644
index 000000000..6fb9f256e
--- /dev/null
+++ b/MediaBrowser.Controller/Drawing/ImageHelper.cs
@@ -0,0 +1,72 @@
+using System;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Drawing
+{
+ public static class ImageHelper
+ {
+ public static ImageSize GetNewImageSize(ImageProcessingOptions options, ImageSize? originalImageSize)
+ {
+ if (originalImageSize.HasValue)
+ {
+ // Determine the output size based on incoming parameters
+ var newSize = DrawingUtils.Resize(originalImageSize.Value, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0);
+
+ return newSize;
+ }
+ return GetSizeEstimate(options);
+ }
+
+ public static IImageProcessor ImageProcessor { get; set; }
+
+ private static ImageSize GetSizeEstimate(ImageProcessingOptions options)
+ {
+ if (options.Width.HasValue && options.Height.HasValue)
+ {
+ return new ImageSize(options.Width.Value, options.Height.Value);
+ }
+
+ var aspect = GetEstimatedAspectRatio(options.Image.Type, options.Item);
+
+ var width = options.Width ?? options.MaxWidth;
+
+ if (width.HasValue)
+ {
+ var heightValue = width.Value / aspect;
+ return new ImageSize(width.Value, heightValue);
+ }
+
+ var height = options.Height ?? options.MaxHeight ?? 200;
+ var widthValue = aspect * height;
+ return new ImageSize(widthValue, height);
+ }
+
+ private static double GetEstimatedAspectRatio(ImageType type, BaseItem item)
+ {
+ switch (type)
+ {
+ case ImageType.Art:
+ case ImageType.Backdrop:
+ case ImageType.Chapter:
+ case ImageType.Screenshot:
+ case ImageType.Thumb:
+ return 1.78;
+ case ImageType.Banner:
+ return 5.4;
+ case ImageType.Box:
+ case ImageType.BoxRear:
+ case ImageType.Disc:
+ case ImageType.Menu:
+ return 1;
+ case ImageType.Logo:
+ return 2.58;
+ case ImageType.Primary:
+ return item.GetDefaultPrimaryImageAspectRatio();
+ default:
+ return 1;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
new file mode 100644
index 000000000..ffc3e6cc0
--- /dev/null
+++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
@@ -0,0 +1,114 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Drawing;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Drawing
+{
+ public class ImageProcessingOptions
+ {
+ public ImageProcessingOptions()
+ {
+ RequiresAutoOrientation = true;
+ }
+
+ public Guid ItemId { get; set; }
+ public BaseItem Item { get; set; }
+
+ public ItemImageInfo Image { get; set; }
+
+ public int ImageIndex { get; set; }
+
+ public bool CropWhiteSpace { get; set; }
+
+ public int? Width { get; set; }
+
+ public int? Height { get; set; }
+
+ public int? MaxWidth { get; set; }
+
+ public int? MaxHeight { get; set; }
+
+ public int Quality { get; set; }
+
+ public IImageEnhancer[] Enhancers { get; set; }
+
+ public ImageFormat[] SupportedOutputFormats { get; set; }
+
+ public bool AddPlayedIndicator { get; set; }
+
+ public int? UnplayedCount { get; set; }
+ public int? Blur { get; set; }
+
+ public double PercentPlayed { get; set; }
+
+ public string BackgroundColor { get; set; }
+ public string ForegroundLayer { get; set; }
+ public bool RequiresAutoOrientation { get; set; }
+
+ private bool HasDefaultOptions(string originalImagePath)
+ {
+ return HasDefaultOptionsWithoutSize(originalImagePath) &&
+ !Width.HasValue &&
+ !Height.HasValue &&
+ !MaxWidth.HasValue &&
+ !MaxHeight.HasValue;
+ }
+
+ public bool HasDefaultOptions(string originalImagePath, ImageSize? size)
+ {
+ if (!size.HasValue)
+ {
+ return HasDefaultOptions(originalImagePath);
+ }
+
+ if (!HasDefaultOptionsWithoutSize(originalImagePath))
+ {
+ return false;
+ }
+
+ var sizeValue = size.Value;
+
+ if (Width.HasValue && !sizeValue.Width.Equals(Width.Value))
+ {
+ return false;
+ }
+ if (Height.HasValue && !sizeValue.Height.Equals(Height.Value))
+ {
+ return false;
+ }
+ if (MaxWidth.HasValue && sizeValue.Width > MaxWidth.Value)
+ {
+ return false;
+ }
+ if (MaxHeight.HasValue && sizeValue.Height > MaxHeight.Value)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool HasDefaultOptionsWithoutSize(string originalImagePath)
+ {
+ return (Quality >= 90) &&
+ IsFormatSupported(originalImagePath) &&
+ !AddPlayedIndicator &&
+ PercentPlayed.Equals(0) &&
+ !UnplayedCount.HasValue &&
+ !Blur.HasValue &&
+ !CropWhiteSpace &&
+ string.IsNullOrEmpty(BackgroundColor) &&
+ string.IsNullOrEmpty(ForegroundLayer);
+ }
+
+ private bool IsFormatSupported(string originalImagePath)
+ {
+ var ext = Path.GetExtension(originalImagePath);
+ return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, "." + outputFormat, StringComparison.OrdinalIgnoreCase));
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs
new file mode 100644
index 000000000..948219bf5
--- /dev/null
+++ b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs
@@ -0,0 +1,25 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Drawing
+{
+ public static class ImageProcessorExtensions
+ {
+ public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType)
+ {
+ return processor.GetImageCacheTag(item, imageType, 0);
+ }
+
+ public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType, int imageIndex)
+ {
+ var imageInfo = item.GetImageInfo(imageType, imageIndex);
+
+ if (imageInfo == null)
+ {
+ return null;
+ }
+
+ return processor.GetImageCacheTag(item, imageInfo);
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Drawing/ImageStream.cs b/MediaBrowser.Controller/Drawing/ImageStream.cs
new file mode 100644
index 000000000..353abaca3
--- /dev/null
+++ b/MediaBrowser.Controller/Drawing/ImageStream.cs
@@ -0,0 +1,28 @@
+using MediaBrowser.Model.Drawing;
+using System;
+using System.IO;
+
+namespace MediaBrowser.Controller.Drawing
+{
+ public class ImageStream : IDisposable
+ {
+ /// <summary>
+ /// Gets or sets the stream.
+ /// </summary>
+ /// <value>The stream.</value>
+ public Stream Stream { get; set; }
+ /// <summary>
+ /// Gets or sets the format.
+ /// </summary>
+ /// <value>The format.</value>
+ public ImageFormat Format { get; set; }
+
+ public void Dispose()
+ {
+ if (Stream != null)
+ {
+ Stream.Dispose();
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs
new file mode 100644
index 000000000..b5ce09028
--- /dev/null
+++ b/MediaBrowser.Controller/Dto/DtoOptions.cs
@@ -0,0 +1,72 @@
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Dto
+{
+ public class DtoOptions
+ {
+ private static readonly ItemFields[] DefaultExcludedFields = new []
+ {
+ ItemFields.SeasonUserData,
+ ItemFields.RefreshState
+ };
+
+ public ItemFields[] Fields { get; set; }
+ public ImageType[] ImageTypes { get; set; }
+ public int ImageTypeLimit { get; set; }
+ public bool EnableImages { get; set; }
+ public bool AddProgramRecordingInfo { get; set; }
+ public bool EnableUserData { get; set; }
+ public bool AddCurrentProgram { get; set; }
+
+ public DtoOptions()
+ : this(true)
+ {
+ }
+
+ private static readonly ImageType[] AllImageTypes = Enum.GetNames(typeof(ImageType))
+ .Select(i => (ImageType)Enum.Parse(typeof(ImageType), i, true))
+ .ToArray();
+
+ private static readonly ItemFields[] AllItemFields = Enum.GetNames(typeof(ItemFields))
+ .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true))
+ .Except(DefaultExcludedFields)
+ .ToArray();
+
+ public bool ContainsField(ItemFields field)
+ {
+ return AllItemFields.Contains(field);
+ }
+
+ public DtoOptions(bool allFields)
+ {
+ ImageTypeLimit = int.MaxValue;
+ EnableImages = true;
+ EnableUserData = true;
+ AddCurrentProgram = true;
+
+ if (allFields)
+ {
+ Fields = AllItemFields;
+ }
+ else
+ {
+ Fields = new ItemFields[] { };
+ }
+
+ ImageTypes = AllImageTypes;
+ }
+
+ public int GetImageLimit(ImageType type)
+ {
+ if (EnableImages && ImageTypes.Contains(type))
+ {
+ return ImageTypeLimit;
+ }
+
+ return 0;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs
new file mode 100644
index 000000000..219b36789
--- /dev/null
+++ b/MediaBrowser.Controller/Dto/IDtoService.cs
@@ -0,0 +1,70 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Sync;
+
+namespace MediaBrowser.Controller.Dto
+{
+ /// <summary>
+ /// Interface IDtoService
+ /// </summary>
+ public interface IDtoService
+ {
+ /// <summary>
+ /// Gets the dto id.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>System.String.</returns>
+ string GetDtoId(BaseItem item);
+
+ /// <summary>
+ /// Attaches the primary image aspect ratio.
+ /// </summary>
+ /// <param name="dto">The dto.</param>
+ /// <param name="item">The item.</param>
+ void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item);
+
+ /// <summary>
+ /// Gets the primary image aspect ratio.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>System.Nullable&lt;System.Double&gt;.</returns>
+ double? GetPrimaryImageAspectRatio(BaseItem item);
+
+ /// <summary>
+ /// Gets the base item dto.
+ /// </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>
+ BaseItemDto GetBaseItemDto(BaseItem item, ItemFields[] fields, User user = null, BaseItem owner = null);
+
+ /// <summary>
+ /// Gets the base item dto.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="options">The options.</param>
+ /// <param name="user">The user.</param>
+ /// <param name="owner">The owner.</param>
+ /// <returns>BaseItemDto.</returns>
+ BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null);
+
+ /// <summary>
+ /// Gets the base item dtos.
+ /// </summary>
+ /// <param name="items">The items.</param>
+ /// <param name="options">The options.</param>
+ /// <param name="user">The user.</param>
+ /// <param name="owner">The owner.</param>
+ BaseItemDto[] GetBaseItemDtos(BaseItem[] items, DtoOptions options, User user = null, BaseItem owner = null);
+
+ BaseItemDto[] GetBaseItemDtos(List<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null);
+
+ /// <summary>
+ /// Gets the item by name dto.
+ /// </summary>
+ BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, User user = null);
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs
new file mode 100644
index 000000000..4f4b3483c
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs
@@ -0,0 +1,219 @@
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Specialized folder that can have items added to it's children by external entities.
+ /// Used for our RootFolder so plug-ins can add items.
+ /// </summary>
+ public class AggregateFolder : Folder
+ {
+ public AggregateFolder()
+ {
+ PhysicalLocationsList = new string[] { };
+ }
+
+ [IgnoreDataMember]
+ public override bool IsPhysicalRoot
+ {
+ get { return true; }
+ }
+
+ public override bool CanDelete()
+ {
+ return false;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPlayedStatus
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// The _virtual children
+ /// </summary>
+ private readonly ConcurrentBag<BaseItem> _virtualChildren = new ConcurrentBag<BaseItem>();
+
+ /// <summary>
+ /// Gets the virtual children.
+ /// </summary>
+ /// <value>The virtual children.</value>
+ public ConcurrentBag<BaseItem> VirtualChildren
+ {
+ get { return _virtualChildren; }
+ }
+
+ [IgnoreDataMember]
+ public override string[] PhysicalLocations
+ {
+ get
+ {
+ return PhysicalLocationsList;
+ }
+ }
+
+ public string[] PhysicalLocationsList { get; set; }
+
+ protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
+ {
+ return CreateResolveArgs(directoryService, true).FileSystemChildren;
+ }
+
+ private Guid[] _childrenIds = null;
+ private readonly object _childIdsLock = new object();
+ protected override List<BaseItem> LoadChildren()
+ {
+ lock (_childIdsLock)
+ {
+ if (_childrenIds == null || _childrenIds.Length == 0)
+ {
+ var list = base.LoadChildren();
+ _childrenIds = list.Select(i => i.Id).ToArray();
+ return list;
+ }
+
+ return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList();
+ }
+ }
+
+ private void ClearCache()
+ {
+ lock (_childIdsLock)
+ {
+ _childrenIds = null;
+ }
+ }
+
+ private bool _requiresRefresh;
+ public override bool RequiresRefresh()
+ {
+ var changed = base.RequiresRefresh() || _requiresRefresh;
+
+ if (!changed)
+ {
+ var locations = PhysicalLocations;
+
+ var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations;
+
+ if (!locations.SequenceEqual(newLocations))
+ {
+ changed = true;
+ }
+ }
+
+ return changed;
+ }
+
+ public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ {
+ ClearCache();
+
+ var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh;
+ _requiresRefresh = false;
+ return changed;
+ }
+
+ private ItemResolveArgs CreateResolveArgs(IDirectoryService directoryService, bool setPhysicalLocations)
+ {
+ ClearCache();
+
+ var path = ContainingFolderPath;
+
+ var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
+ {
+ FileInfo = FileSystem.GetDirectoryInfo(path),
+ Path = path
+ };
+
+ // Gather child folder and files
+ if (args.IsDirectory)
+ {
+ // When resolving the root, we need it's grandchildren (children of user views)
+ var flattenFolderDepth = 2;
+
+ var files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, FileSystem, CollectionFolder.ApplicationHost, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: true);
+
+ // Need to remove subpaths that may have been resolved from shortcuts
+ // Example: if \\server\movies exists, then strip out \\server\movies\action
+ files = LibraryManager.NormalizeRootPathList(files).ToArray();
+
+ args.FileSystemChildren = files;
+ }
+
+ _requiresRefresh = _requiresRefresh || !args.PhysicalLocations.SequenceEqual(PhysicalLocations);
+ if (setPhysicalLocations)
+ {
+ PhysicalLocationsList = args.PhysicalLocations;
+ }
+
+ return args;
+ }
+
+ protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
+ {
+ return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren);
+ }
+
+ protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ {
+ ClearCache();
+
+ await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService)
+ .ConfigureAwait(false);
+
+ ClearCache();
+ }
+
+ /// <summary>
+ /// Adds the virtual child.
+ /// </summary>
+ /// <param name="child">The child.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddVirtualChild(BaseItem child)
+ {
+ if (child == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ _virtualChildren.Add(child);
+ }
+
+ /// <summary>
+ /// Finds the virtual child.
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <returns>BaseItem.</returns>
+ /// <exception cref="System.ArgumentNullException">id</exception>
+ public BaseItem FindVirtualChild(Guid id)
+ {
+ if (id.Equals(Guid.Empty))
+ {
+ throw new ArgumentNullException("id");
+ }
+
+ foreach (var child in _virtualChildren)
+ {
+ if (child.Id == id)
+ {
+ return child;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs
new file mode 100644
index 000000000..d07e31d8a
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs
@@ -0,0 +1,216 @@
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.MediaInfo;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities.Audio
+{
+ /// <summary>
+ /// Class Audio
+ /// </summary>
+ public class Audio : BaseItem,
+ IHasAlbumArtist,
+ IHasArtist,
+ IHasMusicGenres,
+ IHasLookupInfo<SongInfo>,
+ IHasMediaSources
+ {
+ /// <summary>
+ /// Gets or sets the artist.
+ /// </summary>
+ /// <value>The artist.</value>
+ [IgnoreDataMember]
+ public string[] Artists { get; set; }
+
+ [IgnoreDataMember]
+ public string[] AlbumArtists { get; set; }
+
+ public Audio()
+ {
+ Artists = new string[] {};
+ AlbumArtists = new string[] {};
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ return 1;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPlayedStatus
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get { return false; }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsAddingToPlaylist
+ {
+ get { return true; }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsInheritedParentImages
+ {
+ get { return true; }
+ }
+
+ [IgnoreDataMember]
+ protected override bool SupportsOwnedItems
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override Folder LatestItemsIndexContainer
+ {
+ get
+ {
+ return AlbumEntity;
+ }
+ }
+
+ public override bool CanDownload()
+ {
+ return IsFileProtocol;
+ }
+
+ [IgnoreDataMember]
+ public string[] AllArtists
+ {
+ get
+ {
+ var list = new string[AlbumArtists.Length + Artists.Length];
+
+ var index = 0;
+ foreach (var artist in AlbumArtists)
+ {
+ list[index] = artist;
+ index++;
+ }
+ foreach (var artist in Artists)
+ {
+ list[index] = artist;
+ index++;
+ }
+
+ return list;
+
+ }
+ }
+
+ [IgnoreDataMember]
+ public MusicAlbum AlbumEntity
+ {
+ get { return FindParent<MusicAlbum>(); }
+ }
+
+ /// <summary>
+ /// Gets the type of the media.
+ /// </summary>
+ /// <value>The type of the media.</value>
+ [IgnoreDataMember]
+ public override string MediaType
+ {
+ get
+ {
+ return Model.Entities.MediaType.Audio;
+ }
+ }
+
+ /// <summary>
+ /// Creates the name of the sort.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ protected override string CreateSortName()
+ {
+ return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("0000 - ") : "")
+ + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name;
+ }
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000") : string.Empty;
+
+
+ if (ParentIndexNumber.HasValue)
+ {
+ songKey = ParentIndexNumber.Value.ToString("0000") + "-" + songKey;
+ }
+ songKey += Name;
+
+ if (!string.IsNullOrEmpty(Album))
+ {
+ songKey = Album + "-" + songKey;
+ }
+
+ var albumArtist = AlbumArtists.Length == 0 ? null : AlbumArtists[0];
+ if (!string.IsNullOrEmpty(albumArtist))
+ {
+ songKey = albumArtist + "-" + songKey;
+ }
+
+ list.Insert(0, songKey);
+
+ return list;
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ if (SourceType == SourceType.Library)
+ {
+ return UnratedItem.Music;
+ }
+ return base.GetBlockUnratedType();
+ }
+
+ public List<MediaStream> GetMediaStreams(MediaStreamType type)
+ {
+ return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
+ {
+ ItemId = Id,
+ Type = type
+ });
+ }
+
+ public SongInfo GetLookupInfo()
+ {
+ var info = GetItemLookupInfo<SongInfo>();
+
+ info.AlbumArtists = AlbumArtists;
+ info.Album = Album;
+ info.Artists = Artists;
+
+ return info;
+ }
+
+ protected override List<Tuple<BaseItem, MediaSourceType>> GetAllItemsForMediaSources()
+ {
+ var list = new List<Tuple<BaseItem, MediaSourceType>>();
+ list.Add(new Tuple<BaseItem, MediaSourceType>(this, MediaSourceType.Default));
+ return list;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
new file mode 100644
index 000000000..b2dedada4
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs
@@ -0,0 +1,15 @@
+
+namespace MediaBrowser.Controller.Entities.Audio
+{
+ public interface IHasAlbumArtist
+ {
+ string[] AlbumArtists { get; set; }
+ }
+
+ public interface IHasArtist
+ {
+ string[] AllArtists { get; }
+
+ string[] Artists { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs
new file mode 100644
index 000000000..2200d4b75
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Entities.Audio
+{
+ public interface IHasMusicGenres
+ {
+ string[] Genres { get; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
new file mode 100644
index 000000000..48b5c64b2
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
@@ -0,0 +1,272 @@
+using System;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Users;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Library;
+
+namespace MediaBrowser.Controller.Entities.Audio
+{
+ /// <summary>
+ /// Class MusicAlbum
+ /// </summary>
+ public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<AlbumInfo>, IMetadataContainer
+ {
+ public string[] AlbumArtists { get; set; }
+ public string[] Artists { get; set; }
+
+ public MusicAlbum()
+ {
+ Artists = new string[] {};
+ AlbumArtists = new string[] {};
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsAddingToPlaylist
+ {
+ get { return true; }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsInheritedParentImages
+ {
+ get { return true; }
+ }
+
+ [IgnoreDataMember]
+ public MusicArtist MusicArtist
+ {
+ get { return GetMusicArtist(new DtoOptions(true)); }
+ }
+
+ public MusicArtist GetMusicArtist(DtoOptions options)
+ {
+ var parents = GetParents();
+ foreach (var parent in parents)
+ {
+ var artist = parent as MusicArtist;
+ if (artist != null)
+ {
+ return artist;
+ }
+ }
+
+ var name = AlbumArtist;
+ if (!string.IsNullOrEmpty(name))
+ {
+ return LibraryManager.GetArtist(name, options);
+ }
+ return null;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPlayedStatus
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsCumulativeRunTimeTicks
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public string[] AllArtists
+ {
+ get
+ {
+ var list = new string[AlbumArtists.Length + Artists.Length];
+
+ var index = 0;
+ foreach (var artist in AlbumArtists)
+ {
+ list[index] = artist;
+ index++;
+ }
+ foreach (var artist in Artists)
+ {
+ list[index] = artist;
+ index++;
+ }
+
+ return list;
+ }
+ }
+
+ [IgnoreDataMember]
+ public string AlbumArtist
+ {
+ get { return AlbumArtists.Length == 0 ? null : AlbumArtists[0]; }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get { return false; }
+ }
+
+ /// <summary>
+ /// Gets the tracks.
+ /// </summary>
+ /// <value>The tracks.</value>
+ [IgnoreDataMember]
+ public IEnumerable<BaseItem> Tracks
+ {
+ get
+ {
+ return GetRecursiveChildren(i => i is Audio);
+ }
+ }
+
+ protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
+ {
+ return Tracks;
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ return 1;
+ }
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ var albumArtist = AlbumArtist;
+ if (!string.IsNullOrEmpty(albumArtist))
+ {
+ list.Insert(0, albumArtist + "-" + Name);
+ }
+
+ var id = this.GetProviderId(MetadataProviders.MusicBrainzAlbum);
+
+ if (!string.IsNullOrEmpty(id))
+ {
+ list.Insert(0, "MusicAlbum-Musicbrainz-" + id);
+ }
+
+ id = this.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup);
+
+ if (!string.IsNullOrEmpty(id))
+ {
+ list.Insert(0, "MusicAlbum-MusicBrainzReleaseGroup-" + id);
+ }
+
+ return list;
+ }
+
+ protected override bool GetBlockUnratedValue(UserPolicy config)
+ {
+ return config.BlockUnratedItems.Contains(UnratedItem.Music);
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ return UnratedItem.Music;
+ }
+
+ public AlbumInfo GetLookupInfo()
+ {
+ var id = GetItemLookupInfo<AlbumInfo>();
+
+ id.AlbumArtists = AlbumArtists;
+
+ var artist = GetMusicArtist(new DtoOptions(false));
+
+ if (artist != null)
+ {
+ id.ArtistProviderIds = artist.ProviderIds;
+ }
+
+ id.SongInfos = GetRecursiveChildren(i => i is Audio)
+ .Cast<Audio>()
+ .Select(i => i.GetLookupInfo())
+ .ToList();
+
+ var album = id.SongInfos
+ .Select(i => i.Album)
+ .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+
+ if (!string.IsNullOrEmpty(album))
+ {
+ id.Name = album;
+ }
+
+ return id;
+ }
+
+ public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var items = GetRecursiveChildren();
+
+ var totalItems = items.Count;
+ var numComplete = 0;
+
+ var childUpdateType = ItemUpdateType.None;
+
+ // Refresh songs
+ foreach (var item in items)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var updateType = await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
+ childUpdateType = childUpdateType | updateType;
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= totalItems;
+ progress.Report(percent * 95);
+ }
+
+ var parentRefreshOptions = refreshOptions;
+ if (childUpdateType > ItemUpdateType.None)
+ {
+ parentRefreshOptions = new MetadataRefreshOptions(refreshOptions);
+ parentRefreshOptions.MetadataRefreshMode = MetadataRefreshMode.FullRefresh;
+ }
+
+ // Refresh current item
+ await RefreshMetadata(parentRefreshOptions, cancellationToken).ConfigureAwait(false);
+
+ if (!refreshOptions.IsAutomated)
+ {
+ await RefreshArtists(refreshOptions, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private async Task RefreshArtists(MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
+ {
+ var all = AllArtists;
+ foreach (var i in all)
+ {
+ // This should not be necessary but we're seeing some cases of it
+ if (string.IsNullOrEmpty(i))
+ {
+ continue;
+ }
+
+ var artist = LibraryManager.GetArtist(i);
+
+ if (!artist.IsAccessedByName)
+ {
+ continue;
+ }
+
+ await artist.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
new file mode 100644
index 000000000..82dece84b
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -0,0 +1,275 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Users;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Controller.Entities.Audio
+{
+ /// <summary>
+ /// Class MusicArtist
+ /// </summary>
+ public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo>
+ {
+ [IgnoreDataMember]
+ public bool IsAccessedByName
+ {
+ get { return ParentId.Equals(Guid.Empty); }
+ }
+
+ [IgnoreDataMember]
+ public override bool IsFolder
+ {
+ get
+ {
+ return !IsAccessedByName;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsInheritedParentImages
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsCumulativeRunTimeTicks
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool IsDisplayedAsFolder
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsAddingToPlaylist
+ {
+ get { return true; }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPlayedStatus
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ return 1;
+ }
+
+ public override bool CanDelete()
+ {
+ return !IsAccessedByName;
+ }
+
+ public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+ {
+ if (query.IncludeItemTypes.Length == 0)
+ {
+ query.IncludeItemTypes = new[] { typeof(Audio).Name, typeof(MusicVideo).Name, typeof(MusicAlbum).Name };
+ query.ArtistIds = new[] { Id };
+ }
+
+ return LibraryManager.GetItemList(query);
+ }
+
+ [IgnoreDataMember]
+ public override IEnumerable<BaseItem> Children
+ {
+ get
+ {
+ if (IsAccessedByName)
+ {
+ return new List<BaseItem>();
+ }
+
+ return base.Children;
+ }
+ }
+
+ public override int GetChildCount(User user)
+ {
+ if (IsAccessedByName)
+ {
+ return 0;
+ }
+ return base.GetChildCount(user);
+ }
+
+ public override bool IsSaveLocalMetadataEnabled()
+ {
+ if (IsAccessedByName)
+ {
+ return true;
+ }
+
+ return base.IsSaveLocalMetadataEnabled();
+ }
+
+ private readonly Task _cachedTask = Task.FromResult(true);
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ {
+ if (IsAccessedByName)
+ {
+ // Should never get in here anyway
+ return _cachedTask;
+ }
+
+ return base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService);
+ }
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ list.InsertRange(0, GetUserDataKeys(this));
+ return list;
+ }
+
+ /// <summary>
+ /// Returns the folder containing the item.
+ /// If the item is a folder, it returns the folder itself
+ /// </summary>
+ /// <value>The containing folder path.</value>
+ [IgnoreDataMember]
+ public override string ContainingFolderPath
+ {
+ get
+ {
+ return Path;
+ }
+ }
+
+ /// <summary>
+ /// Gets the user data key.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>System.String.</returns>
+ private static List<string> GetUserDataKeys(MusicArtist item)
+ {
+ var list = new List<string>();
+ var id = item.GetProviderId(MetadataProviders.MusicBrainzArtist);
+
+ if (!string.IsNullOrEmpty(id))
+ {
+ list.Add("Artist-Musicbrainz-" + id);
+ }
+
+ list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics());
+ return list;
+ }
+ public override string CreatePresentationUniqueKey()
+ {
+ return "Artist-" + (Name ?? string.Empty).RemoveDiacritics();
+ }
+ protected override bool GetBlockUnratedValue(UserPolicy config)
+ {
+ return config.BlockUnratedItems.Contains(UnratedItem.Music);
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ return UnratedItem.Music;
+ }
+
+ public ArtistInfo GetLookupInfo()
+ {
+ var info = GetItemLookupInfo<ArtistInfo>();
+
+ info.SongInfos = GetRecursiveChildren(i => i is Audio)
+ .Cast<Audio>()
+ .Select(i => i.GetLookupInfo())
+ .ToList();
+
+ return info;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public static string GetPath(string name)
+ {
+ return GetPath(name, true);
+ }
+
+ public static string GetPath(string name, bool normalizeName)
+ {
+ // Trim the period at the end because windows will have a hard time with that
+ var validName = normalizeName ?
+ FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+ name;
+
+ return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.ArtistsPath, validName);
+ }
+
+ private string GetRebasedPath()
+ {
+ return GetPath(System.IO.Path.GetFileName(Path), false);
+ }
+
+ public override bool RequiresRefresh()
+ {
+ if (IsAccessedByName)
+ {
+ var newPath = GetRebasedPath();
+ if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+ {
+ Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+ return true;
+ }
+ }
+ return base.RequiresRefresh();
+ }
+
+ /// <summary>
+ /// This is called before any metadata refresh and returns true or false indicating if changes were made
+ /// </summary>
+ public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ {
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+
+ if (IsAccessedByName)
+ {
+ var newPath = GetRebasedPath();
+ if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+ {
+ Path = newPath;
+ hasChanges = true;
+ }
+ }
+
+ return hasChanges;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
new file mode 100644
index 000000000..d60ce83ad
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Controller.Entities.Audio
+{
+ /// <summary>
+ /// Class MusicGenre
+ /// </summary>
+ public class MusicGenre : BaseItem, IItemByName
+ {
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
+ return list;
+ }
+ public override string CreatePresentationUniqueKey()
+ {
+ return GetUserDataKeys()[0];
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsAddingToPlaylist
+ {
+ get { return true; }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsAncestors
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool IsDisplayedAsFolder
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Returns the folder containing the item.
+ /// If the item is a folder, it returns the folder itself
+ /// </summary>
+ /// <value>The containing folder path.</value>
+ [IgnoreDataMember]
+ public override string ContainingFolderPath
+ {
+ get
+ {
+ return Path;
+ }
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ return 1;
+ }
+
+ public override bool CanDelete()
+ {
+ return false;
+ }
+
+ public override bool IsSaveLocalMetadataEnabled()
+ {
+ return true;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+ {
+ query.GenreIds = new[] { Id };
+ query.IncludeItemTypes = new[] { typeof(MusicVideo).Name, typeof(Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name };
+
+ return LibraryManager.GetItemList(query);
+ }
+
+ public static string GetPath(string name)
+ {
+ return GetPath(name, true);
+ }
+
+ public static string GetPath(string name, bool normalizeName)
+ {
+ // Trim the period at the end because windows will have a hard time with that
+ var validName = normalizeName ?
+ FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+ name;
+
+ return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.MusicGenrePath, validName);
+ }
+
+ private string GetRebasedPath()
+ {
+ return GetPath(System.IO.Path.GetFileName(Path), false);
+ }
+
+ public override bool RequiresRefresh()
+ {
+ var newPath = GetRebasedPath();
+ if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+ {
+ Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+ return true;
+ }
+ return base.RequiresRefresh();
+ }
+
+ /// <summary>
+ /// This is called before any metadata refresh and returns true or false indicating if changes were made
+ /// </summary>
+ public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ {
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+
+ var newPath = GetRebasedPath();
+ if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+ {
+ Path = newPath;
+ hasChanges = true;
+ }
+
+ return hasChanges;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/AudioBook.cs b/MediaBrowser.Controller/Entities/AudioBook.cs
new file mode 100644
index 000000000..679facf64
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/AudioBook.cs
@@ -0,0 +1,69 @@
+using System;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class AudioBook : Audio.Audio, IHasSeries, IHasLookupInfo<SongInfo>
+ {
+ [IgnoreDataMember]
+ public override bool SupportsPositionTicksResume
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPlayedStatus
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public string SeriesPresentationUniqueKey { get; set; }
+ [IgnoreDataMember]
+ public string SeriesName { get; set; }
+ [IgnoreDataMember]
+ public Guid SeriesId { get; set; }
+
+ public string FindSeriesSortName()
+ {
+ return SeriesName;
+ }
+ public string FindSeriesName()
+ {
+ return SeriesName;
+ }
+ public string FindSeriesPresentationUniqueKey()
+ {
+ return SeriesPresentationUniqueKey;
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ return 0;
+ }
+
+ public Guid FindSeriesId()
+ {
+ return SeriesId;
+ }
+
+ public override bool CanDownload()
+ {
+ return IsFileProtocol;
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ return UnratedItem.Book;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
new file mode 100644
index 000000000..053ee1b96
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -0,0 +1,2960 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Collections;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Library;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Users;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.MediaInfo;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class BaseItem
+ /// </summary>
+ public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>
+ {
+ protected static MetadataFields[] EmptyMetadataFieldsArray = new MetadataFields[] { };
+ protected static MediaUrl[] EmptyMediaUrlArray = new MediaUrl[] { };
+ protected static ItemImageInfo[] EmptyItemImageInfoArray = new ItemImageInfo[] { };
+ public static readonly LinkedChild[] EmptyLinkedChildArray = new LinkedChild[] { };
+
+ protected BaseItem()
+ {
+ ThemeSongIds = new Guid[] {};
+ ThemeVideoIds = new Guid[] {};
+ Tags = new string[] {};
+ Genres = new string[] {};
+ Studios = new string[] {};
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ LockedFields = EmptyMetadataFieldsArray;
+ ImageInfos = EmptyItemImageInfoArray;
+ ProductionLocations = new string[] {};
+ RemoteTrailers = new MediaUrl[] { };
+ ExtraIds = new Guid[] {};
+ }
+
+ public static readonly char[] SlugReplaceChars = { '?', '/', '&' };
+ public static char SlugChar = '-';
+
+ /// <summary>
+ /// The supported image extensions
+ /// </summary>
+ public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".tbn", ".gif" };
+ public static readonly List<string> SupportedImageExtensionsList = SupportedImageExtensions.ToList();
+
+ /// <summary>
+ /// The trailer folder name
+ /// </summary>
+ public static string TrailerFolderName = "trailers";
+ public static string ThemeSongsFolderName = "theme-music";
+ public static string ThemeSongFilename = "theme";
+ public static string ThemeVideosFolderName = "backdrops";
+
+ [IgnoreDataMember]
+ public Guid[] ThemeSongIds { get; set; }
+ [IgnoreDataMember]
+ public Guid[] ThemeVideoIds { get; set; }
+
+ [IgnoreDataMember]
+ public string PreferredMetadataCountryCode { get; set; }
+ [IgnoreDataMember]
+ public string PreferredMetadataLanguage { get; set; }
+
+ public long? Size { get; set; }
+ public string Container { get; set; }
+
+ [IgnoreDataMember]
+ public string Tagline { get; set; }
+
+ [IgnoreDataMember]
+ public virtual ItemImageInfo[] ImageInfos { get; set; }
+
+ [IgnoreDataMember]
+ public bool IsVirtualItem { get; set; }
+
+ /// <summary>
+ /// Gets or sets the album.
+ /// </summary>
+ /// <value>The album.</value>
+ [IgnoreDataMember]
+ public string Album { get; set; }
+
+ /// <summary>
+ /// Gets or sets the channel identifier.
+ /// </summary>
+ /// <value>The channel identifier.</value>
+ [IgnoreDataMember]
+ public Guid ChannelId { get; set; }
+
+ [IgnoreDataMember]
+ public virtual bool SupportsAddingToPlaylist
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public virtual bool AlwaysScanInternalMetadataPath
+ {
+ get { return false; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is in mixed folder.
+ /// </summary>
+ /// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool IsInMixedFolder { get; set; }
+
+ [IgnoreDataMember]
+ public virtual bool SupportsPlayedStatus
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public virtual bool SupportsPositionTicksResume
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public virtual bool SupportsRemoteImageDownloading
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ private string _name;
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ [IgnoreDataMember]
+ public virtual string Name
+ {
+ get
+ {
+ return _name;
+ }
+ set
+ {
+ _name = value;
+
+ // lazy load this again
+ _sortName = null;
+ }
+ }
+
+ [IgnoreDataMember]
+ public bool IsUnaired
+ {
+ get { return PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date; }
+ }
+
+ [IgnoreDataMember]
+ public int? TotalBitrate { get; set; }
+ [IgnoreDataMember]
+ public ExtraType? ExtraType { get; set; }
+
+ [IgnoreDataMember]
+ public bool IsThemeMedia
+ {
+ get
+ {
+ return ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || ExtraType.Value == Model.Entities.ExtraType.ThemeVideo);
+ }
+ }
+
+ [IgnoreDataMember]
+ public string OriginalTitle { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ [IgnoreDataMember]
+ public Guid Id { get; set; }
+
+ [IgnoreDataMember]
+ public Guid OwnerId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the audio.
+ /// </summary>
+ /// <value>The audio.</value>
+ [IgnoreDataMember]
+ public ProgramAudio? Audio { get; set; }
+
+ /// <summary>
+ /// Return the id that should be used to key display prefs for this item.
+ /// Default is based on the type for everything except actual generic folders.
+ /// </summary>
+ /// <value>The display prefs id.</value>
+ [IgnoreDataMember]
+ public virtual Guid DisplayPreferencesId
+ {
+ get
+ {
+ var thisType = GetType();
+ return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5();
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ [IgnoreDataMember]
+ public virtual string Path { get; set; }
+
+ [IgnoreDataMember]
+ public virtual SourceType SourceType
+ {
+ get
+ {
+ if (!ChannelId.Equals(Guid.Empty))
+ {
+ return SourceType.Channel;
+ }
+
+ return SourceType.Library;
+ }
+ }
+
+ /// <summary>
+ /// Returns the folder containing the item.
+ /// If the item is a folder, it returns the folder itself
+ /// </summary>
+ [IgnoreDataMember]
+ public virtual string ContainingFolderPath
+ {
+ get
+ {
+ if (IsFolder)
+ {
+ return Path;
+ }
+
+ return FileSystem.GetDirectoryName(Path);
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the name of the service.
+ /// </summary>
+ /// <value>The name of the service.</value>
+ [IgnoreDataMember]
+ public string ServiceName { get; set; }
+
+ /// <summary>
+ /// If this content came from an external service, the id of the content on that service
+ /// </summary>
+ [IgnoreDataMember]
+ public string ExternalId { get; set; }
+
+ [IgnoreDataMember]
+ public string ExternalSeriesId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the etag.
+ /// </summary>
+ /// <value>The etag.</value>
+ [IgnoreDataMember]
+ public string ExternalEtag { get; set; }
+
+ [IgnoreDataMember]
+ public virtual bool IsHidden
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public BaseItem GetOwner()
+ {
+ var ownerId = OwnerId;
+ return ownerId.Equals(Guid.Empty) ? null : LibraryManager.GetItemById(ownerId);
+ }
+
+ /// <summary>
+ /// Gets or sets the type of the location.
+ /// </summary>
+ /// <value>The type of the location.</value>
+ [IgnoreDataMember]
+ public virtual LocationType LocationType
+ {
+ get
+ {
+ //if (IsOffline)
+ //{
+ // return LocationType.Offline;
+ //}
+
+ var path = Path;
+ if (string.IsNullOrEmpty(path))
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ return LocationType.Remote;
+ }
+
+ return LocationType.Virtual;
+ }
+
+ return FileSystem.IsPathFile(path) ? LocationType.FileSystem : LocationType.Remote;
+ }
+ }
+
+ [IgnoreDataMember]
+ public MediaProtocol? PathProtocol
+ {
+ get
+ {
+ var path = Path;
+
+ if (string.IsNullOrEmpty(path))
+ {
+ return null;
+ }
+
+ return MediaSourceManager.GetPathProtocol(path);
+ }
+ }
+
+ public bool IsPathProtocol(MediaProtocol protocol)
+ {
+ var current = PathProtocol;
+
+ return current.HasValue && current.Value == protocol;
+ }
+
+ [IgnoreDataMember]
+ public bool IsFileProtocol
+ {
+ get
+ {
+ return IsPathProtocol(MediaProtocol.File);
+ }
+ }
+
+ [IgnoreDataMember]
+ public bool HasPathProtocol
+ {
+ get
+ {
+ return PathProtocol.HasValue;
+ }
+ }
+
+ [IgnoreDataMember]
+ public virtual bool SupportsLocalMetadata
+ {
+ get
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ return false;
+ }
+
+ return IsFileProtocol;
+ }
+ }
+
+ [IgnoreDataMember]
+ public virtual string FileNameWithoutExtension
+ {
+ get
+ {
+ if (IsFileProtocol)
+ {
+ return System.IO.Path.GetFileNameWithoutExtension(Path);
+ }
+
+ return null;
+ }
+ }
+
+ [IgnoreDataMember]
+ public virtual bool EnableAlphaNumericSorting
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ private List<Tuple<StringBuilder, bool>> GetSortChunks(string s1)
+ {
+ var list = new List<Tuple<StringBuilder, bool>>();
+
+ int thisMarker = 0, thisNumericChunk = 0;
+
+ while (thisMarker < s1.Length)
+ {
+ if (thisMarker >= s1.Length)
+ {
+ break;
+ }
+ char thisCh = s1[thisMarker];
+
+ StringBuilder thisChunk = new StringBuilder();
+
+ while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || SortHelper.InChunk(thisCh, thisChunk[0])))
+ {
+ thisChunk.Append(thisCh);
+ thisMarker++;
+
+ if (thisMarker < s1.Length)
+ {
+ thisCh = s1[thisMarker];
+ }
+ }
+
+ var isNumeric = thisChunk.Length > 0 && char.IsDigit(thisChunk[0]);
+ list.Add(new Tuple<StringBuilder, bool>(thisChunk, isNumeric));
+ }
+
+ return list;
+ }
+
+ /// <summary>
+ /// This is just a helper for convenience
+ /// </summary>
+ /// <value>The primary image path.</value>
+ [IgnoreDataMember]
+ public string PrimaryImagePath
+ {
+ get { return this.GetImagePath(ImageType.Primary); }
+ }
+
+ public bool IsMetadataFetcherEnabled(LibraryOptions libraryOptions, string name)
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ // hack alert
+ return !EnableMediaSourceDisplay;
+ }
+
+ var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
+ if (typeOptions != null)
+ {
+ return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ }
+
+ if (!libraryOptions.EnableInternetProviders)
+ {
+ return false;
+ }
+
+ var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
+
+ return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ }
+
+ public bool IsImageFetcherEnabled(LibraryOptions libraryOptions, string name)
+ {
+ if (this is Channel)
+ {
+ // hack alert
+ return true;
+ }
+ if (SourceType == SourceType.Channel)
+ {
+ // hack alert
+ return !EnableMediaSourceDisplay;
+ }
+
+ var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
+ if (typeOptions != null)
+ {
+ return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ }
+
+ if (!libraryOptions.EnableInternetProviders)
+ {
+ return false;
+ }
+
+ var itemConfig = ConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
+
+ return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
+ }
+
+ public virtual bool CanDelete()
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ return ChannelManager.CanDelete(this);
+ }
+
+ return IsFileProtocol;
+ }
+
+ public virtual bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
+ {
+ if (user.Policy.EnableContentDeletion)
+ {
+ return true;
+ }
+
+ var allowed = user.Policy.EnableContentDeletionFromFolders;
+
+ if (SourceType == SourceType.Channel)
+ {
+ return allowed.Contains(ChannelId.ToString(""), StringComparer.OrdinalIgnoreCase);
+ }
+ else
+ {
+ var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
+
+ foreach (var folder in collectionFolders)
+ {
+ if (allowed.Contains(folder.Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public bool CanDelete(User user, List<Folder> allCollectionFolders)
+ {
+ return CanDelete() && IsAuthorizedToDelete(user, allCollectionFolders);
+ }
+
+ public bool CanDelete(User user)
+ {
+ var allCollectionFolders = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
+
+ return CanDelete(user, allCollectionFolders);
+ }
+
+ public virtual bool CanDownload()
+ {
+ return false;
+ }
+
+ public virtual bool IsAuthorizedToDownload(User user)
+ {
+ return user.Policy.EnableContentDownloading;
+ }
+
+ public bool CanDownload(User user)
+ {
+ return CanDownload() && IsAuthorizedToDownload(user);
+ }
+
+ /// <summary>
+ /// Gets or sets the date created.
+ /// </summary>
+ /// <value>The date created.</value>
+ [IgnoreDataMember]
+ public DateTime DateCreated { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date modified.
+ /// </summary>
+ /// <value>The date modified.</value>
+ [IgnoreDataMember]
+ public DateTime DateModified { get; set; }
+
+ [IgnoreDataMember]
+ public DateTime DateLastSaved { get; set; }
+
+ [IgnoreDataMember]
+ public DateTime DateLastRefreshed { get; set; }
+
+ /// <summary>
+ /// The logger
+ /// </summary>
+ public static ILogger Logger { get; set; }
+ public static ILibraryManager LibraryManager { get; set; }
+ public static IServerConfigurationManager ConfigurationManager { get; set; }
+ public static IProviderManager ProviderManager { get; set; }
+ public static ILocalizationManager LocalizationManager { get; set; }
+ public static IItemRepository ItemRepository { get; set; }
+ public static IFileSystem FileSystem { get; set; }
+ public static IUserDataManager UserDataManager { get; set; }
+ public static IChannelManager ChannelManager { get; set; }
+ public static IMediaSourceManager MediaSourceManager { get; set; }
+
+ /// <summary>
+ /// Returns a <see cref="System.String" /> that represents this instance.
+ /// </summary>
+ /// <returns>A <see cref="System.String" /> that represents this instance.</returns>
+ public override string ToString()
+ {
+ return Name;
+ }
+
+ [IgnoreDataMember]
+ public bool IsLocked { get; set; }
+
+ /// <summary>
+ /// Gets or sets the locked fields.
+ /// </summary>
+ /// <value>The locked fields.</value>
+ [IgnoreDataMember]
+ public MetadataFields[] LockedFields { get; set; }
+
+ /// <summary>
+ /// Gets the type of the media.
+ /// </summary>
+ /// <value>The type of the media.</value>
+ [IgnoreDataMember]
+ public virtual string MediaType
+ {
+ get
+ {
+ return null;
+ }
+ }
+
+ [IgnoreDataMember]
+ public virtual string[] PhysicalLocations
+ {
+ get
+ {
+ if (!IsFileProtocol)
+ {
+ return new string[] { };
+ }
+
+ return new[] { Path };
+ }
+ }
+
+ private string _forcedSortName;
+ /// <summary>
+ /// Gets or sets the name of the forced sort.
+ /// </summary>
+ /// <value>The name of the forced sort.</value>
+ [IgnoreDataMember]
+ public string ForcedSortName
+ {
+ get { return _forcedSortName; }
+ set { _forcedSortName = value; _sortName = null; }
+ }
+
+ private string _sortName;
+ /// <summary>
+ /// Gets the name of the sort.
+ /// </summary>
+ /// <value>The name of the sort.</value>
+ [IgnoreDataMember]
+ public string SortName
+ {
+ get
+ {
+ if (_sortName == null)
+ {
+ if (!string.IsNullOrEmpty(ForcedSortName))
+ {
+ // Need the ToLower because that's what CreateSortName does
+ _sortName = ModifySortChunks(ForcedSortName).ToLower();
+ }
+ else
+ {
+ _sortName = CreateSortName();
+ }
+ }
+ return _sortName;
+ }
+ set
+ {
+ _sortName = value;
+ }
+ }
+
+ public string GetInternalMetadataPath()
+ {
+ var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath;
+
+ return GetInternalMetadataPath(basePath);
+ }
+
+ protected virtual string GetInternalMetadataPath(string basePath)
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ return System.IO.Path.Combine(basePath, "channels", ChannelId.ToString("N"), Id.ToString("N"));
+ }
+
+ var idString = Id.ToString("N");
+
+ basePath = System.IO.Path.Combine(basePath, "library");
+
+ return System.IO.Path.Combine(basePath, idString.Substring(0, 2), idString);
+ }
+
+ /// <summary>
+ /// Creates the name of the sort.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ protected virtual string CreateSortName()
+ {
+ if (Name == null) return null; //some items may not have name filled in properly
+
+ if (!EnableAlphaNumericSorting)
+ {
+ return Name.TrimStart();
+ }
+
+ var sortable = Name.Trim().ToLower();
+
+ foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
+ {
+ sortable = sortable.Replace(removeChar, string.Empty);
+ }
+
+ foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
+ {
+ sortable = sortable.Replace(replaceChar, " ");
+ }
+
+ foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
+ {
+ // Remove from beginning if a space follows
+ if (sortable.StartsWith(search + " "))
+ {
+ sortable = sortable.Remove(0, search.Length + 1);
+ }
+ // Remove from middle if surrounded by spaces
+ sortable = sortable.Replace(" " + search + " ", " ");
+
+ // Remove from end if followed by a space
+ if (sortable.EndsWith(" " + search))
+ {
+ sortable = sortable.Remove(sortable.Length - (search.Length + 1));
+ }
+ }
+
+ return ModifySortChunks(sortable);
+ }
+
+ private string ModifySortChunks(string name)
+ {
+ var chunks = GetSortChunks(name);
+
+ var builder = new StringBuilder();
+
+ foreach (var chunk in chunks)
+ {
+ var chunkBuilder = chunk.Item1;
+
+ // This chunk is numeric
+ if (chunk.Item2)
+ {
+ while (chunkBuilder.Length < 10)
+ {
+ chunkBuilder.Insert(0, '0');
+ }
+ }
+
+ builder.Append(chunkBuilder);
+ }
+ //Logger.Debug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
+ return builder.ToString().RemoveDiacritics();
+ }
+
+ [IgnoreDataMember]
+ public bool EnableMediaSourceDisplay
+ {
+ get
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ return ChannelManager.EnableMediaSourceDisplay(this);
+ }
+
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public Guid ParentId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent.
+ /// </summary>
+ /// <value>The parent.</value>
+ [IgnoreDataMember]
+ public Folder Parent
+ {
+ get { return GetParent() as Folder; }
+ set
+ {
+
+ }
+ }
+
+ public void SetParent(Folder parent)
+ {
+ ParentId = parent == null ? Guid.Empty : parent.Id;
+ }
+
+ public BaseItem GetParent()
+ {
+ var parentId = ParentId;
+ if (!parentId.Equals(Guid.Empty))
+ {
+ return LibraryManager.GetItemById(parentId);
+ }
+
+ return null;
+ }
+
+ public IEnumerable<BaseItem> GetParents()
+ {
+ var parent = GetParent();
+
+ while (parent != null)
+ {
+ yield return parent;
+
+ parent = parent.GetParent();
+ }
+ }
+
+ /// <summary>
+ /// Finds a parent of a given type
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <returns>``0.</returns>
+ public T FindParent<T>()
+ where T : Folder
+ {
+ foreach (var parent in GetParents())
+ {
+ var item = parent as T;
+ if (item != null)
+ {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ [IgnoreDataMember]
+ public virtual Guid DisplayParentId
+ {
+ get
+ {
+ var parentId = ParentId;
+ return parentId;
+ }
+ }
+
+ [IgnoreDataMember]
+ public BaseItem DisplayParent
+ {
+ get
+ {
+ var id = DisplayParentId;
+ if (id.Equals(Guid.Empty))
+ {
+ return null;
+ }
+ return LibraryManager.GetItemById(id);
+ }
+ }
+
+ /// <summary>
+ /// When the item first debuted. For movies this could be premiere date, episodes would be first aired
+ /// </summary>
+ /// <value>The premiere date.</value>
+ [IgnoreDataMember]
+ public DateTime? PremiereDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the end date.
+ /// </summary>
+ /// <value>The end date.</value>
+ [IgnoreDataMember]
+ public DateTime? EndDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the official rating.
+ /// </summary>
+ /// <value>The official rating.</value>
+ [IgnoreDataMember]
+ public string OfficialRating { get; set; }
+
+ [IgnoreDataMember]
+ public int InheritedParentalRatingValue { get; set; }
+
+ /// <summary>
+ /// Gets or sets the critic rating.
+ /// </summary>
+ /// <value>The critic rating.</value>
+ [IgnoreDataMember]
+ public float? CriticRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the custom rating.
+ /// </summary>
+ /// <value>The custom rating.</value>
+ [IgnoreDataMember]
+ public string CustomRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the overview.
+ /// </summary>
+ /// <value>The overview.</value>
+ [IgnoreDataMember]
+ public string Overview { get; set; }
+
+ /// <summary>
+ /// Gets or sets the studios.
+ /// </summary>
+ /// <value>The studios.</value>
+ [IgnoreDataMember]
+ public string[] Studios { get; set; }
+
+ /// <summary>
+ /// Gets or sets the genres.
+ /// </summary>
+ /// <value>The genres.</value>
+ [IgnoreDataMember]
+ public string[] Genres { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tags.
+ /// </summary>
+ /// <value>The tags.</value>
+ [IgnoreDataMember]
+ public string[] Tags { get; set; }
+
+ [IgnoreDataMember]
+ public string[] ProductionLocations { get; set; }
+
+ /// <summary>
+ /// Gets or sets the home page URL.
+ /// </summary>
+ /// <value>The home page URL.</value>
+ [IgnoreDataMember]
+ public string HomePageUrl { get; set; }
+
+ /// <summary>
+ /// Gets or sets the community rating.
+ /// </summary>
+ /// <value>The community rating.</value>
+ [IgnoreDataMember]
+ public float? CommunityRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the run time ticks.
+ /// </summary>
+ /// <value>The run time ticks.</value>
+ [IgnoreDataMember]
+ public long? RunTimeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the production year.
+ /// </summary>
+ /// <value>The production year.</value>
+ [IgnoreDataMember]
+ public int? ProductionYear { get; set; }
+
+ /// <summary>
+ /// If the item is part of a series, this is it's number in the series.
+ /// This could be episode number, album track number, etc.
+ /// </summary>
+ /// <value>The index number.</value>
+ [IgnoreDataMember]
+ public int? IndexNumber { get; set; }
+
+ /// <summary>
+ /// For an episode this could be the season number, or for a song this could be the disc number.
+ /// </summary>
+ /// <value>The parent index number.</value>
+ [IgnoreDataMember]
+ public int? ParentIndexNumber { get; set; }
+
+ [IgnoreDataMember]
+ public virtual bool HasLocalAlternateVersions
+ {
+ get { return false; }
+ }
+
+ [IgnoreDataMember]
+ public string OfficialRatingForComparison
+ {
+ get
+ {
+ var officialRating = OfficialRating;
+ if (!string.IsNullOrEmpty(officialRating))
+ {
+ return officialRating;
+ }
+
+ var parent = DisplayParent;
+ if (parent != null)
+ {
+ return parent.OfficialRatingForComparison;
+ }
+
+ return null;
+ }
+ }
+
+ [IgnoreDataMember]
+ public string CustomRatingForComparison
+ {
+ get
+ {
+ var customRating = CustomRating;
+ if (!string.IsNullOrEmpty(customRating))
+ {
+ return customRating;
+ }
+
+ var parent = DisplayParent;
+ if (parent != null)
+ {
+ return parent.CustomRatingForComparison;
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Gets the play access.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>PlayAccess.</returns>
+ public PlayAccess GetPlayAccess(User user)
+ {
+ if (!user.Policy.EnableMediaPlayback)
+ {
+ return PlayAccess.None;
+ }
+
+ //if (!user.IsParentalScheduleAllowed())
+ //{
+ // return PlayAccess.None;
+ //}
+
+ return PlayAccess.Full;
+ }
+
+ public virtual List<MediaStream> GetMediaStreams()
+ {
+ return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
+ {
+ ItemId = Id
+ });
+ }
+
+ protected virtual bool IsActiveRecording()
+ {
+ return false;
+ }
+
+ public virtual List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ var sources = ChannelManager.GetStaticMediaSources(this, CancellationToken.None)
+ .ToList();
+
+ if (sources.Count > 0)
+ {
+ return sources;
+ }
+ }
+
+ var list = GetAllItemsForMediaSources();
+ var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item1, i.Item2)).ToList();
+
+ if (IsActiveRecording())
+ {
+ foreach (var mediaSource in result)
+ {
+ mediaSource.Type = MediaSourceType.Placeholder;
+ }
+ }
+
+ return result.OrderBy(i =>
+ {
+ if (i.VideoType == VideoType.VideoFile)
+ {
+ return 0;
+ }
+
+ return 1;
+
+ }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0)
+ .ThenByDescending(i =>
+ {
+ var stream = i.VideoStream;
+
+ return stream == null || stream.Width == null ? 0 : stream.Width.Value;
+ })
+ .ToList();
+ }
+
+ protected virtual List<Tuple<BaseItem, MediaSourceType>> GetAllItemsForMediaSources()
+ {
+ return new List<Tuple<BaseItem, MediaSourceType>>();
+ }
+
+ private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("media");
+ }
+
+ var protocol = item.PathProtocol;
+
+ var info = new MediaSourceInfo
+ {
+ Id = item.Id.ToString("N"),
+ Protocol = protocol ?? MediaProtocol.File,
+ MediaStreams = MediaSourceManager.GetMediaStreams(item.Id),
+ Name = GetMediaSourceName(item),
+ Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path,
+ RunTimeTicks = item.RunTimeTicks,
+ Container = item.Container,
+ Size = item.Size,
+ Type = type
+ };
+
+ if (string.IsNullOrEmpty(info.Path))
+ {
+ info.Type = MediaSourceType.Placeholder;
+ }
+
+ if (info.Protocol == MediaProtocol.File)
+ {
+ info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N");
+ }
+
+ var video = item as Video;
+ if (video != null)
+ {
+ info.IsoType = video.IsoType;
+ info.VideoType = video.VideoType;
+ info.Video3DFormat = video.Video3DFormat;
+ info.Timestamp = video.Timestamp;
+
+ if (video.IsShortcut)
+ {
+ info.IsRemote = true;
+ info.Path = video.ShortcutPath;
+ info.Protocol = MediaSourceManager.GetPathProtocol(info.Path);
+ }
+
+ if (string.IsNullOrEmpty(info.Container))
+ {
+ if (video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso)
+ {
+ if (protocol.HasValue && protocol.Value == MediaProtocol.File)
+ {
+ info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
+ }
+ }
+ }
+ }
+
+ if (string.IsNullOrEmpty(info.Container))
+ {
+ if (protocol.HasValue && protocol.Value == MediaProtocol.File)
+ {
+ info.Container = System.IO.Path.GetExtension(item.Path).TrimStart('.');
+ }
+ }
+
+ if (info.SupportsDirectStream && !string.IsNullOrEmpty(info.Path))
+ {
+ info.SupportsDirectStream = MediaSourceManager.SupportsDirectStream(info.Path, info.Protocol);
+ }
+
+ if (video != null && video.VideoType != VideoType.VideoFile)
+ {
+ info.SupportsDirectStream = false;
+ }
+
+ info.Bitrate = item.TotalBitrate;
+ info.InferTotalBitrate();
+
+ return info;
+ }
+
+ private string GetMediaSourceName(BaseItem item)
+ {
+ var terms = new List<string>();
+
+ var path = item.Path;
+ if (item.IsFileProtocol && !string.IsNullOrEmpty(path))
+ {
+ if (HasLocalAlternateVersions)
+ {
+ var displayName = System.IO.Path.GetFileNameWithoutExtension(path)
+ .Replace(System.IO.Path.GetFileName(ContainingFolderPath), string.Empty, StringComparison.OrdinalIgnoreCase)
+ .TrimStart(new char[] { ' ', '-' });
+
+ if (!string.IsNullOrEmpty(displayName))
+ {
+ terms.Add(displayName);
+ }
+ }
+
+ if (terms.Count == 0)
+ {
+ var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
+ terms.Add(displayName);
+ }
+ }
+
+ if (terms.Count == 0)
+ {
+ terms.Add(item.Name);
+ }
+
+ var video = item as Video;
+ if (video != null)
+ {
+ if (video.Video3DFormat.HasValue)
+ {
+ terms.Add("3D");
+ }
+
+ if (video.VideoType == VideoType.BluRay)
+ {
+ terms.Add("Bluray");
+ }
+ else if (video.VideoType == VideoType.Dvd)
+ {
+ terms.Add("DVD");
+ }
+ else if (video.VideoType == VideoType.Iso)
+ {
+ if (video.IsoType.HasValue)
+ {
+ if (video.IsoType.Value == Model.Entities.IsoType.BluRay)
+ {
+ terms.Add("Bluray");
+ }
+ else if (video.IsoType.Value == Model.Entities.IsoType.Dvd)
+ {
+ terms.Add("DVD");
+ }
+ }
+ else
+ {
+ terms.Add("ISO");
+ }
+ }
+ }
+
+ return string.Join("/", terms.ToArray(terms.Count));
+ }
+
+ /// <summary>
+ /// Loads the theme songs.
+ /// </summary>
+ /// <returns>List{Audio.Audio}.</returns>
+ private static Audio.Audio[] LoadThemeSongs(List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
+ {
+ var files = fileSystemChildren.Where(i => i.IsDirectory)
+ .Where(i => string.Equals(i.Name, ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase))
+ .SelectMany(i => FileSystem.GetFiles(i.FullName))
+ .ToList();
+
+ // Support plex/xbmc convention
+ files.AddRange(fileSystemChildren
+ .Where(i => !i.IsDirectory && string.Equals(FileSystem.GetFileNameWithoutExtension(i), ThemeSongFilename, StringComparison.OrdinalIgnoreCase))
+ );
+
+ return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
+ .OfType<Audio.Audio>()
+ .Select(audio =>
+ {
+ // Try to retrieve it from the db. If we don't find it, use the resolved version
+ var dbItem = LibraryManager.GetItemById(audio.Id) as Audio.Audio;
+
+ if (dbItem != null)
+ {
+ audio = dbItem;
+ }
+ else
+ {
+ // item is new
+ audio.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeSong;
+ }
+
+ return audio;
+
+ // Sort them so that the list can be easily compared for changes
+ }).OrderBy(i => i.Path).ToArray();
+ }
+
+ /// <summary>
+ /// Loads the video backdrops.
+ /// </summary>
+ /// <returns>List{Video}.</returns>
+ private static Video[] LoadThemeVideos(IEnumerable<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
+ {
+ var files = fileSystemChildren.Where(i => i.IsDirectory)
+ .Where(i => string.Equals(i.Name, ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase))
+ .SelectMany(i => FileSystem.GetFiles(i.FullName));
+
+ return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions())
+ .OfType<Video>()
+ .Select(item =>
+ {
+ // Try to retrieve it from the db. If we don't find it, use the resolved version
+ var dbItem = LibraryManager.GetItemById(item.Id) as Video;
+
+ if (dbItem != null)
+ {
+ item = dbItem;
+ }
+ else
+ {
+ // item is new
+ item.ExtraType = MediaBrowser.Model.Entities.ExtraType.ThemeVideo;
+ }
+
+ return item;
+
+ // Sort them so that the list can be easily compared for changes
+ }).OrderBy(i => i.Path).ToArray();
+ }
+
+ public Task RefreshMetadata(CancellationToken cancellationToken)
+ {
+ return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), cancellationToken);
+ }
+
+ protected virtual void TriggerOnRefreshStart()
+ {
+
+ }
+
+ protected virtual void TriggerOnRefreshComplete()
+ {
+
+ }
+
+ /// <summary>
+ /// Overrides the base implementation to refresh metadata for local trailers
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>true if a provider reports we changed</returns>
+ public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ TriggerOnRefreshStart();
+
+ var requiresSave = false;
+
+ if (SupportsOwnedItems)
+ {
+ try
+ {
+ var files = IsFileProtocol ?
+ GetFileSystemChildren(options.DirectoryService).ToList() :
+ new List<FileSystemMetadata>();
+
+ var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false);
+
+ if (ownedItemsChanged)
+ {
+ requiresSave = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error refreshing owned items for {0}", ex, Path ?? Name);
+ }
+ }
+
+ try
+ {
+ var refreshOptions = requiresSave
+ ? new MetadataRefreshOptions(options)
+ {
+ ForceSave = true
+ }
+ : options;
+
+ return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ TriggerOnRefreshComplete();
+ }
+ }
+
+ [IgnoreDataMember]
+ protected virtual bool SupportsOwnedItems
+ {
+ get { return !ParentId.Equals(Guid.Empty) && IsFileProtocol; }
+ }
+
+ [IgnoreDataMember]
+ public virtual bool SupportsPeople
+ {
+ get { return false; }
+ }
+
+ [IgnoreDataMember]
+ public virtual bool SupportsThemeMedia
+ {
+ get { return false; }
+ }
+
+ /// <summary>
+ /// Refreshes owned items such as trailers, theme videos, special features, etc.
+ /// Returns true or false indicating if changes were found.
+ /// </summary>
+ /// <param name="options"></param>
+ /// <param name="fileSystemChildren"></param>
+ /// <param name="cancellationToken"></param>
+ /// <returns></returns>
+ protected virtual async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
+ {
+ var themeSongsChanged = false;
+
+ var themeVideosChanged = false;
+
+ var localTrailersChanged = false;
+
+ if (IsFileProtocol && SupportsOwnedItems)
+ {
+ if (SupportsThemeMedia)
+ {
+ if (!IsInMixedFolder)
+ {
+ themeSongsChanged = await RefreshThemeSongs(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
+
+ themeVideosChanged = await RefreshThemeVideos(this, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ var hasTrailers = this as IHasTrailers;
+ if (hasTrailers != null)
+ {
+ localTrailersChanged = await RefreshLocalTrailers(hasTrailers, options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ return themeSongsChanged || themeVideosChanged || localTrailersChanged;
+ }
+
+ protected virtual FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
+ {
+ var path = ContainingFolderPath;
+
+ return directoryService.GetFileSystemEntries(path);
+ }
+
+ private async Task<bool> RefreshLocalTrailers(IHasTrailers item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
+ {
+ var newItems = LibraryManager.FindTrailers(this, fileSystemChildren, options.DirectoryService).ToList();
+
+ var newItemIds = newItems.Select(i => i.Id).ToArray();
+
+ var itemsChanged = !item.LocalTrailerIds.SequenceEqual(newItemIds);
+ var ownerId = item.Id;
+
+ var tasks = newItems.Select(i =>
+ {
+ var subOptions = new MetadataRefreshOptions(options);
+
+ if (!i.ExtraType.HasValue ||
+ i.ExtraType.Value != Model.Entities.ExtraType.Trailer ||
+ i.OwnerId != ownerId ||
+ !i.ParentId.Equals(Guid.Empty))
+ {
+ i.ExtraType = Model.Entities.ExtraType.Trailer;
+ i.OwnerId = ownerId;
+ i.ParentId = Guid.Empty;
+ subOptions.ForceSave = true;
+ }
+
+ return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
+ });
+
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+
+ item.LocalTrailerIds = newItemIds;
+
+ return itemsChanged;
+ }
+
+ private async Task<bool> RefreshThemeVideos(BaseItem item, MetadataRefreshOptions options, IEnumerable<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
+ {
+ var newThemeVideos = LoadThemeVideos(fileSystemChildren, options.DirectoryService);
+
+ var newThemeVideoIds = newThemeVideos.Select(i => i.Id).ToArray(newThemeVideos.Length);
+
+ var themeVideosChanged = !item.ThemeVideoIds.SequenceEqual(newThemeVideoIds);
+
+ var ownerId = item.Id;
+
+ var tasks = newThemeVideos.Select(i =>
+ {
+ var subOptions = new MetadataRefreshOptions(options);
+
+ if (!i.ExtraType.HasValue ||
+ i.ExtraType.Value != Model.Entities.ExtraType.ThemeVideo ||
+ i.OwnerId != ownerId ||
+ !i.ParentId.Equals(Guid.Empty))
+ {
+ i.ExtraType = Model.Entities.ExtraType.ThemeVideo;
+ i.OwnerId = ownerId;
+ i.ParentId = Guid.Empty;
+ subOptions.ForceSave = true;
+ }
+
+ return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
+ });
+
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+
+ item.ThemeVideoIds = newThemeVideoIds;
+
+ return themeVideosChanged;
+ }
+
+ /// <summary>
+ /// Refreshes the theme songs.
+ /// </summary>
+ private async Task<bool> RefreshThemeSongs(BaseItem item, MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
+ {
+ var newThemeSongs = LoadThemeSongs(fileSystemChildren, options.DirectoryService);
+ var newThemeSongIds = newThemeSongs.Select(i => i.Id).ToArray(newThemeSongs.Length);
+
+ var themeSongsChanged = !item.ThemeSongIds.SequenceEqual(newThemeSongIds);
+
+ var ownerId = item.Id;
+
+ var tasks = newThemeSongs.Select(i =>
+ {
+ var subOptions = new MetadataRefreshOptions(options);
+
+ if (!i.ExtraType.HasValue ||
+ i.ExtraType.Value != Model.Entities.ExtraType.ThemeSong ||
+ i.OwnerId != ownerId ||
+ !i.ParentId.Equals(Guid.Empty))
+ {
+ i.ExtraType = Model.Entities.ExtraType.ThemeSong;
+ i.OwnerId = ownerId;
+ i.ParentId = Guid.Empty;
+ subOptions.ForceSave = true;
+ }
+
+ return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
+ });
+
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+
+ item.ThemeSongIds = newThemeSongIds;
+
+ return themeSongsChanged;
+ }
+
+ /// <summary>
+ /// Gets or sets the provider ids.
+ /// </summary>
+ /// <value>The provider ids.</value>
+ [IgnoreDataMember]
+ public Dictionary<string, string> ProviderIds { get; set; }
+
+ [IgnoreDataMember]
+ public virtual Folder LatestItemsIndexContainer
+ {
+ get { return null; }
+ }
+
+ public virtual double GetDefaultPrimaryImageAspectRatio()
+ {
+ return 0;
+ }
+
+ public virtual string CreatePresentationUniqueKey()
+ {
+ return Id.ToString("N");
+ }
+
+ [IgnoreDataMember]
+ public string PresentationUniqueKey { get; set; }
+
+ public string GetPresentationUniqueKey()
+ {
+ return PresentationUniqueKey ?? CreatePresentationUniqueKey();
+ }
+
+ public virtual bool RequiresRefresh()
+ {
+ return false;
+ }
+
+ public virtual List<string> GetUserDataKeys()
+ {
+ var list = new List<string>();
+
+ if (SourceType == SourceType.Channel)
+ {
+ if (!string.IsNullOrEmpty(ExternalId))
+ {
+ list.Add(ExternalId);
+ }
+ }
+
+ list.Add(Id.ToString());
+ return list;
+ }
+
+ internal virtual ItemUpdateType UpdateFromResolvedItem(BaseItem newItem)
+ {
+ var updateType = ItemUpdateType.None;
+
+ if (IsInMixedFolder != newItem.IsInMixedFolder)
+ {
+ IsInMixedFolder = newItem.IsInMixedFolder;
+ updateType |= ItemUpdateType.MetadataImport;
+ }
+
+ return updateType;
+ }
+
+ public void AfterMetadataRefresh()
+ {
+ _sortName = null;
+ }
+
+ /// <summary>
+ /// Gets the preferred metadata language.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ public string GetPreferredMetadataLanguage()
+ {
+ string lang = PreferredMetadataLanguage;
+
+ if (string.IsNullOrEmpty(lang))
+ {
+ lang = GetParents()
+ .Select(i => i.PreferredMetadataLanguage)
+ .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+ }
+
+ if (string.IsNullOrEmpty(lang))
+ {
+ lang = LibraryManager.GetCollectionFolders(this)
+ .Select(i => i.PreferredMetadataLanguage)
+ .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+ }
+
+ if (string.IsNullOrEmpty(lang))
+ {
+ lang = LibraryManager.GetLibraryOptions(this).PreferredMetadataLanguage;
+ }
+
+ if (string.IsNullOrEmpty(lang))
+ {
+ lang = ConfigurationManager.Configuration.PreferredMetadataLanguage;
+ }
+
+ return lang;
+ }
+
+ /// <summary>
+ /// Gets the preferred metadata language.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ public string GetPreferredMetadataCountryCode()
+ {
+ string lang = PreferredMetadataCountryCode;
+
+ if (string.IsNullOrEmpty(lang))
+ {
+ lang = GetParents()
+ .Select(i => i.PreferredMetadataCountryCode)
+ .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+ }
+
+ if (string.IsNullOrEmpty(lang))
+ {
+ lang = LibraryManager.GetCollectionFolders(this)
+ .Select(i => i.PreferredMetadataCountryCode)
+ .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+ }
+
+ if (string.IsNullOrEmpty(lang))
+ {
+ lang = LibraryManager.GetLibraryOptions(this).MetadataCountryCode;
+ }
+
+ if (string.IsNullOrEmpty(lang))
+ {
+ lang = ConfigurationManager.Configuration.MetadataCountryCode;
+ }
+
+ return lang;
+ }
+
+ public virtual bool IsSaveLocalMetadataEnabled()
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ return false;
+ }
+
+ var libraryOptions = LibraryManager.GetLibraryOptions(this);
+
+ return libraryOptions.SaveLocalMetadata;
+ }
+
+ /// <summary>
+ /// Determines if a given user has access to this item
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ public bool IsParentalAllowed(User user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException("user");
+ }
+
+ if (!IsVisibleViaTags(user))
+ {
+ return false;
+ }
+
+ var maxAllowedRating = user.Policy.MaxParentalRating;
+
+ if (maxAllowedRating == null)
+ {
+ return true;
+ }
+
+ var rating = CustomRatingForComparison;
+
+ if (string.IsNullOrEmpty(rating))
+ {
+ rating = OfficialRatingForComparison;
+ }
+
+ if (string.IsNullOrEmpty(rating))
+ {
+ return !GetBlockUnratedValue(user.Policy);
+ }
+
+ var value = LocalizationManager.GetRatingLevel(rating);
+
+ // Could not determine the integer value
+ if (!value.HasValue)
+ {
+ var isAllowed = !GetBlockUnratedValue(user.Policy);
+
+ if (!isAllowed)
+ {
+ Logger.Debug("{0} has an unrecognized parental rating of {1}.", Name, rating);
+ }
+
+ return isAllowed;
+ }
+
+ return value.Value <= maxAllowedRating.Value;
+ }
+
+ public int? GetParentalRatingValue()
+ {
+ var rating = CustomRating;
+
+ if (string.IsNullOrEmpty(rating))
+ {
+ rating = OfficialRating;
+ }
+
+ if (string.IsNullOrEmpty(rating))
+ {
+ return null;
+ }
+
+ return LocalizationManager.GetRatingLevel(rating);
+ }
+
+ public int? GetInheritedParentalRatingValue()
+ {
+ var rating = CustomRatingForComparison;
+
+ if (string.IsNullOrEmpty(rating))
+ {
+ rating = OfficialRatingForComparison;
+ }
+
+ if (string.IsNullOrEmpty(rating))
+ {
+ return null;
+ }
+
+ return LocalizationManager.GetRatingLevel(rating);
+ }
+
+ public List<string> GetInheritedTags()
+ {
+ var list = new List<string>();
+ list.AddRange(Tags);
+
+ foreach (var parent in GetParents())
+ {
+ list.AddRange(parent.Tags);
+ }
+
+ return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
+ }
+
+ private bool IsVisibleViaTags(User user)
+ {
+ var policy = user.Policy;
+ if (policy.BlockedTags.Any(i => Tags.Contains(i, StringComparer.OrdinalIgnoreCase)))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ protected virtual bool IsAllowTagFilterEnforced()
+ {
+ return true;
+ }
+
+ public virtual UnratedItem GetBlockUnratedType()
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ return UnratedItem.ChannelContent;
+ }
+
+ return UnratedItem.Other;
+ }
+
+ /// <summary>
+ /// Gets the block unrated value.
+ /// </summary>
+ /// <param name="config">The configuration.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+ protected virtual bool GetBlockUnratedValue(UserPolicy config)
+ {
+ // Don't block plain folders that are unrated. Let the media underneath get blocked
+ // Special folders like series and albums will override this method.
+ if (IsFolder)
+ {
+ return false;
+ }
+ if (this is IItemByName)
+ {
+ return false;
+ }
+
+ return config.BlockUnratedItems.Contains(GetBlockUnratedType());
+ }
+
+ /// <summary>
+ /// Determines if this folder should be visible to a given user.
+ /// Default is just parental allowed. Can be overridden for more functionality.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ public virtual bool IsVisible(User user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException("user");
+ }
+
+ return IsParentalAllowed(user);
+ }
+
+ public virtual bool IsVisibleStandalone(User user)
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ return IsVisibleStandaloneInternal(user, false) && Channel.IsChannelVisible(this, user);
+ }
+
+ return IsVisibleStandaloneInternal(user, true);
+ }
+
+ [IgnoreDataMember]
+ public virtual bool SupportsInheritedParentImages
+ {
+ get { return false; }
+ }
+
+ protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
+ {
+ if (!IsVisible(user))
+ {
+ return false;
+ }
+
+ if (GetParents().Any(i => !i.IsVisible(user)))
+ {
+ return false;
+ }
+
+ if (checkFolders)
+ {
+ var topParent = GetParents().LastOrDefault() ?? this;
+
+ if (string.IsNullOrEmpty(topParent.Path))
+ {
+ return true;
+ }
+
+ var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id).ToList();
+
+ if (itemCollectionFolders.Count > 0)
+ {
+ var userCollectionFolders = LibraryManager.GetUserRootFolder().GetChildren(user, true).Select(i => i.Id).ToList();
+ if (!itemCollectionFolders.Any(userCollectionFolders.Contains))
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is folder.
+ /// </summary>
+ /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public virtual bool IsFolder
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public virtual bool IsDisplayedAsFolder
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public virtual string GetClientTypeName()
+ {
+ if (IsFolder && SourceType == SourceType.Channel && !(this is Channel))
+ {
+ return "ChannelFolderItem";
+ }
+
+ return GetType().Name;
+ }
+
+ /// <summary>
+ /// Gets the linked child.
+ /// </summary>
+ /// <param name="info">The info.</param>
+ /// <returns>BaseItem.</returns>
+ protected BaseItem GetLinkedChild(LinkedChild info)
+ {
+ // First get using the cached Id
+ if (info.ItemId.HasValue)
+ {
+ if (info.ItemId.Value.Equals(Guid.Empty))
+ {
+ return null;
+ }
+
+ var itemById = LibraryManager.GetItemById(info.ItemId.Value);
+
+ if (itemById != null)
+ {
+ return itemById;
+ }
+ }
+
+ var item = FindLinkedChild(info);
+
+ // If still null, log
+ if (item == null)
+ {
+ // Don't keep searching over and over
+ info.ItemId = Guid.Empty;
+ }
+ else
+ {
+ // Cache the id for next time
+ info.ItemId = item.Id;
+ }
+
+ return item;
+ }
+
+ private BaseItem FindLinkedChild(LinkedChild info)
+ {
+ var path = info.Path;
+
+ if (!string.IsNullOrEmpty(path))
+ {
+ path = FileSystem.MakeAbsolutePath(ContainingFolderPath, path);
+
+ var itemByPath = LibraryManager.FindByPath(path, null);
+
+ if (itemByPath == null)
+ {
+ //Logger.Warn("Unable to find linked item at path {0}", info.Path);
+ }
+
+ return itemByPath;
+ }
+
+ if (!string.IsNullOrEmpty(info.LibraryItemId))
+ {
+ var item = LibraryManager.GetItemById(info.LibraryItemId);
+
+ if (item == null)
+ {
+ //Logger.Warn("Unable to find linked item at path {0}", info.Path);
+ }
+
+ return item;
+ }
+
+ return null;
+ }
+
+ [IgnoreDataMember]
+ public virtual bool EnableRememberingTrackSelections
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Adds a studio to the item
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddStudio(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ var current = Studios;
+
+ if (!current.Contains(name, StringComparer.OrdinalIgnoreCase))
+ {
+ if (current.Length == 0)
+ {
+ Studios = new[] { name };
+ }
+ else
+ {
+ var list = current.ToArray(current.Length + 1);
+ list[list.Length - 1] = name;
+ Studios = list;
+ }
+ }
+ }
+
+ public void SetStudios(IEnumerable<string> names)
+ {
+ Studios = names.Distinct().ToArray();
+ }
+
+ /// <summary>
+ /// Adds a genre to the item
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddGenre(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ var genres = Genres;
+ if (!genres.Contains(name, StringComparer.OrdinalIgnoreCase))
+ {
+ var list = genres.ToList();
+ list.Add(name);
+ Genres = list.ToArray();
+ }
+ }
+
+ /// <summary>
+ /// Marks the played.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="datePlayed">The date played.</param>
+ /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public virtual void MarkPlayed(User user,
+ DateTime? datePlayed,
+ bool resetPosition)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ var data = UserDataManager.GetUserData(user, this);
+
+ if (datePlayed.HasValue)
+ {
+ // Increment
+ data.PlayCount++;
+ }
+
+ // Ensure it's at least one
+ data.PlayCount = Math.Max(data.PlayCount, 1);
+
+ if (resetPosition)
+ {
+ data.PlaybackPositionTicks = 0;
+ }
+
+ data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow;
+ data.Played = true;
+
+ UserDataManager.SaveUserData(user.Id, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Marks the unplayed.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public virtual void MarkUnplayed(User user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ var data = UserDataManager.GetUserData(user, this);
+
+ //I think it is okay to do this here.
+ // if this is only called when a user is manually forcing something to un-played
+ // then it probably is what we want to do...
+ data.PlayCount = 0;
+ data.PlaybackPositionTicks = 0;
+ data.LastPlayedDate = null;
+ data.Played = false;
+
+ UserDataManager.SaveUserData(user.Id, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed.
+ /// </summary>
+ /// <returns>Task.</returns>
+ public virtual void ChangedExternally()
+ {
+ ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(FileSystem)
+ {
+
+ }, RefreshPriority.High);
+ }
+
+ /// <summary>
+ /// Gets an image
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <returns><c>true</c> if the specified type has image; otherwise, <c>false</c>.</returns>
+ /// <exception cref="System.ArgumentException">Backdrops should be accessed using Item.Backdrops</exception>
+ public bool HasImage(ImageType type, int imageIndex)
+ {
+ return GetImageInfo(type, imageIndex) != null;
+ }
+
+ public void SetImage(ItemImageInfo image, int index)
+ {
+ if (image.Type == ImageType.Chapter)
+ {
+ throw new ArgumentException("Cannot set chapter images using SetImagePath");
+ }
+
+ var existingImage = GetImageInfo(image.Type, index);
+
+ if (existingImage != null)
+ {
+ existingImage.Path = image.Path;
+ existingImage.DateModified = image.DateModified;
+ existingImage.Width = image.Width;
+ existingImage.Height = image.Height;
+ }
+
+ else
+ {
+ var currentCount = ImageInfos.Length;
+ var newList = ImageInfos.ToArray(currentCount + 1);
+ newList[currentCount] = image;
+ ImageInfos = newList;
+ }
+ }
+
+ public void SetImagePath(ImageType type, int index, FileSystemMetadata file)
+ {
+ if (type == ImageType.Chapter)
+ {
+ throw new ArgumentException("Cannot set chapter images using SetImagePath");
+ }
+
+ var image = GetImageInfo(type, index);
+
+ if (image == null)
+ {
+ var currentCount = ImageInfos.Length;
+ var newList = ImageInfos.ToArray(currentCount + 1);
+ newList[currentCount] = GetImageInfo(file, type);
+ ImageInfos = newList;
+ }
+ else
+ {
+ var imageInfo = GetImageInfo(file, type);
+
+ image.Path = file.FullName;
+ image.DateModified = imageInfo.DateModified;
+
+ // reset these values
+ image.Width = 0;
+ image.Height = 0;
+ }
+ }
+
+ /// <summary>
+ /// Deletes the image.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <param name="index">The index.</param>
+ /// <returns>Task.</returns>
+ public void DeleteImage(ImageType type, int index)
+ {
+ var info = GetImageInfo(type, index);
+
+ if (info == null)
+ {
+ // Nothing to do
+ return;
+ }
+
+ // Remove it from the item
+ RemoveImage(info);
+
+ if (info.IsLocalFile)
+ {
+ FileSystem.DeleteFile(info.Path);
+ }
+
+ UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
+ }
+
+ public void RemoveImage(ItemImageInfo image)
+ {
+ RemoveImages(new List<ItemImageInfo> { image });
+ }
+
+ public void RemoveImages(List<ItemImageInfo> deletedImages)
+ {
+ ImageInfos = ImageInfos.Except(deletedImages).ToArray();
+ }
+
+ public virtual void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken)
+ {
+ LibraryManager.UpdateItem(this, GetParent(), updateReason, cancellationToken);
+ }
+
+ /// <summary>
+ /// Validates that images within the item are still on the file system
+ /// </summary>
+ public bool ValidateImages(IDirectoryService directoryService)
+ {
+ var allFiles = ImageInfos
+ .Where(i => i.IsLocalFile)
+ .Select(i => FileSystem.GetDirectoryName(i.Path))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .SelectMany(i => directoryService.GetFilePaths(i))
+ .ToList();
+
+ var deletedImages = ImageInfos
+ .Where(image => image.IsLocalFile && !allFiles.Contains(image.Path, StringComparer.OrdinalIgnoreCase))
+ .ToList();
+
+ if (deletedImages.Count > 0)
+ {
+ ImageInfos = ImageInfos.Except(deletedImages).ToArray();
+ }
+
+ return deletedImages.Count > 0;
+ }
+
+ /// <summary>
+ /// Gets the image path.
+ /// </summary>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="System.InvalidOperationException">
+ /// </exception>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ public string GetImagePath(ImageType imageType, int imageIndex)
+ {
+ var info = GetImageInfo(imageType, imageIndex);
+
+ return info == null ? null : info.Path;
+ }
+
+ /// <summary>
+ /// Gets the image information.
+ /// </summary>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <returns>ItemImageInfo.</returns>
+ public ItemImageInfo GetImageInfo(ImageType imageType, int imageIndex)
+ {
+ if (imageType == ImageType.Chapter)
+ {
+ var chapter = ItemRepository.GetChapter(this, imageIndex);
+
+ if (chapter == null)
+ {
+ return null;
+ }
+
+ var path = chapter.ImagePath;
+
+ if (string.IsNullOrEmpty(path))
+ {
+ return null;
+ }
+
+ return new ItemImageInfo
+ {
+ Path = path,
+ DateModified = chapter.ImageDateModified,
+ Type = imageType
+ };
+ }
+
+ return GetImages(imageType)
+ .ElementAtOrDefault(imageIndex);
+ }
+
+ public IEnumerable<ItemImageInfo> GetImages(ImageType imageType)
+ {
+ if (imageType == ImageType.Chapter)
+ {
+ throw new ArgumentException("No image info for chapter images");
+ }
+
+ return ImageInfos.Where(i => i.Type == imageType);
+ }
+
+ /// <summary>
+ /// Adds the images.
+ /// </summary>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="images">The images.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+ /// <exception cref="System.ArgumentException">Cannot call AddImages with chapter images</exception>
+ public bool AddImages(ImageType imageType, List<FileSystemMetadata> images)
+ {
+ if (imageType == ImageType.Chapter)
+ {
+ throw new ArgumentException("Cannot call AddImages with chapter images");
+ }
+
+ var existingImages = GetImages(imageType)
+ .ToList();
+
+ var newImageList = new List<FileSystemMetadata>();
+ var imageAdded = false;
+ var imageUpdated = false;
+
+ foreach (var newImage in images)
+ {
+ if (newImage == null)
+ {
+ throw new ArgumentException("null image found in list");
+ }
+
+ var existing = existingImages
+ .FirstOrDefault(i => string.Equals(i.Path, newImage.FullName, StringComparison.OrdinalIgnoreCase));
+
+ if (existing == null)
+ {
+ newImageList.Add(newImage);
+ imageAdded = true;
+ }
+ else
+ {
+ if (existing.IsLocalFile)
+ {
+ var newDateModified = FileSystem.GetLastWriteTimeUtc(newImage);
+
+ // If date changed then we need to reset saved image dimensions
+ if (existing.DateModified != newDateModified && (existing.Width > 0 || existing.Height > 0))
+ {
+ existing.Width = 0;
+ existing.Height = 0;
+ imageUpdated = true;
+ }
+
+ existing.DateModified = newDateModified;
+ }
+ }
+ }
+
+ if (imageAdded || images.Count != existingImages.Count)
+ {
+ var newImagePaths = images.Select(i => i.FullName).ToList();
+
+ var deleted = existingImages
+ .Where(i => i.IsLocalFile && !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !FileSystem.FileExists(i.Path))
+ .ToList();
+
+ if (deleted.Count > 0)
+ {
+ ImageInfos = ImageInfos.Except(deleted).ToArray();
+ }
+ }
+
+ if (newImageList.Count > 0)
+ {
+ var currentCount = ImageInfos.Length;
+ var newList = ImageInfos.ToArray(currentCount + newImageList.Count);
+
+ foreach (var image in newImageList)
+ {
+ newList[currentCount] = GetImageInfo(image, imageType);
+ currentCount++;
+ }
+
+ ImageInfos = newList;
+ }
+
+ return imageUpdated || newImageList.Count > 0;
+ }
+
+ private ItemImageInfo GetImageInfo(FileSystemMetadata file, ImageType type)
+ {
+ return new ItemImageInfo
+ {
+ Path = file.FullName,
+ Type = type,
+ DateModified = FileSystem.GetLastWriteTimeUtc(file)
+ };
+ }
+
+ /// <summary>
+ /// Gets the file system path to delete when the item is to be deleted
+ /// </summary>
+ /// <returns></returns>
+ public virtual IEnumerable<FileSystemMetadata> GetDeletePaths()
+ {
+ return new[] {
+ new FileSystemMetadata
+ {
+ FullName = Path,
+ IsDirectory = IsFolder
+ }
+ }.Concat(GetLocalMetadataFilesToDelete());
+ }
+
+ protected List<FileSystemMetadata> GetLocalMetadataFilesToDelete()
+ {
+ if (IsFolder || !IsInMixedFolder)
+ {
+ return new List<FileSystemMetadata>();
+ }
+
+ var filename = System.IO.Path.GetFileNameWithoutExtension(Path);
+ var extensions = new List<string> { ".nfo", ".xml", ".srt", ".vtt", ".sub", ".idx", ".txt", ".edl", ".bif", ".smi", ".ttml" };
+ extensions.AddRange(SupportedImageExtensions);
+
+ return FileSystem.GetFiles(FileSystem.GetDirectoryName(Path), extensions.ToArray(extensions.Count), false, false)
+ .Where(i => System.IO.Path.GetFileNameWithoutExtension(i.FullName).StartsWith(filename, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+ }
+
+ public bool AllowsMultipleImages(ImageType type)
+ {
+ return type == ImageType.Backdrop || type == ImageType.Screenshot || type == ImageType.Chapter;
+ }
+
+ public void SwapImages(ImageType type, int index1, int index2)
+ {
+ if (!AllowsMultipleImages(type))
+ {
+ throw new ArgumentException("The change index operation is only applicable to backdrops and screenshots");
+ }
+
+ var info1 = GetImageInfo(type, index1);
+ var info2 = GetImageInfo(type, index2);
+
+ if (info1 == null || info2 == null)
+ {
+ // Nothing to do
+ return;
+ }
+
+ if (!info1.IsLocalFile || !info2.IsLocalFile)
+ {
+ // TODO: Not supported yet
+ return;
+ }
+
+ var path1 = info1.Path;
+ var path2 = info2.Path;
+
+ FileSystem.SwapFiles(path1, path2);
+
+ // Refresh these values
+ info1.DateModified = FileSystem.GetLastWriteTimeUtc(info1.Path);
+ info2.DateModified = FileSystem.GetLastWriteTimeUtc(info2.Path);
+
+ info1.Width = 0;
+ info1.Height = 0;
+ info2.Width = 0;
+ info2.Height = 0;
+
+ UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
+ }
+
+ public virtual bool IsPlayed(User user)
+ {
+ var userdata = UserDataManager.GetUserData(user, this);
+
+ return userdata != null && userdata.Played;
+ }
+
+ public bool IsFavoriteOrLiked(User user)
+ {
+ var userdata = UserDataManager.GetUserData(user, this);
+
+ return userdata != null && (userdata.IsFavorite || (userdata.Likes ?? false));
+ }
+
+ public virtual bool IsUnplayed(User user)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException("user");
+ }
+
+ var userdata = UserDataManager.GetUserData(user, this);
+
+ return userdata == null || !userdata.Played;
+ }
+
+ ItemLookupInfo IHasLookupInfo<ItemLookupInfo>.GetLookupInfo()
+ {
+ return GetItemLookupInfo<ItemLookupInfo>();
+ }
+
+ protected T GetItemLookupInfo<T>()
+ where T : ItemLookupInfo, new()
+ {
+ return new T
+ {
+ MetadataCountryCode = GetPreferredMetadataCountryCode(),
+ MetadataLanguage = GetPreferredMetadataLanguage(),
+ Name = GetNameForMetadataLookup(),
+ ProviderIds = ProviderIds,
+ IndexNumber = IndexNumber,
+ ParentIndexNumber = ParentIndexNumber,
+ Year = ProductionYear,
+ PremiereDate = PremiereDate
+ };
+ }
+
+ protected virtual string GetNameForMetadataLookup()
+ {
+ return Name;
+ }
+
+ /// <summary>
+ /// This is called before any metadata refresh and returns true or false indicating if changes were made
+ /// </summary>
+ public virtual bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ {
+ _sortName = null;
+
+ var hasChanges = false;
+
+ if (string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Path))
+ {
+ Name = FileSystem.GetFileNameWithoutExtension(Path);
+ hasChanges = true;
+ }
+
+ return hasChanges;
+ }
+
+ protected static string GetMappedPath(BaseItem item, string path, MediaProtocol? protocol)
+ {
+ if (protocol.HasValue && protocol.Value == MediaProtocol.File)
+ {
+ return LibraryManager.GetPathAfterNetworkSubstitution(path, item);
+ }
+
+ return path;
+ }
+
+ public virtual void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields)
+ {
+ if (RunTimeTicks.HasValue)
+ {
+ double pct = RunTimeTicks.Value;
+
+ if (pct > 0)
+ {
+ pct = userData.PlaybackPositionTicks / pct;
+
+ if (pct > 0)
+ {
+ dto.PlayedPercentage = 100 * pct;
+ }
+ }
+ }
+ }
+
+ protected Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ var newOptions = new MetadataRefreshOptions(options);
+ newOptions.SearchResult = null;
+
+ var item = this;
+
+ if (copyTitleMetadata)
+ {
+ // Take some data from the main item, for querying purposes
+ if (!item.Genres.SequenceEqual(ownedItem.Genres, StringComparer.Ordinal))
+ {
+ newOptions.ForceSave = true;
+ ownedItem.Genres = item.Genres;
+ }
+ if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal))
+ {
+ newOptions.ForceSave = true;
+ ownedItem.Studios = item.Studios;
+ }
+ if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal))
+ {
+ newOptions.ForceSave = true;
+ ownedItem.ProductionLocations = item.ProductionLocations;
+ }
+ if (item.CommunityRating != ownedItem.CommunityRating)
+ {
+ ownedItem.CommunityRating = item.CommunityRating;
+ newOptions.ForceSave = true;
+ }
+ if (item.CriticRating != ownedItem.CriticRating)
+ {
+ ownedItem.CriticRating = item.CriticRating;
+ newOptions.ForceSave = true;
+ }
+ if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal))
+ {
+ ownedItem.Overview = item.Overview;
+ newOptions.ForceSave = true;
+ }
+ if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal))
+ {
+ ownedItem.OfficialRating = item.OfficialRating;
+ newOptions.ForceSave = true;
+ }
+ if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal))
+ {
+ ownedItem.CustomRating = item.CustomRating;
+ newOptions.ForceSave = true;
+ }
+ }
+
+ return ownedItem.RefreshMetadata(newOptions, cancellationToken);
+ }
+
+ protected Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken)
+ {
+ var newOptions = new MetadataRefreshOptions(options);
+ newOptions.SearchResult = null;
+
+ var id = LibraryManager.GetNewItemId(path, typeof(Video));
+
+ // Try to retrieve it from the db. If we don't find it, use the resolved version
+ var video = LibraryManager.GetItemById(id) as Video;
+
+ if (video == null)
+ {
+ video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video;
+
+ newOptions.ForceSave = true;
+ }
+
+ //var parentId = Id;
+ //if (!video.IsOwnedItem || video.ParentId != parentId)
+ //{
+ // video.IsOwnedItem = true;
+ // video.ParentId = parentId;
+ // newOptions.ForceSave = true;
+ //}
+
+ if (video == null)
+ {
+ return Task.FromResult(true);
+ }
+
+ return RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken);
+ }
+
+ public string GetEtag(User user)
+ {
+ var list = GetEtagValues(user);
+
+ return string.Join("|", list.ToArray(list.Count)).GetMD5().ToString("N");
+ }
+
+ protected virtual List<string> GetEtagValues(User user)
+ {
+ return new List<string>
+ {
+ DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture)
+ };
+ }
+
+ public virtual IEnumerable<Guid> GetAncestorIds()
+ {
+ return GetParents().Select(i => i.Id).Concat(LibraryManager.GetCollectionFolders(this).Select(i => i.Id));
+ }
+
+ public BaseItem GetTopParent()
+ {
+ if (IsTopParent)
+ {
+ return this;
+ }
+
+ foreach (var parent in GetParents())
+ {
+ if (parent.IsTopParent)
+ {
+ return parent;
+ }
+ }
+ return null;
+ }
+
+ [IgnoreDataMember]
+ public virtual bool IsTopParent
+ {
+ get
+ {
+ if (this is BasePluginFolder || this is Channel)
+ {
+ return true;
+ }
+
+ var view = this as IHasCollectionType;
+ if (view != null)
+ {
+ if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ if (GetParent() is AggregateFolder)
+ {
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public virtual bool SupportsAncestors
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public virtual bool StopRefreshIfLocalMetadataFound
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ public virtual IEnumerable<Guid> GetIdsForAncestorQuery()
+ {
+ return new[] { Id };
+ }
+
+ public virtual List<ExternalUrl> GetRelatedUrls()
+ {
+ return new List<ExternalUrl>();
+ }
+
+ public virtual double? GetRefreshProgress()
+ {
+ return null;
+ }
+
+ public virtual ItemUpdateType OnMetadataChanged()
+ {
+ var updateType = ItemUpdateType.None;
+
+ var item = this;
+
+ var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? 0;
+ if (inheritedParentalRatingValue != item.InheritedParentalRatingValue)
+ {
+ item.InheritedParentalRatingValue = inheritedParentalRatingValue;
+ updateType |= ItemUpdateType.MetadataImport;
+ }
+
+ return updateType;
+ }
+
+ /// <summary>
+ /// Updates the official rating based on content and returns true or false indicating if it changed.
+ /// </summary>
+ /// <returns></returns>
+ public bool UpdateRatingToItems(IList<BaseItem> children)
+ {
+ var currentOfficialRating = OfficialRating;
+
+ // Gather all possible ratings
+ var ratings = children
+ .Select(i => i.OfficialRating)
+ .Where(i => !string.IsNullOrEmpty(i))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .Select(i => new Tuple<string, int?>(i, LocalizationManager.GetRatingLevel(i)))
+ .OrderBy(i => i.Item2 ?? 1000)
+ .Select(i => i.Item1);
+
+ OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating;
+
+ return !string.Equals(currentOfficialRating ?? string.Empty, OfficialRating ?? string.Empty,
+ StringComparison.OrdinalIgnoreCase);
+ }
+
+ public IEnumerable<BaseItem> GetThemeSongs()
+ {
+ return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeSong)).OrderBy(i => i.SortName);
+ }
+
+ public IEnumerable<BaseItem> GetThemeVideos()
+ {
+ return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeVideo)).OrderBy(i => i.SortName);
+ }
+
+ public MediaUrl[] RemoteTrailers { get; set; }
+
+ public IEnumerable<BaseItem> GetExtras()
+ {
+ return ThemeVideoIds.Select(LibraryManager.GetItemById).Where(i => i.ExtraType.Equals(Model.Entities.ExtraType.ThemeVideo)).OrderBy(i => i.SortName);
+ }
+
+ public IEnumerable<BaseItem> GetExtras(ExtraType[] unused)
+ {
+ return GetExtras();
+ }
+
+ public IEnumerable<BaseItem> GetDisplayExtras()
+ {
+ return GetExtras();
+ }
+
+ public virtual bool IsHD {
+ get{
+ return Height >= 720;
+ }
+ }
+ public bool IsShortcut{ get; set;}
+ public string ShortcutPath{ get; set;}
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public Guid[] ExtraIds { get; set; }
+ public virtual long GetRunTimeTicksForPlayState() {
+ return RunTimeTicks ?? 0;
+ }
+ // what does this do?
+ public static ExtraType[] DisplayExtraTypes = new[] {Model.Entities.ExtraType.ThemeSong, Model.Entities.ExtraType.ThemeVideo };
+ public virtual bool SupportsExternalTransfer {
+ get {
+ return false;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
new file mode 100644
index 000000000..c56a370a8
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public static class BaseItemExtensions
+ {
+ /// <summary>
+ /// Gets the image path.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <returns>System.String.</returns>
+ public static string GetImagePath(this BaseItem item, ImageType imageType)
+ {
+ return item.GetImagePath(imageType, 0);
+ }
+
+ public static bool HasImage(this BaseItem item, ImageType imageType)
+ {
+ return item.HasImage(imageType, 0);
+ }
+
+ /// <summary>
+ /// Sets the image path.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="file">The file.</param>
+ public static void SetImagePath(this BaseItem item, ImageType imageType, FileSystemMetadata file)
+ {
+ item.SetImagePath(imageType, 0, file);
+ }
+
+ /// <summary>
+ /// Sets the image path.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="file">The file.</param>
+ public static void SetImagePath(this BaseItem item, ImageType imageType, string file)
+ {
+ if (file.StartsWith("http", System.StringComparison.OrdinalIgnoreCase))
+ {
+ item.SetImage(new ItemImageInfo
+ {
+ Path = file,
+ Type = imageType
+ }, 0);
+ }
+ else
+ {
+ item.SetImagePath(imageType, BaseItem.FileSystem.GetFileInfo(file));
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/BasePluginFolder.cs b/MediaBrowser.Controller/Entities/BasePluginFolder.cs
new file mode 100644
index 000000000..c06f1cef4
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/BasePluginFolder.cs
@@ -0,0 +1,54 @@
+
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Plugins derive from and export this class to create a folder that will appear in the root along
+ /// with all the other actual physical folders in the system.
+ /// </summary>
+ public abstract class BasePluginFolder : Folder, ICollectionFolder
+ {
+ [IgnoreDataMember]
+ public virtual string CollectionType
+ {
+ get { return null; }
+ }
+
+ public override bool CanDelete()
+ {
+ return false;
+ }
+
+ public override bool IsSaveLocalMetadataEnabled()
+ {
+ return true;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsInheritedParentImages
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ //public override double? GetDefaultPrimaryImageAspectRatio()
+ //{
+ // double value = 16;
+ // value /= 9;
+
+ // return value;
+ //}
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs
new file mode 100644
index 000000000..6814570c3
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Book.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Linq;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class Book : BaseItem, IHasLookupInfo<BookInfo>, IHasSeries
+ {
+ [IgnoreDataMember]
+ public override string MediaType
+ {
+ get
+ {
+ return Model.Entities.MediaType.Book;
+ }
+ }
+
+ [IgnoreDataMember]
+ public string SeriesPresentationUniqueKey { get; set; }
+ [IgnoreDataMember]
+ public string SeriesName { get; set; }
+ [IgnoreDataMember]
+ public Guid SeriesId { get; set; }
+
+ public string FindSeriesSortName()
+ {
+ return SeriesName;
+ }
+ public string FindSeriesName()
+ {
+ return SeriesName;
+ }
+ public string FindSeriesPresentationUniqueKey()
+ {
+ return SeriesPresentationUniqueKey;
+ }
+
+ public Guid FindSeriesId()
+ {
+ return SeriesId;
+ }
+
+ public override bool CanDownload()
+ {
+ return IsFileProtocol;
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ return UnratedItem.Book;
+ }
+
+ public BookInfo GetLookupInfo()
+ {
+ var info = GetItemLookupInfo<BookInfo>();
+
+ if (string.IsNullOrEmpty(SeriesName))
+ {
+ info.SeriesName = GetParents().Select(i => i.Name).FirstOrDefault();
+ }
+ else
+ {
+ info.SeriesName = SeriesName;
+ }
+
+ return info;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
new file mode 100644
index 000000000..8240a68ff
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -0,0 +1,405 @@
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Specialized Folder class that points to a subset of the physical folders in the system.
+ /// It is created from the user-specific folders within the system root
+ /// </summary>
+ public class CollectionFolder : Folder, ICollectionFolder
+ {
+ public static IXmlSerializer XmlSerializer { get; set; }
+ public static IJsonSerializer JsonSerializer { get; set; }
+ public static IServerApplicationHost ApplicationHost { get; set; }
+
+ public CollectionFolder()
+ {
+ PhysicalLocationsList = new string[] { };
+ PhysicalFolderIds = new Guid[] { };
+ }
+
+ //public override double? GetDefaultPrimaryImageAspectRatio()
+ //{
+ // double value = 16;
+ // value /= 9;
+
+ // return value;
+ //}
+
+ [IgnoreDataMember]
+ public override bool SupportsPlayedStatus
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsInheritedParentImages
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public override bool CanDelete()
+ {
+ return false;
+ }
+
+ public string CollectionType { get; set; }
+
+ private static readonly Dictionary<string, LibraryOptions> LibraryOptions = new Dictionary<string, LibraryOptions>();
+ public LibraryOptions GetLibraryOptions()
+ {
+ return GetLibraryOptions(Path);
+ }
+
+ private static LibraryOptions LoadLibraryOptions(string path)
+ {
+ try
+ {
+ var result = XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) as LibraryOptions;
+
+ if (result == null)
+ {
+ return new LibraryOptions();
+ }
+
+ foreach (var mediaPath in result.PathInfos)
+ {
+ if (!string.IsNullOrEmpty(mediaPath.Path))
+ {
+ mediaPath.Path = ApplicationHost.ExpandVirtualPath(mediaPath.Path);
+ }
+ }
+
+ return result;
+ }
+ catch (FileNotFoundException)
+ {
+ return new LibraryOptions();
+ }
+ catch (IOException)
+ {
+ return new LibraryOptions();
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error loading library options", ex);
+
+ return new LibraryOptions();
+ }
+ }
+
+ private static string GetLibraryOptionsPath(string path)
+ {
+ return System.IO.Path.Combine(path, "options.xml");
+ }
+
+ public void UpdateLibraryOptions(LibraryOptions options)
+ {
+ SaveLibraryOptions(Path, options);
+ }
+
+ public static LibraryOptions GetLibraryOptions(string path)
+ {
+ lock (LibraryOptions)
+ {
+ LibraryOptions options;
+ if (!LibraryOptions.TryGetValue(path, out options))
+ {
+ options = LoadLibraryOptions(path);
+ LibraryOptions[path] = options;
+ }
+
+ return options;
+ }
+ }
+
+ public static void SaveLibraryOptions(string path, LibraryOptions options)
+ {
+ lock (LibraryOptions)
+ {
+ LibraryOptions[path] = options;
+
+ var clone = JsonSerializer.DeserializeFromString<LibraryOptions>(JsonSerializer.SerializeToString(options));
+ foreach (var mediaPath in clone.PathInfos)
+ {
+ if (!string.IsNullOrEmpty(mediaPath.Path))
+ {
+ mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path);
+ }
+ }
+
+ XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path));
+ }
+ }
+
+ public static void OnCollectionFolderChange()
+ {
+ lock (LibraryOptions)
+ {
+ LibraryOptions.Clear();
+ }
+ }
+
+ /// <summary>
+ /// Allow different display preferences for each collection folder
+ /// </summary>
+ /// <value>The display prefs id.</value>
+ [IgnoreDataMember]
+ public override Guid DisplayPreferencesId
+ {
+ get
+ {
+ return Id;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override string[] PhysicalLocations
+ {
+ get
+ {
+ return PhysicalLocationsList;
+ }
+ }
+
+ public override bool IsSaveLocalMetadataEnabled()
+ {
+ return true;
+ }
+
+ public string[] PhysicalLocationsList { get; set; }
+ public Guid[] PhysicalFolderIds { get; set; }
+
+ protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
+ {
+ return CreateResolveArgs(directoryService, true).FileSystemChildren;
+ }
+
+ private bool _requiresRefresh;
+ public override bool RequiresRefresh()
+ {
+ var changed = base.RequiresRefresh() || _requiresRefresh;
+
+ if (!changed)
+ {
+ var locations = PhysicalLocations;
+
+ var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations;
+
+ if (!locations.SequenceEqual(newLocations))
+ {
+ changed = true;
+ }
+ }
+
+ if (!changed)
+ {
+ var folderIds = PhysicalFolderIds;
+
+ var newFolderIds = GetPhysicalFolders(false).Select(i => i.Id).ToList();
+
+ if (!folderIds.SequenceEqual(newFolderIds))
+ {
+ changed = true;
+ }
+ }
+
+ return changed;
+ }
+
+ public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ {
+ var changed = base.BeforeMetadataRefresh(replaceAllMetdata) || _requiresRefresh;
+ _requiresRefresh = false;
+ return changed;
+ }
+
+ public override double? GetRefreshProgress()
+ {
+ var folders = GetPhysicalFolders(true).ToList();
+ double totalProgresses = 0;
+ var foldersWithProgress = 0;
+
+ foreach (var folder in folders)
+ {
+ var progress = ProviderManager.GetRefreshProgress(folder.Id);
+ if (progress.HasValue)
+ {
+ totalProgresses += progress.Value;
+ foldersWithProgress++;
+ }
+ }
+
+ if (foldersWithProgress == 0)
+ {
+ return null;
+ }
+
+ return (totalProgresses / foldersWithProgress);
+ }
+
+ protected override bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren)
+ {
+ return RefreshLinkedChildrenInternal(true);
+ }
+
+ private bool RefreshLinkedChildrenInternal(bool setFolders)
+ {
+ var physicalFolders = GetPhysicalFolders(false)
+ .ToList();
+
+ var linkedChildren = physicalFolders
+ .SelectMany(c => c.LinkedChildren)
+ .ToList();
+
+ var changed = !linkedChildren.SequenceEqual(LinkedChildren, new LinkedChildComparer(FileSystem));
+
+ LinkedChildren = linkedChildren.ToArray(linkedChildren.Count);
+
+ var folderIds = PhysicalFolderIds;
+ var newFolderIds = physicalFolders.Select(i => i.Id).ToArray();
+
+ if (!folderIds.SequenceEqual(newFolderIds))
+ {
+ changed = true;
+ if (setFolders)
+ {
+ PhysicalFolderIds = newFolderIds;
+ }
+ }
+
+ return changed;
+ }
+
+ private ItemResolveArgs CreateResolveArgs(IDirectoryService directoryService, bool setPhysicalLocations)
+ {
+ var path = ContainingFolderPath;
+
+ var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
+ {
+ FileInfo = FileSystem.GetDirectoryInfo(path),
+ Path = path,
+ Parent = GetParent() as Folder,
+ CollectionType = CollectionType
+ };
+
+ // Gather child folder and files
+ if (args.IsDirectory)
+ {
+ var flattenFolderDepth = 0;
+
+ var files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, FileSystem, ApplicationHost, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: true);
+
+ args.FileSystemChildren = files;
+ }
+
+ _requiresRefresh = _requiresRefresh || !args.PhysicalLocations.SequenceEqual(PhysicalLocations);
+
+ if (setPhysicalLocations)
+ {
+ PhysicalLocationsList = args.PhysicalLocations;
+ }
+
+ return args;
+ }
+
+ /// <summary>
+ /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
+ /// ***Currently does not contain logic to maintain items that are unavailable in the file system***
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+ /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
+ /// <param name="refreshOptions">The refresh options.</param>
+ /// <param name="directoryService">The directory service.</param>
+ /// <returns>Task.</returns>
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ {
+ return Task.FromResult(true);
+ }
+
+ /// <summary>
+ /// Our children are actually just references to the ones in the physical root...
+ /// </summary>
+ /// <value>The actual children.</value>
+ [IgnoreDataMember]
+ public override IEnumerable<BaseItem> Children
+ {
+ get { return GetActualChildren(); }
+ }
+
+ public IEnumerable<BaseItem> GetActualChildren()
+ {
+ return GetPhysicalFolders(true).SelectMany(c => c.Children);
+ }
+
+ public IEnumerable<Folder> GetPhysicalFolders()
+ {
+ return GetPhysicalFolders(true);
+ }
+
+ private IEnumerable<Folder> GetPhysicalFolders(bool enableCache)
+ {
+ if (enableCache)
+ {
+ return PhysicalFolderIds.Select(i => LibraryManager.GetItemById(i)).OfType<Folder>();
+ }
+
+ var rootChildren = LibraryManager.RootFolder.Children
+ .OfType<Folder>()
+ .ToList();
+
+ return PhysicalLocations.Where(i => !FileSystem.AreEqual(i, Path)).SelectMany(i => GetPhysicalParents(i, rootChildren)).DistinctBy(i => i.Id);
+ }
+
+ private IEnumerable<Folder> GetPhysicalParents(string path, List<Folder> rootChildren)
+ {
+ var result = rootChildren
+ .Where(i => FileSystem.AreEqual(i.Path, path))
+ .ToList();
+
+ if (result.Count == 0)
+ {
+ var folder = LibraryManager.FindByPath(path, true) as Folder;
+
+ if (folder != null)
+ {
+ result.Add(folder);
+ }
+ }
+
+ return result;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get
+ {
+ return false;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs b/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs
new file mode 100644
index 000000000..166ef66d4
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/DayOfWeekHelper.cs
@@ -0,0 +1,71 @@
+using MediaBrowser.Model.Configuration;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public static class DayOfWeekHelper
+ {
+ public static List<DayOfWeek> GetDaysOfWeek(DynamicDayOfWeek day)
+ {
+ return GetDaysOfWeek(new List<DynamicDayOfWeek> { day });
+ }
+
+ public static List<DayOfWeek> GetDaysOfWeek(List<DynamicDayOfWeek> days)
+ {
+ var list = new List<DayOfWeek>();
+
+ if (days.Contains(DynamicDayOfWeek.Sunday) ||
+ days.Contains(DynamicDayOfWeek.Weekend) ||
+ days.Contains(DynamicDayOfWeek.Everyday))
+ {
+ list.Add(DayOfWeek.Sunday);
+ }
+
+ if (days.Contains(DynamicDayOfWeek.Saturday) ||
+ days.Contains(DynamicDayOfWeek.Weekend) ||
+ days.Contains(DynamicDayOfWeek.Everyday))
+ {
+ list.Add(DayOfWeek.Saturday);
+ }
+
+ if (days.Contains(DynamicDayOfWeek.Monday) ||
+ days.Contains(DynamicDayOfWeek.Weekday) ||
+ days.Contains(DynamicDayOfWeek.Everyday))
+ {
+ list.Add(DayOfWeek.Monday);
+ }
+
+ if (days.Contains(DynamicDayOfWeek.Tuesday) ||
+ days.Contains(DynamicDayOfWeek.Weekday) ||
+ days.Contains(DynamicDayOfWeek.Everyday))
+ {
+ list.Add(DayOfWeek.Tuesday
+ );
+ }
+
+ if (days.Contains(DynamicDayOfWeek.Wednesday) ||
+ days.Contains(DynamicDayOfWeek.Weekday) ||
+ days.Contains(DynamicDayOfWeek.Everyday))
+ {
+ list.Add(DayOfWeek.Wednesday);
+ }
+
+ if (days.Contains(DynamicDayOfWeek.Thursday) ||
+ days.Contains(DynamicDayOfWeek.Weekday) ||
+ days.Contains(DynamicDayOfWeek.Everyday))
+ {
+ list.Add(DayOfWeek.Thursday);
+ }
+
+ if (days.Contains(DynamicDayOfWeek.Friday) ||
+ days.Contains(DynamicDayOfWeek.Weekday) ||
+ days.Contains(DynamicDayOfWeek.Everyday))
+ {
+ list.Add(DayOfWeek.Friday);
+ }
+
+ return list;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs
new file mode 100644
index 000000000..c706cf36c
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Extensions.cs
@@ -0,0 +1,46 @@
+using MediaBrowser.Model.Entities;
+using System;
+using System.Linq;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class Extensions
+ /// </summary>
+ public static class Extensions
+ {
+ /// <summary>
+ /// Adds the trailer URL.
+ /// </summary>
+ public static void AddTrailerUrl(this BaseItem item, string url)
+ {
+ if (string.IsNullOrEmpty(url))
+ {
+ throw new ArgumentNullException("url");
+ }
+
+ var current = item.RemoteTrailers.FirstOrDefault(i => string.Equals(i.Url, url, StringComparison.OrdinalIgnoreCase));
+
+ if (current == null)
+ {
+ var mediaUrl = new MediaUrl
+ {
+ Url = url
+ };
+
+ if (item.RemoteTrailers.Length == 0)
+ {
+ item.RemoteTrailers = new[] { mediaUrl };
+ }
+ else
+ {
+ var list = item.RemoteTrailers.ToArray(item.RemoteTrailers.Length + 1);
+ list[list.Length - 1] = mediaUrl;
+
+ item.RemoteTrailers = list;
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
new file mode 100644
index 000000000..8b9aa5fc3
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -0,0 +1,1803 @@
+using MediaBrowser.Common.Progress;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.Channels;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Controller.Collections;
+using MediaBrowser.Controller.Configuration;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class Folder
+ /// </summary>
+ public class Folder : BaseItem
+ {
+ public static IUserManager UserManager { get; set; }
+ public static IUserViewManager UserViewManager { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is root.
+ /// </summary>
+ /// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value>
+ public bool IsRoot { get; set; }
+
+ public LinkedChild[] LinkedChildren { get; set; }
+
+ [IgnoreDataMember]
+ public DateTime? DateLastMediaAdded { get; set; }
+
+ public Folder()
+ {
+ LinkedChildren = EmptyLinkedChildArray;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsThemeMedia
+ {
+ get { return true; }
+ }
+
+ [IgnoreDataMember]
+ public virtual bool IsPreSorted
+ {
+ get { return false; }
+ }
+
+ [IgnoreDataMember]
+ public virtual bool IsPhysicalRoot
+ {
+ get { return false; }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsInheritedParentImages
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPlayedStatus
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is folder.
+ /// </summary>
+ /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public override bool IsFolder
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool IsDisplayedAsFolder
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public virtual bool SupportsCumulativeRunTimeTicks
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public virtual bool SupportsDateLastMediaAdded
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public override bool CanDelete()
+ {
+ if (IsRoot)
+ {
+ return false;
+ }
+
+ return base.CanDelete();
+ }
+
+ public override bool RequiresRefresh()
+ {
+ var baseResult = base.RequiresRefresh();
+
+ if (SupportsCumulativeRunTimeTicks && !RunTimeTicks.HasValue)
+ {
+ baseResult = true;
+ }
+
+ return baseResult;
+ }
+
+ [IgnoreDataMember]
+ public override string FileNameWithoutExtension
+ {
+ get
+ {
+ if (IsFileProtocol)
+ {
+ return System.IO.Path.GetFileName(Path);
+ }
+
+ return null;
+ }
+ }
+
+ protected override bool IsAllowTagFilterEnforced()
+ {
+ if (this is ICollectionFolder)
+ {
+ return false;
+ }
+ if (this is UserView)
+ {
+ return false;
+ }
+ return true;
+ }
+
+ [IgnoreDataMember]
+ protected virtual bool SupportsShortcutChildren
+ {
+ get { return false; }
+ }
+
+ /// <summary>
+ /// Adds the child.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.InvalidOperationException">Unable to add + item.Name</exception>
+ public void AddChild(BaseItem item, CancellationToken cancellationToken)
+ {
+ item.SetParent(this);
+
+ if (item.Id.Equals(Guid.Empty))
+ {
+ item.Id = LibraryManager.GetNewItemId(item.Path, item.GetType());
+ }
+
+ if (item.DateCreated == DateTime.MinValue)
+ {
+ item.DateCreated = DateTime.UtcNow;
+ }
+ if (item.DateModified == DateTime.MinValue)
+ {
+ item.DateModified = DateTime.UtcNow;
+ }
+
+ LibraryManager.CreateItem(item, this);
+ }
+
+ /// <summary>
+ /// Gets the actual children.
+ /// </summary>
+ /// <value>The actual children.</value>
+ [IgnoreDataMember]
+ public virtual IEnumerable<BaseItem> Children
+ {
+ get
+ {
+ return LoadChildren();
+ }
+ }
+
+ /// <summary>
+ /// thread-safe access to all recursive children of this folder - without regard to user
+ /// </summary>
+ /// <value>The recursive children.</value>
+ [IgnoreDataMember]
+ public IEnumerable<BaseItem> RecursiveChildren
+ {
+ get { return GetRecursiveChildren(); }
+ }
+
+ public override bool IsVisible(User user)
+ {
+ if (this is ICollectionFolder && !(this is BasePluginFolder))
+ {
+ if (user.Policy.BlockedMediaFolders != null)
+ {
+ if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase) ||
+
+ // Backwards compatibility
+ user.Policy.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ }
+ }
+
+ return base.IsVisible(user);
+ }
+
+ /// <summary>
+ /// Loads our children. Validation will occur externally.
+ /// We want this sychronous.
+ /// </summary>
+ protected virtual List<BaseItem> LoadChildren()
+ {
+ //Logger.Debug("Loading children from {0} {1} {2}", GetType().Name, Id, Path);
+ //just load our children from the repo - the library will be validated and maintained in other processes
+ return GetCachedChildren();
+ }
+
+ public override double? GetRefreshProgress()
+ {
+ return ProviderManager.GetRefreshProgress(Id);
+ }
+
+ public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)));
+ }
+
+ /// <summary>
+ /// Validates that the children of the folder still exist
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="metadataRefreshOptions">The metadata refresh options.</param>
+ /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+ /// <returns>Task.</returns>
+ public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true)
+ {
+ return ValidateChildrenInternal(progress, cancellationToken, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService);
+ }
+
+ private Dictionary<Guid, BaseItem> GetActualChildrenDictionary()
+ {
+ var dictionary = new Dictionary<Guid, BaseItem>();
+
+ var childrenList = Children.ToList();
+
+ foreach (var child in childrenList)
+ {
+ var id = child.Id;
+ if (dictionary.ContainsKey(id))
+ {
+ Logger.Error("Found folder containing items with duplicate id. Path: {0}, Child Name: {1}",
+ Path ?? Name,
+ child.Path ?? child.Name);
+ }
+ else
+ {
+ dictionary[id] = child;
+ }
+ }
+
+ return dictionary;
+ }
+
+ protected override void TriggerOnRefreshStart()
+ {
+ }
+
+ protected override void TriggerOnRefreshComplete()
+ {
+ }
+
+ /// <summary>
+ /// Validates the children internal.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+ /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
+ /// <param name="refreshOptions">The refresh options.</param>
+ /// <param name="directoryService">The directory service.</param>
+ /// <returns>Task.</returns>
+ protected virtual async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ {
+ if (recursive)
+ {
+ ProviderManager.OnRefreshStart(this);
+ }
+
+ try
+ {
+ await ValidateChildrenInternal2(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService).ConfigureAwait(false);
+ }
+ finally
+ {
+ if (recursive)
+ {
+ ProviderManager.OnRefreshComplete(this);
+ }
+ }
+ }
+
+ private async Task ValidateChildrenInternal2(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var validChildren = new List<BaseItem>();
+ var validChildrenNeedGeneration = false;
+
+ if (IsFileProtocol)
+ {
+ IEnumerable<BaseItem> nonCachedChildren;
+
+ try
+ {
+ nonCachedChildren = GetNonCachedChildren(directoryService);
+ }
+ catch (Exception ex)
+ {
+ return;
+ }
+
+ progress.Report(5);
+
+ if (recursive)
+ {
+ ProviderManager.OnRefreshProgress(this, 5);
+ }
+
+ //build a dictionary of the current children we have now by Id so we can compare quickly and easily
+ var currentChildren = GetActualChildrenDictionary();
+
+ //create a list for our validated children
+ var newItems = new List<BaseItem>();
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ foreach (var child in nonCachedChildren)
+ {
+ BaseItem currentChild;
+
+ if (currentChildren.TryGetValue(child.Id, out currentChild))
+ {
+ validChildren.Add(currentChild);
+
+ if (currentChild.UpdateFromResolvedItem(child) > ItemUpdateType.None)
+ {
+ currentChild.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken);
+ }
+
+ continue;
+ }
+
+ // Brand new item - needs to be added
+ child.SetParent(this);
+ newItems.Add(child);
+ validChildren.Add(child);
+ }
+
+ // If any items were added or removed....
+ if (newItems.Count > 0 || currentChildren.Count != validChildren.Count)
+ {
+ // That's all the new and changed ones - now see if there are any that are missing
+ var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
+
+ foreach (var item in itemsRemoved)
+ {
+ if (!item.IsFileProtocol)
+ {
+ }
+
+ else
+ {
+ Logger.Debug("Removed item: " + item.Path);
+
+ item.SetParent(null);
+ LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false);
+ }
+ }
+
+ LibraryManager.CreateItems(newItems, this, cancellationToken);
+ }
+ }
+ else
+ {
+ validChildrenNeedGeneration = true;
+ }
+
+ progress.Report(10);
+
+ if (recursive)
+ {
+ ProviderManager.OnRefreshProgress(this, 10);
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (recursive)
+ {
+ var innerProgress = new ActionableProgress<double>();
+
+ var folder = this;
+ innerProgress.RegisterAction(p =>
+ {
+ double newPct = .80 * p + 10;
+ progress.Report(newPct);
+ ProviderManager.OnRefreshProgress(folder, newPct);
+ });
+
+ if (validChildrenNeedGeneration)
+ {
+ validChildren = Children.ToList();
+ validChildrenNeedGeneration = false;
+ }
+
+ await ValidateSubFolders(validChildren.OfType<Folder>().ToList(), directoryService, innerProgress, cancellationToken).ConfigureAwait(false);
+ }
+
+ if (refreshChildMetadata)
+ {
+ progress.Report(90);
+
+ if (recursive)
+ {
+ ProviderManager.OnRefreshProgress(this, 90);
+ }
+
+ var container = this as IMetadataContainer;
+
+ var innerProgress = new ActionableProgress<double>();
+
+ var folder = this;
+ innerProgress.RegisterAction(p =>
+ {
+ double newPct = .10 * p + 90;
+ progress.Report(newPct);
+ if (recursive)
+ {
+ ProviderManager.OnRefreshProgress(folder, newPct);
+ }
+ });
+
+ if (container != null)
+ {
+ await RefreshAllMetadataForContainer(container, refreshOptions, innerProgress, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ if (validChildrenNeedGeneration)
+ {
+ validChildren = Children.ToList();
+ }
+
+ await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellationToken);
+ }
+ }
+ }
+
+ private async Task RefreshMetadataRecursive(List<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var numComplete = 0;
+ var count = children.Count;
+ double currentPercent = 0;
+
+ foreach (var child in children)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var innerProgress = new ActionableProgress<double>();
+
+ // Avoid implicitly captured closure
+ var currentInnerPercent = currentPercent;
+
+ innerProgress.RegisterAction(p =>
+ {
+ double innerPercent = currentInnerPercent;
+ innerPercent += p / (count);
+ progress.Report(innerPercent);
+ });
+
+ await RefreshChildMetadata(child, refreshOptions, recursive && child.IsFolder, innerProgress, cancellationToken)
+ .ConfigureAwait(false);
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+ currentPercent = percent;
+
+ progress.Report(percent);
+ }
+ }
+
+ private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var series = container as Series;
+ if (series != null)
+ {
+ await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
+
+ }
+ await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false);
+ }
+
+ private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var container = child as IMetadataContainer;
+
+ if (container != null)
+ {
+ await RefreshAllMetadataForContainer(container, refreshOptions, progress, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ if (refreshOptions.RefreshItem(child))
+ {
+ await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
+ }
+
+ if (recursive)
+ {
+ var folder = child as Folder;
+
+ if (folder != null)
+ {
+ await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Refreshes the children.
+ /// </summary>
+ /// <param name="children">The children.</param>
+ /// <param name="directoryService">The directory service.</param>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var numComplete = 0;
+ var count = children.Count;
+ double currentPercent = 0;
+
+ foreach (var child in children)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var innerProgress = new ActionableProgress<double>();
+
+ // Avoid implicitly captured closure
+ var currentInnerPercent = currentPercent;
+
+ innerProgress.RegisterAction(p =>
+ {
+ double innerPercent = currentInnerPercent;
+ innerPercent += p / (count);
+ progress.Report(innerPercent);
+ });
+
+ await child.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService)
+ .ConfigureAwait(false);
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= count;
+ percent *= 100;
+ currentPercent = percent;
+
+ progress.Report(percent);
+ }
+ }
+
+ /// <summary>
+ /// Get the children of this folder from the actual file system
+ /// </summary>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
+ {
+ var collectionType = LibraryManager.GetContentType(this);
+ var libraryOptions = LibraryManager.GetLibraryOptions(this);
+
+ return LibraryManager.ResolvePaths(GetFileSystemChildren(directoryService), directoryService, this, libraryOptions, collectionType);
+ }
+
+ /// <summary>
+ /// Get our children from the repo - stubbed for now
+ /// </summary>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ protected List<BaseItem> GetCachedChildren()
+ {
+ return ItemRepository.GetItemList(new InternalItemsQuery
+ {
+ Parent = this,
+ GroupByPresentationUniqueKey = false,
+ DtoOptions = new DtoOptions(true)
+ });
+ }
+
+ public virtual int GetChildCount(User user)
+ {
+ if (LinkedChildren.Length > 0)
+ {
+ if (!(this is ICollectionFolder))
+ {
+ return GetChildren(user, true).Count;
+ }
+ }
+
+ var result = GetItems(new InternalItemsQuery(user)
+ {
+ Recursive = false,
+ Limit = 0,
+ Parent = this,
+ DtoOptions = new DtoOptions(false)
+ {
+ EnableImages = false
+ }
+
+ });
+
+ return result.TotalRecordCount;
+ }
+
+ public virtual int GetRecursiveChildCount(User user)
+ {
+ return GetItems(new InternalItemsQuery(user)
+ {
+ Recursive = true,
+ IsFolder = false,
+ IsVirtualItem = false,
+ EnableTotalRecordCount = true,
+ Limit = 0,
+ DtoOptions = new DtoOptions(false)
+ {
+ EnableImages = false
+ }
+
+ }).TotalRecordCount;
+ }
+
+ public QueryResult<BaseItem> QueryRecursive(InternalItemsQuery query)
+ {
+ var user = query.User;
+
+ if (!query.ForceDirect && RequiresPostFiltering(query))
+ {
+ IEnumerable<BaseItem> items;
+ Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
+
+ if (query.User == null)
+ {
+ items = GetRecursiveChildren(filter);
+ }
+ else
+ {
+ items = GetRecursiveChildren(user, query);
+ }
+
+ return PostFilterAndSort(items, query, true);
+ }
+
+ if (!(this is UserRootFolder) && !(this is AggregateFolder))
+ {
+ if (!query.ParentId.Equals(Guid.Empty))
+ {
+ query.Parent = this;
+ }
+ }
+
+ if (RequiresPostFiltering2(query))
+ {
+ return QueryWithPostFiltering2(query);
+ }
+
+ return LibraryManager.GetItemsResult(query);
+ }
+
+ private QueryResult<BaseItem> QueryWithPostFiltering2(InternalItemsQuery query)
+ {
+ var startIndex = query.StartIndex;
+ var limit = query.Limit;
+
+ query.StartIndex = null;
+ query.Limit = null;
+
+ var itemsList = LibraryManager.GetItemList(query);
+ var user = query.User;
+
+ if (user != null)
+ {
+ // needed for boxsets
+ itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User)).ToList();
+ }
+
+ BaseItem[] returnItems;
+ int totalCount = 0;
+
+ if (query.EnableTotalRecordCount)
+ {
+ var itemsArray = itemsList.ToArray();
+ totalCount = itemsArray.Length;
+ returnItems = itemsArray;
+ }
+ else
+ {
+ returnItems = itemsList.ToArray();
+ }
+
+ if (limit.HasValue)
+ {
+ returnItems = returnItems.Skip(startIndex ?? 0).Take(limit.Value).ToArray();
+ }
+ else if (startIndex.HasValue)
+ {
+ returnItems = returnItems.Skip(startIndex.Value).ToArray();
+ }
+
+ return new QueryResult<BaseItem>
+ {
+ TotalRecordCount = totalCount,
+ Items = returnItems.ToArray()
+ };
+ }
+
+ private bool RequiresPostFiltering2(InternalItemsQuery query)
+ {
+ if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(BoxSet).Name, StringComparison.OrdinalIgnoreCase))
+ {
+ Logger.Debug("Query requires post-filtering due to BoxSet query");
+ return true;
+ }
+
+ return false;
+ }
+
+ private bool RequiresPostFiltering(InternalItemsQuery query)
+ {
+ if (LinkedChildren.Length > 0)
+ {
+ if (!(this is ICollectionFolder))
+ {
+ Logger.Debug("Query requires post-filtering due to LinkedChildren. Type: " + GetType().Name);
+ return true;
+ }
+ }
+
+ // Filter by Video3DFormat
+ if (query.Is3D.HasValue)
+ {
+ Logger.Debug("Query requires post-filtering due to Is3D");
+ return true;
+ }
+
+ if (query.HasOfficialRating.HasValue)
+ {
+ Logger.Debug("Query requires post-filtering due to HasOfficialRating");
+ return true;
+ }
+
+ if (query.IsPlaceHolder.HasValue)
+ {
+ Logger.Debug("Query requires post-filtering due to IsPlaceHolder");
+ return true;
+ }
+
+ if (query.HasSpecialFeature.HasValue)
+ {
+ Logger.Debug("Query requires post-filtering due to HasSpecialFeature");
+ return true;
+ }
+
+ if (query.HasSubtitles.HasValue)
+ {
+ Logger.Debug("Query requires post-filtering due to HasSubtitles");
+ return true;
+ }
+
+ if (query.HasTrailer.HasValue)
+ {
+ Logger.Debug("Query requires post-filtering due to HasTrailer");
+ return true;
+ }
+
+ // Filter by VideoType
+ if (query.VideoTypes.Length > 0)
+ {
+ Logger.Debug("Query requires post-filtering due to VideoTypes");
+ return true;
+ }
+
+ if (CollapseBoxSetItems(query, this, query.User, ConfigurationManager))
+ {
+ Logger.Debug("Query requires post-filtering due to CollapseBoxSetItems");
+ return true;
+ }
+
+ if (!string.IsNullOrEmpty(query.AdjacentTo))
+ {
+ Logger.Debug("Query requires post-filtering due to AdjacentTo");
+ return true;
+ }
+
+ if (query.SeriesStatuses.Length > 0)
+ {
+ Logger.Debug("Query requires post-filtering due to SeriesStatuses");
+ return true;
+ }
+
+ if (query.AiredDuringSeason.HasValue)
+ {
+ Logger.Debug("Query requires post-filtering due to AiredDuringSeason");
+ return true;
+ }
+
+ if (query.IsPlayed.HasValue)
+ {
+ if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(typeof(Series).Name))
+ {
+ Logger.Debug("Query requires post-filtering due to IsPlayed");
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
+ {
+ if (query.ItemIds.Length > 0)
+ {
+ var result = LibraryManager.GetItemsResult(query);
+
+ if (query.OrderBy.Length == 0)
+ {
+ var ids = query.ItemIds.ToList();
+
+ // Try to preserve order
+ result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id)).ToArray();
+ }
+ return result;
+ }
+
+ return GetItemsInternal(query);
+ }
+
+ public BaseItem[] GetItemList(InternalItemsQuery query)
+ {
+ query.EnableTotalRecordCount = false;
+
+ if (query.ItemIds.Length > 0)
+ {
+ var result = LibraryManager.GetItemList(query);
+
+ if (query.OrderBy.Length == 0)
+ {
+ var ids = query.ItemIds.ToList();
+
+ // Try to preserve order
+ return result.OrderBy(i => ids.IndexOf(i.Id)).ToArray();
+ }
+ return result.ToArray(result.Count);
+ }
+
+ return GetItemsInternal(query).Items;
+ }
+
+ protected virtual QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ try
+ {
+ query.Parent = this;
+ query.ChannelIds = new Guid[] { ChannelId };
+
+ // Don't blow up here because it could cause parent screens with other content to fail
+ return ChannelManager.GetChannelItemsInternal(query, new SimpleProgress<double>(), CancellationToken.None).Result;
+ }
+ catch
+ {
+ // Already logged at lower levels
+ return new QueryResult<BaseItem>();
+ }
+ }
+
+ if (query.Recursive)
+ {
+ return QueryRecursive(query);
+ }
+
+ var user = query.User;
+
+ Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
+
+ IEnumerable<BaseItem> items;
+
+ if (query.User == null)
+ {
+ items = Children.Where(filter);
+ }
+ else
+ {
+ items = GetChildren(user, true).Where(filter);
+ }
+
+ return PostFilterAndSort(items, query, true);
+ }
+
+ public static ICollectionManager CollectionManager { get; set; }
+
+ protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool enableSorting)
+ {
+ var user = query.User;
+
+ // Check recursive - don't substitute in plain folder views
+ if (user != null)
+ {
+ items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager);
+ }
+
+ if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater))
+ {
+ items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.CurrentCultureIgnoreCase) < 1);
+ }
+ if (!string.IsNullOrEmpty(query.NameStartsWith))
+ {
+ items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.OrdinalIgnoreCase));
+ }
+
+ if (!string.IsNullOrEmpty(query.NameLessThan))
+ {
+ items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.CurrentCultureIgnoreCase) == 1);
+ }
+
+ // This must be the last filter
+ if (!string.IsNullOrEmpty(query.AdjacentTo))
+ {
+ items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo);
+ }
+
+ return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting);
+ }
+
+ private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(IEnumerable<BaseItem> items,
+ InternalItemsQuery query,
+ BaseItem queryParent,
+ User user,
+ IServerConfigurationManager configurationManager, ICollectionManager collectionManager)
+ {
+ if (items == null)
+ {
+ throw new ArgumentNullException("items");
+ }
+
+ if (CollapseBoxSetItems(query, queryParent, user, configurationManager))
+ {
+ items = collectionManager.CollapseItemsWithinBoxSets(items, user);
+ }
+
+ return items;
+ }
+
+ private static bool CollapseBoxSetItems(InternalItemsQuery query,
+ BaseItem queryParent,
+ User user,
+ IServerConfigurationManager configurationManager)
+ {
+ // Could end up stuck in a loop like this
+ if (queryParent is BoxSet)
+ {
+ return false;
+ }
+ if (queryParent is Series)
+ {
+ return false;
+ }
+ if (queryParent is Season)
+ {
+ return false;
+ }
+ if (queryParent is MusicAlbum)
+ {
+ return false;
+ }
+ if (queryParent is MusicArtist)
+ {
+ return false;
+ }
+
+ var param = query.CollapseBoxSetItems;
+
+ if (!param.HasValue)
+ {
+ if (user != null && !configurationManager.Configuration.EnableGroupingIntoCollections)
+ {
+ return false;
+ }
+
+ if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains("Movie", StringComparer.OrdinalIgnoreCase))
+ {
+ param = true;
+ }
+ }
+
+ return param.HasValue && param.Value && AllowBoxSetCollapsing(query);
+ }
+
+ private static bool AllowBoxSetCollapsing(InternalItemsQuery request)
+ {
+ if (request.IsFavorite.HasValue)
+ {
+ return false;
+ }
+ if (request.IsFavoriteOrLiked.HasValue)
+ {
+ return false;
+ }
+ if (request.IsLiked.HasValue)
+ {
+ return false;
+ }
+ if (request.IsPlayed.HasValue)
+ {
+ return false;
+ }
+ if (request.IsResumable.HasValue)
+ {
+ return false;
+ }
+ if (request.IsFolder.HasValue)
+ {
+ return false;
+ }
+
+ if (request.Genres.Length > 0)
+ {
+ return false;
+ }
+
+ if (request.GenreIds.Length > 0)
+ {
+ return false;
+ }
+
+ if (request.HasImdbId.HasValue)
+ {
+ return false;
+ }
+
+ if (request.HasOfficialRating.HasValue)
+ {
+ return false;
+ }
+
+ if (request.HasOverview.HasValue)
+ {
+ return false;
+ }
+
+ if (request.HasParentalRating.HasValue)
+ {
+ return false;
+ }
+
+ if (request.HasSpecialFeature.HasValue)
+ {
+ return false;
+ }
+
+ if (request.HasSubtitles.HasValue)
+ {
+ return false;
+ }
+
+ if (request.HasThemeSong.HasValue)
+ {
+ return false;
+ }
+
+ if (request.HasThemeVideo.HasValue)
+ {
+ return false;
+ }
+
+ if (request.HasTmdbId.HasValue)
+ {
+ return false;
+ }
+
+ if (request.HasTrailer.HasValue)
+ {
+ return false;
+ }
+
+ if (request.ImageTypes.Length > 0)
+ {
+ return false;
+ }
+
+ if (request.Is3D.HasValue)
+ {
+ return false;
+ }
+
+ if (request.IsHD.HasValue)
+ {
+ return false;
+ }
+
+ if (request.IsLocked.HasValue)
+ {
+ return false;
+ }
+
+ if (request.IsPlaceHolder.HasValue)
+ {
+ return false;
+ }
+
+ if (request.IsPlayed.HasValue)
+ {
+ return false;
+ }
+
+ if (!string.IsNullOrWhiteSpace(request.Person))
+ {
+ return false;
+ }
+
+ if (request.PersonIds.Length > 0)
+ {
+ return false;
+ }
+
+ if (request.ItemIds.Length > 0)
+ {
+ return false;
+ }
+
+ if (request.StudioIds.Length > 0)
+ {
+ return false;
+ }
+
+ if (request.GenreIds.Length > 0)
+ {
+ return false;
+ }
+
+ if (request.VideoTypes.Length > 0)
+ {
+ return false;
+ }
+
+ if (request.Years.Length > 0)
+ {
+ return false;
+ }
+
+ if (request.Tags.Length > 0)
+ {
+ return false;
+ }
+
+ if (request.OfficialRatings.Length > 0)
+ {
+ return false;
+ }
+
+ if (request.MinPlayers.HasValue)
+ {
+ return false;
+ }
+
+ if (request.MaxPlayers.HasValue)
+ {
+ return false;
+ }
+
+ if (request.MinCommunityRating.HasValue)
+ {
+ return false;
+ }
+
+ if (request.MinCriticRating.HasValue)
+ {
+ return false;
+ }
+
+ if (request.MinIndexNumber.HasValue)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ public List<BaseItem> GetChildren(User user, bool includeLinkedChildren)
+ {
+ return GetChildren(user, includeLinkedChildren, null);
+ }
+
+ public virtual List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ //the true root should return our users root folder children
+ if (IsPhysicalRoot) return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren);
+
+ var result = new Dictionary<Guid, BaseItem>();
+
+ AddChildren(user, includeLinkedChildren, result, false, query);
+
+ return result.Values.ToList();
+ }
+
+ protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
+ {
+ return Children;
+ }
+
+ /// <summary>
+ /// Adds the children to list.
+ /// </summary>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query)
+ {
+ foreach (var child in GetEligibleChildrenForRecursiveChildren(user))
+ {
+ bool? isVisibleToUser = null;
+
+ if (query == null || UserViewBuilder.FilterItem(child, query))
+ {
+ isVisibleToUser = child.IsVisible(user);
+
+ if (isVisibleToUser.Value)
+ {
+ result[child.Id] = child;
+ }
+ }
+
+ if (isVisibleToUser ?? child.IsVisible(user))
+ {
+ if (recursive && child.IsFolder)
+ {
+ var folder = (Folder)child;
+
+ folder.AddChildren(user, includeLinkedChildren, result, true, query);
+ }
+ }
+ }
+
+ if (includeLinkedChildren)
+ {
+ foreach (var child in GetLinkedChildren(user))
+ {
+ if (query == null || UserViewBuilder.FilterItem(child, query))
+ {
+ if (child.IsVisible(user))
+ {
+ result[child.Id] = child;
+ }
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets allowed recursive children of an item
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public IEnumerable<BaseItem> GetRecursiveChildren(User user, bool includeLinkedChildren = true)
+ {
+ return GetRecursiveChildren(user, null);
+ }
+
+ public virtual IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
+ {
+ if (user == null)
+ {
+ throw new ArgumentNullException("user");
+ }
+
+ var result = new Dictionary<Guid, BaseItem>();
+
+ AddChildren(user, true, result, true, query);
+
+ return result.Values;
+ }
+
+ /// <summary>
+ /// Gets the recursive children.
+ /// </summary>
+ /// <returns>IList{BaseItem}.</returns>
+ public IList<BaseItem> GetRecursiveChildren()
+ {
+ return GetRecursiveChildren(true);
+ }
+
+ public IList<BaseItem> GetRecursiveChildren(bool includeLinkedChildren)
+ {
+ return GetRecursiveChildren(i => true, includeLinkedChildren);
+ }
+
+ public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter)
+ {
+ return GetRecursiveChildren(filter, true);
+ }
+
+ public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter, bool includeLinkedChildren)
+ {
+ var result = new Dictionary<Guid, BaseItem>();
+
+ AddChildrenToList(result, includeLinkedChildren, true, filter);
+
+ return result.Values.ToList();
+ }
+
+ /// <summary>
+ /// Adds the children to list.
+ /// </summary>
+ private void AddChildrenToList(Dictionary<Guid, BaseItem> result, bool includeLinkedChildren, bool recursive, Func<BaseItem, bool> filter)
+ {
+ foreach (var child in Children)
+ {
+ if (filter == null || filter(child))
+ {
+ result[child.Id] = child;
+ }
+
+ if (recursive && child.IsFolder)
+ {
+ var folder = (Folder)child;
+
+ // We can only support includeLinkedChildren for the first folder, or we might end up stuck in a loop of linked items
+ folder.AddChildrenToList(result, false, true, filter);
+ }
+ }
+
+ if (includeLinkedChildren)
+ {
+ foreach (var child in GetLinkedChildren())
+ {
+ if (filter == null || filter(child))
+ {
+ result[child.Id] = child;
+ }
+ }
+ }
+ }
+
+
+ /// <summary>
+ /// Gets the linked children.
+ /// </summary>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ public List<BaseItem> GetLinkedChildren()
+ {
+ var linkedChildren = LinkedChildren;
+ var list = new List<BaseItem>(linkedChildren.Length);
+
+ foreach (var i in linkedChildren)
+ {
+ var child = GetLinkedChild(i);
+
+ if (child != null)
+ {
+ list.Add(child);
+ }
+ }
+ return list;
+ }
+
+ protected virtual bool FilterLinkedChildrenPerUser
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public bool ContainsLinkedChildByItemId(Guid itemId)
+ {
+ var linkedChildren = LinkedChildren;
+ foreach (var i in linkedChildren)
+ {
+ if (i.ItemId.HasValue && i.ItemId.Value == itemId)
+ {
+ return true;
+ }
+
+ var child = GetLinkedChild(i);
+
+ if (child != null && child.Id == itemId)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public List<BaseItem> GetLinkedChildren(User user)
+ {
+ if (!FilterLinkedChildrenPerUser || user == null)
+ {
+ return GetLinkedChildren();
+ }
+
+ var linkedChildren = LinkedChildren;
+ var list = new List<BaseItem>(linkedChildren.Length);
+
+ if (linkedChildren.Length == 0)
+ {
+ return list;
+ }
+
+ var allUserRootChildren = LibraryManager.GetUserRootFolder()
+ .GetChildren(user, true)
+ .OfType<Folder>()
+ .ToList();
+
+ var collectionFolderIds = allUserRootChildren
+ .Select(i => i.Id)
+ .ToList();
+
+ foreach (var i in linkedChildren)
+ {
+ var child = GetLinkedChild(i);
+
+ if (child == null)
+ {
+ continue;
+ }
+
+ var childOwner = child.GetOwner() ?? child;
+
+ if (childOwner != null && !(child is IItemByName))
+ {
+ var childProtocol = childOwner.PathProtocol;
+ if (!childProtocol.HasValue || childProtocol.Value != Model.MediaInfo.MediaProtocol.File)
+ {
+ if (!childOwner.IsVisibleStandalone(user))
+ {
+ continue;
+ }
+ }
+ else
+ {
+ var itemCollectionFolderIds =
+ LibraryManager.GetCollectionFolders(childOwner, allUserRootChildren).Select(f => f.Id);
+
+ if (!itemCollectionFolderIds.Any(collectionFolderIds.Contains))
+ {
+ continue;
+ }
+ }
+ }
+
+ list.Add(child);
+ }
+
+ return list;
+ }
+
+ /// <summary>
+ /// Gets the linked children.
+ /// </summary>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ public IEnumerable<Tuple<LinkedChild, BaseItem>> GetLinkedChildrenInfos()
+ {
+ return LinkedChildren
+ .Select(i => new Tuple<LinkedChild, BaseItem>(i, GetLinkedChild(i)))
+ .Where(i => i.Item2 != null);
+ }
+
+ [IgnoreDataMember]
+ protected override bool SupportsOwnedItems
+ {
+ get
+ {
+ return base.SupportsOwnedItems || SupportsShortcutChildren;
+ }
+ }
+
+ protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
+ {
+ var changesFound = false;
+
+ if (IsFileProtocol)
+ {
+ if (RefreshLinkedChildren(fileSystemChildren))
+ {
+ changesFound = true;
+ }
+ }
+
+ var baseHasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
+
+ return baseHasChanges || changesFound;
+ }
+
+ /// <summary>
+ /// Refreshes the linked children.
+ /// </summary>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected virtual bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren)
+ {
+ if (SupportsShortcutChildren)
+ {
+ var newShortcutLinks = fileSystemChildren
+ .Where(i => !i.IsDirectory && FileSystem.IsShortcut(i.FullName))
+ .Select(i =>
+ {
+ try
+ {
+ Logger.Debug("Found shortcut at {0}", i.FullName);
+
+ var resolvedPath = CollectionFolder.ApplicationHost.ExpandVirtualPath(FileSystem.ResolveShortcut(i.FullName));
+
+ if (!string.IsNullOrEmpty(resolvedPath))
+ {
+ return new LinkedChild
+ {
+ Path = resolvedPath,
+ Type = LinkedChildType.Shortcut
+ };
+ }
+
+ Logger.Error("Error resolving shortcut {0}", i.FullName);
+
+ return null;
+ }
+ catch (IOException ex)
+ {
+ Logger.ErrorException("Error resolving shortcut {0}", ex, i.FullName);
+ return null;
+ }
+ })
+ .Where(i => i != null)
+ .ToList();
+
+ var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList();
+
+ if (!newShortcutLinks.SequenceEqual(currentShortcutLinks, new LinkedChildComparer(FileSystem)))
+ {
+ Logger.Info("Shortcut links have changed for {0}", Path);
+
+ newShortcutLinks.AddRange(LinkedChildren.Where(i => i.Type == LinkedChildType.Manual));
+ LinkedChildren = newShortcutLinks.ToArray(newShortcutLinks.Count);
+ return true;
+ }
+ }
+
+ foreach (var child in LinkedChildren)
+ {
+ // Reset the cached value
+ child.ItemId = null;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Marks the played.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="datePlayed">The date played.</param>
+ /// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
+ /// <returns>Task.</returns>
+ public override void MarkPlayed(User user,
+ DateTime? datePlayed,
+ bool resetPosition)
+ {
+ var query = new InternalItemsQuery
+ {
+ User = user,
+ Recursive = true,
+ IsFolder = false,
+ EnableTotalRecordCount = false
+ };
+
+ if (!user.Configuration.DisplayMissingEpisodes)
+ {
+ query.IsVirtualItem = false;
+ }
+
+ var itemsResult = GetItemList(query);
+
+ // Sweep through recursively and update status
+ foreach (var item in itemsResult)
+ {
+ if (item.IsVirtualItem)
+ {
+ // The querying doesn't support virtual unaired
+ var episode = item as Episode;
+ if (episode != null && episode.IsUnaired)
+ {
+ continue;
+ }
+ }
+
+ item.MarkPlayed(user, datePlayed, resetPosition);
+ }
+ }
+
+ /// <summary>
+ /// Marks the unplayed.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>Task.</returns>
+ public override void MarkUnplayed(User user)
+ {
+ var itemsResult = GetItemList(new InternalItemsQuery
+ {
+ User = user,
+ Recursive = true,
+ IsFolder = false,
+ EnableTotalRecordCount = false
+
+ });
+
+ // Sweep through recursively and update status
+ foreach (var item in itemsResult)
+ {
+ item.MarkUnplayed(user);
+ }
+ }
+
+ public override bool IsPlayed(User user)
+ {
+ var itemsResult = GetItemList(new InternalItemsQuery(user)
+ {
+ Recursive = true,
+ IsFolder = false,
+ IsVirtualItem = false,
+ EnableTotalRecordCount = false
+
+ });
+
+ return itemsResult
+ .All(i => i.IsPlayed(user));
+ }
+
+ public override bool IsUnplayed(User user)
+ {
+ return !IsPlayed(user);
+ }
+
+ [IgnoreDataMember]
+ public virtual bool SupportsUserDataFromChildren
+ {
+ get
+ {
+ // These are just far too slow.
+ if (this is ICollectionFolder)
+ {
+ return false;
+ }
+ if (this is UserView)
+ {
+ return false;
+ }
+ if (this is UserRootFolder)
+ {
+ return false;
+ }
+ if (this is Channel)
+ {
+ return false;
+ }
+ if (SourceType != SourceType.Library)
+ {
+ return false;
+ }
+ var iItemByName = this as IItemByName;
+ if (iItemByName != null)
+ {
+ var hasDualAccess = this as IHasDualAccess;
+ if (hasDualAccess == null || hasDualAccess.IsAccessedByName)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields)
+ {
+ if (!SupportsUserDataFromChildren)
+ {
+ return;
+ }
+
+ if (itemDto != null)
+ {
+ if (fields.ContainsField(ItemFields.RecursiveItemCount))
+ {
+ itemDto.RecursiveItemCount = GetRecursiveChildCount(user);
+ }
+ }
+
+ if (SupportsPlayedStatus)
+ {
+ var unplayedQueryResult = GetItems(new InternalItemsQuery(user)
+ {
+ Recursive = true,
+ IsFolder = false,
+ IsVirtualItem = false,
+ EnableTotalRecordCount = true,
+ Limit = 0,
+ IsPlayed = false,
+ DtoOptions = new DtoOptions(false)
+ {
+ EnableImages = false
+ }
+
+ });
+
+ double unplayedCount = unplayedQueryResult.TotalRecordCount;
+
+ dto.UnplayedItemCount = unplayedQueryResult.TotalRecordCount;
+
+ if (itemDto != null && itemDto.RecursiveItemCount.HasValue)
+ {
+ if (itemDto.RecursiveItemCount.Value > 0)
+ {
+ var unplayedPercentage = (unplayedCount / itemDto.RecursiveItemCount.Value) * 100;
+ dto.PlayedPercentage = 100 - unplayedPercentage;
+ dto.Played = dto.PlayedPercentage.Value >= 100;
+ }
+ }
+ else
+ {
+ dto.Played = (dto.UnplayedItemCount ?? 0) == 0;
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Game.cs b/MediaBrowser.Controller/Entities/Game.cs
new file mode 100644
index 000000000..e4c417c8a
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Game.cs
@@ -0,0 +1,129 @@
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Serialization;
+using System;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class Game : BaseItem, IHasTrailers, IHasScreenshots, ISupportsPlaceHolders, IHasLookupInfo<GameInfo>
+ {
+ public Game()
+ {
+ MultiPartGameFiles = new string[] {};
+ RemoteTrailers = EmptyMediaUrlArray;
+ LocalTrailerIds = new Guid[] {};
+ RemoteTrailerIds = new Guid[] {};
+ }
+
+ public Guid[] LocalTrailerIds { get; set; }
+ public Guid[] RemoteTrailerIds { get; set; }
+
+ public override bool CanDownload()
+ {
+ return IsFileProtocol;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsThemeMedia
+ {
+ get { return true; }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get { return false; }
+ }
+
+ /// <summary>
+ /// Gets or sets the remote trailers.
+ /// </summary>
+ /// <value>The remote trailers.</value>
+ public MediaUrl[] RemoteTrailers { get; set; }
+
+ /// <summary>
+ /// Gets the type of the media.
+ /// </summary>
+ /// <value>The type of the media.</value>
+ [IgnoreDataMember]
+ public override string MediaType
+ {
+ get { return Model.Entities.MediaType.Game; }
+ }
+
+ /// <summary>
+ /// Gets or sets the players supported.
+ /// </summary>
+ /// <value>The players supported.</value>
+ public int? PlayersSupported { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is place holder.
+ /// </summary>
+ /// <value><c>true</c> if this instance is place holder; otherwise, <c>false</c>.</value>
+ public bool IsPlaceHolder { get; set; }
+
+ /// <summary>
+ /// Gets or sets the game system.
+ /// </summary>
+ /// <value>The game system.</value>
+ public string GameSystem { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is multi part.
+ /// </summary>
+ /// <value><c>true</c> if this instance is multi part; otherwise, <c>false</c>.</value>
+ public bool IsMultiPart { get; set; }
+
+ /// <summary>
+ /// Holds the paths to the game files in the event this is a multipart game
+ /// </summary>
+ public string[] MultiPartGameFiles { get; set; }
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+ var id = this.GetProviderId(MetadataProviders.Gamesdb);
+
+ if (!string.IsNullOrEmpty(id))
+ {
+ list.Insert(0, "Game-Gamesdb-" + id);
+ }
+ return list;
+ }
+
+ public override IEnumerable<FileSystemMetadata> GetDeletePaths()
+ {
+ if (!IsInMixedFolder)
+ {
+ return new[] {
+ new FileSystemMetadata
+ {
+ FullName = FileSystem.GetDirectoryName(Path),
+ IsDirectory = true
+ }
+ };
+ }
+
+ return base.GetDeletePaths();
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ return UnratedItem.Game;
+ }
+
+ public GameInfo GetLookupInfo()
+ {
+ var id = GetItemLookupInfo<GameInfo>();
+
+ id.GameSystem = GameSystem;
+
+ return id;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/GameGenre.cs b/MediaBrowser.Controller/Entities/GameGenre.cs
new file mode 100644
index 000000000..63493ad4a
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/GameGenre.cs
@@ -0,0 +1,128 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class GameGenre : BaseItem, IItemByName
+ {
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
+ return list;
+ }
+
+ public override string CreatePresentationUniqueKey()
+ {
+ return GetUserDataKeys()[0];
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ return 1;
+ }
+
+ /// <summary>
+ /// Returns the folder containing the item.
+ /// If the item is a folder, it returns the folder itself
+ /// </summary>
+ /// <value>The containing folder path.</value>
+ [IgnoreDataMember]
+ public override string ContainingFolderPath
+ {
+ get
+ {
+ return Path;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsAncestors
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public override bool IsSaveLocalMetadataEnabled()
+ {
+ return true;
+ }
+
+ public override bool CanDelete()
+ {
+ return false;
+ }
+
+ public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+ {
+ query.GenreIds = new[] { Id };
+ query.IncludeItemTypes = new[] { typeof(Game).Name };
+
+ return LibraryManager.GetItemList(query);
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public static string GetPath(string name)
+ {
+ return GetPath(name, true);
+ }
+
+ public static string GetPath(string name, bool normalizeName)
+ {
+ // Trim the period at the end because windows will have a hard time with that
+ var validName = normalizeName ?
+ FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+ name;
+
+ return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.GameGenrePath, validName);
+ }
+
+ private string GetRebasedPath()
+ {
+ return GetPath(System.IO.Path.GetFileName(Path), false);
+ }
+
+ public override bool RequiresRefresh()
+ {
+ var newPath = GetRebasedPath();
+ if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+ {
+ Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+ return true;
+ }
+ return base.RequiresRefresh();
+ }
+
+ /// <summary>
+ /// This is called before any metadata refresh and returns true or false indicating if changes were made
+ /// </summary>
+ public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ {
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+
+ var newPath = GetRebasedPath();
+ if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+ {
+ Path = newPath;
+ hasChanges = true;
+ }
+
+ return hasChanges;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/GameSystem.cs b/MediaBrowser.Controller/Entities/GameSystem.cs
new file mode 100644
index 000000000..fb60ce83a
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/GameSystem.cs
@@ -0,0 +1,101 @@
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Users;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class GameSystem
+ /// </summary>
+ public class GameSystem : Folder, IHasLookupInfo<GameSystemInfo>
+ {
+ /// <summary>
+ /// Return the id that should be used to key display prefs for this item.
+ /// Default is based on the type for everything except actual generic folders.
+ /// </summary>
+ /// <value>The display prefs id.</value>
+ [IgnoreDataMember]
+ public override Guid DisplayPreferencesId
+ {
+ get
+ {
+ return Id;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPlayedStatus
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsInheritedParentImages
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ double value = 16;
+ value /= 9;
+
+ return value;
+ }
+
+ /// <summary>
+ /// Gets or sets the game system.
+ /// </summary>
+ /// <value>The game system.</value>
+ public string GameSystemName { get; set; }
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ if (!string.IsNullOrEmpty(GameSystemName))
+ {
+ list.Insert(0, "GameSystem-" + GameSystemName);
+ }
+ return list;
+ }
+
+ protected override bool GetBlockUnratedValue(UserPolicy config)
+ {
+ // Don't block. Determine by game
+ return false;
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ return UnratedItem.Game;
+ }
+
+ public GameSystemInfo GetLookupInfo()
+ {
+ var id = GetItemLookupInfo<GameSystemInfo>();
+
+ id.Path = Path;
+
+ return id;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get
+ {
+ return false;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs
new file mode 100644
index 000000000..94a5984df
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Genre.cs
@@ -0,0 +1,140 @@
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Controller.Entities.Audio;
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class Genre
+ /// </summary>
+ public class Genre : BaseItem, IItemByName
+ {
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
+ return list;
+ }
+ public override string CreatePresentationUniqueKey()
+ {
+ return GetUserDataKeys()[0];
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ return 1;
+ }
+
+ /// <summary>
+ /// Returns the folder containing the item.
+ /// If the item is a folder, it returns the folder itself
+ /// </summary>
+ /// <value>The containing folder path.</value>
+ [IgnoreDataMember]
+ public override string ContainingFolderPath
+ {
+ get
+ {
+ return Path;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool IsDisplayedAsFolder
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsAncestors
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public override bool IsSaveLocalMetadataEnabled()
+ {
+ return true;
+ }
+
+ public override bool CanDelete()
+ {
+ return false;
+ }
+
+ public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+ {
+ query.GenreIds = new[] { Id };
+ query.ExcludeItemTypes = new[] { typeof(Game).Name, typeof(MusicVideo).Name, typeof(Audio.Audio).Name, typeof(MusicAlbum).Name, typeof(MusicArtist).Name };
+
+ return LibraryManager.GetItemList(query);
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public static string GetPath(string name)
+ {
+ return GetPath(name, true);
+ }
+
+ public static string GetPath(string name, bool normalizeName)
+ {
+ // Trim the period at the end because windows will have a hard time with that
+ var validName = normalizeName ?
+ FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+ name;
+
+ return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.GenrePath, validName);
+ }
+
+ private string GetRebasedPath()
+ {
+ return GetPath(System.IO.Path.GetFileName(Path), false);
+ }
+
+ public override bool RequiresRefresh()
+ {
+ var newPath = GetRebasedPath();
+ if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+ {
+ Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+ return true;
+ }
+ return base.RequiresRefresh();
+ }
+
+ /// <summary>
+ /// This is called before any metadata refresh and returns true or false indicating if changes were made
+ /// </summary>
+ public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ {
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+
+ var newPath = GetRebasedPath();
+ if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+ {
+ Path = newPath;
+ hasChanges = true;
+ }
+
+ return hasChanges;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/ICollectionFolder.cs b/MediaBrowser.Controller/Entities/ICollectionFolder.cs
new file mode 100644
index 000000000..b61e7b339
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/ICollectionFolder.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// This is just a marker interface to denote top level folders
+ /// </summary>
+ public interface ICollectionFolder : IHasCollectionType
+ {
+ string Path { get; }
+ string Name { get; }
+ Guid Id { get; }
+ string[] PhysicalLocations { get; }
+ }
+
+ public interface ISupportsUserSpecificView
+ {
+ bool EnableUserSpecificView { get; }
+ }
+
+ public interface IHasCollectionType
+ {
+ string CollectionType { get; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/IHasAspectRatio.cs b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs
new file mode 100644
index 000000000..5aecf4eac
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/IHasAspectRatio.cs
@@ -0,0 +1,14 @@
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Interface IHasAspectRatio
+ /// </summary>
+ public interface IHasAspectRatio
+ {
+ /// <summary>
+ /// Gets or sets the aspect ratio.
+ /// </summary>
+ /// <value>The aspect ratio.</value>
+ string AspectRatio { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs
new file mode 100644
index 000000000..5e1ae2179
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/IHasDisplayOrder.cs
@@ -0,0 +1,15 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Interface IHasDisplayOrder
+ /// </summary>
+ public interface IHasDisplayOrder
+ {
+ /// <summary>
+ /// Gets or sets the display order.
+ /// </summary>
+ /// <value>The display order.</value>
+ string DisplayOrder { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/IHasMediaSources.cs b/MediaBrowser.Controller/Entities/IHasMediaSources.cs
new file mode 100644
index 000000000..a13c95942
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/IHasMediaSources.cs
@@ -0,0 +1,19 @@
+using MediaBrowser.Model.Dto;
+using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
+using System;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public interface IHasMediaSources
+ {
+ /// <summary>
+ /// Gets the media sources.
+ /// </summary>
+ List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution);
+ List<MediaStream> GetMediaStreams();
+ Guid Id { get; set; }
+ long? RunTimeTicks { get; set; }
+ string Path { get; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs
new file mode 100644
index 000000000..0bc9ff81e
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs
@@ -0,0 +1,17 @@
+using MediaBrowser.Model.LiveTv;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public interface IHasProgramAttributes
+ {
+ bool IsMovie { get; set; }
+ bool IsSports { get; }
+ bool IsNews { get; }
+ bool IsKids { get; }
+ bool IsRepeat { get; set; }
+ bool IsSeries { get; set; }
+ ProgramAudio? Audio { get; set; }
+ string EpisodeTitle { get; set; }
+ string ServiceName { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/IHasScreenshots.cs b/MediaBrowser.Controller/Entities/IHasScreenshots.cs
new file mode 100644
index 000000000..2fd402bc2
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/IHasScreenshots.cs
@@ -0,0 +1,10 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Interface IHasScreenshots
+ /// </summary>
+ public interface IHasScreenshots
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/IHasSeries.cs b/MediaBrowser.Controller/Entities/IHasSeries.cs
new file mode 100644
index 000000000..18d66452a
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/IHasSeries.cs
@@ -0,0 +1,20 @@
+
+using System;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public interface IHasSeries
+ {
+ /// <summary>
+ /// Gets the name of the series.
+ /// </summary>
+ /// <value>The name of the series.</value>
+ string SeriesName { get; set; }
+ string FindSeriesName();
+ string FindSeriesSortName();
+ Guid SeriesId { get; set; }
+ Guid FindSeriesId();
+ string SeriesPresentationUniqueKey { get; set; }
+ string FindSeriesPresentationUniqueKey();
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs
new file mode 100644
index 000000000..f4905b7dc
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public interface IHasSpecialFeatures
+ {
+ /// <summary>
+ /// Gets or sets the special feature ids.
+ /// </summary>
+ /// <value>The special feature ids.</value>
+ Guid[] SpecialFeatureIds { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/IHasStartDate.cs b/MediaBrowser.Controller/Entities/IHasStartDate.cs
new file mode 100644
index 000000000..a6714fb96
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/IHasStartDate.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public interface IHasStartDate
+ {
+ DateTime StartDate { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/IHasTrailers.cs b/MediaBrowser.Controller/Entities/IHasTrailers.cs
new file mode 100644
index 000000000..8e7c4e007
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/IHasTrailers.cs
@@ -0,0 +1,39 @@
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public interface IHasTrailers : IHasProviderIds
+ {
+ /// <summary>
+ /// Gets or sets the remote trailers.
+ /// </summary>
+ /// <value>The remote trailers.</value>
+ MediaUrl[] RemoteTrailers { get; set; }
+
+ /// <summary>
+ /// Gets or sets the local trailer ids.
+ /// </summary>
+ /// <value>The local trailer ids.</value>
+ Guid[] LocalTrailerIds { get; set; }
+ Guid[] RemoteTrailerIds { get; set; }
+ Guid Id { get; set; }
+ }
+
+ public static class HasTrailerExtensions
+ {
+ /// <summary>
+ /// Gets the trailer ids.
+ /// </summary>
+ /// <returns>List&lt;Guid&gt;.</returns>
+ public static List<Guid> GetTrailerIds(this IHasTrailers item)
+ {
+ var list = item.LocalTrailerIds.ToList();
+ list.AddRange(item.RemoteTrailerIds);
+ return list;
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/IItemByName.cs b/MediaBrowser.Controller/Entities/IItemByName.cs
new file mode 100644
index 000000000..d21c6ae4d
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/IItemByName.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Marker interface
+ /// </summary>
+ public interface IItemByName
+ {
+ IList<BaseItem> GetTaggedItems(InternalItemsQuery query);
+ }
+
+ public interface IHasDualAccess : IItemByName
+ {
+ bool IsAccessedByName { get; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/IMetadataContainer.cs b/MediaBrowser.Controller/Entities/IMetadataContainer.cs
new file mode 100644
index 000000000..33aa08425
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/IMetadataContainer.cs
@@ -0,0 +1,19 @@
+using MediaBrowser.Controller.Providers;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public interface IMetadataContainer
+ {
+ /// <summary>
+ /// Refreshes all metadata.
+ /// </summary>
+ /// <param name="refreshOptions">The refresh options.</param>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/ISupportsBoxSetGrouping.cs b/MediaBrowser.Controller/Entities/ISupportsBoxSetGrouping.cs
new file mode 100644
index 000000000..fbe5a06d0
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/ISupportsBoxSetGrouping.cs
@@ -0,0 +1,12 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Marker interface to denote a class that supports being hidden underneath it's boxset.
+ /// Just about anything can be placed into a boxset,
+ /// but movies should also only appear underneath and not outside separately (subject to configuration).
+ /// </summary>
+ public interface ISupportsBoxSetGrouping
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs b/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs
new file mode 100644
index 000000000..2507c8ee6
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs
@@ -0,0 +1,12 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+ public interface ISupportsPlaceHolders
+ {
+ /// <summary>
+ /// Gets a value indicating whether this instance is place holder.
+ /// </summary>
+ /// <value><c>true</c> if this instance is place holder; otherwise, <c>false</c>.</value>
+ bool IsPlaceHolder { get; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
new file mode 100644
index 000000000..ff57c2471
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -0,0 +1,261 @@
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Configuration;
+using System.Linq;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Model.Querying;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class InternalItemsQuery
+ {
+ public bool Recursive { get; set; }
+
+ public int? StartIndex { get; set; }
+
+ public int? Limit { get; set; }
+
+ public User User { get; set; }
+
+ public BaseItem SimilarTo { get; set; }
+
+ public bool? IsFolder { get; set; }
+ public bool? IsFavorite { get; set; }
+ public bool? IsFavoriteOrLiked { get; set; }
+ public bool? IsLiked { get; set; }
+ public bool? IsPlayed { get; set; }
+ public bool? IsResumable { get; set; }
+ public bool? IncludeItemsByName { get; set; }
+
+ public string[] MediaTypes { get; set; }
+ public string[] IncludeItemTypes { get; set; }
+ public string[] ExcludeItemTypes { get; set; }
+ public string[] ExcludeTags { get; set; }
+ public string[] ExcludeInheritedTags { get; set; }
+ public string[] Genres { get; set; }
+
+ public bool? IsSpecialSeason { get; set; }
+ public bool? IsMissing { get; set; }
+ public bool? IsUnaired { get; set; }
+ public bool? CollapseBoxSetItems { get; set; }
+
+ public string NameStartsWithOrGreater { get; set; }
+ public string NameStartsWith { get; set; }
+ public string NameLessThan { get; set; }
+ public string NameContains { get; set; }
+ public string MinSortName { get; set; }
+
+ public string PresentationUniqueKey { get; set; }
+ public string Path { get; set; }
+ public string PathNotStartsWith { get; set; }
+ public string Name { get; set; }
+
+ public string Person { get; set; }
+ public Guid[] PersonIds { get; set; }
+ public Guid[] ItemIds { get; set; }
+ public Guid[] ExcludeItemIds { get; set; }
+ public string AdjacentTo { get; set; }
+ public string[] PersonTypes { get; set; }
+
+ public bool? Is3D { get; set; }
+ public bool? IsHD { get; set; }
+ public bool? IsLocked { get; set; }
+ public bool? IsPlaceHolder { get; set; }
+
+ public bool? HasImdbId { get; set; }
+ public bool? HasOverview { get; set; }
+ public bool? HasTmdbId { get; set; }
+ public bool? HasOfficialRating { get; set; }
+ public bool? HasTvdbId { get; set; }
+ public bool? HasThemeSong { get; set; }
+ public bool? HasThemeVideo { get; set; }
+ public bool? HasSubtitles { get; set; }
+ public bool? HasSpecialFeature { get; set; }
+ public bool? HasTrailer { get; set; }
+ public bool? HasParentalRating { get; set; }
+
+ public Guid[] StudioIds { get; set; }
+ public Guid[] GenreIds { get; set; }
+ public ImageType[] ImageTypes { get; set; }
+ public VideoType[] VideoTypes { get; set; }
+ public UnratedItem[] BlockUnratedItems { get; set; }
+ public int[] Years { get; set; }
+ public string[] Tags { get; set; }
+ public string[] OfficialRatings { get; set; }
+
+ public DateTime? MinPremiereDate { get; set; }
+ public DateTime? MaxPremiereDate { get; set; }
+ public DateTime? MinStartDate { get; set; }
+ public DateTime? MaxStartDate { get; set; }
+ public DateTime? MinEndDate { get; set; }
+ public DateTime? MaxEndDate { get; set; }
+ public bool? IsAiring { get; set; }
+
+ public bool? IsMovie { get; set; }
+ public bool? IsSports { get; set; }
+ public bool? IsKids { get; set; }
+ public bool? IsNews { get; set; }
+ public bool? IsSeries { get; set; }
+
+ public int? MinPlayers { get; set; }
+ public int? MaxPlayers { get; set; }
+ public int? MinIndexNumber { get; set; }
+ public int? AiredDuringSeason { get; set; }
+ public double? MinCriticRating { get; set; }
+ public double? MinCommunityRating { get; set; }
+
+ public Guid[] ChannelIds { get; set; }
+
+ public int? ParentIndexNumber { get; set; }
+ public int? ParentIndexNumberNotEquals { get; set; }
+ public int? IndexNumber { get; set; }
+ public int? MinParentalRating { get; set; }
+ public int? MaxParentalRating { get; set; }
+
+ public bool? HasDeadParentId { get; set; }
+ public bool? IsVirtualItem { get; set; }
+
+ public Guid ParentId { get; set; }
+ public string ParentType { get; set; }
+ public Guid[] AncestorIds { get; set; }
+ public Guid[] TopParentIds { get; set; }
+
+ public BaseItem Parent
+ {
+ set
+ {
+ if (value == null)
+ {
+ ParentId = Guid.Empty;
+ ParentType = null;
+ }
+ else
+ {
+ ParentId = value.Id;
+ ParentType = value.GetType().Name;
+ }
+ }
+ }
+
+ public string[] PresetViews { get; set; }
+ public TrailerType[] TrailerTypes { get; set; }
+ public SourceType[] SourceTypes { get; set; }
+
+ public SeriesStatus[] SeriesStatuses { get; set; }
+ public string ExternalSeriesId { get; set; }
+ public string ExternalId { get; set; }
+
+ public Guid[] AlbumIds { get; set; }
+ public Guid[] ArtistIds { get; set; }
+ public Guid[] ExcludeArtistIds { get; set; }
+ public string AncestorWithPresentationUniqueKey { get; set; }
+ public string SeriesPresentationUniqueKey { get; set; }
+
+ public bool GroupByPresentationUniqueKey { get; set; }
+ public bool GroupBySeriesPresentationUniqueKey { get; set; }
+ public bool EnableTotalRecordCount { get; set; }
+ public bool ForceDirect { get; set; }
+ public Dictionary<string, string> ExcludeProviderIds { get; set; }
+ public bool EnableGroupByMetadataKey { get; set; }
+ public bool? HasChapterImages { get; set; }
+
+ // why tuple vs value tuple?
+ //public Tuple<string, SortOrder>[] OrderBy { get; set; }
+ public ValueTuple<string, SortOrder>[] OrderBy { get; set; }
+
+ public DateTime? MinDateCreated { get; set; }
+ public DateTime? MinDateLastSaved { get; set; }
+ public DateTime? MinDateLastSavedForUser { get; set; }
+
+ public DtoOptions DtoOptions { get; set; }
+ public int MinSimilarityScore { get; set; }
+ public string HasNoAudioTrackWithLanguage { get; set; }
+ public string HasNoInternalSubtitleTrackWithLanguage { get; set; }
+ public string HasNoExternalSubtitleTrackWithLanguage { get; set; }
+ public string HasNoSubtitleTrackWithLanguage { get; set; }
+ public bool? IsDeadArtist { get; set; }
+ public bool? IsDeadStudio { get; set; }
+ public bool? IsDeadPerson { get; set; }
+
+ public InternalItemsQuery()
+ {
+ AlbumArtistIds = new Guid[] {};
+ AlbumIds = new Guid[] {};
+ AncestorIds = new Guid[] {};
+ ArtistIds = new Guid[] {};
+ BlockUnratedItems = new UnratedItem[] { };
+ BoxSetLibraryFolders = new Guid[] {};
+ ChannelIds = new Guid[] {};
+ ContributingArtistIds = new Guid[] {};
+ DtoOptions = new DtoOptions();
+ EnableTotalRecordCount = true;
+ ExcludeArtistIds = new Guid[] {};
+ ExcludeInheritedTags = new string[] {};
+ ExcludeItemIds = new Guid[] {};
+ ExcludeItemTypes = new string[] {};
+ ExcludeProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ ExcludeTags = new string[] {};
+ GenreIds = new Guid[] {};
+ Genres = new string[] {};
+ GroupByPresentationUniqueKey = true;
+ HasAnyProviderId = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ ImageTypes = new ImageType[] { };
+ IncludeItemTypes = new string[] {};
+ ItemIds = new Guid[] {};
+ MediaTypes = new string[] {};
+ MinSimilarityScore = 20;
+ OfficialRatings = new string[] {};
+ OrderBy = Array.Empty<ValueTuple<string, SortOrder>>();
+ PersonIds = new Guid[] {};
+ PersonTypes = new string[] {};
+ PresetViews = new string[] {};
+ SeriesStatuses = new SeriesStatus[] { };
+ SourceTypes = new SourceType[] { };
+ StudioIds = new Guid[] {};
+ Tags = new string[] {};
+ TopParentIds = new Guid[] {};
+ TrailerTypes = new TrailerType[] { };
+ VideoTypes = new VideoType[] { };
+ Years = new int[] { };
+ }
+
+ public InternalItemsQuery(User user)
+ : this()
+ {
+ SetUser(user);
+ }
+
+ public void SetUser(User user)
+ {
+ if (user != null)
+ {
+ var policy = user.Policy;
+ MaxParentalRating = policy.MaxParentalRating;
+
+ if (policy.MaxParentalRating.HasValue)
+ {
+ BlockUnratedItems = policy.BlockUnratedItems.Where(i => i != UnratedItem.Other).ToArray();
+ }
+
+ ExcludeInheritedTags = policy.BlockedTags;
+
+ User = user;
+ }
+ }
+
+ public Dictionary<string, string> HasAnyProviderId { get; set; }
+ public Guid[] AlbumArtistIds { get; set; }
+ public Guid[] BoxSetLibraryFolders { get; set; }
+ public Guid[] ContributingArtistIds { get; set; }
+ public bool? HasAired { get; set; }
+ public bool? HasOwnerId { get; set; }
+ public bool? Is4K { get; set; }
+ public int? MaxHeight { get; set; }
+ public int? MaxWidth { get; set; }
+ public int? MinHeight { get; set; }
+ public int? MinWidth { get; set; }
+ public string SearchTerm { get; set; }
+ public string SeriesTimerId { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
new file mode 100644
index 000000000..7e00834e3
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class InternalPeopleQuery
+ {
+ public Guid ItemId { get; set; }
+ public string[] PersonTypes { get; set; }
+ public string[] ExcludePersonTypes { get; set; }
+ public int? MaxListOrder { get; set; }
+ public Guid AppearsInItemId { get; set; }
+ public string NameContains { get; set; }
+
+ public InternalPeopleQuery()
+ {
+ PersonTypes = new string[] { };
+ ExcludePersonTypes = new string[] { };
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs
new file mode 100644
index 000000000..bd0011c4b
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs
@@ -0,0 +1,46 @@
+using MediaBrowser.Model.Entities;
+using System;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class ItemImageInfo
+ {
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ public string Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ public ImageType Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date modified.
+ /// </summary>
+ /// <value>The date modified.</value>
+ public DateTime DateModified { get; set; }
+
+ public int Width { get; set; }
+ public int Height { get; set; }
+
+ [IgnoreDataMember]
+ public bool IsLocalFile
+ {
+ get
+ {
+ if (Path != null)
+ {
+ if (Path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs
new file mode 100644
index 000000000..363a3d6fd
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/LinkedChild.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class LinkedChild
+ {
+ public string Path { get; set; }
+ public LinkedChildType Type { get; set; }
+ public string LibraryItemId { get; set; }
+
+ [IgnoreDataMember]
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Serves as a cache
+ /// </summary>
+ public Guid? ItemId { get; set; }
+
+ public static LinkedChild Create(BaseItem item)
+ {
+ var child = new LinkedChild
+ {
+ Path = item.Path,
+ Type = LinkedChildType.Manual
+ };
+
+ if (string.IsNullOrEmpty(child.Path))
+ {
+ child.LibraryItemId = item.Id.ToString("N");
+ }
+
+ return child;
+ }
+
+ public LinkedChild()
+ {
+ Id = Guid.NewGuid().ToString("N");
+ }
+ }
+
+ public enum LinkedChildType
+ {
+ Manual = 0,
+ Shortcut = 1
+ }
+
+ public class LinkedChildComparer : IEqualityComparer<LinkedChild>
+ {
+ private readonly IFileSystem _fileSystem;
+
+ public LinkedChildComparer(IFileSystem fileSystem)
+ {
+ _fileSystem = fileSystem;
+ }
+
+ public bool Equals(LinkedChild x, LinkedChild y)
+ {
+ if (x.Type == y.Type)
+ {
+ return _fileSystem.AreEqual(x.Path, y.Path);
+ }
+ return false;
+ }
+
+ public int GetHashCode(LinkedChild obj)
+ {
+ return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode();
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
new file mode 100644
index 000000000..5918bf981
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -0,0 +1,263 @@
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Users;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Controller.Entities.Movies
+{
+ /// <summary>
+ /// Class BoxSet
+ /// </summary>
+ public class BoxSet : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<BoxSetInfo>
+ {
+ public BoxSet()
+ {
+ RemoteTrailers = EmptyMediaUrlArray;
+ LocalTrailerIds = new Guid[] { };
+ RemoteTrailerIds = new Guid[] { };
+
+ DisplayOrder = ItemSortBy.PremiereDate;
+ }
+
+ [IgnoreDataMember]
+ protected override bool FilterLinkedChildrenPerUser
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsInheritedParentImages
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get { return true; }
+ }
+
+ public Guid[] LocalTrailerIds { get; set; }
+ public Guid[] RemoteTrailerIds { get; set; }
+
+ /// <summary>
+ /// Gets or sets the remote trailers.
+ /// </summary>
+ /// <value>The remote trailers.</value>
+ public MediaUrl[] RemoteTrailers { get; set; }
+
+ /// <summary>
+ /// Gets or sets the display order.
+ /// </summary>
+ /// <value>The display order.</value>
+ public string DisplayOrder { get; set; }
+
+ protected override bool GetBlockUnratedValue(UserPolicy config)
+ {
+ return config.BlockUnratedItems.Contains(UnratedItem.Movie);
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ double value = 2;
+ value /= 3;
+
+ return value;
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ return UnratedItem.Movie;
+ }
+
+ protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
+ {
+ if (IsLegacyBoxSet)
+ {
+ return base.GetNonCachedChildren(directoryService);
+ }
+ return new List<BaseItem>();
+ }
+
+ protected override List<BaseItem> LoadChildren()
+ {
+ if (IsLegacyBoxSet)
+ {
+ return base.LoadChildren();
+ }
+
+ // Save a trip to the database
+ return new List<BaseItem>();
+ }
+
+ [IgnoreDataMember]
+ private bool IsLegacyBoxSet
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(Path))
+ {
+ return false;
+ }
+
+ if (LinkedChildren.Length > 0)
+ {
+ return false;
+ }
+
+ return !FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, Path);
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool IsPreSorted
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
+ {
+ return true;
+ }
+
+ public override bool IsSaveLocalMetadataEnabled()
+ {
+ return true;
+ }
+
+ public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
+ {
+ var children = base.GetChildren(user, includeLinkedChildren, query);
+
+ if (string.Equals(DisplayOrder, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase))
+ {
+ // Sort by name
+ return LibraryManager.Sort(children, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
+ }
+
+ if (string.Equals(DisplayOrder, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase))
+ {
+ // Sort by release date
+ return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList();
+ }
+
+ // Default sorting
+ return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList();
+ }
+
+ public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
+ {
+ var children = base.GetRecursiveChildren(user, query);
+
+ if (string.Equals(DisplayOrder, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase))
+ {
+ // Sort by release date
+ return LibraryManager.Sort(children, user, new[] { ItemSortBy.ProductionYear, ItemSortBy.PremiereDate, ItemSortBy.SortName }, SortOrder.Ascending).ToList();
+ }
+
+ return children;
+ }
+
+ public BoxSetInfo GetLookupInfo()
+ {
+ return GetItemLookupInfo<BoxSetInfo>();
+ }
+
+ public override bool IsVisible(User user)
+ {
+ if (IsLegacyBoxSet)
+ {
+ return base.IsVisible(user);
+ }
+
+ if (base.IsVisible(user))
+ {
+ if (LinkedChildren.Length == 0)
+ {
+ return true;
+ }
+
+ var userLibraryFolderIds = GetLibraryFolderIds(user);
+ var libraryFolderIds = LibraryFolderIds ?? GetLibraryFolderIds();
+
+ if (libraryFolderIds.Length == 0)
+ {
+ return true;
+ }
+
+ return userLibraryFolderIds.Any(i => libraryFolderIds.Contains(i));
+ }
+
+ return false;
+ }
+
+ public override bool IsVisibleStandalone(User user)
+ {
+ if (IsLegacyBoxSet)
+ {
+ return base.IsVisibleStandalone(user);
+ }
+
+ return IsVisible(user);
+ }
+
+ public Guid[] LibraryFolderIds { get; set; }
+
+ private Guid[] GetLibraryFolderIds(User user)
+ {
+ return LibraryManager.GetUserRootFolder().GetChildren(user, true)
+ .Select(i => i.Id)
+ .ToArray();
+ }
+
+ public Guid[] GetLibraryFolderIds()
+ {
+ var expandedFolders = new List<Guid>() { };
+
+ return FlattenItems(this, expandedFolders)
+ .SelectMany(i => LibraryManager.GetCollectionFolders(i))
+ .Select(i => i.Id)
+ .Distinct()
+ .ToArray();
+ }
+
+ private IEnumerable<BaseItem> FlattenItems(IEnumerable<BaseItem> items, List<Guid> expandedFolders)
+ {
+ return items
+ .SelectMany(i => FlattenItems(i, expandedFolders));
+ }
+
+ private IEnumerable<BaseItem> FlattenItems(BaseItem item, List<Guid> expandedFolders)
+ {
+ var boxset = item as BoxSet;
+ if (boxset != null)
+ {
+ if (!expandedFolders.Contains(item.Id))
+ {
+ expandedFolders.Add(item.Id);
+
+ return FlattenItems(boxset.GetLinkedChildren(), expandedFolders);
+ }
+
+ return new BaseItem[] { };
+ }
+
+ return new[] { item };
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs
new file mode 100644
index 000000000..878b1b860
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs
@@ -0,0 +1,203 @@
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities.Movies
+{
+ /// <summary>
+ /// Class Movie
+ /// </summary>
+ public class Movie : Video, IHasSpecialFeatures, IHasTrailers, IHasLookupInfo<MovieInfo>, ISupportsBoxSetGrouping
+ {
+ public Guid[] SpecialFeatureIds { get; set; }
+
+ public Movie()
+ {
+ SpecialFeatureIds = new Guid[] {};
+ RemoteTrailers = EmptyMediaUrlArray;
+ LocalTrailerIds = new Guid[] {};
+ RemoteTrailerIds = new Guid[] {};
+ }
+
+ public Guid[] LocalTrailerIds { get; set; }
+ public Guid[] RemoteTrailerIds { get; set; }
+
+ public MediaUrl[] RemoteTrailers { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the TMDB collection.
+ /// </summary>
+ /// <value>The name of the TMDB collection.</value>
+ public string TmdbCollectionName { get; set; }
+
+ [IgnoreDataMember]
+ public string CollectionName
+ {
+ get { return TmdbCollectionName; }
+ set { TmdbCollectionName = value; }
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ // hack for tv plugins
+ if (SourceType == SourceType.Channel)
+ {
+ return 0;
+ }
+
+ double value = 2;
+ value /= 3;
+
+ return value;
+ }
+
+ protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
+ {
+ var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
+
+ // Must have a parent to have special features
+ // In other words, it must be part of the Parent/Child tree
+ if (IsFileProtocol && SupportsOwnedItems && !IsInMixedFolder)
+ {
+ var specialFeaturesChanged = await RefreshSpecialFeatures(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
+
+ if (specialFeaturesChanged)
+ {
+ hasChanges = true;
+ }
+ }
+
+ return hasChanges;
+ }
+
+ private async Task<bool> RefreshSpecialFeatures(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
+ {
+ var newItems = LibraryManager.FindExtras(this, fileSystemChildren, options.DirectoryService).ToList();
+ var newItemIds = newItems.Select(i => i.Id).ToArray();
+
+ var itemsChanged = !SpecialFeatureIds.SequenceEqual(newItemIds);
+
+ var ownerId = Id;
+
+ var tasks = newItems.Select(i =>
+ {
+ var subOptions = new MetadataRefreshOptions(options);
+
+ if (i.OwnerId != ownerId)
+ {
+ i.OwnerId = ownerId;
+ subOptions.ForceSave = true;
+ }
+
+ return RefreshMetadataForOwnedItem(i, false, subOptions, cancellationToken);
+ });
+
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+
+ SpecialFeatureIds = newItemIds;
+
+ return itemsChanged;
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ return UnratedItem.Movie;
+ }
+
+ public MovieInfo GetLookupInfo()
+ {
+ var info = GetItemLookupInfo<MovieInfo>();
+
+ if (!IsInMixedFolder)
+ {
+ var name = System.IO.Path.GetFileName(ContainingFolderPath);
+
+ if (VideoType == VideoType.VideoFile || VideoType == VideoType.Iso)
+ {
+ if (string.Equals(name, System.IO.Path.GetFileName(Path), StringComparison.OrdinalIgnoreCase))
+ {
+ // if the folder has the file extension, strip it
+ name = System.IO.Path.GetFileNameWithoutExtension(name);
+ }
+ }
+
+ info.Name = name;
+ }
+
+ return info;
+ }
+
+ public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ {
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+
+ if (!ProductionYear.HasValue)
+ {
+ var info = LibraryManager.ParseName(Name);
+
+ var yearInName = info.Year;
+
+ if (yearInName.HasValue)
+ {
+ ProductionYear = yearInName;
+ hasChanges = true;
+ }
+ else
+ {
+ // Try to get the year from the folder name
+ if (!IsInMixedFolder)
+ {
+ info = LibraryManager.ParseName(System.IO.Path.GetFileName(ContainingFolderPath));
+
+ yearInName = info.Year;
+
+ if (yearInName.HasValue)
+ {
+ ProductionYear = yearInName;
+ hasChanges = true;
+ }
+ }
+ }
+ }
+
+ return hasChanges;
+ }
+
+ public override List<ExternalUrl> GetRelatedUrls()
+ {
+ var list = base.GetRelatedUrls();
+
+ var imdbId = this.GetProviderId(MetadataProviders.Imdb);
+ if (!string.IsNullOrEmpty(imdbId))
+ {
+ list.Add(new ExternalUrl
+ {
+ Name = "Trakt",
+ Url = string.Format("https://trakt.tv/movies/{0}", imdbId)
+ });
+ }
+
+ return list;
+ }
+
+ [IgnoreDataMember]
+ public override bool StopRefreshIfLocalMetadataFound
+ {
+ get
+ {
+ // Need people id's from internet metadata
+ return false;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs
new file mode 100644
index 000000000..78f9d0671
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/MusicVideo.cs
@@ -0,0 +1,79 @@
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using System.Collections.Generic;
+using MediaBrowser.Model.Serialization;
+using System;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class MusicVideo : Video, IHasArtist, IHasMusicGenres, IHasLookupInfo<MusicVideoInfo>
+ {
+ [IgnoreDataMember]
+ public string[] Artists { get; set; }
+
+ public MusicVideo()
+ {
+ Artists = new string[] {};
+ }
+
+ [IgnoreDataMember]
+ public string[] AllArtists
+ {
+ get
+ {
+ return Artists;
+ }
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ return UnratedItem.Music;
+ }
+
+ public MusicVideoInfo GetLookupInfo()
+ {
+ var info = GetItemLookupInfo<MusicVideoInfo>();
+
+ info.Artists = Artists;
+
+ return info;
+ }
+
+ public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ {
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+
+ if (!ProductionYear.HasValue)
+ {
+ var info = LibraryManager.ParseName(Name);
+
+ var yearInName = info.Year;
+
+ if (yearInName.HasValue)
+ {
+ ProductionYear = yearInName;
+ hasChanges = true;
+ }
+ else
+ {
+ // Try to get the year from the folder name
+ if (!IsInMixedFolder)
+ {
+ info = LibraryManager.ParseName(System.IO.Path.GetFileName(ContainingFolderPath));
+
+ yearInName = info.Year;
+
+ if (yearInName.HasValue)
+ {
+ ProductionYear = yearInName;
+ hasChanges = true;
+ }
+ }
+ }
+ }
+
+ return hasChanges;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs
new file mode 100644
index 000000000..9f85b2aea
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs
@@ -0,0 +1,119 @@
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public static class PeopleHelper
+ {
+ public static void AddPerson(List<PersonInfo> people, PersonInfo person)
+ {
+ if (person == null)
+ {
+ throw new ArgumentNullException("person");
+ }
+
+ if (string.IsNullOrEmpty(person.Name))
+ {
+ throw new ArgumentNullException();
+ }
+
+ // Normalize
+ if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
+ {
+ person.Type = PersonType.GuestStar;
+ }
+ else if (string.Equals(person.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase))
+ {
+ person.Type = PersonType.Director;
+ }
+ else if (string.Equals(person.Role, PersonType.Producer, StringComparison.OrdinalIgnoreCase))
+ {
+ person.Type = PersonType.Producer;
+ }
+ else if (string.Equals(person.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase))
+ {
+ person.Type = PersonType.Writer;
+ }
+
+ // If the type is GuestStar and there's already an Actor entry, then update it to avoid dupes
+ if (string.Equals(person.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
+ {
+ var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase));
+
+ if (existing != null)
+ {
+ existing.Type = PersonType.GuestStar;
+ MergeExisting(existing, person);
+ return;
+ }
+ }
+
+ if (string.Equals(person.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase))
+ {
+ // If the actor already exists without a role and we have one, fill it in
+ var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && (p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase) || p.Type.Equals(PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)));
+ if (existing == null)
+ {
+ // Wasn't there - add it
+ people.Add(person);
+ }
+ else
+ {
+ // Was there, if no role and we have one - fill it in
+ if (string.IsNullOrEmpty(existing.Role) && !string.IsNullOrEmpty(person.Role))
+ {
+ existing.Role = person.Role;
+ }
+
+ MergeExisting(existing, person);
+ }
+ }
+ else
+ {
+ var existing = people.FirstOrDefault(p =>
+ string.Equals(p.Name, person.Name, StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(p.Type, person.Type, StringComparison.OrdinalIgnoreCase));
+
+ // Check for dupes based on the combination of Name and Type
+ if (existing == null)
+ {
+ people.Add(person);
+ }
+ else
+ {
+ MergeExisting(existing, person);
+ }
+ }
+ }
+
+ private static void MergeExisting(PersonInfo existing, PersonInfo person)
+ {
+ existing.SortOrder = person.SortOrder ?? existing.SortOrder;
+ existing.ImageUrl = person.ImageUrl ?? existing.ImageUrl;
+
+ foreach (var id in person.ProviderIds)
+ {
+ existing.SetProviderId(id.Key, id.Value);
+ }
+ }
+
+ public static bool ContainsPerson(List<PersonInfo> people, string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ foreach (var i in people)
+ {
+ if (string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs
new file mode 100644
index 000000000..64d775094
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Person.cs
@@ -0,0 +1,216 @@
+using MediaBrowser.Controller.Providers;
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// This is the full Person object that can be retrieved with all of it's data.
+ /// </summary>
+ public class Person : BaseItem, IItemByName, IHasLookupInfo<PersonLookupInfo>
+ {
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
+ return list;
+ }
+ public override string CreatePresentationUniqueKey()
+ {
+ return GetUserDataKeys()[0];
+ }
+
+ public PersonLookupInfo GetLookupInfo()
+ {
+ return GetItemLookupInfo<PersonLookupInfo>();
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ double value = 2;
+ value /= 3;
+
+ return value;
+ }
+
+ public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+ {
+ query.PersonIds = new[] { Id };
+
+ return LibraryManager.GetItemList(query);
+ }
+
+ /// <summary>
+ /// Returns the folder containing the item.
+ /// If the item is a folder, it returns the folder itself
+ /// </summary>
+ /// <value>The containing folder path.</value>
+ [IgnoreDataMember]
+ public override string ContainingFolderPath
+ {
+ get
+ {
+ return Path;
+ }
+ }
+
+ public override bool CanDelete()
+ {
+ return false;
+ }
+
+ public override bool IsSaveLocalMetadataEnabled()
+ {
+ return true;
+ }
+
+ [IgnoreDataMember]
+ public override bool EnableAlphaNumericSorting
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsAncestors
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public static string GetPath(string name)
+ {
+ return GetPath(name, true);
+ }
+
+ public static string GetPath(string name, bool normalizeName)
+ {
+ // Trim the period at the end because windows will have a hard time with that
+ var validFilename = normalizeName ?
+ FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+ name;
+
+ string subFolderPrefix = null;
+
+ foreach (char c in validFilename)
+ {
+ if (char.IsLetterOrDigit(c))
+ {
+ subFolderPrefix = c.ToString();
+ break;
+ }
+ }
+
+ var path = ConfigurationManager.ApplicationPaths.PeoplePath;
+
+ return string.IsNullOrEmpty(subFolderPrefix) ?
+ System.IO.Path.Combine(path, validFilename) :
+ System.IO.Path.Combine(path, subFolderPrefix, validFilename);
+ }
+
+ private string GetRebasedPath()
+ {
+ return GetPath(System.IO.Path.GetFileName(Path), false);
+ }
+
+ public override bool RequiresRefresh()
+ {
+ var newPath = GetRebasedPath();
+ if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+ {
+ Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+ return true;
+ }
+ return base.RequiresRefresh();
+ }
+
+ /// <summary>
+ /// This is called before any metadata refresh and returns true or false indicating if changes were made
+ /// </summary>
+ public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ {
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+
+ var newPath = GetRebasedPath();
+ if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+ {
+ Path = newPath;
+ hasChanges = true;
+ }
+
+ return hasChanges;
+ }
+ }
+
+ /// <summary>
+ /// This is the small Person stub that is attached to BaseItems
+ /// </summary>
+ public class PersonInfo : IHasProviderIds
+ {
+ public PersonInfo()
+ {
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ public Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the role.
+ /// </summary>
+ /// <value>The role.</value>
+ public string Role { get; set; }
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ public string Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the sort order - ascending
+ /// </summary>
+ /// <value>The sort order.</value>
+ public int? SortOrder { get; set; }
+
+ public string ImageUrl { get; set; }
+
+ public Dictionary<string, string> ProviderIds { get; set; }
+
+ /// <summary>
+ /// Returns a <see cref="System.String" /> that represents this instance.
+ /// </summary>
+ /// <returns>A <see cref="System.String" /> that represents this instance.</returns>
+ public override string ToString()
+ {
+ return Name;
+ }
+
+ public bool IsType(string type)
+ {
+ return string.Equals(Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(Role, type, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs
new file mode 100644
index 000000000..01c10831d
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Photo.cs
@@ -0,0 +1,104 @@
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class Photo : BaseItem
+ {
+ [IgnoreDataMember]
+ public override bool SupportsLocalMetadata
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override string MediaType
+ {
+ get
+ {
+ return Model.Entities.MediaType.Photo;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override Folder LatestItemsIndexContainer
+ {
+ get
+ {
+ return AlbumEntity;
+ }
+ }
+
+
+ [IgnoreDataMember]
+ public PhotoAlbum AlbumEntity
+ {
+ get
+ {
+ var parents = GetParents();
+ foreach (var parent in parents)
+ {
+ var photoAlbum = parent as PhotoAlbum;
+ if (photoAlbum != null)
+ {
+ return photoAlbum;
+ }
+ }
+ return null;
+ }
+ }
+
+ public override bool CanDownload()
+ {
+ return true;
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ if (Width.HasValue && Height.HasValue)
+ {
+ double width = Width.Value;
+ double height = Height.Value;
+
+ if (Orientation.HasValue)
+ {
+ switch (Orientation.Value)
+ {
+ case ImageOrientation.LeftBottom:
+ case ImageOrientation.LeftTop:
+ case ImageOrientation.RightBottom:
+ case ImageOrientation.RightTop:
+ var temp = height;
+ height = width;
+ width = temp;
+ break;
+ }
+ }
+
+ width /= Height.Value;
+ return width;
+ }
+
+ return base.GetDefaultPrimaryImageAspectRatio();
+ }
+
+ public int? Width { get; set; }
+ public int? Height { get; set; }
+ public string CameraMake { get; set; }
+ public string CameraModel { get; set; }
+ public string Software { get; set; }
+ public double? ExposureTime { get; set; }
+ public double? FocalLength { get; set; }
+ public ImageOrientation? Orientation { get; set; }
+ public double? Aperture { get; set; }
+ public double? ShutterSpeed { get; set; }
+
+ public double? Latitude { get; set; }
+ public double? Longitude { get; set; }
+ public double? Altitude { get; set; }
+ public int? IsoSpeedRating { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/PhotoAlbum.cs b/MediaBrowser.Controller/Entities/PhotoAlbum.cs
new file mode 100644
index 000000000..af9d8c801
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/PhotoAlbum.cs
@@ -0,0 +1,34 @@
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class PhotoAlbum : Folder
+ {
+ [IgnoreDataMember]
+ public override bool AlwaysScanInternalMetadataPath
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPlayedStatus
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsInheritedParentImages
+ {
+ get
+ {
+ return false;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Share.cs b/MediaBrowser.Controller/Entities/Share.cs
new file mode 100644
index 000000000..4ea0b1ea6
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Share.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public interface IHasShares
+ {
+ Share[] Shares { get; set; }
+ }
+
+ public class Share
+ {
+ public string UserId { get; set; }
+ public bool CanEdit { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/SourceType.cs b/MediaBrowser.Controller/Entities/SourceType.cs
new file mode 100644
index 000000000..9c307b4e6
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/SourceType.cs
@@ -0,0 +1,10 @@
+
+namespace MediaBrowser.Controller.Entities
+{
+ public enum SourceType
+ {
+ Library = 0,
+ Channel = 1,
+ LiveTV = 2
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs
new file mode 100644
index 000000000..29f617539
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Studio.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class Studio
+ /// </summary>
+ public class Studio : BaseItem, IItemByName
+ {
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
+ return list;
+ }
+ public override string CreatePresentationUniqueKey()
+ {
+ return GetUserDataKeys()[0];
+ }
+
+ /// <summary>
+ /// Returns the folder containing the item.
+ /// If the item is a folder, it returns the folder itself
+ /// </summary>
+ /// <value>The containing folder path.</value>
+ [IgnoreDataMember]
+ public override string ContainingFolderPath
+ {
+ get
+ {
+ return Path;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool IsDisplayedAsFolder
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsAncestors
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ double value = 16;
+ value /= 9;
+
+ return value;
+ }
+
+ public override bool CanDelete()
+ {
+ return false;
+ }
+
+ public override bool IsSaveLocalMetadataEnabled()
+ {
+ return true;
+ }
+
+ public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+ {
+ query.StudioIds = new[] { Id };
+
+ return LibraryManager.GetItemList(query);
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public static string GetPath(string name)
+ {
+ return GetPath(name, true);
+ }
+
+ public static string GetPath(string name, bool normalizeName)
+ {
+ // Trim the period at the end because windows will have a hard time with that
+ var validName = normalizeName ?
+ FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+ name;
+
+ return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.StudioPath, validName);
+ }
+
+ private string GetRebasedPath()
+ {
+ return GetPath(System.IO.Path.GetFileName(Path), false);
+ }
+
+ public override bool RequiresRefresh()
+ {
+ var newPath = GetRebasedPath();
+ if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+ {
+ Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+ return true;
+ }
+ return base.RequiresRefresh();
+ }
+
+ /// <summary>
+ /// This is called before any metadata refresh and returns true or false indicating if changes were made
+ /// </summary>
+ public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ {
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+
+ var newPath = GetRebasedPath();
+ if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+ {
+ Path = newPath;
+ hasChanges = true;
+ }
+
+ return hasChanges;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
new file mode 100644
index 000000000..201579731
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -0,0 +1,374 @@
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities.TV
+{
+ /// <summary>
+ /// Class Episode
+ /// </summary>
+ public class Episode : Video, IHasTrailers, IHasLookupInfo<EpisodeInfo>, IHasSeries
+ {
+ public Episode()
+ {
+ RemoteTrailers = EmptyMediaUrlArray;
+ LocalTrailerIds = new Guid[] {};
+ RemoteTrailerIds = new Guid[] {};
+ }
+
+ public Guid[] LocalTrailerIds { get; set; }
+ public Guid[] RemoteTrailerIds { get; set; }
+ public MediaUrl[] RemoteTrailers { get; set; }
+
+ /// <summary>
+ /// Gets the season in which it aired.
+ /// </summary>
+ /// <value>The aired season.</value>
+ public int? AirsBeforeSeasonNumber { get; set; }
+ public int? AirsAfterSeasonNumber { get; set; }
+ public int? AirsBeforeEpisodeNumber { get; set; }
+
+ /// <summary>
+ /// This is the ending episode number for double episodes.
+ /// </summary>
+ /// <value>The index number.</value>
+ public int? IndexNumberEnd { get; set; }
+
+ public string FindSeriesSortName()
+ {
+ var series = Series;
+ return series == null ? SeriesName : series.SortName;
+ }
+
+ [IgnoreDataMember]
+ protected override bool SupportsOwnedItems
+ {
+ get
+ {
+ return IsStacked || MediaSourceCount > 1;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsInheritedParentImages
+ {
+ get { return true; }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get { return true; }
+ }
+
+ [IgnoreDataMember]
+ public int? AiredSeasonNumber
+ {
+ get
+ {
+ return AirsAfterSeasonNumber ?? AirsBeforeSeasonNumber ?? ParentIndexNumber;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override Folder LatestItemsIndexContainer
+ {
+ get
+ {
+ return Series;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override Guid DisplayParentId
+ {
+ get
+ {
+ return SeasonId;
+ }
+ }
+
+ [IgnoreDataMember]
+ protected override bool EnableDefaultVideoUserDataKeys
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ // hack for tv plugins
+ if (SourceType == SourceType.Channel)
+ {
+ return 0;
+ }
+
+ double value = 16;
+ value /= 9;
+
+ return value;
+ }
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ var series = Series;
+ if (series != null && ParentIndexNumber.HasValue && IndexNumber.HasValue)
+ {
+ var seriesUserDataKeys = series.GetUserDataKeys();
+ var take = seriesUserDataKeys.Count;
+ if (seriesUserDataKeys.Count > 1)
+ {
+ take--;
+ }
+ list.InsertRange(0, seriesUserDataKeys.Take(take).Select(i => i + ParentIndexNumber.Value.ToString("000") + IndexNumber.Value.ToString("000")));
+ }
+
+ return list;
+ }
+
+ /// <summary>
+ /// This Episode's Series Instance
+ /// </summary>
+ /// <value>The series.</value>
+ [IgnoreDataMember]
+ public Series Series
+ {
+ get
+ {
+ var seriesId = SeriesId;
+ if (seriesId.Equals(Guid.Empty)) {
+ seriesId = FindSeriesId();
+ }
+ return !seriesId.Equals(Guid.Empty) ? (LibraryManager.GetItemById(seriesId) as Series) : null;
+ }
+ }
+
+ [IgnoreDataMember]
+ public Season Season
+ {
+ get
+ {
+ var seasonId = SeasonId;
+ if (seasonId.Equals(Guid.Empty)) {
+ seasonId = FindSeasonId();
+ }
+ return !seasonId.Equals(Guid.Empty) ? (LibraryManager.GetItemById(seasonId) as Season) : null;
+ }
+ }
+
+ [IgnoreDataMember]
+ public bool IsInSeasonFolder
+ {
+ get
+ {
+ return FindParent<Season>() != null;
+ }
+ }
+
+ [IgnoreDataMember]
+ public string SeriesPresentationUniqueKey { get; set; }
+
+ [IgnoreDataMember]
+ public string SeriesName { get; set; }
+
+ [IgnoreDataMember]
+ public string SeasonName { get; set; }
+
+ public string FindSeriesPresentationUniqueKey()
+ {
+ var series = Series;
+ return series == null ? null : series.PresentationUniqueKey;
+ }
+
+ public string FindSeasonName()
+ {
+ var season = Season;
+
+ if (season == null)
+ {
+ if (ParentIndexNumber.HasValue)
+ {
+ return "Season " + ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture);
+ }
+ return "Season Unknown";
+ }
+
+ return season.Name;
+ }
+
+ public string FindSeriesName()
+ {
+ var series = Series;
+ return series == null ? SeriesName : series.Name;
+ }
+
+ public Guid FindSeasonId()
+ {
+ var season = FindParent<Season>();
+
+ // Episodes directly in series folder
+ if (season == null)
+ {
+ var series = Series;
+
+ if (series != null && ParentIndexNumber.HasValue)
+ {
+ var findNumber = ParentIndexNumber.Value;
+
+ season = series.Children
+ .OfType<Season>()
+ .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == findNumber);
+ }
+ }
+
+ return season == null ? Guid.Empty : season.Id;
+ }
+
+ /// <summary>
+ /// Creates the name of the sort.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ protected override string CreateSortName()
+ {
+ return (ParentIndexNumber != null ? ParentIndexNumber.Value.ToString("000 - ") : "")
+ + (IndexNumber != null ? IndexNumber.Value.ToString("0000 - ") : "") + Name;
+ }
+
+ /// <summary>
+ /// Determines whether [contains episode number] [the specified number].
+ /// </summary>
+ /// <param name="number">The number.</param>
+ /// <returns><c>true</c> if [contains episode number] [the specified number]; otherwise, <c>false</c>.</returns>
+ public bool ContainsEpisodeNumber(int number)
+ {
+ if (IndexNumber.HasValue)
+ {
+ if (IndexNumberEnd.HasValue)
+ {
+ return number >= IndexNumber.Value && number <= IndexNumberEnd.Value;
+ }
+
+ return IndexNumber.Value == number;
+ }
+
+ return false;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsRemoteImageDownloading
+ {
+ get
+ {
+ if (IsMissingEpisode)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public bool IsMissingEpisode
+ {
+ get
+ {
+ return LocationType == LocationType.Virtual;
+ }
+ }
+
+ [IgnoreDataMember]
+ public Guid SeasonId { get; set; }
+ [IgnoreDataMember]
+ public Guid SeriesId { get; set; }
+
+ public Guid FindSeriesId()
+ {
+ var series = FindParent<Series>();
+ return series == null ? Guid.Empty : series.Id;
+ }
+
+ public override IEnumerable<Guid> GetAncestorIds()
+ {
+ var list = base.GetAncestorIds().ToList();
+
+ var seasonId = SeasonId;
+
+ if (!seasonId.Equals(Guid.Empty) && !list.Contains(seasonId))
+ {
+ list.Add(seasonId);
+ }
+
+ return list;
+ }
+
+ public override IEnumerable<FileSystemMetadata> GetDeletePaths()
+ {
+ return new[] {
+ new FileSystemMetadata
+ {
+ FullName = Path,
+ IsDirectory = IsFolder
+ }
+ }.Concat(GetLocalMetadataFilesToDelete());
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ return UnratedItem.Series;
+ }
+
+ public EpisodeInfo GetLookupInfo()
+ {
+ var id = GetItemLookupInfo<EpisodeInfo>();
+
+ var series = Series;
+
+ if (series != null)
+ {
+ id.SeriesProviderIds = series.ProviderIds;
+ id.SeriesDisplayOrder = series.DisplayOrder;
+ }
+
+ id.IsMissingEpisode = IsMissingEpisode;
+ id.IndexNumberEnd = IndexNumberEnd;
+
+ return id;
+ }
+
+ public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ {
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+
+ if (!IsLocked)
+ {
+ if (SourceType == SourceType.Library)
+ {
+ try
+ {
+ if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetdata))
+ {
+ hasChanges = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error in FillMissingEpisodeNumbersFromPath. Episode: {0}", ex, Path ?? Name ?? Id.ToString());
+ }
+ }
+ }
+
+ return hasChanges;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs
new file mode 100644
index 000000000..b5f77df55
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/TV/Season.cs
@@ -0,0 +1,273 @@
+using System;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Users;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities.TV
+{
+ /// <summary>
+ /// Class Season
+ /// </summary>
+ public class Season : Folder, IHasSeries, IHasLookupInfo<SeasonInfo>
+ {
+ [IgnoreDataMember]
+ public override bool SupportsAddingToPlaylist
+ {
+ get { return true; }
+ }
+
+ [IgnoreDataMember]
+ public override bool IsPreSorted
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsDateLastMediaAdded
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get { return true; }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsInheritedParentImages
+ {
+ get { return true; }
+ }
+
+ [IgnoreDataMember]
+ public override Guid DisplayParentId
+ {
+ get { return SeriesId; }
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ double value = 2;
+ value /= 3;
+
+ return value;
+ }
+
+ public string FindSeriesSortName()
+ {
+ var series = Series;
+ return series == null ? SeriesName : series.SortName;
+ }
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ var series = Series;
+ if (series != null)
+ {
+ list.InsertRange(0, series.GetUserDataKeys().Select(i => i + (IndexNumber ?? 0).ToString("000")));
+ }
+
+ return list;
+ }
+
+ public override int GetChildCount(User user)
+ {
+ var result = GetChildren(user, true).Count;
+
+ return result;
+ }
+
+ /// <summary>
+ /// This Episode's Series Instance
+ /// </summary>
+ /// <value>The series.</value>
+ [IgnoreDataMember]
+ public Series Series
+ {
+ get
+ {
+ var seriesId = SeriesId;
+ if (seriesId.Equals(Guid.Empty)) {
+ seriesId = FindSeriesId();
+ }
+ return !seriesId.Equals(Guid.Empty) ? (LibraryManager.GetItemById(seriesId) as Series) : null;
+ }
+ }
+
+ [IgnoreDataMember]
+ public string SeriesPath
+ {
+ get
+ {
+ var series = Series;
+
+ if (series != null)
+ {
+ return series.Path;
+ }
+
+ return FileSystem.GetDirectoryName(Path);
+ }
+ }
+
+ public override string CreatePresentationUniqueKey()
+ {
+ if (IndexNumber.HasValue)
+ {
+ var series = Series;
+ if (series != null)
+ {
+ return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000");
+ }
+ }
+
+ return base.CreatePresentationUniqueKey();
+ }
+
+ /// <summary>
+ /// Creates the name of the sort.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ protected override string CreateSortName()
+ {
+ return IndexNumber != null ? IndexNumber.Value.ToString("0000") : Name;
+ }
+
+ protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
+ {
+ if (query.User == null)
+ {
+ return base.GetItemsInternal(query);
+ }
+
+ var user = query.User;
+
+ Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
+
+ var items = GetEpisodes(user, query.DtoOptions).Where(filter);
+
+ return PostFilterAndSort(items, query, false);
+ }
+
+ /// <summary>
+ /// Gets the episodes.
+ /// </summary>
+ public List<BaseItem> GetEpisodes(User user, DtoOptions options)
+ {
+ return GetEpisodes(Series, user, options);
+ }
+
+ public List<BaseItem> GetEpisodes(Series series, User user, DtoOptions options)
+ {
+ return GetEpisodes(series, user, null, options);
+ }
+
+ public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options)
+ {
+ return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options);
+ }
+
+ public List<BaseItem> GetEpisodes()
+ {
+ return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true));
+ }
+
+ public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
+ {
+ return GetEpisodes(user, new DtoOptions(true));
+ }
+
+ protected override bool GetBlockUnratedValue(UserPolicy config)
+ {
+ // Don't block. Let either the entire series rating or episode rating determine it
+ return false;
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ return UnratedItem.Series;
+ }
+
+ [IgnoreDataMember]
+ public string SeriesPresentationUniqueKey { get; set; }
+
+ [IgnoreDataMember]
+ public string SeriesName { get; set; }
+
+ [IgnoreDataMember]
+ public Guid SeriesId { get; set; }
+
+ public string FindSeriesPresentationUniqueKey()
+ {
+ var series = Series;
+ return series == null ? null : series.PresentationUniqueKey;
+ }
+
+ public string FindSeriesName()
+ {
+ var series = Series;
+ return series == null ? SeriesName : series.Name;
+ }
+
+ public Guid FindSeriesId()
+ {
+ var series = FindParent<Series>();
+ return series == null ? Guid.Empty: series.Id;
+ }
+
+ /// <summary>
+ /// Gets the lookup information.
+ /// </summary>
+ /// <returns>SeasonInfo.</returns>
+ public SeasonInfo GetLookupInfo()
+ {
+ var id = GetItemLookupInfo<SeasonInfo>();
+
+ var series = Series;
+
+ if (series != null)
+ {
+ id.SeriesProviderIds = series.ProviderIds;
+ }
+
+ return id;
+ }
+
+ /// <summary>
+ /// This is called before any metadata refresh and returns true or false indicating if changes were made
+ /// </summary>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+ public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ {
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+
+ if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path))
+ {
+ IndexNumber = IndexNumber ?? LibraryManager.GetSeasonNumberFromPath(Path);
+
+ // If a change was made record it
+ if (IndexNumber.HasValue)
+ {
+ hasChanges = true;
+ }
+ }
+
+ return hasChanges;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
new file mode 100644
index 000000000..88fde1760
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -0,0 +1,540 @@
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Users;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities.TV
+{
+ /// <summary>
+ /// Class Series
+ /// </summary>
+ public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IMetadataContainer
+ {
+ public Series()
+ {
+ RemoteTrailers = EmptyMediaUrlArray;
+ LocalTrailerIds = new Guid[] {};
+ RemoteTrailerIds = new Guid[] {};
+ AirDays = new DayOfWeek[] { };
+ }
+
+ public DayOfWeek[] AirDays { get; set; }
+ public string AirTime { get; set; }
+
+ [IgnoreDataMember]
+ public override bool SupportsAddingToPlaylist
+ {
+ get { return true; }
+ }
+
+ [IgnoreDataMember]
+ public override bool IsPreSorted
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsDateLastMediaAdded
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsInheritedParentImages
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get { return true; }
+ }
+
+ public Guid[] LocalTrailerIds { get; set; }
+ public Guid[] RemoteTrailerIds { get; set; }
+
+ public MediaUrl[] RemoteTrailers { get; set; }
+
+ /// <summary>
+ /// airdate, dvd or absolute
+ /// </summary>
+ public string DisplayOrder { get; set; }
+
+ /// <summary>
+ /// Gets or sets the status.
+ /// </summary>
+ /// <value>The status.</value>
+ public SeriesStatus? Status { get; set; }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ double value = 2;
+ value /= 3;
+
+ return value;
+ }
+
+ public override string CreatePresentationUniqueKey()
+ {
+ if (LibraryManager.GetLibraryOptions(this).EnableAutomaticSeriesGrouping)
+ {
+ var userdatakeys = GetUserDataKeys();
+
+ if (userdatakeys.Count > 1)
+ {
+ return AddLibrariesToPresentationUniqueKey(userdatakeys[0]);
+ }
+ }
+
+ return base.CreatePresentationUniqueKey();
+ }
+
+ private string AddLibrariesToPresentationUniqueKey(string key)
+ {
+ var lang = GetPreferredMetadataLanguage();
+ if (!string.IsNullOrEmpty(lang))
+ {
+ key += "-" + lang;
+ }
+
+ var folders = LibraryManager.GetCollectionFolders(this)
+ .Select(i => i.Id.ToString("N"))
+ .ToArray();
+
+ if (folders.Length == 0)
+ {
+ return key;
+ }
+
+ return key + "-" + string.Join("-", folders);
+ }
+
+ private static string GetUniqueSeriesKey(BaseItem series)
+ {
+ return series.GetPresentationUniqueKey();
+ }
+
+ public override int GetChildCount(User user)
+ {
+ var seriesKey = GetUniqueSeriesKey(this);
+
+ var result = LibraryManager.GetCount(new InternalItemsQuery(user)
+ {
+ AncestorWithPresentationUniqueKey = null,
+ SeriesPresentationUniqueKey = seriesKey,
+ IncludeItemTypes = new[] { typeof(Season).Name },
+ IsVirtualItem = false,
+ Limit = 0,
+ DtoOptions = new Dto.DtoOptions(false)
+ {
+ EnableImages = false
+ }
+ });
+
+ return result;
+ }
+
+ public override int GetRecursiveChildCount(User user)
+ {
+ var seriesKey = GetUniqueSeriesKey(this);
+
+ var query = new InternalItemsQuery(user)
+ {
+ AncestorWithPresentationUniqueKey = null,
+ SeriesPresentationUniqueKey = seriesKey,
+ DtoOptions = new Dto.DtoOptions(false)
+ {
+ EnableImages = false
+ }
+ };
+
+ if (query.IncludeItemTypes.Length == 0)
+ {
+ query.IncludeItemTypes = new[] { typeof(Episode).Name };
+ }
+ query.IsVirtualItem = false;
+ query.Limit = 0;
+ var totalRecordCount = LibraryManager.GetCount(query);
+
+ return totalRecordCount;
+ }
+
+ /// <summary>
+ /// Gets the user data key.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ var key = this.GetProviderId(MetadataProviders.Imdb);
+ if (!string.IsNullOrEmpty(key))
+ {
+ list.Insert(0, key);
+ }
+
+ key = this.GetProviderId(MetadataProviders.Tvdb);
+ if (!string.IsNullOrEmpty(key))
+ {
+ list.Insert(0, key);
+ }
+
+ return list;
+ }
+
+ public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
+ {
+ return GetSeasons(user, new DtoOptions(true));
+ }
+
+ public List<BaseItem> GetSeasons(User user, DtoOptions options)
+ {
+ var query = new InternalItemsQuery(user)
+ {
+ DtoOptions = options
+ };
+
+ SetSeasonQueryOptions(query, user);
+
+ return LibraryManager.GetItemList(query);
+ }
+
+ private void SetSeasonQueryOptions(InternalItemsQuery query, User user)
+ {
+ var seriesKey = GetUniqueSeriesKey(this);
+
+ query.AncestorWithPresentationUniqueKey = null;
+ query.SeriesPresentationUniqueKey = seriesKey;
+ query.IncludeItemTypes = new[] { typeof(Season).Name };
+ query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray();
+
+ if (user != null)
+ {
+ var config = user.Configuration;
+
+ if (!config.DisplayMissingEpisodes)
+ {
+ query.IsMissing = false;
+ }
+ }
+ }
+
+ protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
+ {
+ var user = query.User;
+
+ if (query.Recursive)
+ {
+ var seriesKey = GetUniqueSeriesKey(this);
+
+ query.AncestorWithPresentationUniqueKey = null;
+ query.SeriesPresentationUniqueKey = seriesKey;
+ if (query.OrderBy.Length == 0)
+ {
+ query.OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray();
+ }
+ if (query.IncludeItemTypes.Length == 0)
+ {
+ query.IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name };
+ }
+ query.IsVirtualItem = false;
+ return LibraryManager.GetItemsResult(query);
+ }
+
+ SetSeasonQueryOptions(query, user);
+
+ return LibraryManager.GetItemsResult(query);
+ }
+
+ public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options)
+ {
+ var seriesKey = GetUniqueSeriesKey(this);
+
+ var query = new InternalItemsQuery(user)
+ {
+ AncestorWithPresentationUniqueKey = null,
+ SeriesPresentationUniqueKey = seriesKey,
+ IncludeItemTypes = new[] { typeof(Episode).Name, typeof(Season).Name },
+ OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
+ DtoOptions = options
+ };
+ var config = user.Configuration;
+ if (!config.DisplayMissingEpisodes)
+ {
+ query.IsMissing = false;
+ }
+
+ var allItems = LibraryManager.GetItemList(query);
+
+ var allSeriesEpisodes = allItems.OfType<Episode>().ToList();
+
+ var allEpisodes = allItems.OfType<Season>()
+ .SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options))
+ .Reverse();
+
+ // Specials could appear twice based on above - once in season 0, once in the aired season
+ // This depends on settings for that series
+ // When this happens, remove the duplicate from season 0
+
+ return allEpisodes.DistinctBy(i => i.Id).Reverse();
+ }
+
+ public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ // Refresh bottom up, children first, then the boxset
+ // By then hopefully the movies within will have Tmdb collection values
+ var items = GetRecursiveChildren();
+
+ var totalItems = items.Count;
+ var numComplete = 0;
+
+ // Refresh seasons
+ foreach (var item in items)
+ {
+ if (!(item is Season))
+ {
+ continue;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (refreshOptions.RefreshItem(item))
+ {
+ await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= totalItems;
+ progress.Report(percent * 100);
+ }
+
+ // Refresh episodes and other children
+ foreach (var item in items)
+ {
+ if ((item is Season))
+ {
+ continue;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var skipItem = false;
+
+ var episode = item as Episode;
+
+ if (episode != null
+ && refreshOptions.MetadataRefreshMode != MetadataRefreshMode.FullRefresh
+ && !refreshOptions.ReplaceAllMetadata
+ && episode.IsMissingEpisode
+ && episode.LocationType == LocationType.Virtual
+ && episode.PremiereDate.HasValue
+ && (DateTime.UtcNow - episode.PremiereDate.Value).TotalDays > 30)
+ {
+ skipItem = true;
+ }
+
+ if (!skipItem)
+ {
+ if (refreshOptions.RefreshItem(item))
+ {
+ await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= totalItems;
+ progress.Report(percent * 100);
+ }
+
+ refreshOptions = new MetadataRefreshOptions(refreshOptions);
+ await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
+ }
+
+ public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options)
+ {
+ var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons;
+
+ // add optimization when this setting is not enabled
+ var seriesKey = queryFromSeries ?
+ GetUniqueSeriesKey(this) :
+ GetUniqueSeriesKey(parentSeason);
+
+ var query = new InternalItemsQuery(user)
+ {
+ AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey,
+ SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null,
+ IncludeItemTypes = new[] { typeof(Episode).Name },
+ OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
+ DtoOptions = options
+ };
+ if (user != null)
+ {
+ var config = user.Configuration;
+ if (!config.DisplayMissingEpisodes)
+ {
+ query.IsMissing = false;
+ }
+ }
+
+ var allItems = LibraryManager.GetItemList(query);
+
+ return GetSeasonEpisodes(parentSeason, user, allItems, options);
+ }
+
+ public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options)
+ {
+ if (allSeriesEpisodes == null)
+ {
+ return GetSeasonEpisodes(parentSeason, user, options);
+ }
+
+ var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons);
+
+ var sortBy = (parentSeason.IndexNumber ?? -1) == 0 ? ItemSortBy.SortName : ItemSortBy.AiredEpisodeOrder;
+
+ return LibraryManager.Sort(episodes, user, new[] { sortBy }, SortOrder.Ascending).ToList();
+ }
+
+ /// <summary>
+ /// Filters the episodes by season.
+ /// </summary>
+ public static IEnumerable<BaseItem> FilterEpisodesBySeason(IEnumerable<BaseItem> episodes, Season parentSeason, bool includeSpecials)
+ {
+ var seasonNumber = parentSeason.IndexNumber;
+ var seasonPresentationKey = GetUniqueSeriesKey(parentSeason);
+
+ var supportSpecialsInSeason = includeSpecials && seasonNumber.HasValue && seasonNumber.Value != 0;
+
+ return episodes.Where(episode =>
+ {
+ var episodeItem = (Episode)episode;
+
+ var currentSeasonNumber = supportSpecialsInSeason ? episodeItem.AiredSeasonNumber : episode.ParentIndexNumber;
+ if (currentSeasonNumber.HasValue && seasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber.Value)
+ {
+ return true;
+ }
+
+ if (!currentSeasonNumber.HasValue && !seasonNumber.HasValue && parentSeason.LocationType == LocationType.Virtual)
+ {
+ return true;
+ }
+
+ var season = episodeItem.Season;
+ return season != null && string.Equals(GetUniqueSeriesKey(season), seasonPresentationKey, StringComparison.OrdinalIgnoreCase);
+ });
+ }
+
+ /// <summary>
+ /// Filters the episodes by season.
+ /// </summary>
+ public static IEnumerable<Episode> FilterEpisodesBySeason(IEnumerable<Episode> episodes, int seasonNumber, bool includeSpecials)
+ {
+ if (!includeSpecials || seasonNumber < 1)
+ {
+ return episodes.Where(i => (i.ParentIndexNumber ?? -1) == seasonNumber);
+ }
+
+ return episodes.Where(i =>
+ {
+ var episode = i;
+
+ if (episode != null)
+ {
+ var currentSeasonNumber = episode.AiredSeasonNumber;
+
+ return currentSeasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber;
+ }
+
+ return false;
+ });
+ }
+
+
+ protected override bool GetBlockUnratedValue(UserPolicy config)
+ {
+ return config.BlockUnratedItems.Contains(UnratedItem.Series);
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ return UnratedItem.Series;
+ }
+
+ public SeriesInfo GetLookupInfo()
+ {
+ var info = GetItemLookupInfo<SeriesInfo>();
+
+ return info;
+ }
+
+ public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
+ {
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
+
+ if (!ProductionYear.HasValue)
+ {
+ var info = LibraryManager.ParseName(Name);
+
+ var yearInName = info.Year;
+
+ if (yearInName.HasValue)
+ {
+ ProductionYear = yearInName;
+ hasChanges = true;
+ }
+ }
+
+ return hasChanges;
+ }
+
+ public override List<ExternalUrl> GetRelatedUrls()
+ {
+ var list = base.GetRelatedUrls();
+
+ var imdbId = this.GetProviderId(MetadataProviders.Imdb);
+ if (!string.IsNullOrEmpty(imdbId))
+ {
+ list.Add(new ExternalUrl
+ {
+ Name = "Trakt",
+ Url = string.Format("https://trakt.tv/shows/{0}", imdbId)
+ });
+ }
+
+ return list;
+ }
+
+ [IgnoreDataMember]
+ public override bool StopRefreshIfLocalMetadataFound
+ {
+ get
+ {
+ // Need people id's from internet metadata
+ return false;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/TagExtensions.cs b/MediaBrowser.Controller/Entities/TagExtensions.cs
new file mode 100644
index 000000000..e5d8f35d9
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/TagExtensions.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public static class TagExtensions
+ {
+ public static void AddTag(this BaseItem item, string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentNullException("name");
+ }
+
+ var current = item.Tags;
+
+ if (!current.Contains(name, StringComparer.OrdinalIgnoreCase))
+ {
+ if (current.Length == 0)
+ {
+ item.Tags = new[] { name };
+ }
+ else
+ {
+ var list = current.ToArray(current.Length + 1);
+ list[list.Length - 1] = name;
+
+ item.Tags = list;
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs
new file mode 100644
index 000000000..4f2a5631b
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Trailer.cs
@@ -0,0 +1,111 @@
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Serialization;
+using System;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class Trailer
+ /// </summary>
+ public class Trailer : Video, IHasLookupInfo<TrailerInfo>
+ {
+ public Trailer()
+ {
+ TrailerTypes = new TrailerType[] { };
+ }
+
+ public TrailerType[] TrailerTypes { get; set; }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ double value = 2;
+ value /= 3;
+
+ return value;
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ return UnratedItem.Trailer;
+ }
+
+ public TrailerInfo GetLookupInfo()
+ {
+ var info = GetItemLookupInfo<TrailerInfo>();
+
+ if (!IsInMixedFolder && IsFileProtocol)
+ {
+ info.Name = System.IO.Path.GetFileName(ContainingFolderPath);
+ }
+
+ return info;
+ }
+
+ public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ {
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+
+ if (!ProductionYear.HasValue)
+ {
+ var info = LibraryManager.ParseName(Name);
+
+ var yearInName = info.Year;
+
+ if (yearInName.HasValue)
+ {
+ ProductionYear = yearInName;
+ hasChanges = true;
+ }
+ else
+ {
+ // Try to get the year from the folder name
+ if (!IsInMixedFolder)
+ {
+ info = LibraryManager.ParseName(System.IO.Path.GetFileName(ContainingFolderPath));
+
+ yearInName = info.Year;
+
+ if (yearInName.HasValue)
+ {
+ ProductionYear = yearInName;
+ hasChanges = true;
+ }
+ }
+ }
+ }
+
+ return hasChanges;
+ }
+
+ public override List<ExternalUrl> GetRelatedUrls()
+ {
+ var list = base.GetRelatedUrls();
+
+ var imdbId = this.GetProviderId(MetadataProviders.Imdb);
+ if (!string.IsNullOrEmpty(imdbId))
+ {
+ list.Add(new ExternalUrl
+ {
+ Name = "Trakt",
+ Url = string.Format("https://trakt.tv/movies/{0}", imdbId)
+ });
+ }
+
+ return list;
+ }
+
+ [IgnoreDataMember]
+ public override bool StopRefreshIfLocalMetadataFound
+ {
+ get
+ {
+ // Need people id's from internet metadata
+ return false;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs
new file mode 100644
index 000000000..f569c8021
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/User.cs
@@ -0,0 +1,342 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Connect;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Users;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class User
+ /// </summary>
+ public class User : BaseItem
+ {
+ public static IUserManager UserManager { get; set; }
+ public static IXmlSerializer XmlSerializer { get; set; }
+
+ /// <summary>
+ /// From now on all user paths will be Id-based.
+ /// This is for backwards compatibility.
+ /// </summary>
+ public bool UsesIdForConfigurationPath { get; set; }
+
+ /// <summary>
+ /// Gets or sets the password.
+ /// </summary>
+ /// <value>The password.</value>
+ public string Password { get; set; }
+ public string EasyPassword { get; set; }
+ public string Salt { get; set; }
+
+ public string ConnectUserName { get; set; }
+ public string ConnectUserId { get; set; }
+ public UserLinkType? ConnectLinkType { get; set; }
+ public string ConnectAccessKey { get; set; }
+
+ // Strictly to remove IgnoreDataMember
+ public override ItemImageInfo[] ImageInfos
+ {
+ get
+ {
+ return base.ImageInfos;
+ }
+ set
+ {
+ base.ImageInfos = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ [IgnoreDataMember]
+ public override string Path
+ {
+ get
+ {
+ // Return this so that metadata providers will look in here
+ return ConfigurationDirectoryPath;
+ }
+ set
+ {
+ base.Path = value;
+ }
+ }
+
+ private string _name;
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public override string Name
+ {
+ get
+ {
+ return _name;
+ }
+ set
+ {
+ _name = value;
+
+ // lazy load this again
+ SortName = null;
+ }
+ }
+
+ /// <summary>
+ /// Returns the folder containing the item.
+ /// If the item is a folder, it returns the folder itself
+ /// </summary>
+ /// <value>The containing folder path.</value>
+ [IgnoreDataMember]
+ public override string ContainingFolderPath
+ {
+ get
+ {
+ return Path;
+ }
+ }
+
+ /// <summary>
+ /// Gets the root folder.
+ /// </summary>
+ /// <value>The root folder.</value>
+ [IgnoreDataMember]
+ public Folder RootFolder
+ {
+ get
+ {
+ return LibraryManager.GetUserRootFolder();
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the last login date.
+ /// </summary>
+ /// <value>The last login date.</value>
+ public DateTime? LastLoginDate { get; set; }
+ /// <summary>
+ /// Gets or sets the last activity date.
+ /// </summary>
+ /// <value>The last activity date.</value>
+ public DateTime? LastActivityDate { get; set; }
+
+ private volatile UserConfiguration _config;
+ private readonly object _configSyncLock = new object();
+ [IgnoreDataMember]
+ public UserConfiguration Configuration
+ {
+ get
+ {
+ if (_config == null)
+ {
+ lock (_configSyncLock)
+ {
+ if (_config == null)
+ {
+ _config = UserManager.GetUserConfiguration(this);
+ }
+ }
+ }
+
+ return _config;
+ }
+ set { _config = value; }
+ }
+
+ private volatile UserPolicy _policy;
+ private readonly object _policySyncLock = new object();
+ [IgnoreDataMember]
+ public UserPolicy Policy
+ {
+ get
+ {
+ if (_policy == null)
+ {
+ lock (_policySyncLock)
+ {
+ if (_policy == null)
+ {
+ _policy = UserManager.GetUserPolicy(this);
+ }
+ }
+ }
+
+ return _policy;
+ }
+ set { _policy = value; }
+ }
+
+ /// <summary>
+ /// Renames the user.
+ /// </summary>
+ /// <param name="newName">The new name.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public Task Rename(string newName)
+ {
+ if (string.IsNullOrEmpty(newName))
+ {
+ throw new ArgumentNullException("newName");
+ }
+
+ // If only the casing is changing, leave the file system alone
+ if (!UsesIdForConfigurationPath && !string.Equals(newName, Name, StringComparison.OrdinalIgnoreCase))
+ {
+ UsesIdForConfigurationPath = true;
+
+ // Move configuration
+ var newConfigDirectory = GetConfigurationDirectoryPath(newName);
+ var oldConfigurationDirectory = ConfigurationDirectoryPath;
+
+ // Exceptions will be thrown if these paths already exist
+ if (FileSystem.DirectoryExists(newConfigDirectory))
+ {
+ FileSystem.DeleteDirectory(newConfigDirectory, true);
+ }
+
+ if (FileSystem.DirectoryExists(oldConfigurationDirectory))
+ {
+ FileSystem.MoveDirectory(oldConfigurationDirectory, newConfigDirectory);
+ }
+ else
+ {
+ FileSystem.CreateDirectory(newConfigDirectory);
+ }
+ }
+
+ Name = newName;
+
+ return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem))
+ {
+ ReplaceAllMetadata = true,
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+ MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
+ ForceSave = true
+
+ }, CancellationToken.None);
+ }
+
+ public override void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken)
+ {
+ UserManager.UpdateUser(this);
+ }
+
+ /// <summary>
+ /// Gets the path to the user's configuration directory
+ /// </summary>
+ /// <value>The configuration directory path.</value>
+ [IgnoreDataMember]
+ public string ConfigurationDirectoryPath
+ {
+ get
+ {
+ return GetConfigurationDirectoryPath(Name);
+ }
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ return 1;
+ }
+
+ /// <summary>
+ /// Gets the configuration directory path.
+ /// </summary>
+ /// <param name="username">The username.</param>
+ /// <returns>System.String.</returns>
+ private string GetConfigurationDirectoryPath(string username)
+ {
+ var parentPath = ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath;
+
+ // Legacy
+ if (!UsesIdForConfigurationPath)
+ {
+ if (string.IsNullOrEmpty(username))
+ {
+ throw new ArgumentNullException("username");
+ }
+
+ var safeFolderName = FileSystem.GetValidFilename(username);
+
+ return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, safeFolderName);
+ }
+
+ return System.IO.Path.Combine(parentPath, Id.ToString("N"));
+ }
+
+ public bool IsParentalScheduleAllowed()
+ {
+ return IsParentalScheduleAllowed(DateTime.UtcNow);
+ }
+
+ public bool IsParentalScheduleAllowed(DateTime date)
+ {
+ var schedules = Policy.AccessSchedules;
+
+ if (schedules.Length == 0)
+ {
+ return true;
+ }
+
+ foreach (var i in schedules)
+ {
+ if (IsParentalScheduleAllowed(i, date))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date)
+ {
+ if (date.Kind != DateTimeKind.Utc)
+ {
+ throw new ArgumentException("Utc date expected");
+ }
+
+ var localTime = date.ToLocalTime();
+
+ return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek) &&
+ IsWithinTime(schedule, localTime);
+ }
+
+ private bool IsWithinTime(AccessSchedule schedule, DateTime localTime)
+ {
+ var hour = localTime.TimeOfDay.TotalHours;
+
+ return hour >= schedule.StartHour && hour <= schedule.EndHour;
+ }
+
+ public bool IsFolderGrouped(Guid id)
+ {
+ foreach (var i in Configuration.GroupedFolders)
+ {
+ if (new Guid(i) == id)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public long InternalId { get; set;}
+
+
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs
new file mode 100644
index 000000000..0e1326949
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/UserItemData.cs
@@ -0,0 +1,124 @@
+using System;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class UserItemData
+ /// </summary>
+ public class UserItemData
+ {
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ /// <value>The user id.</value>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the key.
+ /// </summary>
+ /// <value>The key.</value>
+ public string Key { get; set; }
+
+ /// <summary>
+ /// The _rating
+ /// </summary>
+ private double? _rating;
+ /// <summary>
+ /// Gets or sets the users 0-10 rating
+ /// </summary>
+ /// <value>The rating.</value>
+ /// <exception cref="System.ArgumentOutOfRangeException">Rating;A 0 to 10 rating is required for UserItemData.</exception>
+ public double? Rating
+ {
+ get
+ {
+ return _rating;
+ }
+ set
+ {
+ if (value.HasValue)
+ {
+ if (value.Value < 0 || value.Value > 10)
+ {
+ throw new ArgumentOutOfRangeException("value", "A 0 to 10 rating is required for UserItemData.");
+ }
+ }
+
+ _rating = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the playback position ticks.
+ /// </summary>
+ /// <value>The playback position ticks.</value>
+ public long PlaybackPositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the play count.
+ /// </summary>
+ /// <value>The play count.</value>
+ public int PlayCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is favorite.
+ /// </summary>
+ /// <value><c>true</c> if this instance is favorite; otherwise, <c>false</c>.</value>
+ public bool IsFavorite { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last played date.
+ /// </summary>
+ /// <value>The last played date.</value>
+ public DateTime? LastPlayedDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="UserItemData" /> is played.
+ /// </summary>
+ /// <value><c>true</c> if played; otherwise, <c>false</c>.</value>
+ public bool Played { get; set; }
+ /// <summary>
+ /// Gets or sets the index of the audio stream.
+ /// </summary>
+ /// <value>The index of the audio stream.</value>
+ public int? AudioStreamIndex { get; set; }
+ /// <summary>
+ /// Gets or sets the index of the subtitle stream.
+ /// </summary>
+ /// <value>The index of the subtitle stream.</value>
+ public int? SubtitleStreamIndex { get; set; }
+
+ public const double MinLikeValue = 6.5;
+
+ /// <summary>
+ /// This is an interpreted property to indicate likes or dislikes
+ /// This should never be serialized.
+ /// </summary>
+ /// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool? Likes
+ {
+ get
+ {
+ if (Rating != null)
+ {
+ return Rating >= MinLikeValue;
+ }
+
+ return null;
+ }
+ set
+ {
+ if (value.HasValue)
+ {
+ Rating = value.Value ? 10 : 1;
+ }
+ else
+ {
+ Rating = null;
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs
new file mode 100644
index 000000000..f8e843d92
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs
@@ -0,0 +1,158 @@
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Library;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Special class used for User Roots. Children contain actual ones defined for this user
+ /// PLUS the virtual folders from the physical root (added by plug-ins).
+ /// </summary>
+ public class UserRootFolder : Folder
+ {
+ private List<Guid> _childrenIds = null;
+ private readonly object _childIdsLock = new object();
+ protected override List<BaseItem> LoadChildren()
+ {
+ lock (_childIdsLock)
+ {
+ if (_childrenIds == null)
+ {
+ var list = base.LoadChildren();
+ _childrenIds = list.Select(i => i.Id).ToList();
+ return list;
+ }
+
+ return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList();
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsInheritedParentImages
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPlayedStatus
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ private void ClearCache()
+ {
+ lock (_childIdsLock)
+ {
+ _childrenIds = null;
+ }
+ }
+
+ protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
+ {
+ if (query.Recursive)
+ {
+ return QueryRecursive(query);
+ }
+
+ var result = UserViewManager.GetUserViews(new UserViewQuery
+ {
+ UserId = query.User.Id,
+ PresetViews = query.PresetViews
+ });
+
+ var itemsArray = result;
+ var totalCount = itemsArray.Length;
+
+ return new QueryResult<BaseItem>
+ {
+ TotalRecordCount = totalCount,
+ Items = itemsArray
+ };
+ }
+
+ public override int GetChildCount(User user)
+ {
+ return GetChildren(user, true).Count;
+ }
+
+ [IgnoreDataMember]
+ protected override bool SupportsShortcutChildren
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool IsPreSorted
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
+ {
+ var list = base.GetEligibleChildrenForRecursiveChildren(user).ToList();
+ list.AddRange(LibraryManager.RootFolder.VirtualChildren);
+
+ return list;
+ }
+
+ public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ {
+ ClearCache();
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+
+ if (string.Equals("default", Name, StringComparison.OrdinalIgnoreCase))
+ {
+ Name = "Media Folders";
+ hasChanges = true;
+ }
+
+ return hasChanges;
+ }
+
+ protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
+ {
+ ClearCache();
+
+ return base.GetNonCachedChildren(directoryService);
+ }
+
+ protected override async Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ {
+ ClearCache();
+
+ await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService)
+ .ConfigureAwait(false);
+
+ ClearCache();
+
+ // Not the best way to handle this, but it solves an issue
+ // CollectionFolders aren't always getting saved after changes
+ // This means that grabbing the item by Id may end up returning the old one
+ // Fix is in two places - make sure the folder gets saved
+ // And here to remedy it for affected users.
+ // In theory this can be removed eventually.
+ foreach (var item in Children)
+ {
+ LibraryManager.RegisterItem(item);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs
new file mode 100644
index 000000000..984cad481
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/UserView.cs
@@ -0,0 +1,210 @@
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Controller.TV;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.Serialization;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Collections;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class UserView : Folder, IHasCollectionType
+ {
+ public string ViewType { get; set; }
+ public Guid DisplayParentId { get; set; }
+
+ public Guid? UserId { get; set; }
+
+ public static ITVSeriesManager TVSeriesManager;
+ public static IPlaylistManager PlaylistManager;
+
+ [IgnoreDataMember]
+ public string CollectionType
+ {
+ get
+ {
+ return ViewType;
+ }
+ }
+
+ public override IEnumerable<Guid> GetIdsForAncestorQuery()
+ {
+ var list = new List<Guid>();
+
+ if (!DisplayParentId.Equals(Guid.Empty))
+ {
+ list.Add(DisplayParentId);
+ }
+ else if (!ParentId.Equals(Guid.Empty))
+ {
+ list.Add(ParentId);
+ }
+ else
+ {
+ list.Add(Id);
+ }
+ return list;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsInheritedParentImages
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPlayedStatus
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ //public override double? GetDefaultPrimaryImageAspectRatio()
+ //{
+ // double value = 16;
+ // value /= 9;
+
+ // return value;
+ //}
+
+ public override int GetChildCount(User user)
+ {
+ return GetChildren(user, true).Count;
+ }
+
+ protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
+ {
+ var parent = this as Folder;
+
+ if (!DisplayParentId.Equals(Guid.Empty))
+ {
+ parent = LibraryManager.GetItemById(DisplayParentId) as Folder ?? parent;
+ }
+ else if (!ParentId.Equals(Guid.Empty))
+ {
+ parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent;
+ }
+
+ return new UserViewBuilder(UserViewManager, LibraryManager, Logger, UserDataManager, TVSeriesManager, ConfigurationManager, PlaylistManager)
+ .GetUserItems(parent, this, CollectionType, query);
+ }
+
+ public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
+ {
+ if (query == null)
+ {
+ query = new InternalItemsQuery(user);
+ }
+
+ query.EnableTotalRecordCount = false;
+ var result = GetItemList(query);
+
+ return result.ToList();
+ }
+
+ public override bool CanDelete()
+ {
+ return false;
+ }
+
+ public override bool IsSaveLocalMetadataEnabled()
+ {
+ return true;
+ }
+
+ public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
+ {
+ query.SetUser(user);
+ query.Recursive = true;
+ query.EnableTotalRecordCount = false;
+ query.ForceDirect = true;
+
+ return GetItemList(query);
+ }
+
+ protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
+ {
+ return GetChildren(user, false);
+ }
+
+ private static string[] UserSpecificViewTypes = new string[]
+ {
+ MediaBrowser.Model.Entities.CollectionType.Playlists
+ };
+
+ public static bool IsUserSpecific(Folder folder)
+ {
+ var collectionFolder = folder as ICollectionFolder;
+
+ if (collectionFolder == null)
+ {
+ return false;
+ }
+
+ var supportsUserSpecific = folder as ISupportsUserSpecificView;
+ if (supportsUserSpecific != null && supportsUserSpecific.EnableUserSpecificView)
+ {
+ return true;
+ }
+
+ return UserSpecificViewTypes.Contains(collectionFolder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ }
+
+ public static bool IsEligibleForGrouping(Folder folder)
+ {
+ var collectionFolder = folder as ICollectionFolder;
+ return collectionFolder != null && IsEligibleForGrouping(collectionFolder.CollectionType);
+ }
+
+ private static string[] ViewTypesEligibleForGrouping = new string[]
+ {
+ MediaBrowser.Model.Entities.CollectionType.Movies,
+ MediaBrowser.Model.Entities.CollectionType.TvShows,
+ string.Empty
+ };
+
+ public static bool IsEligibleForGrouping(string viewType)
+ {
+ return ViewTypesEligibleForGrouping.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ }
+
+ private static string[] OriginalFolderViewTypes = new string[]
+ {
+ MediaBrowser.Model.Entities.CollectionType.Games,
+ MediaBrowser.Model.Entities.CollectionType.Books,
+ MediaBrowser.Model.Entities.CollectionType.MusicVideos,
+ MediaBrowser.Model.Entities.CollectionType.HomeVideos,
+ MediaBrowser.Model.Entities.CollectionType.Photos,
+ MediaBrowser.Model.Entities.CollectionType.Music,
+ MediaBrowser.Model.Entities.CollectionType.BoxSets
+ };
+
+ public static bool EnableOriginalFolder(string viewType)
+ {
+ return OriginalFolderViewTypes.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ }
+
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, System.Threading.CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService)
+ {
+ return Task.FromResult(true);
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get
+ {
+ return false;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
new file mode 100644
index 000000000..36035a2bb
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -0,0 +1,1070 @@
+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.TV;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Collections;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Controller.Entities
+{
+ public class UserViewBuilder
+ {
+ private readonly IUserViewManager _userViewManager;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger _logger;
+ private readonly IUserDataManager _userDataManager;
+ private readonly ITVSeriesManager _tvSeriesManager;
+ private readonly IServerConfigurationManager _config;
+ private readonly IPlaylistManager _playlistManager;
+
+ public UserViewBuilder(IUserViewManager userViewManager, ILibraryManager libraryManager, ILogger logger, IUserDataManager userDataManager, ITVSeriesManager tvSeriesManager, IServerConfigurationManager config, IPlaylistManager playlistManager)
+ {
+ _userViewManager = userViewManager;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _userDataManager = userDataManager;
+ _tvSeriesManager = tvSeriesManager;
+ _config = config;
+ _playlistManager = playlistManager;
+ }
+
+ public QueryResult<BaseItem> GetUserItems(Folder queryParent, Folder displayParent, string viewType, InternalItemsQuery query)
+ {
+ var user = query.User;
+
+ //if (query.IncludeItemTypes != null &&
+ // query.IncludeItemTypes.Length == 1 &&
+ // string.Equals(query.IncludeItemTypes[0], "Playlist", StringComparison.OrdinalIgnoreCase))
+ //{
+ // if (!string.Equals(viewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+ // {
+ // return await FindPlaylists(queryParent, user, query).ConfigureAwait(false);
+ // }
+ //}
+
+ switch (viewType)
+ {
+ case CollectionType.Folders:
+ return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), queryParent, query);
+
+ case CollectionType.TvShows:
+ return GetTvView(queryParent, user, query);
+
+ case CollectionType.Movies:
+ return GetMovieFolders(queryParent, user, query);
+
+ case SpecialFolder.TvShowSeries:
+ return GetTvSeries(queryParent, user, query);
+
+ case SpecialFolder.TvGenres:
+ return GetTvGenres(queryParent, user, query);
+
+ case SpecialFolder.TvGenre:
+ return GetTvGenreItems(queryParent, displayParent, user, query);
+
+ case SpecialFolder.TvResume:
+ return GetTvResume(queryParent, user, query);
+
+ case SpecialFolder.TvNextUp:
+ return GetTvNextUp(queryParent, query);
+
+ case SpecialFolder.TvLatest:
+ return GetTvLatest(queryParent, user, query);
+
+ case SpecialFolder.MovieFavorites:
+ return GetFavoriteMovies(queryParent, user, query);
+
+ case SpecialFolder.MovieLatest:
+ return GetMovieLatest(queryParent, user, query);
+
+ case SpecialFolder.MovieGenres:
+ return GetMovieGenres(queryParent, user, query);
+
+ case SpecialFolder.MovieGenre:
+ return GetMovieGenreItems(queryParent, displayParent, user, query);
+
+ case SpecialFolder.MovieResume:
+ return GetMovieResume(queryParent, user, query);
+
+ case SpecialFolder.MovieMovies:
+ return GetMovieMovies(queryParent, user, query);
+
+ case SpecialFolder.MovieCollections:
+ return GetMovieCollections(queryParent, user, query);
+
+ case SpecialFolder.TvFavoriteEpisodes:
+ return GetFavoriteEpisodes(queryParent, user, query);
+
+ case SpecialFolder.TvFavoriteSeries:
+ return GetFavoriteSeries(queryParent, user, query);
+
+ default:
+ {
+ if (queryParent is UserView)
+ {
+ return GetResult(GetMediaFolders(user).OfType<Folder>().SelectMany(i => i.GetChildren(user, true)), queryParent, query);
+ }
+ return queryParent.GetItems(query);
+ }
+ }
+ }
+
+ private int GetSpecialItemsLimit()
+ {
+ return 50;
+ }
+
+ private QueryResult<BaseItem> GetMovieFolders(Folder parent, User user, InternalItemsQuery query)
+ {
+ if (query.Recursive)
+ {
+ query.Recursive = true;
+ query.SetUser(user);
+
+ if (query.IncludeItemTypes.Length == 0)
+ {
+ query.IncludeItemTypes = new[] { typeof(Movie).Name };
+ }
+
+ return parent.QueryRecursive(query);
+ }
+
+ var list = new List<BaseItem>();
+
+ list.Add(GetUserView(SpecialFolder.MovieResume, "HeaderContinueWatching", "0", parent));
+ list.Add(GetUserView(SpecialFolder.MovieLatest, "Latest", "1", parent));
+ list.Add(GetUserView(SpecialFolder.MovieMovies, "Movies", "2", parent));
+ list.Add(GetUserView(SpecialFolder.MovieCollections, "Collections", "3", parent));
+ list.Add(GetUserView(SpecialFolder.MovieFavorites, "Favorites", "4", parent));
+ list.Add(GetUserView(SpecialFolder.MovieGenres, "Genres", "5", parent));
+
+ return GetResult(list, parent, query);
+ }
+
+ private QueryResult<BaseItem> GetFavoriteMovies(Folder parent, User user, InternalItemsQuery query)
+ {
+ query.Recursive = true;
+ query.Parent = parent;
+ query.SetUser(user);
+ query.IsFavorite = true;
+ query.IncludeItemTypes = new[] { typeof(Movie).Name };
+
+ return _libraryManager.GetItemsResult(query);
+ }
+
+ private QueryResult<BaseItem> GetFavoriteSeries(Folder parent, User user, InternalItemsQuery query)
+ {
+ query.Recursive = true;
+ query.Parent = parent;
+ query.SetUser(user);
+ query.IsFavorite = true;
+ query.IncludeItemTypes = new[] { typeof(Series).Name };
+
+ return _libraryManager.GetItemsResult(query);
+ }
+
+ private QueryResult<BaseItem> GetFavoriteEpisodes(Folder parent, User user, InternalItemsQuery query)
+ {
+ query.Recursive = true;
+ query.Parent = parent;
+ query.SetUser(user);
+ query.IsFavorite = true;
+ query.IncludeItemTypes = new[] { typeof(Episode).Name };
+
+ return _libraryManager.GetItemsResult(query);
+ }
+
+ private QueryResult<BaseItem> GetMovieMovies(Folder parent, User user, InternalItemsQuery query)
+ {
+ query.Recursive = true;
+ query.Parent = parent;
+ query.SetUser(user);
+
+ query.IncludeItemTypes = new[] { typeof(Movie).Name };
+
+ return _libraryManager.GetItemsResult(query);
+ }
+
+ private QueryResult<BaseItem> GetMovieCollections(Folder parent, User user, InternalItemsQuery query)
+ {
+ query.Parent = null;
+ query.IncludeItemTypes = new[] { typeof(BoxSet).Name };
+ query.SetUser(user);
+ query.Recursive = true;
+
+ return _libraryManager.GetItemsResult(query);
+ }
+
+ private QueryResult<BaseItem> GetMovieLatest(Folder parent, User user, InternalItemsQuery query)
+ {
+ query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray();
+
+ query.Recursive = true;
+ query.Parent = parent;
+ query.SetUser(user);
+ query.Limit = GetSpecialItemsLimit();
+ query.IncludeItemTypes = new[] { typeof(Movie).Name };
+
+ return ConvertToResult(_libraryManager.GetItemList(query));
+ }
+
+ private QueryResult<BaseItem> GetMovieResume(Folder parent, User user, InternalItemsQuery query)
+ {
+ query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray();
+ query.IsResumable = true;
+ query.Recursive = true;
+ query.Parent = parent;
+ query.SetUser(user);
+ query.Limit = GetSpecialItemsLimit();
+ query.IncludeItemTypes = new[] { typeof(Movie).Name };
+
+ return ConvertToResult(_libraryManager.GetItemList(query));
+ }
+
+ private QueryResult<BaseItem> ConvertToResult(List<BaseItem> items)
+ {
+ var arr = items.ToArray();
+ return new QueryResult<BaseItem>
+ {
+ Items = arr,
+ TotalRecordCount = arr.Length
+ };
+ }
+
+ private QueryResult<BaseItem> GetMovieGenres(Folder parent, User user, InternalItemsQuery query)
+ {
+ var genres = parent.QueryRecursive(new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[] { typeof(Movie).Name },
+ Recursive = true,
+ EnableTotalRecordCount = false
+
+ }).Items
+ .SelectMany(i => i.Genres)
+ .DistinctNames()
+ .Select(i =>
+ {
+ try
+ {
+ return _libraryManager.GetGenre(i);
+ }
+ catch
+ {
+ // Full exception logged at lower levels
+ _logger.Error("Error getting genre");
+ return null;
+ }
+
+ })
+ .Where(i => i != null)
+ .Select(i => GetUserViewWithName(i.Name, SpecialFolder.MovieGenre, i.SortName, parent));
+
+ return GetResult(genres, parent, query);
+ }
+
+ private QueryResult<BaseItem> GetMovieGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query)
+ {
+ query.Recursive = true;
+ query.Parent = queryParent;
+ query.GenreIds = new[] { displayParent.Id };
+ query.SetUser(user);
+
+ query.IncludeItemTypes = new[] { typeof(Movie).Name };
+
+ return _libraryManager.GetItemsResult(query);
+ }
+
+ private QueryResult<BaseItem> GetTvView(Folder parent, User user, InternalItemsQuery query)
+ {
+ if (query.Recursive)
+ {
+ query.Recursive = true;
+ query.SetUser(user);
+
+ if (query.IncludeItemTypes.Length == 0)
+ {
+ query.IncludeItemTypes = new[] { typeof(Series).Name, typeof(Season).Name, typeof(Episode).Name };
+ }
+
+ return parent.QueryRecursive(query);
+ }
+
+ var list = new List<BaseItem>();
+
+ list.Add(GetUserView(SpecialFolder.TvResume, "HeaderContinueWatching", "0", parent));
+ list.Add(GetUserView(SpecialFolder.TvNextUp, "HeaderNextUp", "1", parent));
+ list.Add(GetUserView(SpecialFolder.TvLatest, "Latest", "2", parent));
+ list.Add(GetUserView(SpecialFolder.TvShowSeries, "Shows", "3", parent));
+ list.Add(GetUserView(SpecialFolder.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent));
+ list.Add(GetUserView(SpecialFolder.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent));
+ list.Add(GetUserView(SpecialFolder.TvGenres, "Genres", "6", parent));
+
+ return GetResult(list, parent, query);
+ }
+
+ private QueryResult<BaseItem> GetTvLatest(Folder parent, User user, InternalItemsQuery query)
+ {
+ query.OrderBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray();
+
+ query.Recursive = true;
+ query.Parent = parent;
+ query.SetUser(user);
+ query.Limit = GetSpecialItemsLimit();
+ query.IncludeItemTypes = new[] { typeof(Episode).Name };
+ query.IsVirtualItem = false;
+
+ return ConvertToResult(_libraryManager.GetItemList(query));
+ }
+
+ private QueryResult<BaseItem> GetTvNextUp(Folder parent, InternalItemsQuery query)
+ {
+ var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows, string.Empty });
+
+ var result = _tvSeriesManager.GetNextUp(new NextUpQuery
+ {
+ Limit = query.Limit,
+ StartIndex = query.StartIndex,
+ UserId = query.User.Id
+
+ }, parentFolders, query.DtoOptions);
+
+ return result;
+ }
+
+ private QueryResult<BaseItem> GetTvResume(Folder parent, User user, InternalItemsQuery query)
+ {
+ query.OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray();
+ query.IsResumable = true;
+ query.Recursive = true;
+ query.Parent = parent;
+ query.SetUser(user);
+ query.Limit = GetSpecialItemsLimit();
+ query.IncludeItemTypes = new[] { typeof(Episode).Name };
+
+ return ConvertToResult(_libraryManager.GetItemList(query));
+ }
+
+ private QueryResult<BaseItem> GetTvSeries(Folder parent, User user, InternalItemsQuery query)
+ {
+ query.Recursive = true;
+ query.Parent = parent;
+ query.SetUser(user);
+
+ query.IncludeItemTypes = new[] { typeof(Series).Name };
+
+ return _libraryManager.GetItemsResult(query);
+ }
+
+ private QueryResult<BaseItem> GetTvGenres(Folder parent, User user, InternalItemsQuery query)
+ {
+ var genres = parent.QueryRecursive(new InternalItemsQuery(user)
+ {
+ IncludeItemTypes = new[] { typeof(Series).Name },
+ Recursive = true,
+ EnableTotalRecordCount = false
+
+ }).Items
+ .SelectMany(i => i.Genres)
+ .DistinctNames()
+ .Select(i =>
+ {
+ try
+ {
+ return _libraryManager.GetGenre(i);
+ }
+ catch
+ {
+ // Full exception logged at lower levels
+ _logger.Error("Error getting genre");
+ return null;
+ }
+
+ })
+ .Where(i => i != null)
+ .Select(i => GetUserViewWithName(i.Name, SpecialFolder.TvGenre, i.SortName, parent));
+
+ return GetResult(genres, parent, query);
+ }
+
+ private QueryResult<BaseItem> GetTvGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query)
+ {
+ query.Recursive = true;
+ query.Parent = queryParent;
+ query.GenreIds = new[] { displayParent.Id };
+ query.SetUser(user);
+
+ query.IncludeItemTypes = new[] { typeof(Series).Name };
+
+ return _libraryManager.GetItemsResult(query);
+ }
+
+ private QueryResult<BaseItem> GetResult<T>(QueryResult<T> result)
+ where T : BaseItem
+ {
+ return new QueryResult<BaseItem>
+ {
+ Items = result.Items,
+ TotalRecordCount = result.TotalRecordCount
+ };
+ }
+
+ private QueryResult<BaseItem> GetResult<T>(IEnumerable<T> items,
+ BaseItem queryParent,
+ InternalItemsQuery query)
+ where T : BaseItem
+ {
+ items = items.Where(i => Filter(i, query.User, query, _userDataManager, _libraryManager));
+
+ return PostFilterAndSort(items, queryParent, null, query, _libraryManager, _config);
+ }
+
+ public static bool FilterItem(BaseItem item, InternalItemsQuery query)
+ {
+ return Filter(item, query.User, query, BaseItem.UserDataManager, BaseItem.LibraryManager);
+ }
+
+ public static QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items,
+ BaseItem queryParent,
+ int? totalRecordLimit,
+ InternalItemsQuery query,
+ ILibraryManager libraryManager,
+ IServerConfigurationManager configurationManager)
+ {
+ var user = query.User;
+
+ // This must be the last filter
+ if (!string.IsNullOrEmpty(query.AdjacentTo))
+ {
+ items = FilterForAdjacency(items.ToList(), query.AdjacentTo);
+ }
+
+ return SortAndPage(items, totalRecordLimit, query, libraryManager, true);
+ }
+
+ public static QueryResult<BaseItem> SortAndPage(IEnumerable<BaseItem> items,
+ int? totalRecordLimit,
+ InternalItemsQuery query,
+ ILibraryManager libraryManager, bool enableSorting)
+ {
+ if (enableSorting)
+ {
+ if (query.OrderBy.Length > 0)
+ {
+ items = libraryManager.Sort(items, query.User, query.OrderBy);
+ }
+ }
+
+ var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray();
+ var totalCount = itemsArray.Length;
+
+ if (query.Limit.HasValue)
+ {
+ itemsArray = itemsArray.Skip(query.StartIndex ?? 0).Take(query.Limit.Value).ToArray();
+ }
+ else if (query.StartIndex.HasValue)
+ {
+ itemsArray = itemsArray.Skip(query.StartIndex.Value).ToArray();
+ }
+
+ return new QueryResult<BaseItem>
+ {
+ TotalRecordCount = totalCount,
+ Items = itemsArray
+ };
+ }
+
+ public static bool Filter(BaseItem item, User user, InternalItemsQuery query, IUserDataManager userDataManager, ILibraryManager libraryManager)
+ {
+ if (query.MediaTypes.Length > 0 && !query.MediaTypes.Contains(item.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ if (query.IncludeItemTypes.Length > 0 && !query.IncludeItemTypes.Contains(item.GetClientTypeName(), StringComparer.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ if (query.ExcludeItemTypes.Length > 0 && query.ExcludeItemTypes.Contains(item.GetClientTypeName(), StringComparer.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ if (query.IsVirtualItem.HasValue && item.IsVirtualItem != query.IsVirtualItem.Value)
+ {
+ return false;
+ }
+
+ if (query.IsFolder.HasValue && query.IsFolder.Value != item.IsFolder)
+ {
+ return false;
+ }
+
+ UserItemData userData = null;
+
+ if (query.IsLiked.HasValue)
+ {
+ userData = userData ?? userDataManager.GetUserData(user, item);
+
+ if (!userData.Likes.HasValue || userData.Likes != query.IsLiked.Value)
+ {
+ return false;
+ }
+ }
+
+ if (query.IsFavoriteOrLiked.HasValue)
+ {
+ userData = userData ?? userDataManager.GetUserData(user, item);
+ var isFavoriteOrLiked = userData.IsFavorite || (userData.Likes ?? false);
+
+ if (isFavoriteOrLiked != query.IsFavoriteOrLiked.Value)
+ {
+ return false;
+ }
+ }
+
+ if (query.IsFavorite.HasValue)
+ {
+ userData = userData ?? userDataManager.GetUserData(user, item);
+
+ if (userData.IsFavorite != query.IsFavorite.Value)
+ {
+ return false;
+ }
+ }
+
+ if (query.IsResumable.HasValue)
+ {
+ userData = userData ?? userDataManager.GetUserData(user, item);
+ var isResumable = userData.PlaybackPositionTicks > 0;
+
+ if (isResumable != query.IsResumable.Value)
+ {
+ return false;
+ }
+ }
+
+ if (query.IsPlayed.HasValue)
+ {
+ if (item.IsPlayed(user) != query.IsPlayed.Value)
+ {
+ return false;
+ }
+ }
+
+ // Filter by Video3DFormat
+ if (query.Is3D.HasValue)
+ {
+ var val = query.Is3D.Value;
+ var video = item as Video;
+
+ if (video == null || val != video.Video3DFormat.HasValue)
+ {
+ return false;
+ }
+ }
+
+ /*
+ * fuck - fix this
+ if (query.IsHD.HasValue)
+ {
+ if (item.IsHD != query.IsHD.Value)
+ {
+ return false;
+ }
+ }
+ */
+
+ if (query.IsLocked.HasValue)
+ {
+ var val = query.IsLocked.Value;
+ if (item.IsLocked != val)
+ {
+ return false;
+ }
+ }
+
+ if (query.HasOverview.HasValue)
+ {
+ var filterValue = query.HasOverview.Value;
+
+ var hasValue = !string.IsNullOrEmpty(item.Overview);
+
+ if (hasValue != filterValue)
+ {
+ return false;
+ }
+ }
+
+ if (query.HasImdbId.HasValue)
+ {
+ var filterValue = query.HasImdbId.Value;
+
+ var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Imdb));
+
+ if (hasValue != filterValue)
+ {
+ return false;
+ }
+ }
+
+ if (query.HasTmdbId.HasValue)
+ {
+ var filterValue = query.HasTmdbId.Value;
+
+ var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tmdb));
+
+ if (hasValue != filterValue)
+ {
+ return false;
+ }
+ }
+
+ if (query.HasTvdbId.HasValue)
+ {
+ var filterValue = query.HasTvdbId.Value;
+
+ var hasValue = !string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tvdb));
+
+ if (hasValue != filterValue)
+ {
+ return false;
+ }
+ }
+
+ if (query.HasOfficialRating.HasValue)
+ {
+ var filterValue = query.HasOfficialRating.Value;
+
+ var hasValue = !string.IsNullOrEmpty(item.OfficialRating);
+
+ if (hasValue != filterValue)
+ {
+ return false;
+ }
+ }
+
+ if (query.IsPlaceHolder.HasValue)
+ {
+ var filterValue = query.IsPlaceHolder.Value;
+
+ var isPlaceHolder = false;
+
+ var hasPlaceHolder = item as ISupportsPlaceHolders;
+
+ if (hasPlaceHolder != null)
+ {
+ isPlaceHolder = hasPlaceHolder.IsPlaceHolder;
+ }
+
+ if (isPlaceHolder != filterValue)
+ {
+ return false;
+ }
+ }
+
+ if (query.HasSpecialFeature.HasValue)
+ {
+ var filterValue = query.HasSpecialFeature.Value;
+
+ var movie = item as IHasSpecialFeatures;
+
+ if (movie != null)
+ {
+ var ok = filterValue
+ ? movie.SpecialFeatureIds.Length > 0
+ : movie.SpecialFeatureIds.Length == 0;
+
+ if (!ok)
+ {
+ return false;
+ }
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ if (query.HasSubtitles.HasValue)
+ {
+ var val = query.HasSubtitles.Value;
+
+ var video = item as Video;
+
+ if (video == null || val != video.HasSubtitles)
+ {
+ return false;
+ }
+ }
+
+ if (query.HasParentalRating.HasValue)
+ {
+ var val = query.HasParentalRating.Value;
+
+ var rating = item.CustomRating;
+
+ if (string.IsNullOrEmpty(rating))
+ {
+ rating = item.OfficialRating;
+ }
+
+ if (val)
+ {
+ if (string.IsNullOrEmpty(rating))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (!string.IsNullOrEmpty(rating))
+ {
+ return false;
+ }
+ }
+ }
+
+ if (query.HasTrailer.HasValue)
+ {
+ var val = query.HasTrailer.Value;
+ var trailerCount = 0;
+
+ var hasTrailers = item as IHasTrailers;
+ if (hasTrailers != null)
+ {
+ trailerCount = hasTrailers.GetTrailerIds().Count;
+ }
+
+ var ok = val ? trailerCount > 0 : trailerCount == 0;
+
+ if (!ok)
+ {
+ return false;
+ }
+ }
+
+ if (query.HasThemeSong.HasValue)
+ {
+ var filterValue = query.HasThemeSong.Value;
+
+ var themeCount = item.ThemeSongIds.Length;
+ var ok = filterValue ? themeCount > 0 : themeCount == 0;
+
+ if (!ok)
+ {
+ return false;
+ }
+ }
+
+ if (query.HasThemeVideo.HasValue)
+ {
+ var filterValue = query.HasThemeVideo.Value;
+
+ var themeCount = item.ThemeVideoIds.Length;
+ var ok = filterValue ? themeCount > 0 : themeCount == 0;
+
+ if (!ok)
+ {
+ return false;
+ }
+ }
+
+ // Apply genre filter
+ if (query.Genres.Length > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase)))
+ {
+ return false;
+ }
+
+ // Filter by VideoType
+ if (query.VideoTypes.Length > 0)
+ {
+ var video = item as Video;
+ if (video == null || !query.VideoTypes.Contains(video.VideoType))
+ {
+ return false;
+ }
+ }
+
+ if (query.ImageTypes.Length > 0 && !query.ImageTypes.Any(item.HasImage))
+ {
+ return false;
+ }
+
+ // Apply studio filter
+ if (query.StudioIds.Length > 0 && !query.StudioIds.Any(id =>
+ {
+ var studioItem = libraryManager.GetItemById(id);
+ return studioItem != null && item.Studios.Contains(studioItem.Name, StringComparer.OrdinalIgnoreCase);
+ }))
+ {
+ return false;
+ }
+
+ // Apply genre filter
+ if (query.GenreIds.Length > 0 && !query.GenreIds.Any(id =>
+ {
+ var genreItem = libraryManager.GetItemById(id);
+ return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparer.OrdinalIgnoreCase);
+ }))
+ {
+ return false;
+ }
+
+ // Apply year filter
+ if (query.Years.Length > 0)
+ {
+ if (!(item.ProductionYear.HasValue && query.Years.Contains(item.ProductionYear.Value)))
+ {
+ return false;
+ }
+ }
+
+ // Apply official rating filter
+ if (query.OfficialRatings.Length > 0 && !query.OfficialRatings.Contains(item.OfficialRating ?? string.Empty))
+ {
+ return false;
+ }
+
+ if (query.ItemIds.Length > 0)
+ {
+ if (!query.ItemIds.Contains(item.Id))
+ {
+ return false;
+ }
+ }
+
+ // Apply tag filter
+ var tags = query.Tags;
+ if (tags.Length > 0)
+ {
+ if (!tags.Any(v => item.Tags.Contains(v, StringComparer.OrdinalIgnoreCase)))
+ {
+ return false;
+ }
+ }
+
+ if (query.MinPlayers.HasValue)
+ {
+ var filterValue = query.MinPlayers.Value;
+
+ var game = item as Game;
+
+ if (game != null)
+ {
+ var players = game.PlayersSupported ?? 1;
+
+ var ok = players >= filterValue;
+
+ if (!ok)
+ {
+ return false;
+ }
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ if (query.MaxPlayers.HasValue)
+ {
+ var filterValue = query.MaxPlayers.Value;
+
+ var game = item as Game;
+
+ if (game != null)
+ {
+ var players = game.PlayersSupported ?? 1;
+
+ var ok = players <= filterValue;
+
+ if (!ok)
+ {
+ return false;
+ }
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ if (query.MinCommunityRating.HasValue)
+ {
+ var val = query.MinCommunityRating.Value;
+
+ if (!(item.CommunityRating.HasValue && item.CommunityRating >= val))
+ {
+ return false;
+ }
+ }
+
+ if (query.MinCriticRating.HasValue)
+ {
+ var val = query.MinCriticRating.Value;
+
+ if (!(item.CriticRating.HasValue && item.CriticRating >= val))
+ {
+ return false;
+ }
+ }
+
+ if (query.MinIndexNumber.HasValue)
+ {
+ var val = query.MinIndexNumber.Value;
+
+ if (!(item.IndexNumber.HasValue && item.IndexNumber.Value >= val))
+ {
+ return false;
+ }
+ }
+
+ if (query.MinPremiereDate.HasValue)
+ {
+ var val = query.MinPremiereDate.Value;
+
+ if (!(item.PremiereDate.HasValue && item.PremiereDate.Value >= val))
+ {
+ return false;
+ }
+ }
+
+ if (query.MaxPremiereDate.HasValue)
+ {
+ var val = query.MaxPremiereDate.Value;
+
+ if (!(item.PremiereDate.HasValue && item.PremiereDate.Value <= val))
+ {
+ return false;
+ }
+ }
+
+ if (query.ParentIndexNumber.HasValue)
+ {
+ var filterValue = query.ParentIndexNumber.Value;
+
+ if (item.ParentIndexNumber.HasValue && item.ParentIndexNumber.Value != filterValue)
+ {
+ return false;
+ }
+ }
+
+ if (query.SeriesStatuses.Length > 0)
+ {
+ var ok = new[] { item }.OfType<Series>().Any(p => p.Status.HasValue && query.SeriesStatuses.Contains(p.Status.Value));
+ if (!ok)
+ {
+ return false;
+ }
+ }
+
+ if (query.AiredDuringSeason.HasValue)
+ {
+ var episode = item as Episode;
+
+ if (episode == null)
+ {
+ return false;
+ }
+
+ if (!Series.FilterEpisodesBySeason(new[] { episode }, query.AiredDuringSeason.Value, true).Any())
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private IEnumerable<BaseItem> GetMediaFolders(User user)
+ {
+ if (user == null)
+ {
+ return _libraryManager.RootFolder
+ .Children
+ .OfType<Folder>()
+ .Where(UserView.IsEligibleForGrouping);
+ }
+ return _libraryManager.GetUserRootFolder()
+ .GetChildren(user, true)
+ .OfType<Folder>()
+ .Where(i => user.IsFolderGrouped(i.Id) && UserView.IsEligibleForGrouping(i));
+ }
+
+ private BaseItem[] GetMediaFolders(User user, IEnumerable<string> viewTypes)
+ {
+ if (user == null)
+ {
+ return GetMediaFolders(null)
+ .Where(i =>
+ {
+ var folder = i as ICollectionFolder;
+
+ return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ }).ToArray();
+ }
+ return GetMediaFolders(user)
+ .Where(i =>
+ {
+ var folder = i as ICollectionFolder;
+
+ return folder != null && viewTypes.Contains(folder.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ }).ToArray();
+ }
+
+ private BaseItem[] GetMediaFolders(Folder parent, User user, IEnumerable<string> viewTypes)
+ {
+ if (parent == null || parent is UserView)
+ {
+ return GetMediaFolders(user, viewTypes);
+ }
+
+ return new BaseItem[] { parent };
+ }
+
+ private UserView GetUserViewWithName(string name, string type, string sortName, BaseItem parent)
+ {
+ return _userViewManager.GetUserSubView(parent.Id, parent.Id.ToString("N"), type, sortName);
+ }
+
+ private UserView GetUserView(string type, string localizationKey, string sortName, BaseItem parent)
+ {
+ return _userViewManager.GetUserSubView(parent.Id, type, localizationKey, sortName);
+ }
+
+ public static IEnumerable<BaseItem> FilterForAdjacency(List<BaseItem> list, string adjacentToId)
+ {
+ var adjacentToIdGuid = new Guid(adjacentToId);
+ var adjacentToItem = list.FirstOrDefault(i => i.Id == adjacentToIdGuid);
+
+ var index = list.IndexOf(adjacentToItem);
+
+ var previousId = Guid.Empty;
+ var nextId = Guid.Empty;
+
+ if (index > 0)
+ {
+ previousId = list[index - 1].Id;
+ }
+
+ if (index < list.Count - 1)
+ {
+ nextId = list[index + 1].Id;
+ }
+
+ return list.Where(i => i.Id == previousId || i.Id == nextId || i.Id == adjacentToIdGuid);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
new file mode 100644
index 000000000..65f5b8382
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -0,0 +1,626 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.MediaInfo;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.LiveTv;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class Video
+ /// </summary>
+ public class Video : BaseItem,
+ IHasAspectRatio,
+ ISupportsPlaceHolders,
+ IHasMediaSources
+ {
+ [IgnoreDataMember]
+ public string PrimaryVersionId { get; set; }
+
+ public string[] AdditionalParts { get; set; }
+ public string[] LocalAlternateVersions { get; set; }
+ public LinkedChild[] LinkedAlternateVersions { get; set; }
+
+ [IgnoreDataMember]
+ public override bool SupportsPlayedStatus
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get { return true; }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsInheritedParentImages
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPositionTicksResume
+ {
+ get
+ {
+ var extraType = ExtraType;
+ if (extraType.HasValue)
+ {
+ if (extraType.Value == Model.Entities.ExtraType.Sample)
+ {
+ return false;
+ }
+ if (extraType.Value == Model.Entities.ExtraType.ThemeVideo)
+ {
+ return false;
+ }
+ if (extraType.Value == Model.Entities.ExtraType.Trailer)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ public void SetPrimaryVersionId(string id)
+ {
+ if (string.IsNullOrEmpty(id))
+ {
+ PrimaryVersionId = null;
+ }
+ else
+ {
+ PrimaryVersionId = id;
+ }
+
+ PresentationUniqueKey = CreatePresentationUniqueKey();
+ }
+
+ public override string CreatePresentationUniqueKey()
+ {
+ if (!string.IsNullOrEmpty(PrimaryVersionId))
+ {
+ return PrimaryVersionId;
+ }
+
+ return base.CreatePresentationUniqueKey();
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsThemeMedia
+ {
+ get { return true; }
+ }
+
+ /// <summary>
+ /// Gets or sets the timestamp.
+ /// </summary>
+ /// <value>The timestamp.</value>
+ public TransportStreamTimestamp? Timestamp { get; set; }
+
+ /// <summary>
+ /// Gets or sets the subtitle paths.
+ /// </summary>
+ /// <value>The subtitle paths.</value>
+ public string[] SubtitleFiles { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has subtitles.
+ /// </summary>
+ /// <value><c>true</c> if this instance has subtitles; otherwise, <c>false</c>.</value>
+ public bool HasSubtitles { get; set; }
+
+ public bool IsPlaceHolder { get; set; }
+ public bool IsShortcut { get; set; }
+ public string ShortcutPath { get; set; }
+
+ /// <summary>
+ /// Gets or sets the default index of the video stream.
+ /// </summary>
+ /// <value>The default index of the video stream.</value>
+ public int? DefaultVideoStreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the video.
+ /// </summary>
+ /// <value>The type of the video.</value>
+ public VideoType VideoType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the iso.
+ /// </summary>
+ /// <value>The type of the iso.</value>
+ public IsoType? IsoType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the video3 D format.
+ /// </summary>
+ /// <value>The video3 D format.</value>
+ public Video3DFormat? Video3DFormat { get; set; }
+
+ public string[] GetPlayableStreamFileNames(IMediaEncoder mediaEncoder)
+ {
+ var videoType = VideoType;
+
+ if (videoType == VideoType.Iso && IsoType == Model.Entities.IsoType.BluRay)
+ {
+ videoType = VideoType.BluRay;
+ }
+ else if (videoType == VideoType.Iso && IsoType == Model.Entities.IsoType.Dvd)
+ {
+ videoType = VideoType.Dvd;
+ }
+ else
+ {
+ return new string[] {};
+ }
+ return mediaEncoder.GetPlayableStreamFileNames(Path, videoType);
+ }
+
+ /// <summary>
+ /// Gets or sets the aspect ratio.
+ /// </summary>
+ /// <value>The aspect ratio.</value>
+ public string AspectRatio { get; set; }
+
+ public Video()
+ {
+ AdditionalParts = new string[] {};
+ LocalAlternateVersions = new string[] {};
+ SubtitleFiles = new string[] {};
+ LinkedAlternateVersions = EmptyLinkedChildArray;
+ }
+
+ public override bool CanDownload()
+ {
+ if (VideoType == VideoType.Dvd || VideoType == VideoType.BluRay)
+ {
+ return false;
+ }
+
+ return IsFileProtocol;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsAddingToPlaylist
+ {
+ get { return true; }
+ }
+
+ [IgnoreDataMember]
+ public int MediaSourceCount
+ {
+ get
+ {
+ if (!string.IsNullOrEmpty(PrimaryVersionId))
+ {
+ var item = LibraryManager.GetItemById(PrimaryVersionId) as Video;
+ if (item != null)
+ {
+ return item.MediaSourceCount;
+ }
+ }
+ return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
+ }
+ }
+
+ [IgnoreDataMember]
+ public bool IsStacked
+ {
+ get { return AdditionalParts.Length > 0; }
+ }
+
+ [IgnoreDataMember]
+ public override bool HasLocalAlternateVersions
+ {
+ get { return LocalAlternateVersions.Length > 0; }
+ }
+
+ public IEnumerable<Guid> GetAdditionalPartIds()
+ {
+ return AdditionalParts.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
+ }
+
+ public IEnumerable<Guid> GetLocalAlternateVersionIds()
+ {
+ return LocalAlternateVersions.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
+ }
+
+ public static ILiveTvManager LiveTvManager { get; set; }
+
+ [IgnoreDataMember]
+ public override SourceType SourceType
+ {
+ get
+ {
+ if (IsActiveRecording())
+ {
+ return SourceType.LiveTV;
+ }
+
+ return base.SourceType;
+ }
+ }
+
+ protected override bool IsActiveRecording()
+ {
+ return LiveTvManager.GetActiveRecordingInfo(Path) != null;
+ }
+
+ public override bool CanDelete()
+ {
+ if (IsActiveRecording())
+ {
+ return false;
+ }
+
+ return base.CanDelete();
+ }
+
+ [IgnoreDataMember]
+ public bool IsCompleteMedia
+ {
+ get
+ {
+ if (SourceType == SourceType.Channel)
+ {
+ return !Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase);
+ }
+
+ return !IsActiveRecording();
+ }
+ }
+
+ [IgnoreDataMember]
+ protected virtual bool EnableDefaultVideoUserDataKeys
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ if (EnableDefaultVideoUserDataKeys)
+ {
+ if (ExtraType.HasValue)
+ {
+ var key = this.GetProviderId(MetadataProviders.Tmdb);
+ if (!string.IsNullOrEmpty(key))
+ {
+ list.Insert(0, GetUserDataKey(key));
+ }
+
+ key = this.GetProviderId(MetadataProviders.Imdb);
+ if (!string.IsNullOrEmpty(key))
+ {
+ list.Insert(0, GetUserDataKey(key));
+ }
+ }
+ else
+ {
+ var key = this.GetProviderId(MetadataProviders.Imdb);
+ if (!string.IsNullOrEmpty(key))
+ {
+ list.Insert(0, key);
+ }
+
+ key = this.GetProviderId(MetadataProviders.Tmdb);
+ if (!string.IsNullOrEmpty(key))
+ {
+ list.Insert(0, key);
+ }
+ }
+ }
+
+ return list;
+ }
+
+ private string GetUserDataKey(string providerId)
+ {
+ var key = providerId + "-" + ExtraType.ToString().ToLower();
+
+ // Make sure different trailers have their own data.
+ if (RunTimeTicks.HasValue)
+ {
+ key += "-" + RunTimeTicks.Value.ToString(CultureInfo.InvariantCulture);
+ }
+
+ return key;
+ }
+
+ public IEnumerable<Video> GetLinkedAlternateVersions()
+ {
+ return LinkedAlternateVersions
+ .Select(GetLinkedChild)
+ .Where(i => i != null)
+ .OfType<Video>()
+ .OrderBy(i => i.SortName);
+ }
+
+ /// <summary>
+ /// Gets the additional parts.
+ /// </summary>
+ /// <returns>IEnumerable{Video}.</returns>
+ public IEnumerable<Video> GetAdditionalParts()
+ {
+ return GetAdditionalPartIds()
+ .Select(i => LibraryManager.GetItemById(i))
+ .Where(i => i != null)
+ .OfType<Video>()
+ .OrderBy(i => i.SortName);
+ }
+
+ [IgnoreDataMember]
+ public override string ContainingFolderPath
+ {
+ get
+ {
+ if (IsStacked)
+ {
+ return FileSystem.GetDirectoryName(Path);
+ }
+
+ if (!IsPlaceHolder)
+ {
+ if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
+ {
+ return Path;
+ }
+ }
+
+ return base.ContainingFolderPath;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override string FileNameWithoutExtension
+ {
+ get
+ {
+ if (IsFileProtocol)
+ {
+ if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
+ {
+ return System.IO.Path.GetFileName(Path);
+ }
+
+ return System.IO.Path.GetFileNameWithoutExtension(Path);
+ }
+
+ return null;
+ }
+ }
+
+ internal override ItemUpdateType UpdateFromResolvedItem(BaseItem newItem)
+ {
+ var updateType = base.UpdateFromResolvedItem(newItem);
+
+ var newVideo = newItem as Video;
+ if (newVideo != null)
+ {
+ if (!AdditionalParts.SequenceEqual(newVideo.AdditionalParts, StringComparer.Ordinal))
+ {
+ AdditionalParts = newVideo.AdditionalParts;
+ updateType |= ItemUpdateType.MetadataImport;
+ }
+ if (!LocalAlternateVersions.SequenceEqual(newVideo.LocalAlternateVersions, StringComparer.Ordinal))
+ {
+ LocalAlternateVersions = newVideo.LocalAlternateVersions;
+ updateType |= ItemUpdateType.MetadataImport;
+ }
+ if (VideoType != newVideo.VideoType)
+ {
+ VideoType = newVideo.VideoType;
+ updateType |= ItemUpdateType.MetadataImport;
+ }
+ }
+
+ return updateType;
+ }
+
+ public static string[] QueryPlayableStreamFiles(string rootPath, VideoType videoType)
+ {
+ if (videoType == VideoType.Dvd)
+ {
+ return FileSystem.GetFiles(rootPath, new[] { ".vob" }, false, true)
+ .OrderByDescending(i => i.Length)
+ .ThenBy(i => i.FullName)
+ .Take(1)
+ .Select(i => i.FullName)
+ .ToArray();
+ }
+ if (videoType == VideoType.BluRay)
+ {
+ return FileSystem.GetFiles(rootPath, new[] { ".m2ts" }, false, true)
+ .OrderByDescending(i => i.Length)
+ .ThenBy(i => i.FullName)
+ .Take(1)
+ .Select(i => i.FullName)
+ .ToArray();
+ }
+ return new string[] {};
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether [is3 D].
+ /// </summary>
+ /// <value><c>true</c> if [is3 D]; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool Is3D
+ {
+ get { return Video3DFormat.HasValue; }
+ }
+
+ /// <summary>
+ /// Gets the type of the media.
+ /// </summary>
+ /// <value>The type of the media.</value>
+ [IgnoreDataMember]
+ public override string MediaType
+ {
+ get
+ {
+ return Model.Entities.MediaType.Video;
+ }
+ }
+
+ protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
+ {
+ var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
+
+ if (IsStacked)
+ {
+ var tasks = AdditionalParts
+ .Select(i => RefreshMetadataForOwnedVideo(options, true, i, cancellationToken));
+
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+ }
+
+ // Must have a parent to have additional parts or alternate versions
+ // In other words, it must be part of the Parent/Child tree
+ // The additional parts won't have additional parts themselves
+ if (IsFileProtocol && SupportsOwnedItems)
+ {
+ if (!IsStacked)
+ {
+ RefreshLinkedAlternateVersions();
+
+ var tasks = LocalAlternateVersions
+ .Select(i => RefreshMetadataForOwnedVideo(options, false, i, cancellationToken));
+
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+ }
+ }
+
+ return hasChanges;
+ }
+
+ private void RefreshLinkedAlternateVersions()
+ {
+ foreach (var child in LinkedAlternateVersions)
+ {
+ // Reset the cached value
+ if (child.ItemId.HasValue && child.ItemId.Value.Equals(Guid.Empty))
+ {
+ child.ItemId = null;
+ }
+ }
+ }
+
+ public override void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken)
+ {
+ base.UpdateToRepository(updateReason, cancellationToken);
+
+ var localAlternates = GetLocalAlternateVersionIds()
+ .Select(i => LibraryManager.GetItemById(i))
+ .Where(i => i != null);
+
+ foreach (var item in localAlternates)
+ {
+ item.ImageInfos = ImageInfos;
+ item.Overview = Overview;
+ item.ProductionYear = ProductionYear;
+ item.PremiereDate = PremiereDate;
+ item.CommunityRating = CommunityRating;
+ item.OfficialRating = OfficialRating;
+ item.Genres = Genres;
+ item.ProviderIds = ProviderIds;
+
+ item.UpdateToRepository(ItemUpdateType.MetadataDownload, cancellationToken);
+ }
+ }
+
+ public override IEnumerable<FileSystemMetadata> GetDeletePaths()
+ {
+ if (!IsInMixedFolder)
+ {
+ return new[] {
+ new FileSystemMetadata
+ {
+ FullName = ContainingFolderPath,
+ IsDirectory = true
+ }
+ };
+ }
+
+ return base.GetDeletePaths();
+ }
+
+ public virtual MediaStream GetDefaultVideoStream()
+ {
+ if (!DefaultVideoStreamIndex.HasValue)
+ {
+ return null;
+ }
+
+ return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
+ {
+ ItemId = Id,
+ Index = DefaultVideoStreamIndex.Value
+
+ }).FirstOrDefault();
+ }
+
+ protected override List<Tuple<BaseItem, MediaSourceType>> GetAllItemsForMediaSources()
+ {
+ var list = new List<Tuple<BaseItem, MediaSourceType>>();
+
+ list.Add(new Tuple<BaseItem, MediaSourceType>(this, MediaSourceType.Default));
+ list.AddRange(GetLinkedAlternateVersions().Select(i => new Tuple<BaseItem, MediaSourceType>(i, MediaSourceType.Grouping)));
+
+ if (!string.IsNullOrEmpty(PrimaryVersionId))
+ {
+ var primary = LibraryManager.GetItemById(PrimaryVersionId) as Video;
+ if (primary != null)
+ {
+ var existingIds = list.Select(i => i.Item1.Id).ToList();
+ list.Add(new Tuple<BaseItem, MediaSourceType>(primary, MediaSourceType.Grouping));
+ list.AddRange(primary.GetLinkedAlternateVersions().Where(i => !existingIds.Contains(i.Id)).Select(i => new Tuple<BaseItem, MediaSourceType>(i, MediaSourceType.Grouping)));
+ }
+ }
+
+ var localAlternates = list
+ .SelectMany(i =>
+ {
+ var video = i.Item1 as Video;
+ return video == null ? new List<Guid>() : video.GetLocalAlternateVersionIds();
+ })
+ .Select(LibraryManager.GetItemById)
+ .Where(i => i != null)
+ .ToList();
+
+ list.AddRange(localAlternates.Select(i => new Tuple<BaseItem, MediaSourceType>(i, MediaSourceType.Default)));
+
+ return list;
+ }
+
+ public static bool IsHD (Video video) {
+ return video.Height >= 720;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs
new file mode 100644
index 000000000..81e030cea
--- /dev/null
+++ b/MediaBrowser.Controller/Entities/Year.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.Entities
+{
+ /// <summary>
+ /// Class Year
+ /// </summary>
+ public class Year : BaseItem, IItemByName
+ {
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ list.Insert(0, "Year-" + Name);
+ return list;
+ }
+
+ /// <summary>
+ /// Returns the folder containing the item.
+ /// If the item is a folder, it returns the folder itself
+ /// </summary>
+ /// <value>The containing folder path.</value>
+ [IgnoreDataMember]
+ public override string ContainingFolderPath
+ {
+ get
+ {
+ return Path;
+ }
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ double value = 2;
+ value /= 3;
+
+ return value;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsAncestors
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public override bool CanDelete()
+ {
+ return false;
+ }
+
+ public override bool IsSaveLocalMetadataEnabled()
+ {
+ return true;
+ }
+
+ public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+ {
+ int year;
+
+ var usCulture = new CultureInfo("en-US");
+
+ if (!int.TryParse(Name, NumberStyles.Integer, usCulture, out year))
+ {
+ return new List<BaseItem>();
+ }
+
+ query.Years = new[] { year };
+
+ return LibraryManager.GetItemList(query);
+ }
+
+ public int? GetYearValue()
+ {
+ int i;
+
+ if (int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out i))
+ {
+ return i;
+ }
+
+ return null;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public static string GetPath(string name)
+ {
+ return GetPath(name, true);
+ }
+
+ public static string GetPath(string name, bool normalizeName)
+ {
+ // Trim the period at the end because windows will have a hard time with that
+ var validName = normalizeName ?
+ FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
+ name;
+
+ return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.YearPath, validName);
+ }
+
+ private string GetRebasedPath()
+ {
+ return GetPath(System.IO.Path.GetFileName(Path), false);
+ }
+
+ public override bool RequiresRefresh()
+ {
+ var newPath = GetRebasedPath();
+ if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+ {
+ Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
+ return true;
+ }
+ return base.RequiresRefresh();
+ }
+
+ /// <summary>
+ /// This is called before any metadata refresh and returns true or false indicating if changes were made
+ /// </summary>
+ public override bool BeforeMetadataRefresh(bool replaceAllMetdata)
+ {
+ var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata);
+
+ var newPath = GetRebasedPath();
+ if (!string.Equals(Path, newPath, StringComparison.Ordinal))
+ {
+ Path = newPath;
+ hasChanges = true;
+ }
+
+ return hasChanges;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Extensions/StringExtensions.cs b/MediaBrowser.Controller/Extensions/StringExtensions.cs
new file mode 100644
index 000000000..60e7815db
--- /dev/null
+++ b/MediaBrowser.Controller/Extensions/StringExtensions.cs
@@ -0,0 +1,17 @@
+using MediaBrowser.Model.Globalization;
+
+namespace MediaBrowser.Controller.Extensions
+{
+ /// <summary>
+ /// Class BaseExtensions
+ /// </summary>
+ public static class StringExtensions
+ {
+ public static ILocalizationManager LocalizationManager { get; set; }
+
+ public static string RemoveDiacritics(this string text)
+ {
+ return LocalizationManager.RemoveDiacritics(text);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs
new file mode 100644
index 000000000..e1fabee4f
--- /dev/null
+++ b/MediaBrowser.Controller/IO/FileData.cs
@@ -0,0 +1,123 @@
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.IO;
+
+namespace MediaBrowser.Controller.IO
+{
+ /// <summary>
+ /// Provides low level File access that is much faster than the File/Directory api's
+ /// </summary>
+ public static class FileData
+ {
+ private static Dictionary<string, FileSystemMetadata> GetFileSystemDictionary(FileSystemMetadata[] list)
+ {
+ var dict = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var file in list)
+ {
+ dict[file.FullName] = file;
+ }
+ return dict;
+ }
+
+ /// <summary>
+ /// Gets the filtered file system entries.
+ /// </summary>
+ /// <param name="directoryService">The directory service.</param>
+ /// <param name="path">The path.</param>
+ /// <param name="fileSystem">The file system.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="args">The args.</param>
+ /// <param name="flattenFolderDepth">The flatten folder depth.</param>
+ /// <param name="resolveShortcuts">if set to <c>true</c> [resolve shortcuts].</param>
+ /// <returns>Dictionary{System.StringFileSystemInfo}.</returns>
+ /// <exception cref="System.ArgumentNullException">path</exception>
+ public static FileSystemMetadata[] GetFilteredFileSystemEntries(IDirectoryService directoryService,
+ string path,
+ IFileSystem fileSystem,
+ IServerApplicationHost appHost,
+ ILogger logger,
+ ItemResolveArgs args,
+ int flattenFolderDepth = 0,
+ bool resolveShortcuts = true)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+ if (args == null)
+ {
+ throw new ArgumentNullException("args");
+ }
+
+ var entries = directoryService.GetFileSystemEntries(path);
+
+ if (!resolveShortcuts && flattenFolderDepth == 0)
+ {
+ return entries;
+ }
+
+ var dict = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var entry in entries)
+ {
+ var isDirectory = entry.IsDirectory;
+
+ var fullName = entry.FullName;
+
+ if (resolveShortcuts && fileSystem.IsShortcut(fullName))
+ {
+ try
+ {
+ var newPath = appHost.ExpandVirtualPath(fileSystem.ResolveShortcut(fullName));
+
+ if (string.IsNullOrEmpty(newPath))
+ {
+ //invalid shortcut - could be old or target could just be unavailable
+ logger.Warn("Encountered invalid shortcut: " + fullName);
+ continue;
+ }
+
+ // Don't check if it exists here because that could return false for network shares.
+ var data = fileSystem.GetDirectoryInfo(newPath);
+
+ // add to our physical locations
+ args.AddAdditionalLocation(newPath);
+
+ dict[newPath] = data;
+ }
+ catch (Exception ex)
+ {
+ logger.ErrorException("Error resolving shortcut from {0}", ex, fullName);
+ }
+ }
+ else if (flattenFolderDepth > 0 && isDirectory)
+ {
+ foreach (var child in GetFilteredFileSystemEntries(directoryService, fullName, fileSystem, appHost, logger, args, flattenFolderDepth: flattenFolderDepth - 1, resolveShortcuts: resolveShortcuts))
+ {
+ dict[child.FullName] = child;
+ }
+ }
+ else
+ {
+ dict[fullName] = entry;
+ }
+ }
+
+ var returnResult = new FileSystemMetadata[dict.Count];
+ var index = 0;
+ var values = dict.Values;
+ foreach (var value in values)
+ {
+ returnResult[index] = value;
+ index++;
+ }
+ return returnResult;
+ }
+
+ }
+
+}
diff --git a/MediaBrowser.Controller/IResourceFileManager.cs b/MediaBrowser.Controller/IResourceFileManager.cs
new file mode 100644
index 000000000..ae73f4b4c
--- /dev/null
+++ b/MediaBrowser.Controller/IResourceFileManager.cs
@@ -0,0 +1,32 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Plugins;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Reflection;
+using MediaBrowser.Model.Services;
+
+namespace MediaBrowser.Controller
+{
+ public interface IResourceFileManager
+ {
+ Task<object> GetStaticFileResult(IRequest request, string basePath, string virtualPath, string contentType, TimeSpan? cacheDuration);
+
+ Stream GetResourceFileStream(string basePath, string virtualPath);
+
+ string ReadAllText(string basePath, string virtualPath);
+ }
+}
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
new file mode 100644
index 000000000..830c160e3
--- /dev/null
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -0,0 +1,100 @@
+using MediaBrowser.Common;
+using MediaBrowser.Model.System;
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Net;
+using System.Threading;
+
+namespace MediaBrowser.Controller
+{
+ /// <summary>
+ /// Interface IServerApplicationHost
+ /// </summary>
+ public interface IServerApplicationHost : IApplicationHost
+ {
+ event EventHandler HasUpdateAvailableChanged;
+
+ /// <summary>
+ /// Gets the system info.
+ /// </summary>
+ /// <returns>SystemInfo.</returns>
+ Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken);
+
+ Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets a value indicating whether [supports automatic run at startup].
+ /// </summary>
+ /// <value><c>true</c> if [supports automatic run at startup]; otherwise, <c>false</c>.</value>
+ bool SupportsAutoRunAtStartup { get; }
+
+ bool CanLaunchWebBrowser { get; }
+
+ /// <summary>
+ /// Gets the HTTP server port.
+ /// </summary>
+ /// <value>The HTTP server port.</value>
+ int HttpPort { get; }
+
+ /// <summary>
+ /// Gets the HTTPS port.
+ /// </summary>
+ /// <value>The HTTPS port.</value>
+ int HttpsPort { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether [supports HTTPS].
+ /// </summary>
+ /// <value><c>true</c> if [supports HTTPS]; otherwise, <c>false</c>.</value>
+ bool EnableHttps { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance has update available.
+ /// </summary>
+ /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
+ bool HasUpdateAvailable { get; }
+
+ /// <summary>
+ /// Gets the name of the friendly.
+ /// </summary>
+ /// <value>The name of the friendly.</value>
+ string FriendlyName { get; }
+
+ /// <summary>
+ /// Gets the local ip address.
+ /// </summary>
+ /// <value>The local ip address.</value>
+ Task<List<IpAddressInfo>> GetLocalIpAddresses(CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the local API URL.
+ /// </summary>
+ /// <value>The local API URL.</value>
+ Task<string> GetLocalApiUrl(CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the local API URL.
+ /// </summary>
+ /// <param name="host">The host.</param>
+ /// <returns>System.String.</returns>
+ string GetLocalApiUrl(string host);
+
+ /// <summary>
+ /// Gets the local API URL.
+ /// </summary>
+ string GetLocalApiUrl(IpAddressInfo address);
+
+ void LaunchUrl(string url);
+
+ void EnableLoopback(string appName);
+
+ string PackageRuntime { get; }
+
+ WakeOnLanInfo[] GetWakeOnLanInfo();
+
+ string ExpandVirtualPath(string path);
+ string ReverseVirtualPath(string path);
+ }
+}
diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs
new file mode 100644
index 000000000..5fb7968dd
--- /dev/null
+++ b/MediaBrowser.Controller/IServerApplicationPaths.cs
@@ -0,0 +1,109 @@
+using MediaBrowser.Common.Configuration;
+
+namespace MediaBrowser.Controller
+{
+ public interface IServerApplicationPaths : IApplicationPaths
+ {
+ /// <summary>
+ /// Gets the path to the base root media directory
+ /// </summary>
+ /// <value>The root folder path.</value>
+ string RootFolderPath { get; }
+
+ /// <summary>
+ /// Gets the application resources path. This is the path to the folder containing resources that are deployed as part of the application
+ /// For example, this folder contains dashboard-ui and swagger-ui
+ /// </summary>
+ /// <value>The application resources path.</value>
+ string ApplicationResourcesPath { get; }
+
+ /// <summary>
+ /// Gets the path to the default user view directory. Used if no specific user view is defined.
+ /// </summary>
+ /// <value>The default user views path.</value>
+ string DefaultUserViewsPath { get; }
+
+ /// <summary>
+ /// Gets the path to localization data.
+ /// </summary>
+ /// <value>The localization path.</value>
+ string LocalizationPath { get; }
+
+ /// <summary>
+ /// Gets the path to the People directory
+ /// </summary>
+ /// <value>The people path.</value>
+ string PeoplePath { get; }
+
+ /// <summary>
+ /// Gets the path to the Genre directory
+ /// </summary>
+ /// <value>The genre path.</value>
+ string GenrePath { get; }
+
+ /// <summary>
+ /// Gets the music genre path.
+ /// </summary>
+ /// <value>The music genre path.</value>
+ string MusicGenrePath { get; }
+
+ /// <summary>
+ /// Gets the game genre path.
+ /// </summary>
+ /// <value>The game genre path.</value>
+ string GameGenrePath { get; }
+
+ /// <summary>
+ /// Gets the path to the Studio directory
+ /// </summary>
+ /// <value>The studio path.</value>
+ string StudioPath { get; }
+
+ /// <summary>
+ /// Gets the path to the Year directory
+ /// </summary>
+ /// <value>The year path.</value>
+ string YearPath { get; }
+
+ /// <summary>
+ /// Gets the path to the General IBN directory
+ /// </summary>
+ /// <value>The general path.</value>
+ string GeneralPath { get; }
+
+ /// <summary>
+ /// Gets the path to the Ratings IBN directory
+ /// </summary>
+ /// <value>The ratings path.</value>
+ string RatingsPath { get; }
+
+ /// <summary>
+ /// Gets the media info images path.
+ /// </summary>
+ /// <value>The media info images path.</value>
+ string MediaInfoImagesPath { get; }
+
+ /// <summary>
+ /// Gets the path to the user configuration directory
+ /// </summary>
+ /// <value>The user configuration directory path.</value>
+ string UserConfigurationDirectoryPath { get; }
+
+ /// <summary>
+ /// Gets the transcoding temporary path.
+ /// </summary>
+ /// <value>The transcoding temporary path.</value>
+ string TranscodingTempPath { get; }
+
+ /// <summary>
+ /// Gets the internal metadata path.
+ /// </summary>
+ /// <value>The internal metadata path.</value>
+ string InternalMetadataPath { get; }
+ string VirtualInternalMetadataPath { get; }
+
+ string ArtistsPath { get; }
+
+ string GetTranscodingTempPath();
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Library/DeleteOptions.cs b/MediaBrowser.Controller/Library/DeleteOptions.cs
new file mode 100644
index 000000000..822fc3dc3
--- /dev/null
+++ b/MediaBrowser.Controller/Library/DeleteOptions.cs
@@ -0,0 +1,14 @@
+
+namespace MediaBrowser.Controller.Library
+{
+ public class DeleteOptions
+ {
+ public bool DeleteFileLocation { get; set; }
+ public bool DeleteFromExternalProvider { get; set; }
+
+ public DeleteOptions()
+ {
+ DeleteFromExternalProvider = true;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/IIntroProvider.cs b/MediaBrowser.Controller/Library/IIntroProvider.cs
new file mode 100644
index 000000000..611aab387
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IIntroProvider.cs
@@ -0,0 +1,32 @@
+using MediaBrowser.Controller.Entities;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Class BaseIntroProvider
+ /// </summary>
+ public interface IIntroProvider
+ {
+ /// <summary>
+ /// Gets the intros.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>IEnumerable{System.String}.</returns>
+ Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, User user);
+
+ /// <summary>
+ /// Gets all intro files.
+ /// </summary>
+ /// <returns>IEnumerable{System.String}.</returns>
+ IEnumerable<string> GetAllIntroFiles();
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
new file mode 100644
index 000000000..d572716fa
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -0,0 +1,550 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.IO;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Interface ILibraryManager
+ /// </summary>
+ public interface ILibraryManager
+ {
+ /// <summary>
+ /// Resolves the path.
+ /// </summary>
+ /// <param name="fileInfo">The file information.</param>
+ /// <param name="parent">The parent.</param>
+ /// <returns>BaseItem.</returns>
+ BaseItem ResolvePath(FileSystemMetadata fileInfo,
+ Folder parent = null);
+
+ /// <summary>
+ /// Resolves a set of files into a list of BaseItem
+ /// </summary>
+ IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files,
+ IDirectoryService directoryService,
+ Folder parent,
+ LibraryOptions libraryOptions,
+ string collectionType = null);
+
+ /// <summary>
+ /// Gets the root folder.
+ /// </summary>
+ /// <value>The root folder.</value>
+ AggregateFolder RootFolder { get; }
+
+ /// <summary>
+ /// Gets a Person
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{Person}.</returns>
+ Person GetPerson(string name);
+
+ /// <summary>
+ /// Finds the by path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>BaseItem.</returns>
+ BaseItem FindByPath(string path, bool? isFolder);
+
+ /// <summary>
+ /// Gets the artist.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{Artist}.</returns>
+ MusicArtist GetArtist(string name);
+ MusicArtist GetArtist(string name, DtoOptions options);
+ /// <summary>
+ /// Gets a Studio
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{Studio}.</returns>
+ Studio GetStudio(string name);
+
+ /// <summary>
+ /// Gets a Genre
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{Genre}.</returns>
+ Genre GetGenre(string name);
+
+ /// <summary>
+ /// Gets the genre.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{MusicGenre}.</returns>
+ MusicGenre GetMusicGenre(string name);
+
+ /// <summary>
+ /// Gets the game genre.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>Task{GameGenre}.</returns>
+ GameGenre GetGameGenre(string name);
+
+ /// <summary>
+ /// Gets a Year
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <returns>Task{Year}.</returns>
+ /// <exception cref="System.ArgumentOutOfRangeException"></exception>
+ Year GetYear(int value);
+
+ /// <summary>
+ /// Validate and refresh the People sub-set of the IBN.
+ /// The items are stored in the db but not loaded into memory until actually requested by an operation.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress);
+
+ /// <summary>
+ /// Reloads the root media folder
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Queues the library scan.
+ /// </summary>
+ void QueueLibraryScan();
+
+ void UpdateImages(BaseItem item);
+
+ /// <summary>
+ /// Gets the default view.
+ /// </summary>
+ /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
+ List<VirtualFolderInfo> GetVirtualFolders();
+
+ List<VirtualFolderInfo> GetVirtualFolders(bool includeRefreshState);
+
+ /// <summary>
+ /// Gets the item by id.
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <returns>BaseItem.</returns>
+ BaseItem GetItemById(Guid id);
+
+ /// <summary>
+ /// Gets the intros.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>IEnumerable{System.String}.</returns>
+ Task<IEnumerable<Video>> GetIntros(BaseItem item, User user);
+
+ /// <summary>
+ /// Gets all intro files.
+ /// </summary>
+ /// <returns>IEnumerable{System.String}.</returns>
+ IEnumerable<string> GetAllIntroFiles();
+
+ /// <summary>
+ /// Adds the parts.
+ /// </summary>
+ /// <param name="rules">The rules.</param>
+ /// <param name="pluginFolders">The plugin folders.</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 postscan tasks.</param>
+ void AddParts(IEnumerable<IResolverIgnoreRule> rules,
+ IEnumerable<IItemResolver> resolvers,
+ IEnumerable<IIntroProvider> introProviders,
+ IEnumerable<IBaseItemComparer> itemComparers,
+ IEnumerable<ILibraryPostScanTask> postscanTasks);
+
+ /// <summary>
+ /// Sorts the specified items.
+ /// </summary>
+ /// <param name="items">The items.</param>
+ /// <param name="user">The user.</param>
+ /// <param name="sortBy">The sort by.</param>
+ /// <param name="sortOrder">The sort order.</param>
+ /// <returns>IEnumerable{BaseItem}.</returns>
+ IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<string> sortBy, SortOrder sortOrder);
+ IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderBy);
+
+ /// <summary>
+ /// Gets the user root folder.
+ /// </summary>
+ /// <returns>UserRootFolder.</returns>
+ Folder GetUserRootFolder();
+
+ /// <summary>
+ /// Creates the item.
+ /// </summary>
+ void CreateItem(BaseItem item, BaseItem parent);
+
+ /// <summary>
+ /// Creates the items.
+ /// </summary>
+ void CreateItems(IEnumerable<BaseItem> items, BaseItem parent, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Updates the item.
+ /// </summary>
+ void UpdateItems(List<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
+ void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Retrieves the item.
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <returns>BaseItem.</returns>
+ BaseItem RetrieveItem(Guid id);
+
+ bool IsScanRunning { get; }
+
+ /// <summary>
+ /// Occurs when [item added].
+ /// </summary>
+ event EventHandler<ItemChangeEventArgs> ItemAdded;
+
+ /// <summary>
+ /// Occurs when [item updated].
+ /// </summary>
+ event EventHandler<ItemChangeEventArgs> ItemUpdated;
+ /// <summary>
+ /// Occurs when [item removed].
+ /// </summary>
+ event EventHandler<ItemChangeEventArgs> ItemRemoved;
+
+ /// <summary>
+ /// Finds the type of the collection.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>System.String.</returns>
+ string GetContentType(BaseItem item);
+
+ /// <summary>
+ /// Gets the type of the inherited content.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>System.String.</returns>
+ string GetInheritedContentType(BaseItem item);
+
+ /// <summary>
+ /// Gets the type of the configured content.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>System.String.</returns>
+ string GetConfiguredContentType(BaseItem item);
+
+ /// <summary>
+ /// Gets the type of the configured content.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>System.String.</returns>
+ string GetConfiguredContentType(string path);
+
+ /// <summary>
+ /// Normalizes the root path list.
+ /// </summary>
+ /// <param name="paths">The paths.</param>
+ /// <returns>IEnumerable{System.String}.</returns>
+ List<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths);
+
+ /// <summary>
+ /// Registers the item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ void RegisterItem(BaseItem item);
+
+ /// <summary>
+ /// Deletes the item.
+ /// </summary>
+ void DeleteItem(BaseItem item, DeleteOptions options);
+
+ /// <summary>
+ /// Deletes the item.
+ /// </summary>
+ void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem);
+
+ /// <summary>
+ /// Deletes the item.
+ /// </summary>
+ void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem);
+
+ /// <summary>
+ /// Gets the named view.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="name">The name.</param>
+ /// <param name="parentId">The parent identifier.</param>
+ /// <param name="viewType">Type of the view.</param>
+ /// <param name="sortName">Name of the sort.</param>
+ UserView GetNamedView(User user,
+ string name,
+ Guid parentId,
+ string viewType,
+ string sortNamen);
+
+ /// <summary>
+ /// Gets the named view.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="name">The name.</param>
+ /// <param name="viewType">Type of the view.</param>
+ /// <param name="sortName">Name of the sort.</param>
+ UserView GetNamedView(User user,
+ string name,
+ string viewType,
+ string sortName);
+
+ /// <summary>
+ /// Gets the named view.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="viewType">Type of the view.</param>
+ /// <param name="sortName">Name of the sort.</param>
+ UserView GetNamedView(string name,
+ string viewType,
+ string sortName);
+
+ /// <summary>
+ /// Gets the named view.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="parentId">The parent identifier.</param>
+ /// <param name="viewType">Type of the view.</param>
+ /// <param name="sortName">Name of the sort.</param>
+ /// <param name="uniqueId">The unique identifier.</param>
+ UserView GetNamedView(string name,
+ Guid parentId,
+ string viewType,
+ string sortName,
+ string uniqueId);
+
+ /// <summary>
+ /// Gets the shadow view.
+ /// </summary>
+ /// <param name="parent">The parent.</param>
+ /// <param name="viewType">Type of the view.</param>
+ /// <param name="sortName">Name of the sort.</param>
+ UserView GetShadowView(BaseItem parent,
+ string viewType,
+ string sortName);
+
+ /// <summary>
+ /// Determines whether [is video file] [the specified path].
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if [is video file] [the specified path]; otherwise, <c>false</c>.</returns>
+ bool IsVideoFile(string path);
+
+ /// <summary>
+ /// Determines whether [is audio file] [the specified path].
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if [is audio file] [the specified path]; otherwise, <c>false</c>.</returns>
+ bool IsAudioFile(string path);
+
+ bool IsAudioFile(string path, LibraryOptions libraryOptions);
+ bool IsVideoFile(string path, LibraryOptions libraryOptions);
+
+ /// <summary>
+ /// Gets the season number from path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>System.Nullable&lt;System.Int32&gt;.</returns>
+ int? GetSeasonNumberFromPath(string path);
+
+ /// <summary>
+ /// Fills the missing episode numbers from path.
+ /// </summary>
+ bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh);
+
+ /// <summary>
+ /// Parses the name.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>ItemInfo.</returns>
+ ItemLookupInfo ParseName(string name);
+
+ /// <summary>
+ /// Gets the new item identifier.
+ /// </summary>
+ /// <param name="key">The key.</param>
+ /// <param name="type">The type.</param>
+ /// <returns>Guid.</returns>
+ Guid GetNewItemId(string key, Type type);
+
+ /// <summary>
+ /// Finds the trailers.
+ /// </summary>
+ /// <param name="owner">The owner.</param>
+ /// <param name="fileSystemChildren">The file system children.</param>
+ /// <param name="directoryService">The directory service.</param>
+ /// <returns>IEnumerable&lt;Trailer&gt;.</returns>
+ IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren,
+ IDirectoryService directoryService);
+
+ /// <summary>
+ /// Finds the extras.
+ /// </summary>
+ /// <param name="owner">The owner.</param>
+ /// <param name="fileSystemChildren">The file system children.</param>
+ /// <param name="directoryService">The directory service.</param>
+ /// <returns>IEnumerable&lt;Video&gt;.</returns>
+ IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren,
+ IDirectoryService directoryService);
+
+ /// <summary>
+ /// Gets the collection folders.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>IEnumerable&lt;Folder&gt;.</returns>
+ List<Folder> GetCollectionFolders(BaseItem item);
+
+ List<Folder> GetCollectionFolders(BaseItem item, List<Folder> allUserRootChildren);
+
+ LibraryOptions GetLibraryOptions(BaseItem item);
+
+ /// <summary>
+ /// Gets the people.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>List&lt;PersonInfo&gt;.</returns>
+ List<PersonInfo> GetPeople(BaseItem item);
+
+ /// <summary>
+ /// Gets the people.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>List&lt;PersonInfo&gt;.</returns>
+ List<PersonInfo> GetPeople(InternalPeopleQuery query);
+
+ /// <summary>
+ /// Gets the people items.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>List&lt;Person&gt;.</returns>
+ List<Person> GetPeopleItems(InternalPeopleQuery query);
+
+ /// <summary>
+ /// Updates the people.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="people">The people.</param>
+ void UpdatePeople(BaseItem item, List<PersonInfo> people);
+
+ /// <summary>
+ /// Gets the item ids.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>List&lt;Guid&gt;.</returns>
+ List<Guid> GetItemIds(InternalItemsQuery query);
+
+ /// <summary>
+ /// Gets the people names.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>List&lt;System.String&gt;.</returns>
+ List<string> GetPeopleNames(InternalPeopleQuery query);
+
+ /// <summary>
+ /// Queries the items.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>QueryResult&lt;BaseItem&gt;.</returns>
+ QueryResult<BaseItem> QueryItems(InternalItemsQuery query);
+
+ string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem = null);
+
+ /// <summary>
+ /// Substitutes the path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="from">From.</param>
+ /// <param name="to">To.</param>
+ /// <returns>System.String.</returns>
+ string SubstitutePath(string path, string from, string to);
+
+ /// <summary>
+ /// Converts the image to local.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="image">The image.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <returns>Task.</returns>
+ Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex);
+
+ /// <summary>
+ /// Gets the items.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>QueryResult&lt;BaseItem&gt;.</returns>
+ List<BaseItem> GetItemList(InternalItemsQuery query);
+
+ List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent);
+
+ /// <summary>
+ /// Gets the items.
+ /// </summary>
+ List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents);
+
+ /// <summary>
+ /// Gets the items result.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>QueryResult&lt;BaseItem&gt;.</returns>
+ QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query);
+
+ /// <summary>
+ /// Ignores the file.
+ /// </summary>
+ /// <param name="file">The file.</param>
+ /// <param name="parent">The parent.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+ bool IgnoreFile(FileSystemMetadata file, BaseItem parent);
+
+ Guid GetStudioId(string name);
+
+ Guid GetGenreId(string name);
+
+ Guid GetMusicGenreId(string name);
+
+ Guid GetGameGenreId(string name);
+
+ Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary);
+ Task RemoveVirtualFolder(string name, bool refreshLibrary);
+ void AddMediaPath(string virtualFolderName, MediaPathInfo path);
+ void UpdateMediaPath(string virtualFolderName, MediaPathInfo path);
+ void RemoveMediaPath(string virtualFolderName, string path);
+
+ QueryResult<Tuple<BaseItem, ItemCounts>> GetGenres(InternalItemsQuery query);
+ QueryResult<Tuple<BaseItem, ItemCounts>> GetMusicGenres(InternalItemsQuery query);
+ QueryResult<Tuple<BaseItem, ItemCounts>> GetGameGenres(InternalItemsQuery query);
+ QueryResult<Tuple<BaseItem, ItemCounts>> GetStudios(InternalItemsQuery query);
+ QueryResult<Tuple<BaseItem, ItemCounts>> GetArtists(InternalItemsQuery query);
+ QueryResult<Tuple<BaseItem, ItemCounts>> GetAlbumArtists(InternalItemsQuery query);
+ QueryResult<Tuple<BaseItem, ItemCounts>> GetAllArtists(InternalItemsQuery query);
+
+ int GetCount(InternalItemsQuery query);
+
+ void AddExternalSubtitleStreams(List<MediaStream> streams,
+ string videoPath,
+ string[] files);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Library/ILibraryMonitor.cs b/MediaBrowser.Controller/Library/ILibraryMonitor.cs
new file mode 100644
index 000000000..e965e47d6
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ILibraryMonitor.cs
@@ -0,0 +1,43 @@
+using System;
+
+namespace MediaBrowser.Controller.Library
+{
+ public interface ILibraryMonitor : IDisposable
+ {
+ /// <summary>
+ /// Starts this instance.
+ /// </summary>
+ void Start();
+
+ /// <summary>
+ /// Stops this instance.
+ /// </summary>
+ void Stop();
+
+ /// <summary>
+ /// Reports the file system change beginning.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ void ReportFileSystemChangeBeginning(string path);
+
+ /// <summary>
+ /// Reports the file system change complete.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="refreshPath">if set to <c>true</c> [refresh path].</param>
+ void ReportFileSystemChangeComplete(string path, bool refreshPath);
+
+ /// <summary>
+ /// Reports the file system changed.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ void ReportFileSystemChanged(string path);
+
+ /// <summary>
+ /// Determines whether [is path locked] [the specified path].
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if [is path locked] [the specified path]; otherwise, <c>false</c>.</returns>
+ bool IsPathLocked(string path);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Library/ILibraryPostScanTask.cs b/MediaBrowser.Controller/Library/ILibraryPostScanTask.cs
new file mode 100644
index 000000000..694422907
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ILibraryPostScanTask.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// An interface for tasks that run after the media library scan
+ /// </summary>
+ public interface ILibraryPostScanTask
+ {
+ /// <summary>
+ /// Runs the specified progress.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task Run(IProgress<double> progress, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ILiveStream.cs b/MediaBrowser.Controller/Library/ILiveStream.cs
new file mode 100644
index 000000000..e00da7340
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ILiveStream.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Controller.Library
+{
+ public interface ILiveStream
+ {
+ Task Open(CancellationToken openCancellationToken);
+ Task Close();
+ int ConsumerCount { get; set; }
+ string OriginalStreamId { get; set; }
+ string TunerHostId { get; }
+ bool EnableStreamSharing { get; }
+ MediaSourceInfo MediaSource { get; set; }
+ string UniqueId { get; }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
new file mode 100644
index 000000000..8541c4fd9
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs
@@ -0,0 +1,100 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.MediaInfo;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using System.IO;
+
+namespace MediaBrowser.Controller.Library
+{
+ public interface IMediaSourceManager
+ {
+ /// <summary>
+ /// Adds the parts.
+ /// </summary>
+ /// <param name="providers">The providers.</param>
+ void AddParts(IEnumerable<IMediaSourceProvider> providers);
+
+ /// <summary>
+ /// Gets the media streams.
+ /// </summary>
+ /// <param name="itemId">The item identifier.</param>
+ /// <returns>IEnumerable&lt;MediaStream&gt;.</returns>
+ List<MediaStream> GetMediaStreams(Guid itemId);
+ /// <summary>
+ /// Gets the media streams.
+ /// </summary>
+ /// <param name="mediaSourceId">The media source identifier.</param>
+ /// <returns>IEnumerable&lt;MediaStream&gt;.</returns>
+ List<MediaStream> GetMediaStreams(string mediaSourceId);
+ /// <summary>
+ /// Gets the media streams.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>IEnumerable&lt;MediaStream&gt;.</returns>
+ List<MediaStream> GetMediaStreams(MediaStreamQuery query);
+
+ /// <summary>
+ /// Gets the playack media sources.
+ /// </summary>
+ Task<List<MediaSourceInfo>> GetPlayackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the static media sources.
+ /// </summary>
+ List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null);
+
+ /// <summary>
+ /// Gets the static media source.
+ /// </summary>
+ Task<MediaSourceInfo> GetMediaSource(BaseItem item, string mediaSourceId, string liveStreamId, bool enablePathSubstitution, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Opens the media source.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task&lt;MediaSourceInfo&gt;.</returns>
+ Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken);
+
+ Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the live stream.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task&lt;MediaSourceInfo&gt;.</returns>
+ Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken);
+
+ Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Closes the media source.
+ /// </summary>
+ /// <param name="id">The live stream identifier.</param>
+ /// <returns>Task.</returns>
+ Task CloseLiveStream(string id);
+
+ Task<MediaSourceInfo> GetLiveStreamMediaInfo(string id, CancellationToken cancellationToken);
+
+ bool SupportsDirectStream(string path, MediaProtocol protocol);
+
+ MediaProtocol GetPathProtocol(string path);
+
+ void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user);
+
+ Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, string cacheKey, bool addProbeDelay, bool isLiveStream, CancellationToken cancellationToken);
+
+ Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken);
+ }
+
+ public interface IDirectStreamProvider
+ {
+ Task CopyToAsync(Stream stream, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs
new file mode 100644
index 000000000..eec138532
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs
@@ -0,0 +1,25 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Dto;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using System;
+
+namespace MediaBrowser.Controller.Library
+{
+ public interface IMediaSourceProvider
+ {
+ /// <summary>
+ /// Gets the media sources.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task&lt;IEnumerable&lt;MediaSourceInfo&gt;&gt;.</returns>
+ Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Opens the media source.
+ /// </summary>
+ Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Library/IMetadataFileSaver.cs b/MediaBrowser.Controller/Library/IMetadataFileSaver.cs
new file mode 100644
index 000000000..e66fbcbc8
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IMetadataFileSaver.cs
@@ -0,0 +1,19 @@
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Library
+{
+ public interface IMetadataFileSaver : IMetadataSaver
+ {
+ /// <summary>
+ /// Gets the save path.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>System.String.</returns>
+ string GetSavePath(BaseItem item);
+ }
+
+ public interface IConfigurableProvider
+ {
+ bool IsEnabled { get; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Library/IMetadataSaver.cs b/MediaBrowser.Controller/Library/IMetadataSaver.cs
new file mode 100644
index 000000000..f71afa656
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IMetadataSaver.cs
@@ -0,0 +1,33 @@
+using MediaBrowser.Controller.Entities;
+using System.Threading;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Interface IMetadataSaver
+ /// </summary>
+ public interface IMetadataSaver
+ {
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ /// <summary>
+ /// Determines whether [is enabled for] [the specified item].
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="updateType">Type of the update.</param>
+ /// <returns><c>true</c> if [is enabled for] [the specified item]; otherwise, <c>false</c>.</returns>
+ bool IsEnabledFor(BaseItem item, ItemUpdateType updateType);
+
+ /// <summary>
+ /// Saves the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ void Save(BaseItem item, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs
new file mode 100644
index 000000000..535e6df7e
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IMusicManager.cs
@@ -0,0 +1,25 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Dto;
+
+namespace MediaBrowser.Controller.Library
+{
+ public interface IMusicManager
+ {
+ /// <summary>
+ /// Gets the instant mix from song.
+ /// </summary>
+ List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions);
+
+ /// <summary>
+ /// Gets the instant mix from artist.
+ /// </summary>
+ List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User user, DtoOptions dtoOptions);
+
+ /// <summary>
+ /// Gets the instant mix from genre.
+ /// </summary>
+ List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User user, DtoOptions dtoOptions);
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ISearchEngine.cs b/MediaBrowser.Controller/Library/ISearchEngine.cs
new file mode 100644
index 000000000..715f16407
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ISearchEngine.cs
@@ -0,0 +1,18 @@
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Search;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Interface ILibrarySearchEngine
+ /// </summary>
+ public interface ISearchEngine
+ {
+ /// <summary>
+ /// Gets the search hints.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>Task{IEnumerable{SearchHintInfo}}.</returns>
+ QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query);
+ }
+}
diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs
new file mode 100644
index 000000000..11d77f81a
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IUserDataManager.cs
@@ -0,0 +1,67 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Threading;
+using MediaBrowser.Model.Querying;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Interface IUserDataManager
+ /// </summary>
+ public interface IUserDataManager
+ {
+ /// <summary>
+ /// Occurs when [user data saved].
+ /// </summary>
+ event EventHandler<UserDataSaveEventArgs> UserDataSaved;
+
+ /// <summary>
+ /// Saves the user data.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="userData">The user data.</param>
+ /// <param name="reason">The reason.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken);
+ void SaveUserData(User userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken);
+
+ UserItemData GetUserData(User user, BaseItem item);
+
+ UserItemData GetUserData(string userId, BaseItem item);
+ UserItemData GetUserData(Guid userId, BaseItem item);
+
+ /// <summary>
+ /// Gets the user data dto.
+ /// </summary>
+ UserItemDataDto GetUserDataDto(BaseItem item, User user);
+
+ UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions dto_options);
+
+ /// <summary>
+ /// Get all user data for the given user
+ /// </summary>
+ /// <param name="userId"></param>
+ /// <returns></returns>
+ List<UserItemData> GetAllUserData(Guid userId);
+
+ /// <summary>
+ /// Save the all provided user data for the given user
+ /// </summary>
+ /// <param name="userId"></param>
+ /// <param name="userData"></param>
+ /// <param name="cancellationToken"></param>
+ /// <returns></returns>
+ void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Updates playstate for an item and returns true or false indicating if it was played to completion
+ /// </summary>
+ bool UpdatePlayState(BaseItem item, UserItemData data, long? positionTicks);
+ }
+}
diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs
new file mode 100644
index 000000000..d29b164ef
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IUserManager.cs
@@ -0,0 +1,207 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Events;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Users;
+using MediaBrowser.Controller.Authentication;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Interface IUserManager
+ /// </summary>
+ public interface IUserManager
+ {
+ /// <summary>
+ /// Gets the users.
+ /// </summary>
+ /// <value>The users.</value>
+ IEnumerable<User> Users { get; }
+
+ /// <summary>
+ /// Occurs when [user updated].
+ /// </summary>
+ event EventHandler<GenericEventArgs<User>> UserUpdated;
+
+ /// <summary>
+ /// Occurs when [user deleted].
+ /// </summary>
+ event EventHandler<GenericEventArgs<User>> UserDeleted;
+
+ event EventHandler<GenericEventArgs<User>> UserCreated;
+ event EventHandler<GenericEventArgs<User>> UserPolicyUpdated;
+ event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated;
+ event EventHandler<GenericEventArgs<User>> UserPasswordChanged;
+ event EventHandler<GenericEventArgs<User>> UserLockedOut;
+
+ /// <summary>
+ /// Gets a User by Id
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <returns>User.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ User GetUserById(Guid id);
+
+ /// <summary>
+ /// Gets the user by identifier.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <returns>User.</returns>
+ User GetUserById(string id);
+
+ /// <summary>
+ /// Gets the name of the user by.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>User.</returns>
+ User GetUserByName(string name);
+
+ /// <summary>
+ /// Refreshes metadata for each user
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task RefreshUsersMetadata(CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Renames the user.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="newName">The new name.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ /// <exception cref="System.ArgumentException"></exception>
+ Task RenameUser(User user, string newName);
+
+ /// <summary>
+ /// Updates the user.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ /// <exception cref="System.ArgumentException"></exception>
+ void UpdateUser(User user);
+
+ /// <summary>
+ /// Creates the user.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>User.</returns>
+ /// <exception cref="System.ArgumentNullException">name</exception>
+ /// <exception cref="System.ArgumentException"></exception>
+ Task<User> CreateUser(string name);
+
+ /// <summary>
+ /// Deletes the user.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">user</exception>
+ /// <exception cref="System.ArgumentException"></exception>
+ Task DeleteUser(User user);
+
+ /// <summary>
+ /// Resets the password.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>Task.</returns>
+ Task ResetPassword(User user);
+
+ /// <summary>
+ /// Gets the offline user dto.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>UserDto.</returns>
+ UserDto GetOfflineUserDto(User user);
+
+ /// <summary>
+ /// Resets the easy password.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>Task.</returns>
+ void ResetEasyPassword(User user);
+
+ /// <summary>
+ /// Changes the password.
+ /// </summary>
+ Task ChangePassword(User user, string newPassword);
+
+ /// <summary>
+ /// Changes the easy password.
+ /// </summary>
+ void ChangeEasyPassword(User user, string newPassword, string newPasswordSha1);
+
+ /// <summary>
+ /// Gets the user dto.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="remoteEndPoint">The remote end point.</param>
+ /// <returns>UserDto.</returns>
+ UserDto GetUserDto(User user, string remoteEndPoint = null);
+
+ /// <summary>
+ /// Authenticates the user.
+ /// </summary>
+ Task<User> AuthenticateUser(string username, string password, string passwordSha1, string remoteEndPoint, bool isUserSession);
+
+ /// <summary>
+ /// Starts the forgot password process.
+ /// </summary>
+ /// <param name="enteredUsername">The entered username.</param>
+ /// <param name="isInNetwork">if set to <c>true</c> [is in network].</param>
+ /// <returns>ForgotPasswordResult.</returns>
+ Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork);
+
+ /// <summary>
+ /// Redeems the password reset pin.
+ /// </summary>
+ /// <param name="pin">The pin.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+ Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
+
+ /// <summary>
+ /// Gets the user policy.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>UserPolicy.</returns>
+ UserPolicy GetUserPolicy(User user);
+
+ /// <summary>
+ /// Gets the user configuration.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns>UserConfiguration.</returns>
+ UserConfiguration GetUserConfiguration(User user);
+
+ /// <summary>
+ /// Updates the configuration.
+ /// </summary>
+ /// <param name="userId">The user identifier.</param>
+ /// <param name="newConfiguration">The new configuration.</param>
+ /// <returns>Task.</returns>
+ void UpdateConfiguration(Guid userId, UserConfiguration newConfiguration);
+
+ void UpdateConfiguration(User user, UserConfiguration newConfiguration);
+
+ /// <summary>
+ /// Updates the user policy.
+ /// </summary>
+ /// <param name="userId">The user identifier.</param>
+ /// <param name="userPolicy">The user policy.</param>
+ void UpdateUserPolicy(Guid userId, UserPolicy userPolicy);
+
+ /// <summary>
+ /// Makes the valid username.
+ /// </summary>
+ /// <param name="username">The username.</param>
+ /// <returns>System.String.</returns>
+ string MakeValidUsername(string username);
+
+ void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders);
+
+ NameIdPair[] GetAuthenticationProviders();
+ }
+}
diff --git a/MediaBrowser.Controller/Library/IUserViewManager.cs b/MediaBrowser.Controller/Library/IUserViewManager.cs
new file mode 100644
index 000000000..f4649777c
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IUserViewManager.cs
@@ -0,0 +1,19 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Library;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Dto;
+
+namespace MediaBrowser.Controller.Library
+{
+ public interface IUserViewManager
+ {
+ Folder[] GetUserViews(UserViewQuery query);
+ UserView GetUserSubView(Guid parentId, string type, string localizationKey, string sortName);
+
+ List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request, DtoOptions options);
+ }
+}
diff --git a/MediaBrowser.Controller/Library/IntroInfo.cs b/MediaBrowser.Controller/Library/IntroInfo.cs
new file mode 100644
index 000000000..d0e61d0f0
--- /dev/null
+++ b/MediaBrowser.Controller/Library/IntroInfo.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace MediaBrowser.Controller.Library
+{
+ public class IntroInfo
+ {
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ public string Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item id.
+ /// </summary>
+ /// <value>The item id.</value>
+ public Guid? ItemId { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs b/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs
new file mode 100644
index 000000000..e671490d3
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ItemChangeEventArgs.cs
@@ -0,0 +1,24 @@
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Class ItemChangeEventArgs
+ /// </summary>
+ public class ItemChangeEventArgs
+ {
+ /// <summary>
+ /// Gets or sets the item.
+ /// </summary>
+ /// <value>The item.</value>
+ public BaseItem Item { get; set; }
+
+ public BaseItem Parent { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item.
+ /// </summary>
+ /// <value>The item.</value>
+ public ItemUpdateType UpdateReason { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
new file mode 100644
index 000000000..7197425f3
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -0,0 +1,281 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.IO;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// These are arguments relating to the file system that are collected once and then referred to
+ /// whenever needed. Primarily for entity resolution.
+ /// </summary>
+ public class ItemResolveArgs : EventArgs
+ {
+ /// <summary>
+ /// The _app paths
+ /// </summary>
+ private readonly IServerApplicationPaths _appPaths;
+
+ public IDirectoryService DirectoryService { get; private set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ItemResolveArgs" /> class.
+ /// </summary>
+ /// <param name="appPaths">The app paths.</param>
+ /// <param name="directoryService">The directory service.</param>
+ public ItemResolveArgs(IServerApplicationPaths appPaths, IDirectoryService directoryService)
+ {
+ _appPaths = appPaths;
+ DirectoryService = directoryService;
+ }
+
+ /// <summary>
+ /// Gets the file system children.
+ /// </summary>
+ /// <value>The file system children.</value>
+ public FileSystemMetadata[] FileSystemChildren { get; set; }
+
+ public LibraryOptions LibraryOptions { get; set; }
+
+ public LibraryOptions GetLibraryOptions()
+ {
+ return LibraryOptions ?? (LibraryOptions = (Parent == null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent)));
+ }
+
+ /// <summary>
+ /// Gets or sets the parent.
+ /// </summary>
+ /// <value>The parent.</value>
+ public Folder Parent { get; set; }
+
+ /// <summary>
+ /// Gets or sets the file info.
+ /// </summary>
+ /// <value>The file info.</value>
+ public FileSystemMetadata FileInfo { get; set; }
+
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ public string Path { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is directory.
+ /// </summary>
+ /// <value><c>true</c> if this instance is directory; otherwise, <c>false</c>.</value>
+ public bool IsDirectory
+ {
+ get
+ {
+ return FileInfo.IsDirectory;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is vf.
+ /// </summary>
+ /// <value><c>true</c> if this instance is vf; otherwise, <c>false</c>.</value>
+ public bool IsVf
+ {
+ // we should be considered a virtual folder if we are a child of one of the children of the system root folder.
+ // this is a bit of a trick to determine that... the directory name of a sub-child of the root will start with
+ // the root but not be equal to it
+ get
+ {
+ if (!IsDirectory)
+ {
+ return false;
+ }
+
+ var parentDir = BaseItem.FileSystem.GetDirectoryName(Path) ?? string.Empty;
+
+ return parentDir.Length > _appPaths.RootFolderPath.Length
+ && parentDir.StartsWith(_appPaths.RootFolderPath, StringComparison.OrdinalIgnoreCase);
+
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is physical root.
+ /// </summary>
+ /// <value><c>true</c> if this instance is physical root; otherwise, <c>false</c>.</value>
+ public bool IsPhysicalRoot
+ {
+ get
+ {
+ return IsDirectory && BaseItem.FileSystem.AreEqual(Path, _appPaths.RootFolderPath);
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the additional locations.
+ /// </summary>
+ /// <value>The additional locations.</value>
+ private List<string> AdditionalLocations { get; set; }
+
+ public bool HasParent<T>()
+ where T : Folder
+ {
+ var parent = Parent;
+
+ if (parent != null)
+ {
+ var item = parent as T;
+
+ // Just in case the user decided to nest episodes.
+ // Not officially supported but in some cases we can handle it.
+ if (item == null)
+ {
+ var parents = parent.GetParents();
+ foreach (var currentParent in parents)
+ {
+ if (currentParent is T)
+ {
+ return true;
+ }
+ }
+ }
+
+ return item != null;
+
+ }
+ return false;
+ }
+
+ /// <summary>
+ /// Adds the additional location.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void AddAdditionalLocation(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (AdditionalLocations == null)
+ {
+ AdditionalLocations = new List<string>();
+ }
+
+ AdditionalLocations.Add(path);
+ }
+
+ /// <summary>
+ /// Gets the physical locations.
+ /// </summary>
+ /// <value>The physical locations.</value>
+ public string[] PhysicalLocations
+ {
+ get
+ {
+ var paths = string.IsNullOrEmpty(Path) ? new string[] { } : new[] { Path };
+ return AdditionalLocations == null ? paths : paths.Concat(AdditionalLocations).ToArray();
+ }
+ }
+
+ /// <summary>
+ /// Gets the name of the file system entry by.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>FileSystemInfo.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public FileSystemMetadata GetFileSystemEntryByName(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentNullException();
+ }
+
+ return GetFileSystemEntryByPath(System.IO.Path.Combine(Path, name));
+ }
+
+ /// <summary>
+ /// Gets the file system entry by path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>FileSystemInfo.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public FileSystemMetadata GetFileSystemEntryByPath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException();
+ }
+
+ foreach (var file in FileSystemChildren)
+ {
+ if (string.Equals(file.FullName, path, StringComparison.Ordinal))
+ {
+ return file;
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Determines whether [contains file system entry by name] [the specified name].
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns><c>true</c> if [contains file system entry by name] [the specified name]; otherwise, <c>false</c>.</returns>
+ public bool ContainsFileSystemEntryByName(string name)
+ {
+ return GetFileSystemEntryByName(name) != null;
+ }
+
+ public string GetCollectionType()
+ {
+ return CollectionType;
+ }
+
+ public string CollectionType { get; set; }
+
+ #region Equality Overrides
+
+ /// <summary>
+ /// Determines whether the specified <see cref="System.Object" /> is equal to this instance.
+ /// </summary>
+ /// <param name="obj">The object to compare with the current object.</param>
+ /// <returns><c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.</returns>
+ public override bool Equals(object obj)
+ {
+ return Equals(obj as ItemResolveArgs);
+ }
+
+ /// <summary>
+ /// Returns a hash code for this instance.
+ /// </summary>
+ /// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
+ public override int GetHashCode()
+ {
+ return Path.GetHashCode();
+ }
+
+ /// <summary>
+ /// Equalses the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected bool Equals(ItemResolveArgs args)
+ {
+ if (args != null)
+ {
+ if (args.Path == null && Path == null) return true;
+ return args.Path != null && BaseItem.FileSystem.AreEqual(args.Path, Path);
+ }
+ return false;
+ }
+
+ #endregion
+ }
+
+}
diff --git a/MediaBrowser.Controller/Library/ItemUpdateType.cs b/MediaBrowser.Controller/Library/ItemUpdateType.cs
new file mode 100644
index 000000000..cf6263356
--- /dev/null
+++ b/MediaBrowser.Controller/Library/ItemUpdateType.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace MediaBrowser.Controller.Library
+{
+ [Flags]
+ public enum ItemUpdateType
+ {
+ None = 1,
+ MetadataImport = 2,
+ ImageUpdate = 4,
+ MetadataDownload = 8,
+ MetadataEdit = 16
+ }
+}
diff --git a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs
new file mode 100644
index 000000000..ec69bea6e
--- /dev/null
+++ b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs
@@ -0,0 +1,13 @@
+using System;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Library
+{
+ public static class LibraryManagerExtensions
+ {
+ public static BaseItem GetItemById(this ILibraryManager manager, string id)
+ {
+ return manager.GetItemById(new Guid(id));
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs b/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs
new file mode 100644
index 000000000..dc2fa0f99
--- /dev/null
+++ b/MediaBrowser.Controller/Library/MetadataConfigurationStore.cs
@@ -0,0 +1,29 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Library
+{
+ public class MetadataConfigurationStore : IConfigurationFactory
+ {
+ public IEnumerable<ConfigurationStore> GetConfigurations()
+ {
+ return new ConfigurationStore[]
+ {
+ new ConfigurationStore
+ {
+ Key = "metadata",
+ ConfigurationType = typeof(MetadataConfiguration)
+ }
+ };
+ }
+ }
+
+ public static class MetadataConfigurationExtensions
+ {
+ public static MetadataConfiguration GetMetadataConfiguration(this IConfigurationManager config)
+ {
+ return config.GetConfiguration<MetadataConfiguration>("metadata");
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/NameExtensions.cs b/MediaBrowser.Controller/Library/NameExtensions.cs
new file mode 100644
index 000000000..bab334a6d
--- /dev/null
+++ b/MediaBrowser.Controller/Library/NameExtensions.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Controller.Library
+{
+ public static class NameExtensions
+ {
+ private static string RemoveDiacritics(string name)
+ {
+ if (name == null)
+ {
+ return string.Empty;
+ }
+
+ //return name;
+ return name.RemoveDiacritics();
+ }
+
+ public static IEnumerable<string> DistinctNames(this IEnumerable<string> names)
+ {
+ return names.DistinctBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs
new file mode 100644
index 000000000..00d9932a7
--- /dev/null
+++ b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs
@@ -0,0 +1,34 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Dto;
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Holds information about a playback progress event
+ /// </summary>
+ public class PlaybackProgressEventArgs : EventArgs
+ {
+ public List<User> Users { get; set; }
+ public long? PlaybackPositionTicks { get; set; }
+ public BaseItem Item { get; set; }
+ public BaseItemDto MediaInfo { get; set; }
+ public string MediaSourceId { get; set; }
+ public bool IsPaused { get; set; }
+ public bool IsAutomated { get; set; }
+
+ public string DeviceId { get; set; }
+ public string DeviceName { get; set; }
+ public string ClientName { get; set; }
+
+ public string PlaySessionId { get; set; }
+ public SessionInfo Session { get; set; }
+
+ public PlaybackProgressEventArgs()
+ {
+ Users = new List<User>();
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/PlaybackStopEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackStopEventArgs.cs
new file mode 100644
index 000000000..b0f6799fc
--- /dev/null
+++ b/MediaBrowser.Controller/Library/PlaybackStopEventArgs.cs
@@ -0,0 +1,11 @@
+namespace MediaBrowser.Controller.Library
+{
+ public class PlaybackStopEventArgs : PlaybackProgressEventArgs
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether [played to completion].
+ /// </summary>
+ /// <value><c>true</c> if [played to completion]; otherwise, <c>false</c>.</value>
+ public bool PlayedToCompletion { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Library/Profiler.cs b/MediaBrowser.Controller/Library/Profiler.cs
new file mode 100644
index 000000000..3957c3020
--- /dev/null
+++ b/MediaBrowser.Controller/Library/Profiler.cs
@@ -0,0 +1,76 @@
+using MediaBrowser.Model.Logging;
+using System;
+using System.Diagnostics;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Class Profiler
+ /// </summary>
+ public class Profiler : IDisposable
+ {
+ /// <summary>
+ /// The name
+ /// </summary>
+ readonly string _name;
+ /// <summary>
+ /// The stopwatch
+ /// </summary>
+ readonly Stopwatch _stopwatch;
+
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Profiler" /> class.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="logger">The logger.</param>
+ public Profiler(string name, ILogger logger)
+ {
+ this._name = name;
+
+ _logger = logger;
+
+ _stopwatch = new Stopwatch();
+ _stopwatch.Start();
+ }
+ #region IDisposable Members
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ /// <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)
+ {
+ _stopwatch.Stop();
+ string message;
+ if (_stopwatch.ElapsedMilliseconds > 300000)
+ {
+ message = string.Format("{0} took {1} minutes.",
+ _name, ((float)_stopwatch.ElapsedMilliseconds / 60000).ToString("F"));
+ }
+ else
+ {
+ message = string.Format("{0} took {1} seconds.",
+ _name, ((float)_stopwatch.ElapsedMilliseconds / 1000).ToString("#0.000"));
+ }
+ _logger.Info(message);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/MediaBrowser.Controller/Library/SearchHintInfo.cs b/MediaBrowser.Controller/Library/SearchHintInfo.cs
new file mode 100644
index 000000000..f832811c2
--- /dev/null
+++ b/MediaBrowser.Controller/Library/SearchHintInfo.cs
@@ -0,0 +1,22 @@
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Class SearchHintInfo
+ /// </summary>
+ public class SearchHintInfo
+ {
+ /// <summary>
+ /// Gets or sets the item.
+ /// </summary>
+ /// <value>The item.</value>
+ public BaseItem Item { get; set; }
+
+ /// <summary>
+ /// Gets or sets the matched term.
+ /// </summary>
+ /// <value>The matched term.</value>
+ public string MatchedTerm { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/TVUtils.cs b/MediaBrowser.Controller/Library/TVUtils.cs
new file mode 100644
index 000000000..dc95ba112
--- /dev/null
+++ b/MediaBrowser.Controller/Library/TVUtils.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Class TVUtils
+ /// </summary>
+ public static class TVUtils
+ {
+ /// <summary>
+ /// The TVDB API key
+ /// </summary>
+ public static readonly string TvdbApiKey = "B89CE93890E9419B";
+ public static readonly string TvdbBaseUrl = "https://www.thetvdb.com/";
+ /// <summary>
+ /// The banner URL
+ /// </summary>
+ public static readonly string BannerUrl = TvdbBaseUrl + "banners/";
+
+ /// <summary>
+ /// Gets the air days.
+ /// </summary>
+ /// <param name="day">The day.</param>
+ /// <returns>List{DayOfWeek}.</returns>
+ public static DayOfWeek[] GetAirDays(string day)
+ {
+ if (!string.IsNullOrEmpty(day))
+ {
+ if (string.Equals(day, "Daily", StringComparison.OrdinalIgnoreCase))
+ {
+ return new DayOfWeek[]
+ {
+ DayOfWeek.Sunday,
+ DayOfWeek.Monday,
+ DayOfWeek.Tuesday,
+ DayOfWeek.Wednesday,
+ DayOfWeek.Thursday,
+ DayOfWeek.Friday,
+ DayOfWeek.Saturday
+ };
+ }
+
+ DayOfWeek value;
+
+ if (Enum.TryParse(day, true, out value))
+ {
+ return new DayOfWeek[]
+ {
+ value
+ };
+ }
+
+ return new DayOfWeek[]{};
+ }
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs
new file mode 100644
index 000000000..d921a7077
--- /dev/null
+++ b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs
@@ -0,0 +1,39 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Library
+{
+ /// <summary>
+ /// Class UserDataSaveEventArgs
+ /// </summary>
+ public class UserDataSaveEventArgs : EventArgs
+ {
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ /// <value>The user id.</value>
+ public Guid UserId { get; set; }
+
+ public List<string> Keys { get; set; }
+
+ /// <summary>
+ /// Gets or sets the save reason.
+ /// </summary>
+ /// <value>The save reason.</value>
+ public UserDataSaveReason SaveReason { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user data.
+ /// </summary>
+ /// <value>The user data.</value>
+ public UserItemData UserData { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item.
+ /// </summary>
+ /// <value>The item.</value>
+ public BaseItem Item { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs
new file mode 100644
index 000000000..c000da852
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs
@@ -0,0 +1,74 @@
+using MediaBrowser.Model.LiveTv;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ /// <summary>
+ /// Class ChannelInfo
+ /// </summary>
+ public class ChannelInfo
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the number.
+ /// </summary>
+ /// <value>The number.</value>
+ public string Number { get; set; }
+
+ /// <summary>
+ /// Get or sets the Id.
+ /// </summary>
+ /// <value>The id of the channel.</value>
+ public string Id { get; set; }
+
+ public string Path { get; set; }
+
+ public string TunerChannelId { get; set; }
+
+ public string CallSign { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tuner host identifier.
+ /// </summary>
+ /// <value>The tuner host identifier.</value>
+ public string TunerHostId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the channel.
+ /// </summary>
+ /// <value>The type of the channel.</value>
+ public ChannelType ChannelType { get; set; }
+
+ /// <summary>
+ /// Supply the image path if it can be accessed directly from the file system
+ /// </summary>
+ /// <value>The image path.</value>
+ public string ImagePath { get; set; }
+
+ /// <summary>
+ /// Supply the image url if it can be downloaded
+ /// </summary>
+ /// <value>The image URL.</value>
+ public string ImageUrl { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has image.
+ /// </summary>
+ /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value>
+ public bool? HasImage { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is favorite.
+ /// </summary>
+ /// <value><c>null</c> if [is favorite] contains no value, <c>true</c> if [is favorite]; otherwise, <c>false</c>.</value>
+ public bool? IsFavorite { get; set; }
+
+ public bool? IsHD { get; set; }
+ public string AudioCodec { get; set; }
+ public string VideoCodec { get; set; }
+ public string[] Tags { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs
new file mode 100644
index 000000000..faf4a34df
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs
@@ -0,0 +1,19 @@
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.LiveTv;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ public interface IListingsProvider
+ {
+ string Name { get; }
+ string Type { get; }
+ Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken);
+ Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings);
+ Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location);
+ Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
new file mode 100644
index 000000000..a7f675034
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
@@ -0,0 +1,293 @@
+using System;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Querying;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Controller.Library;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ /// <summary>
+ /// Manages all live tv services installed on the server
+ /// </summary>
+ public interface ILiveTvManager
+ {
+ /// <summary>
+ /// Gets the services.
+ /// </summary>
+ /// <value>The services.</value>
+ IReadOnlyList<ILiveTvService> Services { get; }
+
+ /// <summary>
+ /// Gets the new timer defaults asynchronous.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{TimerInfo}.</returns>
+ Task<SeriesTimerInfoDto> GetNewTimerDefaults(CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the new timer defaults.
+ /// </summary>
+ /// <param name="programId">The program identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{SeriesTimerInfoDto}.</returns>
+ Task<SeriesTimerInfoDto> GetNewTimerDefaults(string programId, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Cancels the timer.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <returns>Task.</returns>
+ Task CancelTimer(string id);
+
+ /// <summary>
+ /// Cancels the series timer.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <returns>Task.</returns>
+ Task CancelSeriesTimer(string id);
+
+ /// <summary>
+ /// Adds the parts.
+ /// </summary>
+ /// <param name="services">The services.</param>
+ /// <param name="tunerHosts">The tuner hosts.</param>
+ /// <param name="listingProviders">The listing providers.</param>
+ void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<ITunerHost> tunerHosts, IEnumerable<IListingsProvider> listingProviders);
+
+ /// <summary>
+ /// Gets the timer.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{TimerInfoDto}.</returns>
+ Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the series timer.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{TimerInfoDto}.</returns>
+ Task<SeriesTimerInfoDto> GetSeriesTimer(string id, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the recordings.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="options">The options.</param>
+ QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options);
+
+ /// <summary>
+ /// Gets the timers.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{QueryResult{TimerInfoDto}}.</returns>
+ Task<QueryResult<TimerInfoDto>> GetTimers(TimerQuery query, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the series timers.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{QueryResult{SeriesTimerInfoDto}}.</returns>
+ Task<QueryResult<SeriesTimerInfoDto>> GetSeriesTimers(SeriesTimerQuery query, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the channel stream.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="mediaSourceId">The media source identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{StreamResponseInfo}.</returns>
+ Task<Tuple<MediaSourceInfo, ILiveStream>> GetChannelStream(string id, string mediaSourceId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the program.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>Task{ProgramInfoDto}.</returns>
+ Task<BaseItemDto> GetProgram(string id, CancellationToken cancellationToken, User user = null);
+
+ /// <summary>
+ /// Gets the programs.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="options">The options.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>IEnumerable{ProgramInfo}.</returns>
+ Task<QueryResult<BaseItemDto>> GetPrograms(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Updates the timer.
+ /// </summary>
+ /// <param name="timer">The timer.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task UpdateTimer(TimerInfoDto timer, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Updates the timer.
+ /// </summary>
+ /// <param name="timer">The timer.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task UpdateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Creates the timer.
+ /// </summary>
+ /// <param name="timer">The timer.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task CreateTimer(TimerInfoDto timer, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Creates the series timer.
+ /// </summary>
+ /// <param name="timer">The timer.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task CreateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the guide information.
+ /// </summary>
+ /// <returns>GuideInfo.</returns>
+ GuideInfo GetGuideInfo();
+
+ /// <summary>
+ /// Gets the recommended programs.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <param name="options">The options.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ QueryResult<BaseItemDto> GetRecommendedPrograms(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the recommended programs internal.
+ /// </summary>
+ QueryResult<BaseItem> GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the live tv information.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{LiveTvInfo}.</returns>
+ LiveTvInfo GetLiveTvInfo(CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Resets the tuner.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task ResetTuner(string id, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the live tv folder.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ Folder GetInternalLiveTvFolder(CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the enabled users.
+ /// </summary>
+ /// <returns>IEnumerable{User}.</returns>
+ IEnumerable<User> GetEnabledUsers();
+
+ /// <summary>
+ /// Gets the internal channels.
+ /// </summary>
+ QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the channel media sources.
+ /// </summary>
+ Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(BaseItem item, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Adds the information to program dto.
+ /// </summary>
+ /// <param name="programs">The programs.</param>
+ /// <param name="fields">The fields.</param>
+ /// <param name="user">The user.</param>
+ /// <returns>Task.</returns>
+ Task AddInfoToProgramDto(List<Tuple<BaseItem, BaseItemDto>> programs, ItemFields[] fields, User user = null);
+
+ /// <summary>
+ /// Saves the tuner host.
+ /// </summary>
+ Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true);
+ /// <summary>
+ /// Saves the listing provider.
+ /// </summary>
+ /// <param name="info">The information.</param>
+ /// <param name="validateLogin">if set to <c>true</c> [validate login].</param>
+ /// <param name="validateListings">if set to <c>true</c> [validate listings].</param>
+ /// <returns>Task.</returns>
+ Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings);
+
+ void DeleteListingsProvider(string id);
+
+ Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber);
+
+ TunerChannelMapping GetTunerChannelMapping(ChannelInfo channel, NameValuePair[] mappings, List<ChannelInfo> providerChannels);
+
+ /// <summary>
+ /// Gets the lineups.
+ /// </summary>
+ /// <param name="providerType">Type of the provider.</param>
+ /// <param name="providerId">The provider identifier.</param>
+ /// <param name="country">The country.</param>
+ /// <param name="location">The location.</param>
+ /// <returns>Task&lt;List&lt;NameIdPair&gt;&gt;.</returns>
+ Task<List<NameIdPair>> GetLineups(string providerType, string providerId, string country, string location);
+
+ /// <summary>
+ /// Adds the channel information.
+ /// </summary>
+ /// <param name="items">The items.</param>
+ /// <param name="options">The options.</param>
+ /// <param name="user">The user.</param>
+ void AddChannelInfo(List<Tuple<BaseItemDto, LiveTvChannel>> items, DtoOptions options, User user);
+
+ Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken);
+ Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken);
+
+ IListingsProvider[] ListingProviders { get; }
+
+ List<NameIdPair> GetTunerHostTypes();
+ Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken);
+
+ event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
+ event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled;
+ event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated;
+ event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated;
+
+ string GetEmbyTvActiveRecordingPath(string id);
+
+ ActiveRecordingInfo GetActiveRecordingInfo(string path);
+
+ void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, User user = null);
+
+ List<BaseItem> GetRecordingFolders(User user);
+ }
+
+ public class ActiveRecordingInfo
+ {
+ public string Id { get; set; }
+ public string Path { get; set; }
+ public TimerInfo Timer { get; set; }
+ public CancellationTokenSource CancellationTokenSource { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs
new file mode 100644
index 000000000..601fb69aa
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs
@@ -0,0 +1,190 @@
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Model.Dto;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Library;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ /// <summary>
+ /// Represents a single live tv back end (next pvr, media portal, etc).
+ /// </summary>
+ public interface ILiveTvService
+ {
+ /// <summary>
+ /// Occurs when [data source changed].
+ /// </summary>
+ event EventHandler DataSourceChanged;
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ /// <summary>
+ /// Gets the home page URL.
+ /// </summary>
+ /// <value>The home page URL.</value>
+ string HomePageUrl { get; }
+
+ /// <summary>
+ /// Gets the channels async.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{ChannelInfo}}.</returns>
+ Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Cancels the timer asynchronous.
+ /// </summary>
+ /// <param name="timerId">The timer identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task CancelTimerAsync(string timerId, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Cancels the series timer asynchronous.
+ /// </summary>
+ /// <param name="timerId">The timer identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Creates the timer asynchronous.
+ /// </summary>
+ /// <param name="info">The information.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Creates the series timer asynchronous.
+ /// </summary>
+ /// <param name="info">The information.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Updates the timer asynchronous.
+ /// </summary>
+ /// <param name="info">The information.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task UpdateTimerAsync(TimerInfo info, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Updates the series timer asynchronous.
+ /// </summary>
+ /// <param name="info">The information.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the recordings asynchronous.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{RecordingInfo}}.</returns>
+ Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the new timer defaults asynchronous.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="program">The program.</param>
+ /// <returns>Task{SeriesTimerInfo}.</returns>
+ Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null);
+
+ /// <summary>
+ /// Gets the series timers asynchronous.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{SeriesTimerInfo}}.</returns>
+ Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the programs asynchronous.
+ /// </summary>
+ /// <param name="channelId">The channel identifier.</param>
+ /// <param name="startDateUtc">The start date UTC.</param>
+ /// <param name="endDateUtc">The end date UTC.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{ProgramInfo}}.</returns>
+ Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the channel stream.
+ /// </summary>
+ /// <param name="channelId">The channel identifier.</param>
+ /// <param name="streamId">The stream identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{Stream}.</returns>
+ Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the channel stream media sources.
+ /// </summary>
+ /// <param name="channelId">The channel identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task&lt;List&lt;MediaSourceInfo&gt;&gt;.</returns>
+ Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Closes the live stream.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task CloseLiveStream(string id, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Records the live stream.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task RecordLiveStream(string id, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Resets the tuner.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task ResetTuner(string id, CancellationToken cancellationToken);
+ }
+
+ public interface ISupportsNewTimerIds
+ {
+ /// <summary>
+ /// Creates the timer asynchronous.
+ /// </summary>
+ /// <param name="info">The information.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Creates the series timer asynchronous.
+ /// </summary>
+ /// <param name="info">The information.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken);
+ }
+
+ public interface ISupportsDirectStreamProvider
+ {
+ Task<ILiveStream> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
+ }
+
+ public interface ISupportsUpdatingDefaults
+ {
+ Task UpdateTimerDefaults(SeriesTimerInfo info, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs
new file mode 100644
index 000000000..d5a0e2115
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs
@@ -0,0 +1,62 @@
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.LiveTv;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Library;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ public interface ITunerHost
+ {
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+ /// <summary>
+ /// Gets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ string Type { get; }
+ /// <summary>
+ /// Gets the channels.
+ /// </summary>
+ /// <returns>Task&lt;IEnumerable&lt;ChannelInfo&gt;&gt;.</returns>
+ Task<List<ChannelInfo>> GetChannels(bool enableCache, CancellationToken cancellationToken);
+ /// <summary>
+ /// Gets the tuner infos.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task&lt;List&lt;LiveTvTunerInfo&gt;&gt;.</returns>
+ Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken);
+ /// <summary>
+ /// Gets the channel stream.
+ /// </summary>
+ /// <param name="channelId">The channel identifier.</param>
+ /// <param name="streamId">The stream identifier.</param>
+ Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
+ /// <summary>
+ /// Gets the channel stream media sources.
+ /// </summary>
+ /// <param name="channelId">The channel identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task&lt;List&lt;MediaSourceInfo&gt;&gt;.</returns>
+ Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken);
+
+ Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken);
+ bool IsSupported
+ {
+ get;
+ }
+ }
+ public interface IConfigurableTunerHost
+ {
+ /// <summary>
+ /// Validates the specified information.
+ /// </summary>
+ /// <param name="info">The information.</param>
+ /// <returns>Task.</returns>
+ Task Validate(TunerHostInfo info);
+ }
+}
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
new file mode 100644
index 000000000..9e2d29eb6
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
@@ -0,0 +1,197 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.MediaInfo;
+using System.Collections.Generic;
+using System.Globalization;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Linq;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ public class LiveTvChannel : BaseItem, IHasMediaSources, IHasProgramAttributes
+ {
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ if (!ConfigurationManager.Configuration.DisableLiveTvChannelUserDataName)
+ {
+ list.Insert(0, GetClientTypeName() + "-" + Name);
+ }
+
+ return list;
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ return UnratedItem.LiveTvChannel;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPositionTicksResume
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override SourceType SourceType
+ {
+ get { return SourceType.LiveTV; }
+ }
+
+ [IgnoreDataMember]
+ public override bool EnableRememberingTrackSelections
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the number.
+ /// </summary>
+ /// <value>The number.</value>
+ public string Number { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the channel.
+ /// </summary>
+ /// <value>The type of the channel.</value>
+ public ChannelType ChannelType { get; set; }
+
+ [IgnoreDataMember]
+ public override LocationType LocationType
+ {
+ get
+ {
+ // TODO: This should be removed
+ return LocationType.Remote;
+ }
+ }
+
+ protected override string CreateSortName()
+ {
+ if (!string.IsNullOrEmpty(Number))
+ {
+ double number = 0;
+
+ if (double.TryParse(Number, NumberStyles.Any, CultureInfo.InvariantCulture, out number))
+ {
+ return string.Format("{0:00000.0}", number) + "-" + (Name ?? string.Empty);
+ }
+ }
+
+ return (Number ?? string.Empty) + "-" + (Name ?? string.Empty);
+ }
+
+ [IgnoreDataMember]
+ public override string MediaType
+ {
+ get
+ {
+ return ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video;
+ }
+ }
+
+ public override string GetClientTypeName()
+ {
+ return "TvChannel";
+ }
+
+ public IEnumerable<BaseItem> GetTaggedItems(IEnumerable<BaseItem> inputItems)
+ {
+ return new List<BaseItem>();
+ }
+
+ public override List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution)
+ {
+ var list = new List<MediaSourceInfo>();
+
+ var info = new MediaSourceInfo
+ {
+ Id = Id.ToString("N"),
+ Protocol = PathProtocol ?? MediaProtocol.File,
+ MediaStreams = new List<MediaStream>(),
+ Name = Name,
+ Path = Path,
+ RunTimeTicks = RunTimeTicks,
+ Type = MediaSourceType.Placeholder,
+ IsInfiniteStream = RunTimeTicks == null
+ };
+
+ list.Add(info);
+
+ return list;
+ }
+
+ public override List<MediaStream> GetMediaStreams()
+ {
+ return new List<MediaStream>();
+ }
+
+ protected override string GetInternalMetadataPath(string basePath)
+ {
+ return System.IO.Path.Combine(basePath, "livetv", Id.ToString("N"), "metadata");
+ }
+
+ public override bool CanDelete()
+ {
+ return false;
+ }
+
+ [IgnoreDataMember]
+ public bool IsMovie { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is sports.
+ /// </summary>
+ /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool IsSports { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is series.
+ /// </summary>
+ /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool IsSeries { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is news.
+ /// </summary>
+ /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool IsNews { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is kids.
+ /// </summary>
+ /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool IsKids
+ {
+ get
+ {
+ return Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ [IgnoreDataMember]
+ public bool IsRepeat { get; set; }
+
+ /// <summary>
+ /// Gets or sets the episode title.
+ /// </summary>
+ /// <value>The episode title.</value>
+ [IgnoreDataMember]
+ public string EpisodeTitle { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs b/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs
new file mode 100644
index 000000000..a7735ad80
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ /// <summary>
+ /// Class LiveTvConflictException.
+ /// </summary>
+ public class LiveTvConflictException : Exception
+ {
+ public LiveTvConflictException()
+ {
+
+ }
+ public LiveTvConflictException(string message)
+ : base(message)
+ {
+
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
new file mode 100644
index 000000000..fa3aab4f2
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
@@ -0,0 +1,349 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.LiveTv;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ public class LiveTvProgram : BaseItem, IHasLookupInfo<ItemLookupInfo>, IHasStartDate, IHasProgramAttributes
+ {
+ public LiveTvProgram()
+ {
+ IsVirtualItem = true;
+ }
+
+ public override List<string> GetUserDataKeys()
+ {
+ var list = base.GetUserDataKeys();
+
+ if (!IsSeries)
+ {
+ var key = this.GetProviderId(MetadataProviders.Imdb);
+ if (!string.IsNullOrEmpty(key))
+ {
+ list.Insert(0, key);
+ }
+
+ key = this.GetProviderId(MetadataProviders.Tmdb);
+ if (!string.IsNullOrEmpty(key))
+ {
+ list.Insert(0, key);
+ }
+ }
+ else if (!string.IsNullOrEmpty(EpisodeTitle))
+ {
+ var name = GetClientTypeName();
+
+ list.Insert(0, name + "-" + Name + (EpisodeTitle ?? string.Empty));
+ }
+
+ return list;
+ }
+
+ public static double GetDefaultPrimaryImageAspectRatio(IHasProgramAttributes item)
+ {
+ var serviceName = item.ServiceName;
+
+ if (item.IsMovie)
+ {
+ if (string.Equals(serviceName, EmbyServiceName, StringComparison.OrdinalIgnoreCase) || string.Equals(serviceName, "Next Pvr", StringComparison.OrdinalIgnoreCase))
+ {
+ double value = 2;
+ value /= 3;
+
+ return value;
+ }
+ else
+ {
+ double value = 16;
+ value /= 9;
+
+ return value;
+ }
+ }
+ else
+ {
+ if (string.Equals(serviceName, EmbyServiceName, StringComparison.OrdinalIgnoreCase) || string.Equals(serviceName, "Next Pvr", StringComparison.OrdinalIgnoreCase))
+ {
+ double value = 2;
+ value /= 3;
+
+ return value;
+ }
+ else
+ {
+ double value = 16;
+ value /= 9;
+
+ return value;
+ }
+ }
+ }
+
+ private static string EmbyServiceName = "Emby";
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ return GetDefaultPrimaryImageAspectRatio(this);
+ }
+
+ [IgnoreDataMember]
+ public override SourceType SourceType
+ {
+ get { return SourceType.LiveTV; }
+ }
+
+ /// <summary>
+ /// The start date of the program, in UTC.
+ /// </summary>
+ [IgnoreDataMember]
+ public DateTime StartDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is repeat.
+ /// </summary>
+ /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool IsRepeat { get; set; }
+
+ /// <summary>
+ /// Gets or sets the episode title.
+ /// </summary>
+ /// <value>The episode title.</value>
+ [IgnoreDataMember]
+ public string EpisodeTitle { get; set; }
+
+ [IgnoreDataMember]
+ public string ShowId { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is movie.
+ /// </summary>
+ /// <value><c>true</c> if this instance is movie; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool IsMovie { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is sports.
+ /// </summary>
+ /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool IsSports
+ {
+ get
+ {
+ return Tags.Contains("Sports", StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is series.
+ /// </summary>
+ /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool IsSeries { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is live.
+ /// </summary>
+ /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool IsLive
+ {
+ get
+ {
+ return Tags.Contains("Live", StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is news.
+ /// </summary>
+ /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool IsNews
+ {
+ get
+ {
+ return Tags.Contains("News", StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is kids.
+ /// </summary>
+ /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool IsKids
+ {
+ get
+ {
+ return Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is premiere.
+ /// </summary>
+ /// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool IsPremiere
+ {
+ get
+ {
+ return Tags.Contains("Premiere", StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ /// <summary>
+ /// Returns the folder containing the item.
+ /// If the item is a folder, it returns the folder itself
+ /// </summary>
+ /// <value>The containing folder path.</value>
+ [IgnoreDataMember]
+ public override string ContainingFolderPath
+ {
+ get
+ {
+ return Path;
+ }
+ }
+
+ //[IgnoreDataMember]
+ //public override string MediaType
+ //{
+ // get
+ // {
+ // return ChannelType == ChannelType.TV ? Model.Entities.MediaType.Video : Model.Entities.MediaType.Audio;
+ // }
+ //}
+
+ [IgnoreDataMember]
+ public bool IsAiring
+ {
+ get
+ {
+ var now = DateTime.UtcNow;
+
+ return now >= StartDate && now < EndDate;
+ }
+ }
+
+ [IgnoreDataMember]
+ public bool HasAired
+ {
+ get
+ {
+ var now = DateTime.UtcNow;
+
+ return now >= EndDate;
+ }
+ }
+
+ public override string GetClientTypeName()
+ {
+ return "Program";
+ }
+
+ public override UnratedItem GetBlockUnratedType()
+ {
+ return UnratedItem.LiveTvProgram;
+ }
+
+ protected override string GetInternalMetadataPath(string basePath)
+ {
+ return System.IO.Path.Combine(basePath, "livetv", Id.ToString("N"));
+ }
+
+ public override bool CanDelete()
+ {
+ return false;
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPeople
+ {
+ get
+ {
+ // Optimization
+ if (IsNews || IsSports)
+ {
+ return false;
+ }
+
+ return base.SupportsPeople;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsAncestors
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ private LiveTvOptions GetConfiguration()
+ {
+ return ConfigurationManager.GetConfiguration<LiveTvOptions>("livetv");
+ }
+
+ private ListingsProviderInfo GetListingsProviderInfo()
+ {
+ if (string.Equals(ServiceName, "Emby", StringComparison.OrdinalIgnoreCase))
+ {
+ var config = GetConfiguration();
+
+ return config.ListingProviders.FirstOrDefault(i => !string.IsNullOrEmpty(i.MoviePrefix));
+ }
+
+ return null;
+ }
+
+ protected override string GetNameForMetadataLookup()
+ {
+ var name = base.GetNameForMetadataLookup();
+
+ var listings = GetListingsProviderInfo();
+
+ if (listings != null)
+ {
+ if (!string.IsNullOrEmpty(listings.MoviePrefix) && name.StartsWith(listings.MoviePrefix, StringComparison.OrdinalIgnoreCase))
+ {
+ name = name.Substring(listings.MoviePrefix.Length).Trim();
+ }
+ }
+
+ return name;
+ }
+
+ public override List<ExternalUrl> GetRelatedUrls()
+ {
+ var list = base.GetRelatedUrls();
+
+ var imdbId = this.GetProviderId(MetadataProviders.Imdb);
+ if (!string.IsNullOrEmpty(imdbId))
+ {
+ if (IsMovie)
+ {
+ list.Add(new ExternalUrl
+ {
+ Name = "Trakt",
+ Url = string.Format("https://trakt.tv/movies/{0}", imdbId)
+ });
+ }
+ }
+
+ return list;
+ }
+
+ public string SeriesName { get; set;}
+ }
+}
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs
new file mode 100644
index 000000000..4da238acf
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs
@@ -0,0 +1,49 @@
+using MediaBrowser.Model.LiveTv;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ public class LiveTvServiceStatusInfo
+ {
+ /// <summary>
+ /// Gets or sets the status.
+ /// </summary>
+ /// <value>The status.</value>
+ public LiveTvServiceStatus Status { get; set; }
+
+ /// <summary>
+ /// Gets or sets the status message.
+ /// </summary>
+ /// <value>The status message.</value>
+ public string StatusMessage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the version.
+ /// </summary>
+ /// <value>The version.</value>
+ public string Version { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has update available.
+ /// </summary>
+ /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
+ public bool HasUpdateAvailable { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tuners.
+ /// </summary>
+ /// <value>The tuners.</value>
+ public List<LiveTvTunerInfo> Tuners { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is visible.
+ /// </summary>
+ /// <value><c>true</c> if this instance is visible; otherwise, <c>false</c>.</value>
+ public bool IsVisible { get; set; }
+
+ public LiveTvServiceStatusInfo()
+ {
+ Tuners = new List<LiveTvTunerInfo>();
+ IsVisible = true;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs
new file mode 100644
index 000000000..5c001f288
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs
@@ -0,0 +1,73 @@
+using MediaBrowser.Model.LiveTv;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ public class LiveTvTunerInfo
+ {
+ /// <summary>
+ /// Gets or sets the type of the source.
+ /// </summary>
+ /// <value>The type of the source.</value>
+ public string SourceType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the identifier.
+ /// </summary>
+ /// <value>The identifier.</value>
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the URL.
+ /// </summary>
+ /// <value>The URL.</value>
+ public string Url { get; set; }
+
+ /// <summary>
+ /// Gets or sets the status.
+ /// </summary>
+ /// <value>The status.</value>
+ public LiveTvTunerStatus Status { get; set; }
+
+ /// <summary>
+ /// Gets or sets the channel identifier.
+ /// </summary>
+ /// <value>The channel identifier.</value>
+ public string ChannelId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the recording identifier.
+ /// </summary>
+ /// <value>The recording identifier.</value>
+ public string RecordingId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the program.
+ /// </summary>
+ /// <value>The name of the program.</value>
+ public string ProgramName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the clients.
+ /// </summary>
+ /// <value>The clients.</value>
+ public List<string> Clients { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance can reset.
+ /// </summary>
+ /// <value><c>true</c> if this instance can reset; otherwise, <c>false</c>.</value>
+ public bool CanReset { get; set; }
+
+ public LiveTvTunerInfo()
+ {
+ Clients = new List<string>();
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs
new file mode 100644
index 000000000..9e3cbdded
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs
@@ -0,0 +1,213 @@
+using MediaBrowser.Model.LiveTv;
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ public class ProgramInfo
+ {
+ /// <summary>
+ /// Id of the program.
+ /// </summary>
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the channel identifier.
+ /// </summary>
+ /// <value>The channel identifier.</value>
+ public string ChannelId { get; set; }
+
+ /// <summary>
+ /// Name of the program
+ /// </summary>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the official rating.
+ /// </summary>
+ /// <value>The official rating.</value>
+ public string OfficialRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the overview.
+ /// </summary>
+ /// <value>The overview.</value>
+ public string Overview { get; set; }
+ /// <summary>
+ /// Gets or sets the short overview.
+ /// </summary>
+ /// <value>The short overview.</value>
+ public string ShortOverview { get; set; }
+
+ /// <summary>
+ /// The start date of the program, in UTC.
+ /// </summary>
+ public DateTime StartDate { get; set; }
+
+ /// <summary>
+ /// The end date of the program, in UTC.
+ /// </summary>
+ public DateTime EndDate { get; set; }
+
+ /// <summary>
+ /// Genre of the program.
+ /// </summary>
+ public List<string> Genres { get; set; }
+
+ /// <summary>
+ /// Gets or sets the original air date.
+ /// </summary>
+ /// <value>The original air date.</value>
+ public DateTime? OriginalAirDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is hd.
+ /// </summary>
+ /// <value><c>true</c> if this instance is hd; otherwise, <c>false</c>.</value>
+ public bool? IsHD { get; set; }
+
+ public bool? Is3D { get; set; }
+
+ /// <summary>
+ /// Gets or sets the audio.
+ /// </summary>
+ /// <value>The audio.</value>
+ public ProgramAudio? Audio { get; set; }
+
+ /// <summary>
+ /// Gets or sets the community rating.
+ /// </summary>
+ /// <value>The community rating.</value>
+ public float? CommunityRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is repeat.
+ /// </summary>
+ /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value>
+ public bool IsRepeat { get; set; }
+
+ public bool IsSubjectToBlackout { get; set; }
+
+ /// <summary>
+ /// Gets or sets the episode title.
+ /// </summary>
+ /// <value>The episode title.</value>
+ public string EpisodeTitle { get; set; }
+
+ /// <summary>
+ /// Supply the image path if it can be accessed directly from the file system
+ /// </summary>
+ /// <value>The image path.</value>
+ public string ImagePath { get; set; }
+
+ /// <summary>
+ /// Supply the image url if it can be downloaded
+ /// </summary>
+ /// <value>The image URL.</value>
+ public string ImageUrl { get; set; }
+
+ public string ThumbImageUrl { get; set; }
+
+ public string LogoImageUrl { get; set; }
+
+ public string BackdropImageUrl { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has image.
+ /// </summary>
+ /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value>
+ public bool? HasImage { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is movie.
+ /// </summary>
+ /// <value><c>true</c> if this instance is movie; otherwise, <c>false</c>.</value>
+ public bool IsMovie { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is sports.
+ /// </summary>
+ /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value>
+ public bool IsSports { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is series.
+ /// </summary>
+ /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value>
+ public bool IsSeries { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is live.
+ /// </summary>
+ /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value>
+ public bool IsLive { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is news.
+ /// </summary>
+ /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value>
+ public bool IsNews { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is kids.
+ /// </summary>
+ /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value>
+ public bool IsKids { get; set; }
+
+ public bool IsEducational { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is premiere.
+ /// </summary>
+ /// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value>
+ public bool IsPremiere { get; set; }
+
+ /// <summary>
+ /// Gets or sets the production year.
+ /// </summary>
+ /// <value>The production year.</value>
+ public int? ProductionYear { get; set; }
+ /// <summary>
+ /// Gets or sets the home page URL.
+ /// </summary>
+ /// <value>The home page URL.</value>
+ public string HomePageUrl { get; set; }
+ /// <summary>
+ /// Gets or sets the series identifier.
+ /// </summary>
+ /// <value>The series identifier.</value>
+ public string SeriesId { get; set; }
+ /// <summary>
+ /// Gets or sets the show identifier.
+ /// </summary>
+ /// <value>The show identifier.</value>
+ public string ShowId { get; set; }
+ /// <summary>
+ /// Gets or sets the season number.
+ /// </summary>
+ /// <value>The season number.</value>
+ public int? SeasonNumber { get; set; }
+ /// <summary>
+ /// Gets or sets the episode number.
+ /// </summary>
+ /// <value>The episode number.</value>
+ public int? EpisodeNumber { get; set; }
+ /// <summary>
+ /// Gets or sets the etag.
+ /// </summary>
+ /// <value>The etag.</value>
+ public string Etag { get; set; }
+
+ public Dictionary<string, string> ProviderIds { get; set; }
+ public Dictionary<string, string> SeriesProviderIds { get; set; }
+
+ public ProgramInfo()
+ {
+ Genres = new List<string>();
+
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs
new file mode 100644
index 000000000..3006b9bbe
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs
@@ -0,0 +1,205 @@
+using MediaBrowser.Model.LiveTv;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ public class RecordingInfo
+ {
+ /// <summary>
+ /// Id of the recording.
+ /// </summary>
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the series timer identifier.
+ /// </summary>
+ /// <value>The series timer identifier.</value>
+ public string SeriesTimerId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the timer identifier.
+ /// </summary>
+ /// <value>The timer identifier.</value>
+ public string TimerId { get; set; }
+
+ /// <summary>
+ /// ChannelId of the recording.
+ /// </summary>
+ public string ChannelId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the channel.
+ /// </summary>
+ /// <value>The type of the channel.</value>
+ public ChannelType ChannelType { get; set; }
+
+ /// <summary>
+ /// Name of the recording.
+ /// </summary>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ public string Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets the URL.
+ /// </summary>
+ /// <value>The URL.</value>
+ public string Url { get; set; }
+
+ /// <summary>
+ /// Gets or sets the overview.
+ /// </summary>
+ /// <value>The overview.</value>
+ public string Overview { get; set; }
+
+ /// <summary>
+ /// The start date of the recording, in UTC.
+ /// </summary>
+ public DateTime StartDate { get; set; }
+
+ /// <summary>
+ /// The end date of the recording, in UTC.
+ /// </summary>
+ public DateTime EndDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the program identifier.
+ /// </summary>
+ /// <value>The program identifier.</value>
+ public string ProgramId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the status.
+ /// </summary>
+ /// <value>The status.</value>
+ public RecordingStatus Status { get; set; }
+
+ /// <summary>
+ /// Genre of the program.
+ /// </summary>
+ public List<string> Genres { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is repeat.
+ /// </summary>
+ /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value>
+ public bool IsRepeat { get; set; }
+
+ /// <summary>
+ /// Gets or sets the episode title.
+ /// </summary>
+ /// <value>The episode title.</value>
+ public string EpisodeTitle { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is hd.
+ /// </summary>
+ /// <value><c>true</c> if this instance is hd; otherwise, <c>false</c>.</value>
+ public bool? IsHD { get; set; }
+
+ /// <summary>
+ /// Gets or sets the audio.
+ /// </summary>
+ /// <value>The audio.</value>
+ public ProgramAudio? Audio { get; set; }
+
+ /// <summary>
+ /// Gets or sets the original air date.
+ /// </summary>
+ /// <value>The original air date.</value>
+ public DateTime? OriginalAirDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is movie.
+ /// </summary>
+ /// <value><c>true</c> if this instance is movie; otherwise, <c>false</c>.</value>
+ public bool IsMovie { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is sports.
+ /// </summary>
+ /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value>
+ public bool IsSports { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is series.
+ /// </summary>
+ /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value>
+ public bool IsSeries { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is live.
+ /// </summary>
+ /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value>
+ public bool IsLive { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is news.
+ /// </summary>
+ /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value>
+ public bool IsNews { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is kids.
+ /// </summary>
+ /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value>
+ public bool IsKids { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is premiere.
+ /// </summary>
+ /// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value>
+ public bool IsPremiere { get; set; }
+
+ /// <summary>
+ /// Gets or sets the official rating.
+ /// </summary>
+ /// <value>The official rating.</value>
+ public string OfficialRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the community rating.
+ /// </summary>
+ /// <value>The community rating.</value>
+ public float? CommunityRating { get; set; }
+
+ /// <summary>
+ /// Supply the image path if it can be accessed directly from the file system
+ /// </summary>
+ /// <value>The image path.</value>
+ public string ImagePath { get; set; }
+
+ /// <summary>
+ /// Supply the image url if it can be downloaded
+ /// </summary>
+ /// <value>The image URL.</value>
+ public string ImageUrl { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has image.
+ /// </summary>
+ /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value>
+ public bool? HasImage { get; set; }
+ /// <summary>
+ /// Gets or sets the show identifier.
+ /// </summary>
+ /// <value>The show identifier.</value>
+ public string ShowId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date last updated.
+ /// </summary>
+ /// <value>The date last updated.</value>
+ public DateTime DateLastUpdated { get; set; }
+
+ public RecordingInfo()
+ {
+ Genres = new List<string>();
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs b/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs
new file mode 100644
index 000000000..90ea329fe
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs
@@ -0,0 +1,12 @@
+using MediaBrowser.Model.LiveTv;
+using System;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ public class RecordingStatusChangedEventArgs : EventArgs
+ {
+ public string RecordingId { get; set; }
+
+ public RecordingStatus NewStatus { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs
new file mode 100644
index 000000000..5c73ed833
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.LiveTv;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ public class SeriesTimerInfo
+ {
+ /// <summary>
+ /// Id of the recording.
+ /// </summary>
+ public string Id { get; set; }
+
+ /// <summary>
+ /// ChannelId of the recording.
+ /// </summary>
+ public string ChannelId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the program identifier.
+ /// </summary>
+ /// <value>The program identifier.</value>
+ public string ProgramId { get; set; }
+
+ /// <summary>
+ /// Name of the recording.
+ /// </summary>
+ public string Name { get; set; }
+
+ public string ServiceName { get; set; }
+
+ /// <summary>
+ /// Description of the recording.
+ /// </summary>
+ public string Overview { get; set; }
+
+ /// <summary>
+ /// The start date of the recording, in UTC.
+ /// </summary>
+ public DateTime StartDate { get; set; }
+
+ /// <summary>
+ /// The end date of the recording, in UTC.
+ /// </summary>
+ public DateTime EndDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [record any time].
+ /// </summary>
+ /// <value><c>true</c> if [record any time]; otherwise, <c>false</c>.</value>
+ public bool RecordAnyTime { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [record any channel].
+ /// </summary>
+ /// <value><c>true</c> if [record any channel]; otherwise, <c>false</c>.</value>
+ public bool RecordAnyChannel { get; set; }
+
+ public int KeepUpTo { get; set; }
+ public KeepUntil KeepUntil { get; set; }
+
+ public bool SkipEpisodesInLibrary { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [record new only].
+ /// </summary>
+ /// <value><c>true</c> if [record new only]; otherwise, <c>false</c>.</value>
+ public bool RecordNewOnly { get; set; }
+
+ /// <summary>
+ /// Gets or sets the days.
+ /// </summary>
+ /// <value>The days.</value>
+ public List<DayOfWeek> Days { get; set; }
+
+ /// <summary>
+ /// Gets or sets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public int Priority { get; set; }
+
+ /// <summary>
+ /// Gets or sets the pre padding seconds.
+ /// </summary>
+ /// <value>The pre padding seconds.</value>
+ public int PrePaddingSeconds { get; set; }
+
+ /// <summary>
+ /// Gets or sets the post padding seconds.
+ /// </summary>
+ /// <value>The post padding seconds.</value>
+ public int PostPaddingSeconds { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is pre padding required.
+ /// </summary>
+ /// <value><c>true</c> if this instance is pre padding required; otherwise, <c>false</c>.</value>
+ public bool IsPrePaddingRequired { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is post padding required.
+ /// </summary>
+ /// <value><c>true</c> if this instance is post padding required; otherwise, <c>false</c>.</value>
+ public bool IsPostPaddingRequired { get; set; }
+
+ /// <summary>
+ /// Gets or sets the series identifier.
+ /// </summary>
+ /// <value>The series identifier.</value>
+ public string SeriesId { get; set; }
+
+ public SeriesTimerInfo()
+ {
+ Days = new List<DayOfWeek>();
+ SkipEpisodesInLibrary = true;
+ KeepUntil = KeepUntil.UntilDeleted;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs
new file mode 100644
index 000000000..5b71a26a2
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/TimerEventInfo.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ public class TimerEventInfo
+ {
+ public string Id { get; set; }
+ public Guid ProgramId { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/LiveTv/TimerInfo.cs b/MediaBrowser.Controller/LiveTv/TimerInfo.cs
new file mode 100644
index 000000000..baf0b0b13
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/TimerInfo.cs
@@ -0,0 +1,175 @@
+using MediaBrowser.Model.LiveTv;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Controller.LiveTv
+{
+ public class TimerInfo
+ {
+ public TimerInfo()
+ {
+ Genres = new string[] { };
+ KeepUntil = KeepUntil.UntilDeleted;
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ Tags = new string[] { };
+ }
+
+ public Dictionary<string, string> ProviderIds { get; set; }
+ public Dictionary<string, string> SeriesProviderIds { get; set; }
+ public string[] Tags { get; set; }
+
+ /// <summary>
+ /// Id of the recording.
+ /// </summary>
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the series timer identifier.
+ /// </summary>
+ /// <value>The series timer identifier.</value>
+ public string SeriesTimerId { get; set; }
+
+ /// <summary>
+ /// ChannelId of the recording.
+ /// </summary>
+ public string ChannelId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the program identifier.
+ /// </summary>
+ /// <value>The program identifier.</value>
+ public string ProgramId { get; set; }
+
+ public string ShowId { get; set; }
+
+ /// <summary>
+ /// Name of the recording.
+ /// </summary>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Description of the recording.
+ /// </summary>
+ public string Overview { get; set; }
+
+ public string SeriesId { get; set; }
+
+ /// <summary>
+ /// The start date of the recording, in UTC.
+ /// </summary>
+ public DateTime StartDate { get; set; }
+
+ /// <summary>
+ /// The end date of the recording, in UTC.
+ /// </summary>
+ public DateTime EndDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the status.
+ /// </summary>
+ /// <value>The status.</value>
+ public RecordingStatus Status { get; set; }
+
+ /// <summary>
+ /// Gets or sets the pre padding seconds.
+ /// </summary>
+ /// <value>The pre padding seconds.</value>
+ public int PrePaddingSeconds { get; set; }
+
+ /// <summary>
+ /// Gets or sets the post padding seconds.
+ /// </summary>
+ /// <value>The post padding seconds.</value>
+ public int PostPaddingSeconds { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is pre padding required.
+ /// </summary>
+ /// <value><c>true</c> if this instance is pre padding required; otherwise, <c>false</c>.</value>
+ public bool IsPrePaddingRequired { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is post padding required.
+ /// </summary>
+ /// <value><c>true</c> if this instance is post padding required; otherwise, <c>false</c>.</value>
+ public bool IsPostPaddingRequired { get; set; }
+
+ public bool IsManual { get; set; }
+
+ /// <summary>
+ /// Gets or sets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public int Priority { get; set; }
+
+ public int RetryCount { get; set; }
+
+ // Program properties
+ public int? SeasonNumber { get; set; }
+ /// <summary>
+ /// Gets or sets the episode number.
+ /// </summary>
+ /// <value>The episode number.</value>
+ public int? EpisodeNumber { get; set; }
+ public bool IsMovie { get; set; }
+ public bool IsKids
+ {
+ get
+ {
+ return Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase);
+ }
+ }
+ public bool IsSports
+ {
+ get
+ {
+ return Tags.Contains("Sports", StringComparer.OrdinalIgnoreCase);
+ }
+ }
+ public bool IsNews
+ {
+ get
+ {
+ return Tags.Contains("News", StringComparer.OrdinalIgnoreCase);
+ }
+ }
+ public bool IsSeries { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is live.
+ /// </summary>
+ /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool IsLive
+ {
+ get
+ {
+ return Tags.Contains("Live", StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ [IgnoreDataMember]
+ public bool IsPremiere
+ {
+ get
+ {
+ return Tags.Contains("Premiere", StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ public int? ProductionYear { get; set; }
+ public string EpisodeTitle { get; set; }
+ public DateTime? OriginalAirDate { get; set; }
+ public bool IsProgramSeries { get; set; }
+ public bool IsRepeat { get; set; }
+ public string HomePageUrl { get; set; }
+ public float? CommunityRating { get; set; }
+ public string OfficialRating { get; set; }
+ public string[] Genres { get; set; }
+ public string RecordingPath { get; set; }
+ public KeepUntil KeepUntil { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs b/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs
new file mode 100644
index 000000000..3b2df0471
--- /dev/null
+++ b/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs
@@ -0,0 +1,10 @@
+namespace MediaBrowser.Controller.LiveTv
+{
+ public class TunerChannelMapping
+ {
+ public string Name { get; set; }
+ public string ProviderChannelName { get; set; }
+ public string ProviderChannelId { get; set; }
+ public string Id { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
new file mode 100644
index 000000000..8e816080c
--- /dev/null
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <ItemGroup>
+ <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
+ <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Compile Include="..\SharedVersion.cs"/>
+ </ItemGroup>
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ </PropertyGroup>
+
+</Project>
diff --git a/MediaBrowser.Controller/MediaEncoding/DroidSansFallback.ttf.REMOVED.git-id b/MediaBrowser.Controller/MediaEncoding/DroidSansFallback.ttf.REMOVED.git-id
new file mode 100644
index 000000000..3c0ca209e
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/DroidSansFallback.ttf.REMOVED.git-id
@@ -0,0 +1 @@
+4366f8d8bf9886d71e3e9ddae8480d953caf02cf \ No newline at end of file
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
new file mode 100644
index 000000000..881e318ce
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -0,0 +1,2503 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Reflection;
+
+namespace MediaBrowser.Controller.MediaEncoding
+{
+ public class EncodingHelper
+ {
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IFileSystem _fileSystem;
+ private readonly ISubtitleEncoder _subtitleEncoder;
+
+ public EncodingHelper(IMediaEncoder mediaEncoder, IFileSystem fileSystem, ISubtitleEncoder subtitleEncoder)
+ {
+ _mediaEncoder = mediaEncoder;
+ _fileSystem = fileSystem;
+ _subtitleEncoder = subtitleEncoder;
+ }
+
+ public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
+ {
+ var defaultEncoder = "libx264";
+
+ // Only use alternative encoders for video files.
+ // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
+ // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
+ if (state.VideoType == VideoType.VideoFile)
+ {
+ var hwType = encodingOptions.HardwareAccelerationType;
+
+ if (!encodingOptions.EnableHardwareEncoding)
+ {
+ hwType = null;
+ }
+
+ if (string.Equals(hwType, "qsv", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(hwType, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetAvailableEncoder("h264_qsv", defaultEncoder);
+ }
+
+ if (string.Equals(hwType, "nvenc", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetAvailableEncoder("h264_nvenc", defaultEncoder);
+ }
+ if (string.Equals(hwType, "amf", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetAvailableEncoder("h264_amf", defaultEncoder);
+ }
+ if (string.Equals(hwType, "omx", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetAvailableEncoder("h264_omx", defaultEncoder);
+ }
+ if (string.Equals(hwType, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetAvailableEncoder("h264_v4l2m2m", defaultEncoder);
+ }
+ if (string.Equals(hwType, "mediacodec", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetAvailableEncoder("h264_mediacodec", defaultEncoder);
+ }
+ if (string.Equals(hwType, "vaapi", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(encodingOptions.VaapiDevice))
+ {
+ if (IsVaapiSupported(state))
+ {
+ return GetAvailableEncoder("h264_vaapi", defaultEncoder);
+ }
+ }
+ }
+
+ return defaultEncoder;
+ }
+
+ private string GetAvailableEncoder(string preferredEncoder, string defaultEncoder)
+ {
+ if (_mediaEncoder.SupportsEncoder(preferredEncoder))
+ {
+ return preferredEncoder;
+ }
+ return defaultEncoder;
+ }
+
+ private bool IsVaapiSupported(EncodingJobInfo state)
+ {
+ var videoStream = state.VideoStream;
+
+ if (videoStream != null)
+ {
+ // vaapi will throw an error with this input
+ // [vaapi @ 0x7faed8000960] No VAAPI support for codec mpeg4 profile -99.
+ if (string.Equals(videoStream.Codec, "mpeg4", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /// <summary>
+ /// Gets the name of the output video codec
+ /// </summary>
+ public string GetVideoEncoder(EncodingJobInfo state, EncodingOptions encodingOptions)
+ {
+ var codec = state.OutputVideoCodec;
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetH264Encoder(state, encodingOptions);
+ }
+ if (string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase))
+ {
+ return "libvpx";
+ }
+ if (string.Equals(codec, "wmv", StringComparison.OrdinalIgnoreCase))
+ {
+ return "wmv2";
+ }
+ if (string.Equals(codec, "theora", StringComparison.OrdinalIgnoreCase))
+ {
+ return "libtheora";
+ }
+
+ return codec.ToLower();
+ }
+
+ return "copy";
+ }
+
+ /// <summary>
+ /// Gets the user agent param.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ /// <returns>System.String.</returns>
+ public string GetUserAgentParam(EncodingJobInfo state)
+ {
+ string useragent = null;
+
+ state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent);
+
+ if (!string.IsNullOrEmpty(useragent))
+ {
+ return "-user_agent \"" + useragent + "\"";
+ }
+
+ return string.Empty;
+ }
+
+ public string GetInputFormat(string container)
+ {
+ if (string.IsNullOrEmpty(container))
+ {
+ return null;
+ }
+
+ container = container.Replace("mkv", "matroska", StringComparison.OrdinalIgnoreCase);
+
+ if (string.Equals(container, "ts", StringComparison.OrdinalIgnoreCase))
+ {
+ return "mpegts";
+ }
+
+ // For these need to find out the ffmpeg names
+ if (string.Equals(container, "m2ts", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ if (string.Equals(container, "wmv", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ if (string.Equals(container, "mts", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ if (string.Equals(container, "vob", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ if (string.Equals(container, "mpg", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ if (string.Equals(container, "mpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ if (string.Equals(container, "rec", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ if (string.Equals(container, "dvr-ms", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ if (string.Equals(container, "ogm", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ if (string.Equals(container, "divx", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ if (string.Equals(container, "tp", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ if (string.Equals(container, "rmvb", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ if (string.Equals(container, "rtp", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ // Seeing reported failures here, not sure yet if this is related to specfying input format
+ if (string.Equals(container, "m4v", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ // obviously don't do this for strm files
+ if (string.Equals(container, "strm", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ return container;
+ }
+
+ public string GetDecoderFromCodec(string codec)
+ {
+ // For these need to find out the ffmpeg names
+ if (string.Equals(codec, "mp2", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ if (string.Equals(codec, "aac_latm", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ if (string.Equals(codec, "eac3", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ if (_mediaEncoder.SupportsDecoder(codec))
+ {
+ return codec;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Infers the audio codec based on the url
+ /// </summary>
+ public string InferAudioCodec(string container)
+ {
+ var ext = "." + (container ?? string.Empty);
+
+ if (string.Equals(ext, ".mp3", StringComparison.OrdinalIgnoreCase))
+ {
+ return "mp3";
+ }
+ if (string.Equals(ext, ".aac", StringComparison.OrdinalIgnoreCase))
+ {
+ return "aac";
+ }
+ if (string.Equals(ext, ".wma", StringComparison.OrdinalIgnoreCase))
+ {
+ return "wma";
+ }
+ if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase))
+ {
+ return "vorbis";
+ }
+ if (string.Equals(ext, ".oga", StringComparison.OrdinalIgnoreCase))
+ {
+ return "vorbis";
+ }
+ if (string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase))
+ {
+ return "vorbis";
+ }
+ if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase))
+ {
+ return "vorbis";
+ }
+ if (string.Equals(ext, ".webma", StringComparison.OrdinalIgnoreCase))
+ {
+ return "vorbis";
+ }
+
+ return "copy";
+ }
+
+ /// <summary>
+ /// Infers the video codec.
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <returns>System.Nullable{VideoCodecs}.</returns>
+ public string InferVideoCodec(string url)
+ {
+ var ext = Path.GetExtension(url);
+
+ if (string.Equals(ext, ".asf", StringComparison.OrdinalIgnoreCase))
+ {
+ return "wmv";
+ }
+ if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase))
+ {
+ return "vpx";
+ }
+ if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase))
+ {
+ return "theora";
+ }
+ if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase))
+ {
+ return "h264";
+ }
+
+ return "copy";
+ }
+
+ public int GetVideoProfileScore(string profile)
+ {
+ var list = new []
+ {
+ "Constrained Baseline",
+ "Baseline",
+ "Extended",
+ "Main",
+ "High",
+ "Progressive High",
+ "Constrained High"
+ };
+
+ // strip spaces because they may be stripped out on the query string
+ return Array.FindIndex(list, t => string.Equals(t.Replace(" ", ""), profile.Replace(" ", ""), StringComparison.OrdinalIgnoreCase));
+ }
+
+ public string GetInputPathArgument(EncodingJobInfo state)
+ {
+ var protocol = state.InputProtocol;
+ var mediaPath = state.MediaPath ?? string.Empty;
+
+ var inputPath = new[] { mediaPath };
+
+ if (state.IsInputVideo)
+ {
+ if (!(state.VideoType == VideoType.Iso && state.IsoMount == null))
+ {
+ inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, mediaPath, state.InputProtocol, state.IsoMount, state.PlayableStreamFileNames);
+ }
+ }
+
+ return _mediaEncoder.GetInputArgument(inputPath, protocol);
+ }
+
+ /// <summary>
+ /// Gets the audio encoder.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ /// <returns>System.String.</returns>
+ public string GetAudioEncoder(EncodingJobInfo state)
+ {
+ var codec = state.OutputAudioCodec;
+
+ if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase))
+ {
+ return "aac -strict experimental";
+ }
+ if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
+ {
+ return "libmp3lame";
+ }
+ if (string.Equals(codec, "vorbis", StringComparison.OrdinalIgnoreCase))
+ {
+ return "libvorbis";
+ }
+ if (string.Equals(codec, "wma", StringComparison.OrdinalIgnoreCase))
+ {
+ return "wmav2";
+ }
+ if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase))
+ {
+ return "libopus";
+ }
+
+ return codec.ToLower();
+ }
+
+ /// <summary>
+ /// Gets the input argument.
+ /// </summary>
+ public string GetInputArgument(EncodingJobInfo state, EncodingOptions encodingOptions)
+ {
+ var request = state.BaseRequest;
+
+ var arg = string.Format("-i {0}", GetInputPathArgument(state));
+
+ if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+ {
+ if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
+ {
+ if (state.VideoStream != null && state.VideoStream.Width.HasValue)
+ {
+ // This is hacky but not sure how to get the exact subtitle resolution
+ double height = state.VideoStream.Width.Value;
+ height /= 16;
+ height *= 9;
+
+ arg += string.Format(" -canvas_size {0}:{1}", state.VideoStream.Width.Value.ToString(CultureInfo.InvariantCulture), Convert.ToInt32(height).ToString(CultureInfo.InvariantCulture));
+ }
+
+ var subtitlePath = state.SubtitleStream.Path;
+
+ if (string.Equals(Path.GetExtension(subtitlePath), ".sub", StringComparison.OrdinalIgnoreCase))
+ {
+ var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
+ if (_fileSystem.FileExists(idxFile))
+ {
+ subtitlePath = idxFile;
+ }
+ }
+
+ arg += " -i \"" + subtitlePath + "\"";
+ }
+ }
+
+ if (state.IsVideoRequest)
+ {
+ if (GetVideoEncoder(state, encodingOptions).IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+ var hwOutputFormat = "vaapi";
+
+ if (hasGraphicalSubs)
+ {
+ hwOutputFormat = "yuv420p";
+ }
+
+ arg = "-hwaccel vaapi -hwaccel_output_format " + hwOutputFormat + " -vaapi_device " + encodingOptions.VaapiDevice + " " + arg;
+ }
+ }
+
+ return arg.Trim();
+ }
+
+ /// <summary>
+ /// Determines whether the specified stream is H264.
+ /// </summary>
+ /// <param name="stream">The stream.</param>
+ /// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns>
+ public bool IsH264(MediaStream stream)
+ {
+ var codec = stream.Codec ?? string.Empty;
+
+ return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 ||
+ codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
+ }
+
+ public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec)
+ {
+ var bitrate = state.OutputVideoBitrate;
+
+ if (bitrate.HasValue)
+ {
+ if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
+ {
+ // With vpx when crf is used, b:v becomes a max rate
+ // https://trac.ffmpeg.org/wiki/vpxEncodingGuide.
+ return string.Format(" -maxrate:v {0} -bufsize:v {1} -b:v {0}", bitrate.Value.ToString(_usCulture), (bitrate.Value * 2).ToString(_usCulture));
+ }
+
+ if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase))
+ {
+ return string.Format(" -b:v {0}", bitrate.Value.ToString(_usCulture));
+ }
+
+ if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
+ {
+ // h264
+ return string.Format(" -maxrate {0} -bufsize {1}",
+ bitrate.Value.ToString(_usCulture),
+ (bitrate.Value * 2).ToString(_usCulture));
+ }
+
+ // h264
+ return string.Format(" -b:v {0} -maxrate {0} -bufsize {1}",
+ bitrate.Value.ToString(_usCulture),
+ (bitrate.Value * 2).ToString(_usCulture));
+ }
+
+ return string.Empty;
+ }
+
+ public string NormalizeTranscodingLevel(string videoCodec, string level)
+ {
+ double requestLevel;
+
+ // Clients may direct play higher than level 41, but there's no reason to transcode higher
+ if (double.TryParse(level, NumberStyles.Any, _usCulture, out requestLevel))
+ {
+ if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
+ {
+ if (requestLevel > 41)
+ {
+ return "41";
+ }
+ }
+ }
+
+ return level;
+ }
+
+ /// <summary>
+ /// Gets the text subtitle param.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ /// <returns>System.String.</returns>
+ public string GetTextSubtitleParam(EncodingJobInfo state)
+ {
+ var seconds = Math.Round(TimeSpan.FromTicks(state.StartTimeTicks ?? 0).TotalSeconds);
+
+ // hls always copies timestamps
+ var setPtsParam = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive
+ ? string.Empty
+ : string.Format(",setpts=PTS -{0}/TB", seconds.ToString(_usCulture));
+
+ string fallbackFontParam = string.Empty;
+
+ var mediaPath = state.MediaPath ?? string.Empty;
+
+ return string.Format("subtitles='{0}:si={1}'{2}{3}",
+ _mediaEncoder.EscapeSubtitleFilterPath(mediaPath),
+ state.InternalSubtitleStreamOffset.ToString(_usCulture),
+ fallbackFontParam,
+ setPtsParam);
+ }
+
+ public double? GetFramerateParam(EncodingJobInfo state)
+ {
+ var request = state.BaseRequest;
+
+ if (request.Framerate.HasValue)
+ {
+ return request.Framerate.Value;
+ }
+
+ var maxrate = request.MaxFramerate;
+
+ if (maxrate.HasValue && state.VideoStream != null)
+ {
+ var contentRate = state.VideoStream.AverageFrameRate ?? state.VideoStream.RealFrameRate;
+
+ if (contentRate.HasValue && contentRate.Value > maxrate.Value)
+ {
+ return maxrate;
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Gets the video bitrate to specify on the command line
+ /// </summary>
+ public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, string defaultH264Preset)
+ {
+ var param = string.Empty;
+
+ var isVc1 = state.VideoStream != null &&
+ string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
+
+ if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
+ {
+ if (!string.IsNullOrEmpty(encodingOptions.H264Preset))
+ {
+ param += "-preset " + encodingOptions.H264Preset;
+ }
+ else
+ {
+ param += "-preset " + defaultH264Preset;
+ }
+
+ if (encodingOptions.H264Crf >= 0 && encodingOptions.H264Crf <= 51)
+ {
+ param += " -crf " + encodingOptions.H264Crf.ToString(CultureInfo.InvariantCulture);
+ }
+ else
+ {
+ param += " -crf 23";
+ }
+ }
+
+ else if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
+ {
+ param += "-preset fast";
+
+ param += " -crf 28";
+ }
+
+ // h264 (h264_qsv)
+ else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ {
+ string[] valid_h264_qsv = new string[] { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
+
+ if (valid_h264_qsv.Contains(encodingOptions.H264Preset, StringComparer.OrdinalIgnoreCase))
+ {
+ param += "-preset " + encodingOptions.H264Preset;
+ }
+ else
+ {
+ param += "-preset 7";
+ }
+
+ param += " -look_ahead 0";
+
+ }
+
+ // h264 (h264_nvenc)
+ else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
+ {
+ switch (encodingOptions.H264Preset)
+ {
+ case "veryslow":
+
+ param += "-preset slow"; //lossless is only supported on maxwell and newer(2014+)
+ break;
+
+ case "slow":
+ case "slower":
+ param += "-preset slow";
+ break;
+
+ case "medium":
+ param += "-preset medium";
+ break;
+
+ case "fast":
+ case "faster":
+ case "veryfast":
+ case "superfast":
+ case "ultrafast":
+ param += "-preset fast";
+ break;
+
+ default:
+ param += "-preset default";
+ break;
+ }
+ }
+
+ // webm
+ else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase))
+ {
+ // Values 0-3, 0 being highest quality but slower
+ var profileScore = 0;
+
+ string crf;
+ var qmin = "0";
+ var qmax = "50";
+
+ crf = "10";
+
+ if (isVc1)
+ {
+ profileScore++;
+ }
+
+ // Max of 2
+ profileScore = Math.Min(profileScore, 2);
+
+ // http://www.webmproject.org/docs/encoder-parameters/
+ param += string.Format("-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
+ profileScore.ToString(_usCulture),
+ crf,
+ qmin,
+ qmax);
+ }
+
+ else if (string.Equals(videoEncoder, "mpeg4", StringComparison.OrdinalIgnoreCase))
+ {
+ param += "-mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2";
+ }
+
+ // asf/wmv
+ else if (string.Equals(videoEncoder, "wmv2", StringComparison.OrdinalIgnoreCase))
+ {
+ param += "-qmin 2";
+ }
+
+ else if (string.Equals(videoEncoder, "msmpeg4", StringComparison.OrdinalIgnoreCase))
+ {
+ param += "-mbd 2";
+ }
+
+ param += GetVideoBitrateParam(state, videoEncoder);
+
+ var framerate = GetFramerateParam(state);
+ if (framerate.HasValue)
+ {
+ param += string.Format(" -r {0}", framerate.Value.ToString(_usCulture));
+ }
+
+ var targetVideoCodec = state.ActualOutputVideoCodec;
+
+ var request = state.BaseRequest;
+ var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault();
+ if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -profile:v 578";
+ }
+ else if (!string.IsNullOrEmpty(profile))
+ {
+ if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) &&
+ !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) &&
+ !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
+ {
+ // not supported by h264_omx
+ param += " -profile:v " + profile;
+ }
+ }
+
+ var level = state.GetRequestedLevel(targetVideoCodec);
+
+ if (!string.IsNullOrEmpty(level))
+ {
+ level = NormalizeTranscodingLevel(state.OutputVideoCodec, level);
+
+ // h264_qsv and h264_nvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format
+ // also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307
+ if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
+ {
+ switch (level)
+ {
+ case "30":
+ param += " -level 3.0";
+ break;
+ case "31":
+ param += " -level 3.1";
+ break;
+ case "32":
+ param += " -level 3.2";
+ break;
+ case "40":
+ param += " -level 4.0";
+ break;
+ case "41":
+ param += " -level 4.1";
+ break;
+ case "42":
+ param += " -level 4.2";
+ break;
+ case "50":
+ param += " -level 5.0";
+ break;
+ case "51":
+ param += " -level 5.1";
+ break;
+ case "52":
+ param += " -level 5.2";
+ break;
+ default:
+ param += " -level " + level;
+ break;
+ }
+ }
+ // nvenc doesn't decode with param -level set ?!
+ else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
+ {
+ //param += "";
+ }
+ else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -level " + level;
+ }
+ }
+
+ if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -x264opts:0 subme=0:me_range=4:rc_lookahead=10:me=dia:no_chroma_me:8x8dct=0:partitions=none";
+ }
+
+ if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) &&
+ !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) &&
+ !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) &&
+ !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
+ {
+ param = "-pix_fmt yuv420p " + param;
+ }
+
+ if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
+ {
+ param = "-pix_fmt nv21 " + param;
+ }
+
+ return param;
+ }
+
+ public bool CanStreamCopyVideo(EncodingJobInfo state, MediaStream videoStream)
+ {
+ var request = state.BaseRequest;
+
+ if (!request.AllowVideoStreamCopy)
+ {
+ return false;
+ }
+
+ if (videoStream.IsInterlaced)
+ {
+ if (state.DeInterlace(videoStream.Codec, false))
+ {
+ return false;
+ }
+ }
+
+ if (videoStream.IsAnamorphic ?? false)
+ {
+ if (request.RequireNonAnamorphic)
+ {
+ return false;
+ }
+ }
+
+ // Can't stream copy if we're burning in subtitles
+ if (request.SubtitleStreamIndex.HasValue)
+ {
+ if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+ {
+ return false;
+ }
+ }
+
+ if (string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
+ {
+ if (videoStream.IsAVC.HasValue && !videoStream.IsAVC.Value && request.RequireAvc)
+ {
+ return false;
+ }
+ }
+
+ // Source and target codecs must match
+ if (string.IsNullOrEmpty(videoStream.Codec) || !state.SupportedVideoCodecs.Contains(videoStream.Codec, StringComparer.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ var requestedProfiles = state.GetRequestedProfiles(videoStream.Codec);
+
+ // If client is requesting a specific video profile, it must match the source
+ if (requestedProfiles.Length > 0)
+ {
+ if (string.IsNullOrEmpty(videoStream.Profile))
+ {
+ //return false;
+ }
+
+ var requestedProfile = requestedProfiles[0];
+ // strip spaces because they may be stripped out on the query string as well
+ if (!string.IsNullOrEmpty(videoStream.Profile) && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", ""), StringComparer.OrdinalIgnoreCase))
+ {
+ var currentScore = GetVideoProfileScore(videoStream.Profile);
+ var requestedScore = GetVideoProfileScore(requestedProfile);
+
+ if (currentScore == -1 || currentScore > requestedScore)
+ {
+ return false;
+ }
+ }
+ }
+
+ // Video width must fall within requested value
+ if (request.MaxWidth.HasValue)
+ {
+ if (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value)
+ {
+ return false;
+ }
+ }
+
+ // Video height must fall within requested value
+ if (request.MaxHeight.HasValue)
+ {
+ if (!videoStream.Height.HasValue || videoStream.Height.Value > request.MaxHeight.Value)
+ {
+ return false;
+ }
+ }
+
+ // Video framerate must fall within requested value
+ var requestedFramerate = request.MaxFramerate ?? request.Framerate;
+ if (requestedFramerate.HasValue)
+ {
+ var videoFrameRate = videoStream.AverageFrameRate ?? videoStream.RealFrameRate;
+
+ if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value)
+ {
+ return false;
+ }
+ }
+
+ // Video bitrate must fall within requested value
+ if (request.VideoBitRate.HasValue)
+ {
+ if (!videoStream.BitRate.HasValue || videoStream.BitRate.Value > request.VideoBitRate.Value)
+ {
+ return false;
+ }
+ }
+
+ var maxBitDepth = state.GetRequestedVideoBitDepth(videoStream.Codec);
+ if (maxBitDepth.HasValue)
+ {
+ if (videoStream.BitDepth.HasValue && videoStream.BitDepth.Value > maxBitDepth.Value)
+ {
+ return false;
+ }
+ }
+
+ var maxRefFrames = state.GetRequestedMaxRefFrames(videoStream.Codec);
+ if (maxRefFrames.HasValue)
+ {
+ if (videoStream.RefFrames.HasValue && videoStream.RefFrames.Value > maxRefFrames.Value)
+ {
+ return false;
+ }
+ }
+
+ // If a specific level was requested, the source must match or be less than
+ var level = state.GetRequestedLevel(videoStream.Codec);
+ if (!string.IsNullOrEmpty(level))
+ {
+ double requestLevel;
+
+ if (double.TryParse(level, NumberStyles.Any, _usCulture, out requestLevel))
+ {
+ if (!videoStream.Level.HasValue)
+ {
+ //return false;
+ }
+
+ if (videoStream.Level.HasValue && videoStream.Level.Value > requestLevel)
+ {
+ return false;
+ }
+ }
+ }
+
+ if (string.Equals(state.InputContainer, "avi", StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase) &&
+ !(videoStream.IsAVC ?? false))
+ {
+ // see Coach S01E01 - Kelly and the Professor(0).avi
+ return false;
+ }
+
+ return request.EnableAutoStreamCopy;
+ }
+
+ public bool CanStreamCopyAudio(EncodingJobInfo state, MediaStream audioStream, string[] supportedAudioCodecs)
+ {
+ var request = state.BaseRequest;
+
+ if (!request.AllowAudioStreamCopy)
+ {
+ return false;
+ }
+
+ var maxBitDepth = state.GetRequestedAudioBitDepth(audioStream.Codec);
+ if (maxBitDepth.HasValue)
+ {
+ if (audioStream.BitDepth.HasValue && audioStream.BitDepth.Value > maxBitDepth.Value)
+ {
+ return false;
+ }
+ }
+
+ // Source and target codecs must match
+ if (string.IsNullOrEmpty(audioStream.Codec) || !supportedAudioCodecs.Contains(audioStream.Codec, StringComparer.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ // Channels must fall within requested value
+ var channels = state.GetRequestedAudioChannels(audioStream.Codec);
+ if (channels.HasValue)
+ {
+ if (!audioStream.Channels.HasValue || audioStream.Channels.Value <= 0)
+ {
+ return false;
+ }
+ if (audioStream.Channels.Value > channels.Value)
+ {
+ return false;
+ }
+ }
+
+ // Sample rate must fall within requested value
+ if (request.AudioSampleRate.HasValue)
+ {
+ if (!audioStream.SampleRate.HasValue || audioStream.SampleRate.Value <= 0)
+ {
+ return false;
+ }
+ if (audioStream.SampleRate.Value > request.AudioSampleRate.Value)
+ {
+ return false;
+ }
+ }
+
+ // Video bitrate must fall within requested value
+ if (request.AudioBitRate.HasValue)
+ {
+ if (!audioStream.BitRate.HasValue || audioStream.BitRate.Value <= 0)
+ {
+ return false;
+ }
+ if (audioStream.BitRate.Value > request.AudioBitRate.Value)
+ {
+ return false;
+ }
+ }
+
+ return request.EnableAutoStreamCopy;
+ }
+
+ public int? GetVideoBitrateParamValue(BaseEncodingJobOptions request, MediaStream videoStream, string outputVideoCodec)
+ {
+ var bitrate = request.VideoBitRate;
+
+ if (videoStream != null)
+ {
+ var isUpscaling = request.Height.HasValue && videoStream.Height.HasValue &&
+ request.Height.Value > videoStream.Height.Value && request.Width.HasValue && videoStream.Width.HasValue &&
+ request.Width.Value > videoStream.Width.Value;
+
+ // Don't allow bitrate increases unless upscaling
+ if (!isUpscaling)
+ {
+ if (bitrate.HasValue && videoStream.BitRate.HasValue)
+ {
+ bitrate = GetMinBitrate(videoStream.BitRate.Value, bitrate.Value);
+ }
+ }
+ }
+
+ if (bitrate.HasValue)
+ {
+ var inputVideoCodec = videoStream == null ? null : videoStream.Codec;
+ bitrate = ResolutionNormalizer.ScaleBitrate(bitrate.Value, inputVideoCodec, outputVideoCodec);
+
+ // If a max bitrate was requested, don't let the scaled bitrate exceed it
+ if (request.VideoBitRate.HasValue)
+ {
+ bitrate = Math.Min(bitrate.Value, request.VideoBitRate.Value);
+ }
+ }
+
+ return bitrate;
+ }
+
+ private int GetMinBitrate(int sourceBitrate, int requestedBitrate)
+ {
+ if (sourceBitrate <= 2000000)
+ {
+ sourceBitrate = Convert.ToInt32(sourceBitrate * 2.5);
+ }
+ else if (sourceBitrate <= 3000000)
+ {
+ sourceBitrate = Convert.ToInt32(sourceBitrate * 2);
+ }
+
+ var bitrate = Math.Min(sourceBitrate, requestedBitrate);
+
+ return bitrate;
+ }
+
+ public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream)
+ {
+ if (request.AudioBitRate.HasValue)
+ {
+ // Make sure we don't request a bitrate higher than the source
+ var currentBitrate = audioStream == null ? request.AudioBitRate.Value : audioStream.BitRate ?? request.AudioBitRate.Value;
+
+ // Don't encode any higher than this
+ return Math.Min(384000, request.AudioBitRate.Value);
+ //return Math.Min(currentBitrate, request.AudioBitRate.Value);
+ }
+
+ return null;
+ }
+
+ public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions, bool isHls)
+ {
+ var channels = state.OutputAudioChannels;
+
+ var filters = new List<string>();
+
+ // Boost volume to 200% when downsampling from 6ch to 2ch
+ if (channels.HasValue && channels.Value <= 2)
+ {
+ if (state.AudioStream != null && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5 && !encodingOptions.DownMixAudioBoost.Equals(1))
+ {
+ filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(_usCulture));
+ }
+ }
+
+ var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive;
+ if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode && !isCopyingTimestamps)
+ {
+ var seconds = TimeSpan.FromTicks(state.StartTimeTicks ?? 0).TotalSeconds;
+
+ filters.Add(string.Format("asetpts=PTS-{0}/TB", Math.Round(seconds).ToString(_usCulture)));
+ }
+
+ if (filters.Count > 0)
+ {
+ return "-af \"" + string.Join(",", filters.ToArray()) + "\"";
+ }
+
+ return string.Empty;
+ }
+
+ /// <summary>
+ /// Gets the number of audio channels to specify on the command line
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <param name="audioStream">The audio stream.</param>
+ /// <param name="outputAudioCodec">The output audio codec.</param>
+ /// <returns>System.Nullable{System.Int32}.</returns>
+ public int? GetNumAudioChannelsParam(EncodingJobInfo state, MediaStream audioStream, string outputAudioCodec)
+ {
+ var request = state.BaseRequest;
+
+ var inputChannels = audioStream == null
+ ? null
+ : audioStream.Channels;
+
+ if (inputChannels <= 0)
+ {
+ inputChannels = null;
+ }
+
+ int? transcoderChannelLimit = null;
+ var codec = outputAudioCodec ?? string.Empty;
+
+ if (codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ // wmav2 currently only supports two channel output
+ transcoderChannelLimit = 2;
+ }
+
+ else if (codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ // libmp3lame currently only supports two channel output
+ transcoderChannelLimit = 2;
+ }
+ else
+ {
+ // If we don't have any media info then limit it to 6 to prevent encoding errors due to asking for too many channels
+ transcoderChannelLimit = 6;
+ }
+
+ var isTranscodingAudio = !string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase);
+
+ int? resultChannels = state.GetRequestedAudioChannels(codec);
+ if (isTranscodingAudio)
+ {
+ resultChannels = GetMinValue(request.TranscodingMaxAudioChannels, resultChannels);
+ }
+
+ if (inputChannels.HasValue)
+ {
+ resultChannels = resultChannels.HasValue
+ ? Math.Min(resultChannels.Value, inputChannels.Value)
+ : inputChannels.Value;
+ }
+
+ if (isTranscodingAudio && transcoderChannelLimit.HasValue)
+ {
+ resultChannels = resultChannels.HasValue
+ ? Math.Min(resultChannels.Value, transcoderChannelLimit.Value)
+ : transcoderChannelLimit.Value;
+ }
+
+ return resultChannels;
+ }
+
+ private int? GetMinValue(int? val1, int? val2)
+ {
+ if (!val1.HasValue)
+ {
+ return val2;
+ }
+ if (!val2.HasValue)
+ {
+ return val1;
+ }
+
+ return Math.Min(val1.Value, val2.Value);
+ }
+
+ /// <summary>
+ /// Enforces the resolution limit.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ public void EnforceResolutionLimit(EncodingJobInfo state)
+ {
+ var videoRequest = state.BaseRequest;
+
+ // Switch the incoming params to be ceilings rather than fixed values
+ videoRequest.MaxWidth = videoRequest.MaxWidth ?? videoRequest.Width;
+ videoRequest.MaxHeight = videoRequest.MaxHeight ?? videoRequest.Height;
+
+ videoRequest.Width = null;
+ videoRequest.Height = null;
+ }
+
+ /// <summary>
+ /// Gets the fast seek command line parameter.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <returns>System.String.</returns>
+ /// <value>The fast seek command line parameter.</value>
+ public string GetFastSeekCommandLineParameter(BaseEncodingJobOptions request)
+ {
+ var time = request.StartTimeTicks ?? 0;
+
+ if (time > 0)
+ {
+ return string.Format("-ss {0}", _mediaEncoder.GetTimeParameter(time));
+ }
+
+ return string.Empty;
+ }
+
+ /// <summary>
+ /// Gets the map args.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ /// <returns>System.String.</returns>
+ public string GetMapArgs(EncodingJobInfo state)
+ {
+ // If we don't have known media info
+ // If input is video, use -sn to drop subtitles
+ // Otherwise just return empty
+ if (state.VideoStream == null && state.AudioStream == null)
+ {
+ return state.IsInputVideo ? "-sn" : string.Empty;
+ }
+
+ // We have media info, but we don't know the stream indexes
+ if (state.VideoStream != null && state.VideoStream.Index == -1)
+ {
+ return "-sn";
+ }
+
+ // We have media info, but we don't know the stream indexes
+ if (state.AudioStream != null && state.AudioStream.Index == -1)
+ {
+ return state.IsInputVideo ? "-sn" : string.Empty;
+ }
+
+ var args = string.Empty;
+
+ if (state.VideoStream != null)
+ {
+ args += string.Format("-map 0:{0}", state.VideoStream.Index);
+ }
+ else
+ {
+ // No known video stream
+ args += "-vn";
+ }
+
+ if (state.AudioStream != null)
+ {
+ args += string.Format(" -map 0:{0}", state.AudioStream.Index);
+ }
+
+ else
+ {
+ args += " -map -0:a";
+ }
+
+ var subtitleMethod = state.SubtitleDeliveryMethod;
+ if (state.SubtitleStream == null || subtitleMethod == SubtitleDeliveryMethod.Hls)
+ {
+ args += " -map -0:s";
+ }
+ else if (subtitleMethod == SubtitleDeliveryMethod.Embed)
+ {
+ args += string.Format(" -map 0:{0}", state.SubtitleStream.Index);
+ }
+ else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
+ {
+ args += " -map 1:0 -sn";
+ }
+
+ return args;
+ }
+
+ /// <summary>
+ /// Determines which stream will be used for playback
+ /// </summary>
+ /// <param name="allStream">All stream.</param>
+ /// <param name="desiredIndex">Index of the desired.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="returnFirstIfNoIndex">if set to <c>true</c> [return first if no index].</param>
+ /// <returns>MediaStream.</returns>
+ public MediaStream GetMediaStream(IEnumerable<MediaStream> allStream, int? desiredIndex, MediaStreamType type, bool returnFirstIfNoIndex = true)
+ {
+ var streams = allStream.Where(s => s.Type == type).OrderBy(i => i.Index).ToList();
+
+ if (desiredIndex.HasValue)
+ {
+ var stream = streams.FirstOrDefault(s => s.Index == desiredIndex.Value);
+
+ if (stream != null)
+ {
+ return stream;
+ }
+ }
+
+ if (returnFirstIfNoIndex && type == MediaStreamType.Audio)
+ {
+ return streams.FirstOrDefault(i => i.Channels.HasValue && i.Channels.Value > 0) ??
+ streams.FirstOrDefault();
+ }
+
+ // Just return the first one
+ return returnFirstIfNoIndex ? streams.FirstOrDefault() : null;
+ }
+
+ /// <summary>
+ /// Gets the internal graphical subtitle param.
+ /// </summary>
+ public string GetGraphicalSubtitleParam(EncodingJobInfo state, EncodingOptions options, string outputVideoCodec)
+ {
+ var outputSizeParam = string.Empty;
+
+ var request = state.BaseRequest;
+
+ // Add resolution params, if specified
+ if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue)
+ {
+ outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"');
+
+ if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ {
+ var index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase);
+ if (index != -1)
+ {
+ outputSizeParam = "," + outputSizeParam.Substring(index);
+ }
+ }
+ else
+ {
+ var index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase);
+ if (index != -1)
+ {
+ outputSizeParam = "," + outputSizeParam.Substring(index);
+ }
+ }
+ }
+
+ if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && outputSizeParam.Length == 0)
+ {
+ outputSizeParam = ",format=nv12|vaapi,hwupload";
+ }
+
+ var videoSizeParam = string.Empty;
+
+ if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue)
+ {
+ videoSizeParam = string.Format("scale={0}:{1}", state.VideoStream.Width.Value.ToString(_usCulture), state.VideoStream.Height.Value.ToString(_usCulture));
+
+ videoSizeParam += ":force_original_aspect_ratio=decrease";
+ }
+
+ var mapPrefix = state.SubtitleStream.IsExternal ?
+ 1 :
+ 0;
+
+ var subtitleStreamIndex = state.SubtitleStream.IsExternal
+ ? 0
+ : state.SubtitleStream.Index;
+
+ return string.Format(" -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay{3}\"",
+ mapPrefix.ToString(_usCulture),
+ subtitleStreamIndex.ToString(_usCulture),
+ state.VideoStream.Index.ToString(_usCulture),
+ outputSizeParam,
+ videoSizeParam);
+ }
+
+ private Tuple<int?, int?> GetFixedOutputSize(int? videoWidth,
+ int? videoHeight,
+ int? requestedWidth,
+ int? requestedHeight,
+ int? requestedMaxWidth,
+ int? requestedMaxHeight)
+ {
+ if (!videoWidth.HasValue && !requestedWidth.HasValue)
+ {
+ return new Tuple<int?, int?>(null, null);
+ }
+ if (!videoHeight.HasValue && !requestedHeight.HasValue)
+ {
+ return new Tuple<int?, int?>(null, null);
+ }
+
+ decimal inputWidth = Convert.ToDecimal(videoWidth ?? requestedWidth);
+ decimal inputHeight = Convert.ToDecimal(videoHeight ?? requestedHeight);
+ decimal outputWidth = requestedWidth.HasValue ? Convert.ToDecimal(requestedWidth.Value) : inputWidth;
+ decimal outputHeight = requestedHeight.HasValue ? Convert.ToDecimal(requestedHeight.Value) : inputHeight;
+ decimal maximumWidth = requestedMaxWidth.HasValue ? Convert.ToDecimal(requestedMaxWidth.Value) : outputWidth;
+ decimal maximumHeight = requestedMaxHeight.HasValue ? Convert.ToDecimal(requestedMaxHeight.Value) : outputHeight;
+
+ if (outputWidth > maximumWidth || outputHeight > maximumHeight)
+ {
+ var scale = Math.Min(maximumWidth / outputWidth, maximumHeight / outputHeight);
+ outputWidth = Math.Min(maximumWidth, Math.Truncate(outputWidth * scale));
+ outputHeight = Math.Min(maximumHeight, Math.Truncate(outputHeight * scale));
+ }
+
+ outputWidth = 2 * Math.Truncate(outputWidth / 2);
+ outputHeight = 2 * Math.Truncate(outputHeight / 2);
+
+ return new Tuple<int?, int?>(Convert.ToInt32(outputWidth), Convert.ToInt32(outputHeight));
+ }
+
+ public List<string> GetScalingFilters(int? videoWidth,
+ int? videoHeight,
+ Video3DFormat? threedFormat,
+ string videoDecoder,
+ string videoEncoder,
+ int? requestedWidth,
+ int? requestedHeight,
+ int? requestedMaxWidth,
+ int? requestedMaxHeight)
+ {
+ var filters = new List<string>();
+ var fixedOutputSize = GetFixedOutputSize(videoWidth, videoHeight, requestedWidth, requestedHeight, requestedMaxWidth, requestedMaxHeight);
+
+ if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && fixedOutputSize.Item1.HasValue && fixedOutputSize.Item2.HasValue)
+ {
+ // Work around vaapi's reduced scaling features
+ var scaler = "scale_vaapi";
+
+ // Given the input dimensions (inputWidth, inputHeight), determine the output dimensions
+ // (outputWidth, outputHeight). The user may request precise output dimensions or maximum
+ // output dimensions. Output dimensions are guaranteed to be even.
+ var outputWidth = fixedOutputSize.Item1.Value;
+ var outputHeight = fixedOutputSize.Item2.Value;
+
+ if (!videoWidth.HasValue || outputWidth != videoWidth.Value || !videoHeight.HasValue || outputHeight != videoHeight.Value)
+ {
+ filters.Add(string.Format("{0}=w={1}:h={2}", scaler, outputWidth.ToString(_usCulture), outputHeight.ToString(_usCulture)));
+ }
+ }
+ else if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1 && fixedOutputSize.Item1.HasValue && fixedOutputSize.Item2.HasValue)
+ {
+ // Nothing to do, it's handled as an input resize filter
+ }
+ else
+ {
+ var isExynosV4L2 = string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase);
+
+ // If fixed dimensions were supplied
+ if (requestedWidth.HasValue && requestedHeight.HasValue)
+ {
+ if (isExynosV4L2)
+ {
+ var widthParam = requestedWidth.Value.ToString(_usCulture);
+ var heightParam = requestedHeight.Value.ToString(_usCulture);
+
+ filters.Add(string.Format("scale=trunc({0}/64)*64:trunc({1}/2)*2", widthParam, heightParam));
+ }
+ else
+ {
+ filters.Add(GetFixedSizeScalingFilter(threedFormat, requestedWidth.Value, requestedHeight.Value));
+ }
+ }
+
+ // If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size
+ else if (requestedMaxWidth.HasValue && requestedMaxHeight.HasValue)
+ {
+ var maxWidthParam = requestedMaxWidth.Value.ToString(_usCulture);
+ var maxHeightParam = requestedMaxHeight.Value.ToString(_usCulture);
+
+ if (isExynosV4L2)
+ {
+ filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/64)*64:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", maxWidthParam, maxHeightParam));
+ }
+ else
+ {
+ filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/2)*2:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", maxWidthParam, maxHeightParam));
+ }
+ }
+
+ // If a fixed width was requested
+ else if (requestedWidth.HasValue)
+ {
+ if (threedFormat.HasValue)
+ {
+ // This method can handle 0 being passed in for the requested height
+ filters.Add(GetFixedSizeScalingFilter(threedFormat, requestedWidth.Value, 0));
+ }
+ else
+ {
+ var widthParam = requestedWidth.Value.ToString(_usCulture);
+
+ filters.Add(string.Format("scale={0}:trunc(ow/a/2)*2", widthParam));
+ }
+ }
+
+ // If a fixed height was requested
+ else if (requestedHeight.HasValue)
+ {
+ var heightParam = requestedHeight.Value.ToString(_usCulture);
+
+ if (isExynosV4L2)
+ {
+ filters.Add(string.Format("scale=trunc(oh*a/64)*64:{0}", heightParam));
+ }
+ else
+ {
+ filters.Add(string.Format("scale=trunc(oh*a/2)*2:{0}", heightParam));
+ }
+ }
+
+ // If a max width was requested
+ else if (requestedMaxWidth.HasValue)
+ {
+ var maxWidthParam = requestedMaxWidth.Value.ToString(_usCulture);
+
+ if (isExynosV4L2)
+ {
+ filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,{0})/64)*64:trunc(ow/dar/2)*2", maxWidthParam));
+ }
+ else
+ {
+ filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,{0})/2)*2:trunc(ow/dar/2)*2", maxWidthParam));
+ }
+ }
+
+ // If a max height was requested
+ else if (requestedMaxHeight.HasValue)
+ {
+ var maxHeightParam = requestedMaxHeight.Value.ToString(_usCulture);
+
+ if (isExynosV4L2)
+ {
+ filters.Add(string.Format("scale=trunc(oh*a/64)*64:min(max(iw/dar\\,ih)\\,{0})", maxHeightParam));
+ }
+ else
+ {
+ filters.Add(string.Format("scale=trunc(oh*a/2)*2:min(max(iw/dar\\,ih)\\,{0})", maxHeightParam));
+ }
+ }
+ }
+
+ return filters;
+ }
+
+ private string GetFixedSizeScalingFilter(Video3DFormat? threedFormat, int requestedWidth, int requestedHeight)
+ {
+ var widthParam = requestedWidth.ToString(_usCulture);
+ var heightParam = requestedHeight.ToString(_usCulture);
+
+ string filter = null;
+
+ if (threedFormat.HasValue)
+ {
+ switch (threedFormat.Value)
+ {
+ case Video3DFormat.HalfSideBySide:
+ filter = "crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2";
+ // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may have made the scale width to requestedWidth. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not.
+ break;
+ case Video3DFormat.FullSideBySide:
+ filter = "crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2";
+ //fsbs crop width in half,set the display aspect,crop out any black bars we may have made the scale width to requestedWidth.
+ break;
+ case Video3DFormat.HalfTopAndBottom:
+ filter = "crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2";
+ //htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may have made the scale width to requestedWidth
+ break;
+ case Video3DFormat.FullTopAndBottom:
+ filter = "crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale={0}:trunc({0}/dar/2)*2";
+ // ftab crop height in half, set the display aspect,crop out any black bars we may have made the scale width to requestedWidth
+ break;
+ default:
+ break;
+ }
+ }
+
+ // default
+ if (filter == null)
+ {
+ if (requestedHeight > 0)
+ {
+ filter = "scale=trunc({0}/2)*2:trunc({1}/2)*2";
+ }
+ else
+ {
+ filter = "scale={0}:trunc({0}/dar/2)*2";
+ }
+ }
+
+ return string.Format(filter, widthParam, heightParam);
+ }
+
+ /// <summary>
+ /// If we're going to put a fixed size on the command line, this will calculate it
+ /// </summary>
+ public string GetOutputSizeParam(EncodingJobInfo state,
+ EncodingOptions options,
+ string outputVideoCodec,
+ bool allowTimeStampCopy = true)
+ {
+ // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
+
+ var request = state.BaseRequest;
+
+ var filters = new List<string>();
+
+ if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ {
+ filters.Add("format=nv12|vaapi");
+ filters.Add("hwupload");
+ }
+
+ if (state.DeInterlace("h264", true) && string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ {
+ filters.Add(string.Format("deinterlace_vaapi"));
+ }
+
+ var videoStream = state.VideoStream;
+
+ if (state.DeInterlace("h264", true) && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
+ {
+ var inputFramerate = videoStream == null ? null : videoStream.RealFrameRate;
+
+ // If it is already 60fps then it will create an output framerate that is much too high for roku and others to handle
+ if (string.Equals(options.DeinterlaceMethod, "bobandweave", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30)
+ {
+ filters.Add("yadif=1:-1:0");
+ }
+ else
+ {
+ filters.Add("yadif=0:-1:0");
+ }
+ }
+
+ var inputWidth = videoStream == null ? null : videoStream.Width;
+ var inputHeight = videoStream == null ? null : videoStream.Height;
+ var threeDFormat = state.MediaSource.Video3DFormat;
+
+ var videoDecoder = GetVideoDecoder(state, options);
+
+ filters.AddRange(GetScalingFilters(inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight));
+
+ var output = string.Empty;
+
+ if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
+ {
+ var subParam = GetTextSubtitleParam(state);
+
+ filters.Add(subParam);
+
+ if (allowTimeStampCopy)
+ {
+ output += " -copyts";
+ }
+ }
+
+ if (filters.Count > 0)
+ {
+ output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray()));
+ }
+
+ return output;
+ }
+
+
+ /// <summary>
+ /// Gets the number of threads.
+ /// </summary>
+ public int GetNumberOfThreads(EncodingJobInfo state, EncodingOptions encodingOptions, string outputVideoCodec)
+ {
+ if (string.Equals(outputVideoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
+ {
+ // per docs:
+ // -threads number of threads to use for encoding, can't be 0 [auto] with VP8 (recommended value : number of real cores - 1)
+ return Math.Max(Environment.ProcessorCount - 1, 1);
+ }
+
+ var threads = state.BaseRequest.CpuCoreLimit ?? encodingOptions.EncodingThreadCount;
+
+ // Automatic
+ if (threads <= 0 || threads >= Environment.ProcessorCount)
+ {
+ return 0;
+ }
+
+ return threads;
+ }
+
+ public void TryStreamCopy(EncodingJobInfo state)
+ {
+ if (state.VideoStream != null && CanStreamCopyVideo(state, state.VideoStream))
+ {
+ state.OutputVideoCodec = "copy";
+ }
+ else
+ {
+ var user = state.User;
+
+ // If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not
+ if (user != null && !user.Policy.EnableVideoPlaybackTranscoding)
+ {
+ state.OutputVideoCodec = "copy";
+ }
+ }
+
+ if (state.AudioStream != null && CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs))
+ {
+ state.OutputAudioCodec = "copy";
+ }
+ else
+ {
+ var user = state.User;
+
+ // If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not
+ if (user != null && !user.Policy.EnableAudioPlaybackTranscoding)
+ {
+ state.OutputAudioCodec = "copy";
+ }
+ }
+ }
+
+ public static string GetProbeSizeArgument(int numInputFiles)
+ {
+ return numInputFiles > 1 ? "-probesize 1G" : "";
+ }
+
+ public static string GetAnalyzeDurationArgument(int numInputFiles)
+ {
+ return numInputFiles > 1 ? "-analyzeduration 200M" : "";
+ }
+
+ public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions)
+ {
+ var inputModifier = string.Empty;
+
+ var numInputFiles = state.PlayableStreamFileNames.Length > 0 ? state.PlayableStreamFileNames.Length : 1;
+ var probeSizeArgument = GetProbeSizeArgument(numInputFiles);
+
+ string analyzeDurationArgument;
+ if (state.MediaSource.AnalyzeDurationMs.HasValue)
+ {
+ analyzeDurationArgument = "-analyzeduration " + (state.MediaSource.AnalyzeDurationMs.Value * 1000).ToString(CultureInfo.InvariantCulture);
+ }
+ else
+ {
+ analyzeDurationArgument = GetAnalyzeDurationArgument(numInputFiles);
+ }
+
+ if (!string.IsNullOrEmpty(probeSizeArgument))
+ {
+ inputModifier += " " + probeSizeArgument;
+ }
+
+ if (!string.IsNullOrEmpty(analyzeDurationArgument))
+ {
+ inputModifier += " " + analyzeDurationArgument;
+ }
+
+ inputModifier = inputModifier.Trim();
+
+ var userAgentParam = GetUserAgentParam(state);
+
+ if (!string.IsNullOrEmpty(userAgentParam))
+ {
+ inputModifier += " " + userAgentParam;
+ }
+
+ inputModifier = inputModifier.Trim();
+
+ inputModifier += " " + GetFastSeekCommandLineParameter(state.BaseRequest);
+ inputModifier = inputModifier.Trim();
+
+ if (state.InputProtocol == MediaProtocol.Rtsp)
+ {
+ inputModifier += " -rtsp_transport tcp -rtsp_transport udp -rtsp_flags prefer_tcp";
+ }
+
+ if (!string.IsNullOrEmpty(state.InputAudioSync))
+ {
+ inputModifier += " -async " + state.InputAudioSync;
+ }
+
+ if (!string.IsNullOrEmpty(state.InputVideoSync))
+ {
+ inputModifier += " -vsync " + state.InputVideoSync;
+ }
+
+ if (state.ReadInputAtNativeFramerate && state.InputProtocol != MediaProtocol.Rtsp)
+ {
+ inputModifier += " -re";
+ }
+
+ var flags = new List<string>();
+ if (state.IgnoreInputDts)
+ {
+ flags.Add("+igndts");
+ }
+ if (state.IgnoreInputIndex)
+ {
+ flags.Add("+ignidx");
+ }
+ if (state.GenPtsInput)
+ {
+ flags.Add("+genpts");
+ }
+ if (state.DiscardCorruptFramesInput)
+ {
+ flags.Add("+discardcorrupt");
+ }
+ if (state.EnableFastSeekInput)
+ {
+ flags.Add("+fastseek");
+ }
+
+ if (flags.Count > 0)
+ {
+ inputModifier += " -fflags " + string.Join("", flags.ToArray());
+ }
+
+ var videoDecoder = GetVideoDecoder(state, encodingOptions);
+ if (!string.IsNullOrEmpty(videoDecoder))
+ {
+ inputModifier += " " + videoDecoder;
+
+ var videoStream = state.VideoStream;
+ var inputWidth = videoStream == null ? null : videoStream.Width;
+ var inputHeight = videoStream == null ? null : videoStream.Height;
+ var request = state.BaseRequest;
+
+ var fixedOutputSize = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
+
+ if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1 && fixedOutputSize.Item1.HasValue && fixedOutputSize.Item2.HasValue)
+ {
+ inputModifier += string.Format(" -resize {0}x{1}", fixedOutputSize.Item1.Value.ToString(_usCulture), fixedOutputSize.Item2.Value.ToString(_usCulture));
+ }
+ }
+
+ if (state.IsVideoRequest)
+ {
+ var outputVideoCodec = GetVideoEncoder(state, encodingOptions);
+
+ // Important: If this is ever re-enabled, make sure not to use it with wtv because it breaks seeking
+ if (!string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase) &&
+ state.TranscodingType != TranscodingJobType.Progressive &&
+ state.EnableBreakOnNonKeyFrames(outputVideoCodec))
+ {
+ inputModifier += " -noaccurate_seek";
+ }
+
+ if (!string.IsNullOrEmpty(state.InputContainer) && state.VideoType == VideoType.VideoFile && string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
+ {
+ var inputFormat = GetInputFormat(state.InputContainer);
+ if (!string.IsNullOrEmpty(inputFormat))
+ {
+ inputModifier += " -f " + inputFormat;
+ }
+ }
+ }
+
+ if (state.MediaSource.RequiresLooping)
+ {
+ inputModifier += " -stream_loop -1";
+ }
+
+ return inputModifier;
+ }
+
+
+ public void AttachMediaSourceInfo(EncodingJobInfo state,
+ MediaSourceInfo mediaSource,
+ string requestedUrl)
+ {
+ if (state == null)
+ {
+ throw new ArgumentNullException("state");
+ }
+ if (mediaSource == null)
+ {
+ throw new ArgumentNullException("mediaSource");
+ }
+
+ var path = mediaSource.Path;
+ var protocol = mediaSource.Protocol;
+
+ if (!string.IsNullOrEmpty(mediaSource.EncoderPath) && mediaSource.EncoderProtocol.HasValue)
+ {
+ path = mediaSource.EncoderPath;
+ protocol = mediaSource.EncoderProtocol.Value;
+ }
+
+ state.MediaPath = path;
+ state.InputProtocol = protocol;
+ state.InputContainer = mediaSource.Container;
+ state.RunTimeTicks = mediaSource.RunTimeTicks;
+ state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
+
+ state.IsoType = mediaSource.IsoType;
+
+ if (mediaSource.VideoType.HasValue)
+ {
+ state.VideoType = mediaSource.VideoType.Value;
+
+ if (mediaSource.VideoType.Value == VideoType.BluRay || mediaSource.VideoType.Value == VideoType.Dvd)
+ {
+ state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, mediaSource.VideoType.Value).Select(Path.GetFileName).ToArray();
+ }
+ else if (mediaSource.VideoType.Value == VideoType.Iso && state.IsoType == IsoType.BluRay)
+ {
+ state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, VideoType.BluRay).Select(Path.GetFileName).ToArray();
+ }
+ else if (mediaSource.VideoType.Value == VideoType.Iso && state.IsoType == IsoType.Dvd)
+ {
+ state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, VideoType.Dvd).Select(Path.GetFileName).ToArray();
+ }
+ else
+ {
+ state.PlayableStreamFileNames = new string[] { };
+ }
+ }
+ else
+ {
+ state.PlayableStreamFileNames = new string[] { };
+ }
+
+ if (mediaSource.Timestamp.HasValue)
+ {
+ state.InputTimestamp = mediaSource.Timestamp.Value;
+ }
+
+ state.RunTimeTicks = mediaSource.RunTimeTicks;
+ state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
+ state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
+
+ if (state.ReadInputAtNativeFramerate ||
+ mediaSource.Protocol == MediaProtocol.File && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase))
+ {
+ state.InputVideoSync = "-1";
+ state.InputAudioSync = "1";
+ }
+
+ if (string.Equals(mediaSource.Container, "wma", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(mediaSource.Container, "asf", StringComparison.OrdinalIgnoreCase))
+ {
+ // Seeing some stuttering when transcoding wma to audio-only HLS
+ state.InputAudioSync = "1";
+ }
+
+ var mediaStreams = mediaSource.MediaStreams;
+
+ if (state.IsVideoRequest)
+ {
+ var videoRequest = state.BaseRequest;
+
+ if (string.IsNullOrEmpty(videoRequest.VideoCodec))
+ {
+ if (string.IsNullOrEmpty(requestedUrl))
+ {
+ requestedUrl = "test." + videoRequest.OutputContainer;
+ }
+
+ videoRequest.VideoCodec = InferVideoCodec(requestedUrl);
+ }
+
+ state.VideoStream = GetMediaStream(mediaStreams, videoRequest.VideoStreamIndex, MediaStreamType.Video);
+ state.SubtitleStream = GetMediaStream(mediaStreams, videoRequest.SubtitleStreamIndex, MediaStreamType.Subtitle, false);
+ state.SubtitleDeliveryMethod = videoRequest.SubtitleMethod;
+ state.AudioStream = GetMediaStream(mediaStreams, videoRequest.AudioStreamIndex, MediaStreamType.Audio);
+
+ if (state.SubtitleStream != null && !state.SubtitleStream.IsExternal)
+ {
+ state.InternalSubtitleStreamOffset = mediaStreams.Where(i => i.Type == MediaStreamType.Subtitle && !i.IsExternal).ToList().IndexOf(state.SubtitleStream);
+ }
+
+ EnforceResolutionLimit(state);
+
+ NormalizeSubtitleEmbed(state);
+ }
+ else
+ {
+ state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true);
+ }
+
+ state.MediaSource = mediaSource;
+ }
+
+ private void NormalizeSubtitleEmbed(EncodingJobInfo state)
+ {
+ if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed)
+ {
+ return;
+ }
+
+ // This is tricky to remux in, after converting to dvdsub it's not positioned correctly
+ // Therefore, let's just burn it in
+ if (string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase))
+ {
+ state.SubtitleDeliveryMethod = SubtitleDeliveryMethod.Encode;
+ }
+ }
+
+ /// <summary>
+ /// Gets the name of the output video codec
+ /// </summary>
+ protected string GetVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions)
+ {
+ if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ return GetVideoDecoder(state.MediaSource.VideoType ?? VideoType.VideoFile, state.VideoStream, encodingOptions);
+ }
+
+ public string GetVideoDecoder(VideoType videoType, MediaStream videoStream, EncodingOptions encodingOptions)
+ {
+ // Only use alternative encoders for video files.
+ // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
+ // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
+ if (videoType != VideoType.VideoFile)
+ {
+ return null;
+ }
+
+ if (videoStream != null &&
+ !string.IsNullOrEmpty(videoStream.Codec) &&
+ !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
+ {
+ if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
+ {
+ switch (videoStream.Codec.ToLower())
+ {
+ case "avc":
+ case "h264":
+ if (_mediaEncoder.SupportsDecoder("h264_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
+ {
+ // qsv decoder does not support 10-bit input
+ if ((videoStream.BitDepth ?? 8) > 8)
+ {
+ return null;
+ }
+ return "-c:v h264_qsv ";
+ }
+ break;
+ case "hevc":
+ case "h265":
+ if (_mediaEncoder.SupportsDecoder("hevc_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase))
+ {
+ //return "-c:v hevc_qsv -load_plugin hevc_hw ";
+ return "-c:v hevc_qsv ";
+ }
+ break;
+ case "mpeg2video":
+ if (_mediaEncoder.SupportsDecoder("mpeg2_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase))
+ {
+ return "-c:v mpeg2_qsv ";
+ }
+ break;
+ case "vc1":
+ if (_mediaEncoder.SupportsDecoder("vc1_qsv") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase))
+ {
+ return "-c:v vc1_qsv ";
+ }
+ break;
+ }
+ }
+
+ else if (string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
+ {
+ switch (videoStream.Codec.ToLower())
+ {
+ case "avc":
+ case "h264":
+ if (_mediaEncoder.SupportsDecoder("h264_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
+ {
+ return "-c:v h264_cuvid ";
+ }
+ break;
+ case "hevc":
+ case "h265":
+ if (_mediaEncoder.SupportsDecoder("hevc_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase))
+ {
+ return "-c:v hevc_cuvid ";
+ }
+ break;
+ case "mpeg2video":
+ if (_mediaEncoder.SupportsDecoder("mpeg2_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase))
+ {
+ return "-c:v mpeg2_cuvid ";
+ }
+ break;
+ case "vc1":
+ if (_mediaEncoder.SupportsDecoder("vc1_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("vc1", StringComparer.OrdinalIgnoreCase))
+ {
+ return "-c:v vc1_cuvid ";
+ }
+ break;
+ case "mpeg4":
+ if (_mediaEncoder.SupportsDecoder("mpeg4_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase))
+ {
+ return "-c:v mpeg4_cuvid ";
+ }
+ break;
+ }
+ }
+
+ else if (string.Equals(encodingOptions.HardwareAccelerationType, "mediacodec", StringComparison.OrdinalIgnoreCase))
+ {
+ switch (videoStream.Codec.ToLower())
+ {
+ case "avc":
+ case "h264":
+ if (_mediaEncoder.SupportsDecoder("h264_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
+ {
+ return "-c:v h264_mediacodec ";
+ }
+ break;
+ case "hevc":
+ case "h265":
+ if (_mediaEncoder.SupportsDecoder("hevc_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase))
+ {
+ return "-c:v hevc_mediacodec ";
+ }
+ break;
+ case "mpeg2video":
+ if (_mediaEncoder.SupportsDecoder("mpeg2_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase))
+ {
+ return "-c:v mpeg2_mediacodec ";
+ }
+ break;
+ case "mpeg4":
+ if (_mediaEncoder.SupportsDecoder("mpeg4_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg4", StringComparer.OrdinalIgnoreCase))
+ {
+ return "-c:v mpeg4_mediacodec ";
+ }
+ break;
+ case "vp8":
+ if (_mediaEncoder.SupportsDecoder("vp8_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp8", StringComparer.OrdinalIgnoreCase))
+ {
+ return "-c:v vp8_mediacodec ";
+ }
+ break;
+ case "vp9":
+ if (_mediaEncoder.SupportsDecoder("vp9_mediacodec") && encodingOptions.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase))
+ {
+ return "-c:v vp9_mediacodec ";
+ }
+ break;
+ }
+ }
+
+ else if (string.Equals(encodingOptions.HardwareAccelerationType, "omx", StringComparison.OrdinalIgnoreCase))
+ {
+ switch (videoStream.Codec.ToLower())
+ {
+ case "avc":
+ case "h264":
+ if (_mediaEncoder.SupportsDecoder("h264_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
+ {
+ return "-c:v h264_mmal";
+ }
+ break;
+ case "mpeg2video":
+ if (_mediaEncoder.SupportsDecoder("mpeg2_mmal") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase))
+ {
+ return "-c:v mpeg2_mmal";
+ }
+ break;
+ }
+ }
+
+ else if (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase))
+ {
+ switch (videoStream.Codec.ToLower())
+ {
+ case "avc":
+ case "h264":
+ if (_mediaEncoder.SupportsDecoder("h264_amf") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase))
+ {
+ return "-c:v h264_amf";
+ }
+ break;
+ case "mpeg2video":
+ if (_mediaEncoder.SupportsDecoder("hevc_amf") && encodingOptions.HardwareDecodingCodecs.Contains("mpeg2video", StringComparer.OrdinalIgnoreCase))
+ {
+ return "-c:v mpeg2_mmal";
+ }
+ break;
+ }
+ }
+ }
+
+ // leave blank so ffmpeg will decide
+ return null;
+ }
+
+ public string GetSubtitleEmbedArguments(EncodingJobInfo state)
+ {
+ if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed)
+ {
+ return string.Empty;
+ }
+
+ var format = state.SupportedSubtitleCodecs.FirstOrDefault();
+ string codec;
+
+ if (string.IsNullOrEmpty(format) || string.Equals(format, state.SubtitleStream.Codec, StringComparison.OrdinalIgnoreCase))
+ {
+ codec = "copy";
+ }
+ else
+ {
+ codec = format;
+ }
+
+ var args = " -codec:s:0 " + codec;
+
+ args += " -disposition:s:0 default";
+
+ return args;
+ }
+
+ public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string outputPath, string defaultH264Preset)
+ {
+ // Get the output codec name
+ var videoCodec = GetVideoEncoder(state, encodingOptions);
+
+ var format = string.Empty;
+ var keyFrame = string.Empty;
+
+ if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase) &&
+ state.BaseRequest.Context == EncodingContext.Streaming)
+ {
+ // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
+ format = " -f mp4 -movflags frag_keyframe+empty_moov";
+ }
+
+ var threads = GetNumberOfThreads(state, encodingOptions, videoCodec);
+
+ var inputModifier = GetInputModifier(state, encodingOptions);
+
+ return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -map_chapters -1 -threads {5} {6}{7}{8} -y \"{9}\"",
+ inputModifier,
+ GetInputArgument(state, encodingOptions),
+ keyFrame,
+ GetMapArgs(state),
+ GetProgressiveVideoArguments(state, encodingOptions, videoCodec, defaultH264Preset),
+ threads,
+ GetProgressiveVideoAudioArguments(state, encodingOptions),
+ GetSubtitleEmbedArguments(state),
+ format,
+ outputPath
+ ).Trim();
+ }
+
+ public string GetOutputFFlags(EncodingJobInfo state)
+ {
+ var flags = new List<string>();
+ if (state.GenPtsOutput)
+ {
+ flags.Add("+genpts");
+ }
+
+ if (flags.Count > 0)
+ {
+ return " -fflags " + string.Join("", flags.ToArray());
+ }
+
+ return string.Empty;
+ }
+
+ public string GetProgressiveVideoArguments(EncodingJobInfo state, EncodingOptions encodingOptions, string videoCodec, string defaultH264Preset)
+ {
+ var args = "-codec:v:0 " + videoCodec;
+
+ if (state.BaseRequest.EnableMpegtsM2TsMode)
+ {
+ args += " -mpegts_m2ts_mode 1";
+ }
+
+ if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ if (state.VideoStream != null && IsH264(state.VideoStream) &&
+ string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) &&
+ !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
+ {
+ args += " -bsf:v h264_mp4toannexb";
+ }
+
+ if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps)
+ {
+ args += " -copyts -avoid_negative_ts disabled -start_at_zero";
+ }
+
+ if (!state.RunTimeTicks.HasValue)
+ {
+ args += " -flags -global_header -fflags +genpts";
+ }
+ }
+ else
+ {
+ var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
+ 5.ToString(_usCulture));
+
+ args += keyFrameArg;
+
+ var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
+
+ var hasCopyTs = false;
+ // Add resolution params, if specified
+ if (!hasGraphicalSubs)
+ {
+ var outputSizeParam = GetOutputSizeParam(state, encodingOptions, videoCodec);
+ args += outputSizeParam;
+ hasCopyTs = outputSizeParam.IndexOf("copyts", StringComparison.OrdinalIgnoreCase) != -1;
+ }
+
+ if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps)
+ {
+ if (!hasCopyTs)
+ {
+ args += " -copyts";
+ }
+ args += " -avoid_negative_ts disabled -start_at_zero";
+ }
+
+ // This is for internal graphical subs
+ if (hasGraphicalSubs)
+ {
+ args += GetGraphicalSubtitleParam(state, encodingOptions, videoCodec);
+ }
+
+ var qualityParam = GetVideoQualityParam(state, videoCodec, encodingOptions, defaultH264Preset);
+
+ if (!string.IsNullOrEmpty(qualityParam))
+ {
+ args += " " + qualityParam.Trim();
+ }
+
+ if (!state.RunTimeTicks.HasValue)
+ {
+ args += " -flags -global_header";
+ }
+ }
+
+ if (!string.IsNullOrEmpty(state.OutputVideoSync))
+ {
+ args += " -vsync " + state.OutputVideoSync;
+ }
+
+ args += GetOutputFFlags(state);
+
+ return args;
+ }
+
+ public string GetProgressiveVideoAudioArguments(EncodingJobInfo state, EncodingOptions encodingOptions)
+ {
+ // If the video doesn't have an audio stream, return a default.
+ if (state.AudioStream == null && state.VideoStream != null)
+ {
+ return string.Empty;
+ }
+
+ // Get the output codec name
+ var codec = GetAudioEncoder(state);
+
+ var args = "-codec:a:0 " + codec;
+
+ if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ return args;
+ }
+
+ // Add the number of audio channels
+ var channels = state.OutputAudioChannels;
+
+ if (channels.HasValue)
+ {
+ args += " -ac " + channels.Value;
+ }
+
+ var bitrate = state.OutputAudioBitrate;
+
+ if (bitrate.HasValue)
+ {
+ args += " -ab " + bitrate.Value.ToString(_usCulture);
+ }
+
+ if (state.OutputAudioSampleRate.HasValue)
+ {
+ args += " -ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture);
+ }
+
+ args += " " + GetAudioFilterParam(state, encodingOptions, false);
+
+ return args;
+ }
+
+ public string GetProgressiveAudioFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string outputPath)
+ {
+ var audioTranscodeParams = new List<string>();
+
+ var bitrate = state.OutputAudioBitrate;
+
+ if (bitrate.HasValue)
+ {
+ audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(_usCulture));
+ }
+
+ if (state.OutputAudioChannels.HasValue)
+ {
+ audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(_usCulture));
+ }
+
+ // opus will fail on 44100
+ if (!string.Equals(state.OutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
+ {
+ if (state.OutputAudioSampleRate.HasValue)
+ {
+ audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture));
+ }
+ }
+
+ var albumCoverInput = string.Empty;
+ var mapArgs = string.Empty;
+ var metadata = string.Empty;
+ var vn = string.Empty;
+
+ var hasArt = !string.IsNullOrEmpty(state.AlbumCoverPath);
+ hasArt = false;
+
+ if (hasArt)
+ {
+ albumCoverInput = " -i \"" + state.AlbumCoverPath + "\"";
+ mapArgs = " -map 0:a -map 1:v -c:1:v copy";
+ metadata = " -metadata:s:v title=\"Album cover\" -metadata:s:v comment=\"Cover(Front)\"";
+ }
+ else
+ {
+ vn = " -vn";
+ }
+
+ var threads = GetNumberOfThreads(state, encodingOptions, null);
+
+ var inputModifier = GetInputModifier(state, encodingOptions);
+
+ return string.Format("{0} {1}{7}{8} -threads {2}{3} {4} -id3v2_version 3 -write_id3v1 1{6} -y \"{5}\"",
+ inputModifier,
+ GetInputArgument(state, encodingOptions),
+ threads,
+ vn,
+ string.Join(" ", audioTranscodeParams.ToArray()),
+ outputPath,
+ metadata,
+ albumCoverInput,
+ mapArgs).Trim();
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
new file mode 100644
index 000000000..3d2871e65
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -0,0 +1,775 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.MediaEncoding
+{
+ // For now, a common base class until the API and MediaEncoding classes are unified
+ public abstract class EncodingJobInfo
+ {
+ private readonly ILogger _logger;
+
+ public MediaStream VideoStream { get; set; }
+ public VideoType VideoType { get; set; }
+ public Dictionary<string, string> RemoteHttpHeaders { get; set; }
+ public string OutputVideoCodec { get; set; }
+ public MediaProtocol InputProtocol { get; set; }
+ public string MediaPath { get; set; }
+ public bool IsInputVideo { get; set; }
+ public IIsoMount IsoMount { get; set; }
+ public string[] PlayableStreamFileNames { get; set; }
+ public string OutputAudioCodec { get; set; }
+ public int? OutputVideoBitrate { get; set; }
+ public MediaStream SubtitleStream { get; set; }
+ public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
+ public string[] SupportedSubtitleCodecs { get; set; }
+
+ public int InternalSubtitleStreamOffset { get; set; }
+ public MediaSourceInfo MediaSource { get; set; }
+ public User User { get; set; }
+
+ public long? RunTimeTicks { get; set; }
+
+ public bool ReadInputAtNativeFramerate { get; set; }
+
+ private TranscodeReason[] _transcodeReasons = null;
+ public TranscodeReason[] TranscodeReasons
+ {
+ get
+ {
+ if (_transcodeReasons == null)
+ {
+ _transcodeReasons = (BaseRequest.TranscodeReasons ?? string.Empty)
+ .Split(',')
+ .Where(i => !string.IsNullOrEmpty(i))
+ .Select(v => (TranscodeReason)Enum.Parse(typeof(TranscodeReason), v, true))
+ .ToArray();
+ }
+
+ return _transcodeReasons;
+ }
+ }
+
+ public bool IgnoreInputDts
+ {
+ get
+ {
+ return MediaSource.IgnoreDts;
+ }
+ }
+
+ public bool IgnoreInputIndex
+ {
+ get
+ {
+ return MediaSource.IgnoreIndex;
+ }
+ }
+
+ public bool GenPtsInput
+ {
+ get
+ {
+ return MediaSource.GenPtsInput;
+ }
+ }
+
+ public bool DiscardCorruptFramesInput
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public bool EnableFastSeekInput
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public bool GenPtsOutput
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public string OutputContainer { get; set; }
+
+ public string OutputVideoSync
+ {
+ get
+ {
+ // For live tv + in progress recordings
+ if (string.Equals(InputContainer, "mpegts", StringComparison.OrdinalIgnoreCase) || string.Equals(InputContainer, "ts", StringComparison.OrdinalIgnoreCase))
+ {
+ if (!MediaSource.RunTimeTicks.HasValue)
+ {
+ return "cfr";
+ }
+ }
+
+ return "-1";
+ }
+ }
+
+ public string AlbumCoverPath { get; set; }
+
+ public string InputAudioSync { get; set; }
+ public string InputVideoSync { get; set; }
+ public TransportStreamTimestamp InputTimestamp { get; set; }
+
+ public MediaStream AudioStream { get; set; }
+ public string[] SupportedAudioCodecs { get; set; }
+ public string[] SupportedVideoCodecs { get; set; }
+ public string InputContainer { get; set; }
+ public IsoType? IsoType { get; set; }
+
+ public BaseEncodingJobOptions BaseRequest { get; set; }
+
+ public long? StartTimeTicks
+ {
+ get { return BaseRequest.StartTimeTicks; }
+ }
+
+ public bool CopyTimestamps
+ {
+ get { return BaseRequest.CopyTimestamps; }
+ }
+
+ public int? OutputAudioBitrate;
+ public int? OutputAudioChannels;
+
+ public bool DeInterlace(string videoCodec, bool forceDeinterlaceIfSourceIsInterlaced)
+ {
+ var videoStream = VideoStream;
+ var isInputInterlaced = videoStream != null && videoStream.IsInterlaced;
+
+ if (!isInputInterlaced)
+ {
+ return false;
+ }
+
+ // Support general param
+ if (BaseRequest.DeInterlace)
+ {
+ return true;
+ }
+
+ if (!string.IsNullOrEmpty(videoCodec))
+ {
+ if (string.Equals(BaseRequest.GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ if (forceDeinterlaceIfSourceIsInterlaced)
+ {
+ if (isInputInterlaced)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public string[] GetRequestedProfiles(string codec)
+ {
+ if (!string.IsNullOrEmpty(BaseRequest.Profile))
+ {
+ return BaseRequest.Profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
+ }
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ var profile = BaseRequest.GetOption(codec, "profile");
+
+ if (!string.IsNullOrEmpty(profile))
+ {
+ return profile.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
+ }
+ }
+
+ return new string[] { };
+ }
+
+ public string GetRequestedLevel(string codec)
+ {
+ if (!string.IsNullOrEmpty(BaseRequest.Level))
+ {
+ return BaseRequest.Level;
+ }
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ return BaseRequest.GetOption(codec, "level");
+ }
+
+ return null;
+ }
+
+ public int? GetRequestedMaxRefFrames(string codec)
+ {
+ if (BaseRequest.MaxRefFrames.HasValue)
+ {
+ return BaseRequest.MaxRefFrames;
+ }
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ var value = BaseRequest.GetOption(codec, "maxrefframes");
+ int result;
+ if (!string.IsNullOrEmpty(value) && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result))
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ public int? GetRequestedVideoBitDepth(string codec)
+ {
+ if (BaseRequest.MaxVideoBitDepth.HasValue)
+ {
+ return BaseRequest.MaxVideoBitDepth;
+ }
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ var value = BaseRequest.GetOption(codec, "videobitdepth");
+ int result;
+ if (!string.IsNullOrEmpty(value) && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result))
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ public int? GetRequestedAudioBitDepth(string codec)
+ {
+ if (BaseRequest.MaxAudioBitDepth.HasValue)
+ {
+ return BaseRequest.MaxAudioBitDepth;
+ }
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ var value = BaseRequest.GetOption(codec, "audiobitdepth");
+ int result;
+ if (!string.IsNullOrEmpty(value) && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result))
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ public int? GetRequestedAudioChannels(string codec)
+ {
+ if (BaseRequest.MaxAudioChannels.HasValue)
+ {
+ return BaseRequest.MaxAudioChannels;
+ }
+ if (BaseRequest.AudioChannels.HasValue)
+ {
+ return BaseRequest.AudioChannels;
+ }
+
+ if (!string.IsNullOrEmpty(codec))
+ {
+ var value = BaseRequest.GetOption(codec, "audiochannels");
+ int result;
+ if (!string.IsNullOrEmpty(value) && int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result))
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ public bool IsVideoRequest { get; set; }
+ public TranscodingJobType TranscodingType { get; set; }
+
+ public EncodingJobInfo(ILogger logger, IMediaSourceManager unused, TranscodingJobType jobType)
+ {
+ _logger = logger;
+ TranscodingType = jobType;
+ RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ PlayableStreamFileNames = new string[] { };
+ SupportedAudioCodecs = new string[] { };
+ SupportedVideoCodecs = new string[] { };
+ SupportedSubtitleCodecs = new string[] { };
+ }
+
+ public bool IsSegmentedLiveStream
+ {
+ get
+ {
+ return TranscodingType != TranscodingJobType.Progressive && !RunTimeTicks.HasValue;
+ }
+ }
+
+ public bool EnableBreakOnNonKeyFrames(string videoCodec)
+ {
+ if (TranscodingType != TranscodingJobType.Progressive)
+ {
+ if (IsSegmentedLiveStream)
+ {
+ return false;
+ }
+
+ return BaseRequest.BreakOnNonKeyFrames && string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase);
+ }
+
+ return false;
+ }
+
+ public int? TotalOutputBitrate
+ {
+ get
+ {
+ return (OutputAudioBitrate ?? 0) + (OutputVideoBitrate ?? 0);
+ }
+ }
+
+ public int? OutputWidth
+ {
+ get
+ {
+ if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue)
+ {
+ var size = new ImageSize
+ {
+ Width = VideoStream.Width.Value,
+ Height = VideoStream.Height.Value
+ };
+
+ var newSize = DrawingUtils.Resize(size,
+ BaseRequest.Width ?? 0,
+ BaseRequest.Height ?? 0,
+ BaseRequest.MaxWidth ?? 0,
+ BaseRequest.MaxHeight ?? 0);
+
+ return Convert.ToInt32(newSize.Width);
+ }
+
+ if (!IsVideoRequest)
+ {
+ return null;
+ }
+
+ return BaseRequest.MaxWidth ?? BaseRequest.Width;
+ }
+ }
+
+ public int? OutputHeight
+ {
+ get
+ {
+ if (VideoStream != null && VideoStream.Width.HasValue && VideoStream.Height.HasValue)
+ {
+ var size = new ImageSize
+ {
+ Width = VideoStream.Width.Value,
+ Height = VideoStream.Height.Value
+ };
+
+ var newSize = DrawingUtils.Resize(size,
+ BaseRequest.Width ?? 0,
+ BaseRequest.Height ?? 0,
+ BaseRequest.MaxWidth ?? 0,
+ BaseRequest.MaxHeight ?? 0);
+
+ return Convert.ToInt32(newSize.Height);
+ }
+
+ if (!IsVideoRequest)
+ {
+ return null;
+ }
+
+ return BaseRequest.MaxHeight ?? BaseRequest.Height;
+ }
+ }
+
+ public int? OutputAudioSampleRate
+ {
+ get
+ {
+ if (BaseRequest.Static || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ if (AudioStream != null)
+ {
+ return AudioStream.SampleRate;
+ }
+ }
+
+ else if (BaseRequest.AudioSampleRate.HasValue)
+ {
+ // Don't exceed what the encoder supports
+ // Seeing issues of attempting to encode to 88200
+ return Math.Min(44100, BaseRequest.AudioSampleRate.Value);
+ }
+
+ return null;
+ }
+ }
+
+ public int? OutputAudioBitDepth
+ {
+ get
+ {
+ if (BaseRequest.Static || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ if (AudioStream != null)
+ {
+ return AudioStream.BitDepth;
+ }
+ }
+
+ //else if (BaseRequest.AudioSampleRate.HasValue)
+ //{
+ // // Don't exceed what the encoder supports
+ // // Seeing issues of attempting to encode to 88200
+ // return Math.Min(44100, BaseRequest.AudioSampleRate.Value);
+ //}
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Predicts the audio sample rate that will be in the output stream
+ /// </summary>
+ public double? TargetVideoLevel
+ {
+ get
+ {
+ if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ return VideoStream == null ? null : VideoStream.Level;
+ }
+
+ var level = GetRequestedLevel(ActualOutputVideoCodec);
+ double result;
+ if (!string.IsNullOrEmpty(level) && double.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out result))
+ {
+ return result;
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Predicts the audio sample rate that will be in the output stream
+ /// </summary>
+ public int? TargetVideoBitDepth
+ {
+ get
+ {
+ if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ return VideoStream == null ? null : VideoStream.BitDepth;
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Gets the target reference frames.
+ /// </summary>
+ /// <value>The target reference frames.</value>
+ public int? TargetRefFrames
+ {
+ get
+ {
+ if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ return VideoStream == null ? null : VideoStream.RefFrames;
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Predicts the audio sample rate that will be in the output stream
+ /// </summary>
+ public float? TargetFramerate
+ {
+ get
+ {
+ if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ return VideoStream == null ? null : (VideoStream.AverageFrameRate ?? VideoStream.RealFrameRate);
+ }
+
+ return BaseRequest.MaxFramerate ?? BaseRequest.Framerate;
+ }
+ }
+
+ public TransportStreamTimestamp TargetTimestamp
+ {
+ get
+ {
+ var defaultValue = string.Equals(OutputContainer, "m2ts", StringComparison.OrdinalIgnoreCase) ?
+ TransportStreamTimestamp.Valid :
+ TransportStreamTimestamp.None;
+
+ return !BaseRequest.Static
+ ? defaultValue
+ : InputTimestamp;
+ }
+ }
+
+ /// <summary>
+ /// Predicts the audio sample rate that will be in the output stream
+ /// </summary>
+ public int? TargetPacketLength
+ {
+ get
+ {
+ if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ return VideoStream == null ? null : VideoStream.PacketLength;
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Predicts the audio sample rate that will be in the output stream
+ /// </summary>
+ public string TargetVideoProfile
+ {
+ get
+ {
+ if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ return VideoStream == null ? null : VideoStream.Profile;
+ }
+
+ var requestedProfile = GetRequestedProfiles(ActualOutputVideoCodec).FirstOrDefault();
+ if (!string.IsNullOrEmpty(requestedProfile))
+ {
+ return requestedProfile;
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Predicts the audio sample rate that will be in the output stream
+ /// </summary>
+ public string TargetVideoRange
+ {
+ get
+ {
+ if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ return VideoStream == null ? null : VideoStream.VideoRange;
+ }
+
+ return "SDR";
+ }
+ }
+
+ public string TargetAudioProfile
+ {
+ get
+ {
+ if (BaseRequest.Static || string.Equals(OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ return AudioStream == null ? null : AudioStream.Profile;
+ }
+
+ return null;
+ }
+ }
+
+ public string TargetVideoCodecTag
+ {
+ get
+ {
+ if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ return VideoStream == null ? null : VideoStream.CodecTag;
+ }
+
+ return null;
+ }
+ }
+
+ public bool? IsTargetAnamorphic
+ {
+ get
+ {
+ if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ return VideoStream == null ? null : VideoStream.IsAnamorphic;
+ }
+
+ return false;
+ }
+ }
+
+ public string ActualOutputVideoCodec
+ {
+ get
+ {
+ var codec = OutputVideoCodec;
+
+ if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ var stream = VideoStream;
+
+ if (stream != null)
+ {
+ return stream.Codec;
+ }
+
+ return null;
+ }
+
+ return codec;
+ }
+ }
+
+ public bool? IsTargetInterlaced
+ {
+ get
+ {
+ if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ return VideoStream == null ? (bool?)null : VideoStream.IsInterlaced;
+ }
+
+ if (DeInterlace(ActualOutputVideoCodec, true))
+ {
+ return false;
+ }
+
+ return VideoStream == null ? (bool?)null : VideoStream.IsInterlaced;
+ }
+ }
+
+ public bool? IsTargetAVC
+ {
+ get
+ {
+ if (BaseRequest.Static || string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
+ {
+ return VideoStream == null ? null : VideoStream.IsAVC;
+ }
+
+ return false;
+ }
+ }
+
+ public int? TargetVideoStreamCount
+ {
+ get
+ {
+ if (BaseRequest.Static)
+ {
+ return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue);
+ }
+ return GetMediaStreamCount(MediaStreamType.Video, 1);
+ }
+ }
+
+ public int? TargetAudioStreamCount
+ {
+ get
+ {
+ if (BaseRequest.Static)
+ {
+ return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue);
+ }
+ return GetMediaStreamCount(MediaStreamType.Audio, 1);
+ }
+ }
+
+ private int? GetMediaStreamCount(MediaStreamType type, int limit)
+ {
+ var count = MediaSource.GetStreamCount(type);
+
+ if (count.HasValue)
+ {
+ count = Math.Min(count.Value, limit);
+ }
+
+ return count;
+ }
+
+ protected void DisposeIsoMount()
+ {
+ if (IsoMount != null)
+ {
+ try
+ {
+ IsoMount.Dispose();
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error disposing iso mount", ex);
+ }
+
+ IsoMount = null;
+ }
+ }
+
+ public IProgress<double> Progress { get; set; }
+ public virtual void ReportTranscodingProgress(TimeSpan? transcodingPosition, float framerate, double? percentComplete, long bytesTranscoded, int? bitRate) {
+ Progress.Report(percentComplete.Value);
+ }
+
+ public virtual void Dispose () {
+ }
+ }
+
+ /// <summary>
+ /// Enum TranscodingJobType
+ /// </summary>
+ public enum TranscodingJobType
+ {
+ /// <summary>
+ /// The progressive
+ /// </summary>
+ Progressive,
+ /// <summary>
+ /// The HLS
+ /// </summary>
+ Hls,
+ /// <summary>
+ /// The dash
+ /// </summary>
+ Dash
+ }
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
new file mode 100644
index 000000000..7333149c2
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs
@@ -0,0 +1,265 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Services;
+
+namespace MediaBrowser.Controller.MediaEncoding
+{
+ public class EncodingJobOptions : BaseEncodingJobOptions
+ {
+ public string OutputDirectory { get; set; }
+
+ public string DeviceId { get; set; }
+ public string ItemId { get; set; }
+ public string MediaSourceId { get; set; }
+ public string AudioCodec { get; set; }
+
+ public DeviceProfile DeviceProfile { get; set; }
+
+ public bool ReadInputAtNativeFramerate { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance has fixed resolution.
+ /// </summary>
+ /// <value><c>true</c> if this instance has fixed resolution; otherwise, <c>false</c>.</value>
+ public bool HasFixedResolution
+ {
+ get
+ {
+ return Width.HasValue || Height.HasValue;
+ }
+ }
+
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+ public EncodingJobOptions(StreamInfo info, DeviceProfile deviceProfile)
+ {
+ OutputContainer = info.Container;
+ StartTimeTicks = info.StartPositionTicks;
+ MaxWidth = info.MaxWidth;
+ MaxHeight = info.MaxHeight;
+ MaxFramerate = info.MaxFramerate;
+ ItemId = info.ItemId.ToString("");
+ MediaSourceId = info.MediaSourceId;
+ AudioCodec = info.TargetAudioCodec.FirstOrDefault();
+ MaxAudioChannels = info.GlobalMaxAudioChannels;
+ AudioBitRate = info.AudioBitrate;
+ AudioSampleRate = info.TargetAudioSampleRate;
+ DeviceProfile = deviceProfile;
+ VideoCodec = info.TargetVideoCodec.FirstOrDefault();
+ VideoBitRate = info.VideoBitrate;
+ AudioStreamIndex = info.AudioStreamIndex;
+ SubtitleMethod = info.SubtitleDeliveryMethod;
+ Context = info.Context;
+ TranscodingMaxAudioChannels = info.TranscodingMaxAudioChannels;
+
+ if (info.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External)
+ {
+ SubtitleStreamIndex = info.SubtitleStreamIndex;
+ }
+ StreamOptions = info.StreamOptions;
+ }
+ }
+
+ // For now until api and media encoding layers are unified
+ public class BaseEncodingJobOptions
+ {
+ [ApiMember(Name = "EnableAutoStreamCopy", Description = "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+ public bool EnableAutoStreamCopy { get; set; }
+
+ public bool AllowVideoStreamCopy { get; set; }
+ public bool AllowAudioStreamCopy { get; set; }
+ public bool BreakOnNonKeyFrames { get; set; }
+
+ /// <summary>
+ /// Gets or sets the audio sample rate.
+ /// </summary>
+ /// <value>The audio sample rate.</value>
+ [ApiMember(Name = "AudioSampleRate", Description = "Optional. Specify a specific audio sample rate, e.g. 44100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+ public int? AudioSampleRate { get; set; }
+
+ public int? MaxAudioBitDepth { get; set; }
+
+ /// <summary>
+ /// Gets or sets the audio bit rate.
+ /// </summary>
+ /// <value>The audio bit rate.</value>
+ [ApiMember(Name = "AudioBitRate", Description = "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+ public int? AudioBitRate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the audio channels.
+ /// </summary>
+ /// <value>The audio channels.</value>
+ [ApiMember(Name = "AudioChannels", Description = "Optional. Specify a specific number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+ public int? AudioChannels { get; set; }
+
+ [ApiMember(Name = "MaxAudioChannels", Description = "Optional. Specify a maximum number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+ public int? MaxAudioChannels { get; set; }
+
+ [ApiMember(Name = "Static", Description = "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+ public bool Static { get; set; }
+
+ /// <summary>
+ /// Gets or sets the profile.
+ /// </summary>
+ /// <value>The profile.</value>
+ [ApiMember(Name = "Profile", Description = "Optional. Specify a specific h264 profile, e.g. main, baseline, high.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+ public string Profile { get; set; }
+
+ /// <summary>
+ /// Gets or sets the level.
+ /// </summary>
+ /// <value>The level.</value>
+ [ApiMember(Name = "Level", Description = "Optional. Specify a level for the h264 profile, e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+ public string Level { get; set; }
+
+ /// <summary>
+ /// Gets or sets the framerate.
+ /// </summary>
+ /// <value>The framerate.</value>
+ [ApiMember(Name = "Framerate", Description = "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")]
+ public float? Framerate { get; set; }
+
+ [ApiMember(Name = "MaxFramerate", Description = "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")]
+ public float? MaxFramerate { get; set; }
+
+ [ApiMember(Name = "CopyTimestamps", Description = "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+ public bool CopyTimestamps { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start time ticks.
+ /// </summary>
+ /// <value>The start time ticks.</value>
+ [ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+ public long? StartTimeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the width.
+ /// </summary>
+ /// <value>The width.</value>
+ [ApiMember(Name = "Width", Description = "Optional. The fixed horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+ public int? Width { get; set; }
+
+ /// <summary>
+ /// Gets or sets the height.
+ /// </summary>
+ /// <value>The height.</value>
+ [ApiMember(Name = "Height", Description = "Optional. The fixed vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+ public int? Height { get; set; }
+
+ /// <summary>
+ /// Gets or sets the width of the max.
+ /// </summary>
+ /// <value>The width of the max.</value>
+ [ApiMember(Name = "MaxWidth", Description = "Optional. The maximum horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+ public int? MaxWidth { get; set; }
+
+ /// <summary>
+ /// Gets or sets the height of the max.
+ /// </summary>
+ /// <value>The height of the max.</value>
+ [ApiMember(Name = "MaxHeight", Description = "Optional. The maximum vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+ public int? MaxHeight { get; set; }
+
+ /// <summary>
+ /// Gets or sets the video bit rate.
+ /// </summary>
+ /// <value>The video bit rate.</value>
+ [ApiMember(Name = "VideoBitRate", Description = "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+ public int? VideoBitRate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index of the subtitle stream.
+ /// </summary>
+ /// <value>The index of the subtitle stream.</value>
+ [ApiMember(Name = "SubtitleStreamIndex", Description = "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+ public int? SubtitleStreamIndex { get; set; }
+
+ [ApiMember(Name = "SubtitleMethod", Description = "Optional. Specify the subtitle delivery method.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+ public SubtitleDeliveryMethod SubtitleMethod { get; set; }
+
+ [ApiMember(Name = "MaxRefFrames", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+ public int? MaxRefFrames { get; set; }
+
+ [ApiMember(Name = "MaxVideoBitDepth", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+ public int? MaxVideoBitDepth { get; set; }
+ public bool RequireAvc { get; set; }
+ public bool DeInterlace { get; set; }
+ public bool RequireNonAnamorphic { get; set; }
+ public int? TranscodingMaxAudioChannels { get; set; }
+ public int? CpuCoreLimit { get; set; }
+ public string OutputContainer { get; set; }
+ public string LiveStreamId { get; set; }
+
+ public bool EnableMpegtsM2TsMode { get; set; }
+
+ /// <summary>
+ /// Gets or sets the video codec.
+ /// </summary>
+ /// <value>The video codec.</value>
+ [ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+ public string VideoCodec { get; set; }
+
+ public string SubtitleCodec { get; set; }
+
+ public string TranscodeReasons { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index of the audio stream.
+ /// </summary>
+ /// <value>The index of the audio stream.</value>
+ [ApiMember(Name = "AudioStreamIndex", Description = "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+ public int? AudioStreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index of the video stream.
+ /// </summary>
+ /// <value>The index of the video stream.</value>
+ [ApiMember(Name = "VideoStreamIndex", Description = "Optional. The index of the video stream to use. If omitted the first video stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+ public int? VideoStreamIndex { get; set; }
+
+ public EncodingContext Context { get; set; }
+
+ public Dictionary<string, string> StreamOptions { get; set; }
+
+ public string GetOption(string qualifier, string name)
+ {
+ var value = GetOption(qualifier + "-" + name);
+
+ if (string.IsNullOrEmpty(value))
+ {
+ value = GetOption(name);
+ }
+
+ return value;
+ }
+
+ public string GetOption(string name)
+ {
+ string value;
+ if (StreamOptions.TryGetValue(name, out value))
+ {
+ return value;
+ }
+
+ return null;
+ }
+
+ public BaseEncodingJobOptions()
+ {
+ EnableAutoStreamCopy = true;
+ AllowVideoStreamCopy = true;
+ AllowAudioStreamCopy = true;
+ Context = EncodingContext.Streaming;
+ StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ public string TempDirectory { get; set; }
+ public string DeviceId { get; set; }
+ public Guid Id { get; set; }
+ public string Container { get; set; }
+ public DeviceProfile DeviceProfile { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
new file mode 100644
index 000000000..7d50efd5e
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Controller.Providers;
+
+namespace MediaBrowser.Controller.MediaEncoding
+{
+ public interface IEncodingManager
+ {
+ /// <summary>
+ /// Refreshes the chapter images.
+ /// </summary>
+ Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, List<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
new file mode 100644
index 000000000..2b85b8975
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -0,0 +1,124 @@
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.MediaInfo;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.IO;
+
+namespace MediaBrowser.Controller.MediaEncoding
+{
+ /// <summary>
+ /// Interface IMediaEncoder
+ /// </summary>
+ public interface IMediaEncoder : ITranscoderSupport
+ {
+ string EncoderLocationType { get; }
+
+ /// <summary>
+ /// Gets the encoder path.
+ /// </summary>
+ /// <value>The encoder path.</value>
+ string EncoderPath { get; }
+
+ /// <summary>
+ /// Supportses the decoder.
+ /// </summary>
+ /// <param name="decoder">The decoder.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+ bool SupportsDecoder(string decoder);
+
+ /// <summary>
+ /// Extracts the audio image.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="imageStreamIndex">Index of the image stream.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{Stream}.</returns>
+ Task<string> ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Extracts the video image.
+ /// </summary>
+ Task<string> ExtractVideoImage(string[] inputFiles, string container, MediaProtocol protocol, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken);
+
+ Task<string> ExtractVideoImage(string[] inputFiles, string container, MediaProtocol protocol, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Extracts the video images on interval.
+ /// </summary>
+ Task ExtractVideoImagesOnInterval(string[] inputFiles,
+ string container,
+ MediaStream videoStream,
+ MediaProtocol protocol,
+ Video3DFormat? threedFormat,
+ TimeSpan interval,
+ string targetDirectory,
+ string filenamePrefix,
+ int? maxWidth,
+ CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the media info.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the input argument.
+ /// </summary>
+ /// <param name="inputFiles">The input files.</param>
+ /// <param name="protocol">The protocol.</param>
+ /// <returns>System.String.</returns>
+ string GetInputArgument(string[] inputFiles, MediaProtocol protocol);
+
+ /// <summary>
+ /// Gets the time parameter.
+ /// </summary>
+ /// <param name="ticks">The ticks.</param>
+ /// <returns>System.String.</returns>
+ string GetTimeParameter(long ticks);
+
+ /// <summary>
+ /// Encodes the audio.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task<string> EncodeAudio(EncodingJobOptions options,
+ IProgress<double> progress,
+ CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Encodes the video.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task&lt;System.String&gt;.</returns>
+ Task<string> EncodeVideo(EncodingJobOptions options,
+ IProgress<double> progress,
+ CancellationToken cancellationToken);
+
+ Task ConvertImage(string inputPath, string outputPath);
+
+ /// <summary>
+ /// Escapes the subtitle filter path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>System.String.</returns>
+ string EscapeSubtitleFilterPath(string path);
+
+ void Init();
+
+ void UpdateEncoderPath(string path, string pathType);
+ bool SupportsEncoder(string encoder);
+
+ string[] GetPlayableStreamFileNames(string path, VideoType videoType);
+ IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, IIsoMount isoMount, uint? titleNumber);
+ }
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
new file mode 100644
index 000000000..de7496d42
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs
@@ -0,0 +1,33 @@
+using MediaBrowser.Model.MediaInfo;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.MediaEncoding
+{
+ public interface ISubtitleEncoder
+ {
+ /// <summary>
+ /// Gets the subtitles.
+ /// </summary>
+ /// <returns>Task{Stream}.</returns>
+ Task<Stream> GetSubtitles(BaseItem item,
+ string mediaSourceId,
+ int subtitleStreamIndex,
+ string outputFormat,
+ long startTimeTicks,
+ long endTimeTicks,
+ bool preserveOriginalTimestamps,
+ CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the subtitle language encoding parameter.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="protocol">The protocol.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>System.String.</returns>
+ Task<string> GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs b/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs
new file mode 100644
index 000000000..a8d1e5a0f
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs
@@ -0,0 +1,20 @@
+
+namespace MediaBrowser.Controller.MediaEncoding
+{
+ public class ImageEncodingOptions
+ {
+ public string InputPath { get; set; }
+
+ public int? Width { get; set; }
+
+ public int? Height { get; set; }
+
+ public int? MaxWidth { get; set; }
+
+ public int? MaxHeight { get; set; }
+
+ public int? Quality { get; set; }
+
+ public string Format { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
new file mode 100644
index 000000000..5f3f79d77
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
@@ -0,0 +1,149 @@
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace MediaBrowser.Controller.MediaEncoding
+{
+ public class JobLogger
+ {
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+ private readonly ILogger _logger;
+
+ public JobLogger(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public async void StartStreamingLog(EncodingJobInfo state, Stream source, Stream target)
+ {
+ try
+ {
+ using (var reader = new StreamReader(source))
+ {
+ while (!reader.EndOfStream)
+ {
+ var line = await reader.ReadLineAsync().ConfigureAwait(false);
+
+ ParseLogLine(line, state);
+
+ var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
+
+ await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+ await target.FlushAsync().ConfigureAwait(false);
+ }
+ }
+ }
+ catch (ObjectDisposedException)
+ {
+ // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error reading ffmpeg log", ex);
+ }
+ }
+
+ private void ParseLogLine(string line, EncodingJobInfo state)
+ {
+ float? framerate = null;
+ double? percent = null;
+ TimeSpan? transcodingPosition = null;
+ long? bytesTranscoded = null;
+ int? bitRate = null;
+
+ var parts = line.Split(' ');
+
+ var totalMs = state.RunTimeTicks.HasValue
+ ? TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds
+ : 0;
+
+ var startMs = state.BaseRequest.StartTimeTicks.HasValue
+ ? TimeSpan.FromTicks(state.BaseRequest.StartTimeTicks.Value).TotalMilliseconds
+ : 0;
+
+ for (var i = 0; i < parts.Length; i++)
+ {
+ var part = parts[i];
+
+ if (string.Equals(part, "fps=", StringComparison.OrdinalIgnoreCase) &&
+ (i + 1 < parts.Length))
+ {
+ var rate = parts[i + 1];
+ float val;
+
+ if (float.TryParse(rate, NumberStyles.Any, _usCulture, out val))
+ {
+ framerate = val;
+ }
+ }
+ else if (state.RunTimeTicks.HasValue &&
+ part.StartsWith("time=", StringComparison.OrdinalIgnoreCase))
+ {
+ var time = part.Split(new[] { '=' }, 2).Last();
+ TimeSpan val;
+
+ if (TimeSpan.TryParse(time, _usCulture, out val))
+ {
+ var currentMs = startMs + val.TotalMilliseconds;
+
+ var percentVal = currentMs / totalMs;
+ percent = 100 * percentVal;
+
+ transcodingPosition = val;
+ }
+ }
+ else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase))
+ {
+ var size = part.Split(new[] { '=' }, 2).Last();
+
+ int? scale = null;
+ if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ scale = 1024;
+ size = size.Replace("kb", string.Empty, StringComparison.OrdinalIgnoreCase);
+ }
+
+ if (scale.HasValue)
+ {
+ long val;
+
+ if (long.TryParse(size, NumberStyles.Any, _usCulture, out val))
+ {
+ bytesTranscoded = val * scale.Value;
+ }
+ }
+ }
+ else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase))
+ {
+ var rate = part.Split(new[] { '=' }, 2).Last();
+
+ int? scale = null;
+ if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ scale = 1024;
+ rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase);
+ }
+
+ if (scale.HasValue)
+ {
+ float val;
+
+ if (float.TryParse(rate, NumberStyles.Any, _usCulture, out val))
+ {
+ bitRate = (int)Math.Ceiling(val * scale.Value);
+ }
+ }
+ }
+ }
+
+ if (framerate.HasValue || percent.HasValue)
+ {
+ state.ReportTranscodingProgress(transcodingPosition, 0, percent, 0, bitRate);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs
new file mode 100644
index 000000000..70e4db84f
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs
@@ -0,0 +1,53 @@
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using System;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Controller.MediaEncoding
+{
+ /// <summary>
+ /// Class MediaEncoderHelpers
+ /// </summary>
+ public static class MediaEncoderHelpers
+ {
+ /// <summary>
+ /// Gets the input argument.
+ /// </summary>
+ /// <param name="fileSystem">The file system.</param>
+ /// <param name="videoPath">The video path.</param>
+ /// <param name="protocol">The protocol.</param>
+ /// <param name="isoMount">The iso mount.</param>
+ /// <param name="playableStreamFileNames">The playable stream file names.</param>
+ /// <returns>System.String[][].</returns>
+ public static string[] GetInputArgument(IFileSystem fileSystem, string videoPath, MediaProtocol protocol, IIsoMount isoMount, string[] playableStreamFileNames)
+ {
+ if (playableStreamFileNames.Length > 0)
+ {
+ if (isoMount == null)
+ {
+ return GetPlayableStreamFiles(fileSystem, videoPath, playableStreamFileNames);
+ }
+ return GetPlayableStreamFiles(fileSystem, isoMount.MountedPath, playableStreamFileNames);
+ }
+
+ return new[] {videoPath};
+ }
+
+ private static string[] GetPlayableStreamFiles(IFileSystem fileSystem, string rootPath, string[] filenames)
+ {
+ if (filenames.Length == 0)
+ {
+ return new string[]{};
+ }
+
+ var allFiles = fileSystem
+ .GetFilePaths(rootPath, true)
+ .ToArray();
+
+ return filenames.Select(name => allFiles.FirstOrDefault(f => string.Equals(Path.GetFileName(f), name, StringComparison.OrdinalIgnoreCase)))
+ .Where(f => !string.IsNullOrEmpty(f))
+ .ToArray();
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs
new file mode 100644
index 000000000..1d7222801
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs
@@ -0,0 +1,24 @@
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using System.Collections.Generic;
+using MediaBrowser.Model.Dto;
+using System;
+
+namespace MediaBrowser.Controller.MediaEncoding
+{
+ public class MediaInfoRequest
+ {
+ public MediaSourceInfo MediaSource { get; set; }
+ public bool ExtractChapters { get; set; }
+ public DlnaProfileType MediaType { get; set; }
+ public IIsoMount MountedIso { get; set; }
+ public string[] PlayableStreamFileNames { get; set; }
+
+ public MediaInfoRequest()
+ {
+ PlayableStreamFileNames = new string[] {};
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs
new file mode 100644
index 000000000..2f31b8e66
--- /dev/null
+++ b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs
@@ -0,0 +1,69 @@
+using System;
+using MediaBrowser.Model.Services;
+
+namespace MediaBrowser.Controller.Net
+{
+ public class AuthenticatedAttribute : Attribute, IHasRequestFilter, IAuthenticationAttributes
+ {
+ public static IAuthService AuthService { get; set; }
+
+ /// <summary>
+ /// Gets or sets the roles.
+ /// </summary>
+ /// <value>The roles.</value>
+ public string Roles { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [escape parental control].
+ /// </summary>
+ /// <value><c>true</c> if [escape parental control]; otherwise, <c>false</c>.</value>
+ public bool EscapeParentalControl { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [allow before startup wizard].
+ /// </summary>
+ /// <value><c>true</c> if [allow before startup wizard]; otherwise, <c>false</c>.</value>
+ public bool AllowBeforeStartupWizard { get; set; }
+
+ public bool AllowLocal { get; set; }
+
+ /// <summary>
+ /// The request filter is executed before the service.
+ /// </summary>
+ /// <param name="request">The http request wrapper</param>
+ /// <param name="response">The http response wrapper</param>
+ /// <param name="requestDto">The request DTO</param>
+ public void RequestFilter(IRequest request, IResponse response, object requestDto)
+ {
+ AuthService.Authenticate(request, this);
+ }
+
+ /// <summary>
+ /// Order in which Request Filters are executed.
+ /// &lt;0 Executed before global request filters
+ /// &gt;0 Executed after global request filters
+ /// </summary>
+ /// <value>The priority.</value>
+ public int Priority
+ {
+ get { return 0; }
+ }
+
+ public string[] GetRoles()
+ {
+ return (Roles ?? string.Empty).Split(new []{ ',' }, StringSplitOptions.RemoveEmptyEntries);
+ }
+
+ public bool AllowLocalOnly { get; set; }
+ }
+
+ public interface IAuthenticationAttributes
+ {
+ bool EscapeParentalControl { get; }
+ bool AllowBeforeStartupWizard { get; }
+ bool AllowLocal { get; }
+ bool AllowLocalOnly { get; }
+
+ string[] GetRoles();
+ }
+}
diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs
new file mode 100644
index 000000000..a68060db5
--- /dev/null
+++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs
@@ -0,0 +1,52 @@
+using MediaBrowser.Controller.Entities;
+using System;
+
+
+namespace MediaBrowser.Controller.Net
+{
+ public class AuthorizationInfo
+ {
+ /// <summary>
+ /// Gets or sets the user identifier.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public Guid UserId {
+ get {
+ if (User == null) {
+ return Guid.Empty;
+ }
+ else {
+ return User.Id;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the device identifier.
+ /// </summary>
+ /// <value>The device identifier.</value>
+ public string DeviceId { get; set; }
+ /// <summary>
+ /// Gets or sets the device.
+ /// </summary>
+ /// <value>The device.</value>
+ public string Device { get; set; }
+ /// <summary>
+ /// Gets or sets the client.
+ /// </summary>
+ /// <value>The client.</value>
+ public string Client { get; set; }
+ /// <summary>
+ /// Gets or sets the version.
+ /// </summary>
+ /// <value>The version.</value>
+ public string Version { get; set; }
+ /// <summary>
+ /// Gets or sets the token.
+ /// </summary>
+ /// <value>The token.</value>
+ public string Token { get; set; }
+
+ public User User { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
new file mode 100644
index 000000000..7df96b777
--- /dev/null
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -0,0 +1,322 @@
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Threading;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+using System.Threading;
+using System;
+
+namespace MediaBrowser.Controller.Net
+{
+ /// <summary>
+ /// Starts sending data over a web socket periodically when a message is received, and then stops when a corresponding stop message is received
+ /// </summary>
+ /// <typeparam name="TReturnDataType">The type of the T return data type.</typeparam>
+ /// <typeparam name="TStateType">The type of the T state type.</typeparam>
+ public abstract class BasePeriodicWebSocketListener<TReturnDataType, TStateType> : IWebSocketListener, IDisposable
+ where TStateType : WebSocketListenerState, new()
+ where TReturnDataType : class
+ {
+ /// <summary>
+ /// The _active connections
+ /// </summary>
+ protected readonly List<Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType>> ActiveConnections =
+ new List<Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType>>();
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ protected abstract string Name { get; }
+
+ /// <summary>
+ /// Gets the data to send.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ /// <returns>Task{`1}.</returns>
+ protected abstract Task<TReturnDataType> GetDataToSend(TStateType state, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// The logger
+ /// </summary>
+ protected ILogger Logger;
+
+ protected ITimerFactory TimerFactory { get; private set; }
+
+ protected BasePeriodicWebSocketListener(ILogger logger)
+ {
+ if (logger == null)
+ {
+ throw new ArgumentNullException("logger");
+ }
+
+ Logger = logger;
+ }
+
+ /// <summary>
+ /// Processes the message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <returns>Task.</returns>
+ public Task ProcessMessage(WebSocketMessageInfo message)
+ {
+ if (message == null)
+ {
+ throw new ArgumentNullException("message");
+ }
+
+ if (string.Equals(message.MessageType, Name + "Start", StringComparison.OrdinalIgnoreCase))
+ {
+ Start(message);
+ }
+
+ if (string.Equals(message.MessageType, Name + "Stop", StringComparison.OrdinalIgnoreCase))
+ {
+ Stop(message);
+ }
+
+ return Task.FromResult(true);
+ }
+
+ protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ protected virtual bool SendOnTimer
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ protected virtual void ParseMessageParams(string[] values)
+ {
+
+ }
+
+ /// <summary>
+ /// Starts sending messages over a web socket
+ /// </summary>
+ /// <param name="message">The message.</param>
+ private void Start(WebSocketMessageInfo message)
+ {
+ var vals = message.Data.Split(',');
+
+ var dueTimeMs = long.Parse(vals[0], UsCulture);
+ var periodMs = long.Parse(vals[1], UsCulture);
+
+ if (vals.Length > 2)
+ {
+ ParseMessageParams(vals.Skip(2).ToArray());
+ }
+
+ var cancellationTokenSource = new CancellationTokenSource();
+
+ Logger.Debug("{1} Begin transmitting over websocket to {0}", message.Connection.RemoteEndPoint, GetType().Name);
+
+ var timer = SendOnTimer ?
+ TimerFactory.Create(TimerCallback, message.Connection, Timeout.Infinite, Timeout.Infinite) :
+ null;
+
+ var state = new TStateType
+ {
+ IntervalMs = periodMs,
+ InitialDelayMs = dueTimeMs
+ };
+
+ lock (ActiveConnections)
+ {
+ ActiveConnections.Add(new Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType>(message.Connection, cancellationTokenSource, timer, state));
+ }
+
+ if (timer != null)
+ {
+ timer.Change(TimeSpan.FromMilliseconds(dueTimeMs), TimeSpan.FromMilliseconds(periodMs));
+ }
+ }
+
+ /// <summary>
+ /// Timers the callback.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ private void TimerCallback(object state)
+ {
+ var connection = (IWebSocketConnection)state;
+
+ Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType> tuple;
+
+ lock (ActiveConnections)
+ {
+ tuple = ActiveConnections.FirstOrDefault(c => c.Item1 == connection);
+ }
+
+ if (tuple == null)
+ {
+ return;
+ }
+
+ if (connection.State != WebSocketState.Open || tuple.Item2.IsCancellationRequested)
+ {
+ DisposeConnection(tuple);
+ return;
+ }
+
+ SendData(tuple);
+ }
+
+ protected void SendData(bool force)
+ {
+ Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType>[] tuples;
+
+ lock (ActiveConnections)
+ {
+ tuples = ActiveConnections
+ .Where(c =>
+ {
+ if (c.Item1.State == WebSocketState.Open && !c.Item2.IsCancellationRequested)
+ {
+ var state = c.Item4;
+
+ if (force || (DateTime.UtcNow - state.DateLastSendUtc).TotalMilliseconds >= state.IntervalMs)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ })
+ .ToArray();
+ }
+
+ foreach (var tuple in tuples)
+ {
+ SendData(tuple);
+ }
+ }
+
+ private async void SendData(Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType> tuple)
+ {
+ var connection = tuple.Item1;
+
+ try
+ {
+ var state = tuple.Item4;
+
+ var cancellationToken = tuple.Item2.Token;
+
+ var data = await GetDataToSend(state, cancellationToken).ConfigureAwait(false);
+
+ if (data != null)
+ {
+ await connection.SendAsync(new WebSocketMessage<TReturnDataType>
+ {
+ MessageType = Name,
+ Data = data
+
+ }, cancellationToken).ConfigureAwait(false);
+
+ state.DateLastSendUtc = DateTime.UtcNow;
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ if (tuple.Item2.IsCancellationRequested)
+ {
+ DisposeConnection(tuple);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error sending web socket message {0}", ex, Name);
+ DisposeConnection(tuple);
+ }
+ }
+
+ /// <summary>
+ /// Stops sending messages over a web socket
+ /// </summary>
+ /// <param name="message">The message.</param>
+ private void Stop(WebSocketMessageInfo message)
+ {
+ lock (ActiveConnections)
+ {
+ var connection = ActiveConnections.FirstOrDefault(c => c.Item1 == message.Connection);
+
+ if (connection != null)
+ {
+ DisposeConnection(connection);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Disposes the connection.
+ /// </summary>
+ /// <param name="connection">The connection.</param>
+ private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, ITimer, TStateType> connection)
+ {
+ Logger.Debug("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name);
+
+ var timer = connection.Item3;
+
+ if (timer != null)
+ {
+ try
+ {
+ timer.Dispose();
+ }
+ catch (ObjectDisposedException)
+ {
+
+ }
+ }
+
+ try
+ {
+ connection.Item2.Cancel();
+ connection.Item2.Dispose();
+ }
+ catch (ObjectDisposedException)
+ {
+
+ }
+
+ ActiveConnections.Remove(connection);
+ }
+
+ /// <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)
+ {
+ lock (ActiveConnections)
+ {
+ foreach (var connection in ActiveConnections.ToArray())
+ {
+ DisposeConnection(connection);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+ }
+
+ public class WebSocketListenerState
+ {
+ public DateTime DateLastSendUtc { get; set; }
+ public long InitialDelayMs { get; set; }
+ public long IntervalMs { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs
new file mode 100644
index 000000000..361320250
--- /dev/null
+++ b/MediaBrowser.Controller/Net/IAuthService.cs
@@ -0,0 +1,9 @@
+using MediaBrowser.Model.Services;
+
+namespace MediaBrowser.Controller.Net
+{
+ public interface IAuthService
+ {
+ void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues);
+ }
+}
diff --git a/MediaBrowser.Controller/Net/IAuthorizationContext.cs b/MediaBrowser.Controller/Net/IAuthorizationContext.cs
new file mode 100644
index 000000000..5a9d0aa30
--- /dev/null
+++ b/MediaBrowser.Controller/Net/IAuthorizationContext.cs
@@ -0,0 +1,21 @@
+using MediaBrowser.Model.Services;
+
+namespace MediaBrowser.Controller.Net
+{
+ public interface IAuthorizationContext
+ {
+ /// <summary>
+ /// Gets the authorization information.
+ /// </summary>
+ /// <param name="requestContext">The request context.</param>
+ /// <returns>AuthorizationInfo.</returns>
+ AuthorizationInfo GetAuthorizationInfo(object requestContext);
+
+ /// <summary>
+ /// Gets the authorization information.
+ /// </summary>
+ /// <param name="requestContext">The request context.</param>
+ /// <returns>AuthorizationInfo.</returns>
+ AuthorizationInfo GetAuthorizationInfo(IRequest requestContext);
+ }
+}
diff --git a/MediaBrowser.Controller/Net/IHasResultFactory.cs b/MediaBrowser.Controller/Net/IHasResultFactory.cs
new file mode 100644
index 000000000..03144e4b8
--- /dev/null
+++ b/MediaBrowser.Controller/Net/IHasResultFactory.cs
@@ -0,0 +1,17 @@
+using MediaBrowser.Model.Services;
+
+namespace MediaBrowser.Controller.Net
+{
+ /// <summary>
+ /// Interface IHasResultFactory
+ /// Services that require a ResultFactory should implement this
+ /// </summary>
+ public interface IHasResultFactory : IRequiresRequest
+ {
+ /// <summary>
+ /// Gets or sets the result factory.
+ /// </summary>
+ /// <value>The result factory.</value>
+ IHttpResultFactory ResultFactory { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Net/IHttpResultFactory.cs b/MediaBrowser.Controller/Net/IHttpResultFactory.cs
new file mode 100644
index 000000000..f8e631de3
--- /dev/null
+++ b/MediaBrowser.Controller/Net/IHttpResultFactory.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Services;
+
+namespace MediaBrowser.Controller.Net
+{
+ /// <summary>
+ /// Interface IHttpResultFactory
+ /// </summary>
+ public interface IHttpResultFactory
+ {
+ /// <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>
+ object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null);
+
+ object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null);
+ object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null);
+ object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null);
+
+ object GetRedirectResult(string url);
+
+ object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null)
+ where T : class;
+
+ /// <summary>
+ /// Gets the static result.
+ /// </summary>
+ /// <param name="requestContext">The request context.</param>
+ /// <param name="cacheKey">The cache key.</param>
+ /// <param name="lastDateModified">The last date modified.</param>
+ /// <param name="cacheDuration">Duration of the cache.</param>
+ /// <param name="contentType">Type of the content.</param>
+ /// <param name="factoryFn">The factory fn.</param>
+ /// <param name="responseHeaders">The response headers.</param>
+ /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
+ /// <returns>System.Object.</returns>
+ 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);
+
+ /// <summary>
+ /// Gets the static result.
+ /// </summary>
+ /// <param name="requestContext">The request context.</param>
+ /// <param name="options">The options.</param>
+ /// <returns>System.Object.</returns>
+ Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options);
+
+ /// <summary>
+ /// Gets the static file result.
+ /// </summary>
+ /// <param name="requestContext">The request context.</param>
+ /// <param name="path">The path.</param>
+ /// <param name="fileShare">The file share.</param>
+ /// <returns>System.Object.</returns>
+ Task<object> GetStaticFileResult(IRequest requestContext, string path, FileShareMode fileShare = FileShareMode.Read);
+
+ /// <summary>
+ /// Gets the static file result.
+ /// </summary>
+ /// <param name="requestContext">The request context.</param>
+ /// <param name="options">The options.</param>
+ /// <returns>System.Object.</returns>
+ Task<object> GetStaticFileResult(IRequest requestContext,
+ StaticFileResultOptions options);
+ }
+}
diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs
new file mode 100644
index 000000000..d2ebadcfa
--- /dev/null
+++ b/MediaBrowser.Controller/Net/IHttpServer.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Services;
+using MediaBrowser.Model.Events;
+
+namespace MediaBrowser.Controller.Net
+{
+ /// <summary>
+ /// Interface IHttpServer
+ /// </summary>
+ public interface IHttpServer : IDisposable
+ {
+ /// <summary>
+ /// Gets the URL prefix.
+ /// </summary>
+ /// <value>The URL prefix.</value>
+ string[] UrlPrefixes { get; }
+
+ /// <summary>
+ /// Stops this instance.
+ /// </summary>
+ void Stop();
+
+ /// <summary>
+ /// Occurs when [web socket connected].
+ /// </summary>
+ event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
+
+ /// <summary>
+ /// Inits this instance.
+ /// </summary>
+ void Init(IEnumerable<IService> services, IEnumerable<IWebSocketListener> listener);
+
+ /// <summary>
+ /// If set, all requests will respond with this message
+ /// </summary>
+ string GlobalResponse { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Net/ISessionContext.cs b/MediaBrowser.Controller/Net/ISessionContext.cs
new file mode 100644
index 000000000..37ddbc2b3
--- /dev/null
+++ b/MediaBrowser.Controller/Net/ISessionContext.cs
@@ -0,0 +1,16 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Session;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Services;
+
+namespace MediaBrowser.Controller.Net
+{
+ public interface ISessionContext
+ {
+ SessionInfo GetSession(object requestContext);
+ User GetUser(object requestContext);
+
+ SessionInfo GetSession(IRequest requestContext);
+ User GetUser(IRequest requestContext);
+ }
+}
diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
new file mode 100644
index 000000000..816e9afca
--- /dev/null
+++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
@@ -0,0 +1,85 @@
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Services;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+using System.Threading;
+using System;
+
+namespace MediaBrowser.Controller.Net
+{
+ public interface IWebSocketConnection : IDisposable
+ {
+ /// <summary>
+ /// Occurs when [closed].
+ /// </summary>
+ event EventHandler<EventArgs> Closed;
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ Guid Id { get; }
+
+ /// <summary>
+ /// Gets the last activity date.
+ /// </summary>
+ /// <value>The last activity date.</value>
+ DateTime LastActivityDate { get; }
+
+ /// <summary>
+ /// Gets or sets the URL.
+ /// </summary>
+ /// <value>The URL.</value>
+ string Url { get; set; }
+ /// <summary>
+ /// Gets or sets the query string.
+ /// </summary>
+ /// <value>The query string.</value>
+ QueryParamCollection QueryString { get; set; }
+
+ /// <summary>
+ /// Gets or sets the receive action.
+ /// </summary>
+ /// <value>The receive action.</value>
+ Func<WebSocketMessageInfo, Task> OnReceive { get; set; }
+
+ /// <summary>
+ /// Gets the state.
+ /// </summary>
+ /// <value>The state.</value>
+ WebSocketState State { get; }
+
+ /// <summary>
+ /// Gets the remote end point.
+ /// </summary>
+ /// <value>The remote end point.</value>
+ string RemoteEndPoint { get; }
+
+ /// <summary>
+ /// Sends a message asynchronously.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="message">The message.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">message</exception>
+ Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Sends a message asynchronously.
+ /// </summary>
+ /// <param name="buffer">The buffer.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SendAsync(byte[] buffer, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Sends a message asynchronously.
+ /// </summary>
+ /// <param name="text">The text.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException">buffer</exception>
+ Task SendAsync(string text, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Net/IWebSocketListener.cs b/MediaBrowser.Controller/Net/IWebSocketListener.cs
new file mode 100644
index 000000000..29698c1a4
--- /dev/null
+++ b/MediaBrowser.Controller/Net/IWebSocketListener.cs
@@ -0,0 +1,17 @@
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Net
+{
+ /// <summary>
+ ///This is an interface for listening to messages coming through a web socket connection
+ /// </summary>
+ public interface IWebSocketListener
+ {
+ /// <summary>
+ /// Processes the message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <returns>Task.</returns>
+ Task ProcessMessage(WebSocketMessageInfo message);
+ }
+}
diff --git a/MediaBrowser.Controller/Net/SecurityException.cs b/MediaBrowser.Controller/Net/SecurityException.cs
new file mode 100644
index 000000000..b251ab9a9
--- /dev/null
+++ b/MediaBrowser.Controller/Net/SecurityException.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace MediaBrowser.Controller.Net
+{
+ public class SecurityException : Exception
+ {
+ public SecurityException(string message)
+ : base(message)
+ {
+
+ }
+
+ public SecurityExceptionType SecurityExceptionType { get; set; }
+ }
+
+ public enum SecurityExceptionType
+ {
+ Unauthenticated = 0,
+ ParentalControl = 1
+ }
+}
diff --git a/MediaBrowser.Controller/Net/StaticResultOptions.cs b/MediaBrowser.Controller/Net/StaticResultOptions.cs
new file mode 100644
index 000000000..1c9b2586d
--- /dev/null
+++ b/MediaBrowser.Controller/Net/StaticResultOptions.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+
+using MediaBrowser.Model.IO;
+
+namespace MediaBrowser.Controller.Net
+{
+ public class StaticResultOptions
+ {
+ public string ContentType { get; set; }
+ public TimeSpan? CacheDuration { get; set; }
+ public DateTime? DateLastModified { get; set; }
+ public Guid CacheKey { get; set; }
+
+ public Func<Task<Stream>> ContentFactory { get; set; }
+
+ public bool IsHeadRequest { get; set; }
+
+ public IDictionary<string, string> ResponseHeaders { get; set; }
+
+ public Action OnComplete { get; set; }
+ public Action OnError { get; set; }
+
+ public string Path { get; set; }
+ public long? ContentLength { get; set; }
+
+ public FileShareMode FileShare { get; set; }
+
+ public StaticResultOptions()
+ {
+ ResponseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ FileShare = FileShareMode.Read;
+ }
+ }
+
+ public class StaticFileResultOptions : StaticResultOptions
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs b/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs
new file mode 100644
index 000000000..b200f883a
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketConnectEventArgs.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Specialized;
+using MediaBrowser.Model.Services;
+
+namespace MediaBrowser.Controller.Net
+{
+ /// <summary>
+ /// Class WebSocketConnectEventArgs
+ /// </summary>
+
+ public class WebSocketConnectingEventArgs : EventArgs
+ {
+ /// <summary>
+ /// Gets or sets the URL.
+ /// </summary>
+ /// <value>The URL.</value>
+ public string Url { get; set; }
+ /// <summary>
+ /// Gets or sets the endpoint.
+ /// </summary>
+ /// <value>The endpoint.</value>
+ public string Endpoint { get; set; }
+ /// <summary>
+ /// Gets or sets the query string.
+ /// </summary>
+ /// <value>The query string.</value>
+ public QueryParamCollection QueryString { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether [allow connection].
+ /// </summary>
+ /// <value><c>true</c> if [allow connection]; otherwise, <c>false</c>.</value>
+ public bool AllowConnection { get; set; }
+
+ public WebSocketConnectingEventArgs()
+ {
+ QueryString = new QueryParamCollection();
+ AllowConnection = true;
+ }
+ }
+
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs
new file mode 100644
index 000000000..332f16420
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs
@@ -0,0 +1,16 @@
+using MediaBrowser.Model.Net;
+
+namespace MediaBrowser.Controller.Net
+{
+ /// <summary>
+ /// Class WebSocketMessageInfo
+ /// </summary>
+ public class WebSocketMessageInfo : WebSocketMessage<string>
+ {
+ /// <summary>
+ /// Gets or sets the connection.
+ /// </summary>
+ /// <value>The connection.</value>
+ public IWebSocketConnection Connection { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Notifications/INotificationManager.cs b/MediaBrowser.Controller/Notifications/INotificationManager.cs
new file mode 100644
index 000000000..161f0ffba
--- /dev/null
+++ b/MediaBrowser.Controller/Notifications/INotificationManager.cs
@@ -0,0 +1,41 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Notifications;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using System.Threading;
+
+namespace MediaBrowser.Controller.Notifications
+{
+ public interface INotificationManager
+ {
+ /// <summary>
+ /// Sends the notification.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SendNotification(NotificationRequest request, CancellationToken cancellationToken);
+
+ Task SendNotification(NotificationRequest request, BaseItem relatedItem, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Adds the parts.
+ /// </summary>
+ /// <param name="services">The services.</param>
+ /// <param name="notificationTypeFactories">The notification type factories.</param>
+ void AddParts(IEnumerable<INotificationService> services, IEnumerable<INotificationTypeFactory> notificationTypeFactories);
+
+ /// <summary>
+ /// Gets the notification types.
+ /// </summary>
+ /// <returns>IEnumerable{NotificationTypeInfo}.</returns>
+ List<NotificationTypeInfo> GetNotificationTypes();
+
+ /// <summary>
+ /// Gets the notification services.
+ /// </summary>
+ /// <returns>IEnumerable{NotificationServiceInfo}.</returns>
+ IEnumerable<NameIdPair> GetNotificationServices();
+ }
+}
diff --git a/MediaBrowser.Controller/Notifications/INotificationService.cs b/MediaBrowser.Controller/Notifications/INotificationService.cs
new file mode 100644
index 000000000..b1e313b87
--- /dev/null
+++ b/MediaBrowser.Controller/Notifications/INotificationService.cs
@@ -0,0 +1,30 @@
+using MediaBrowser.Controller.Entities;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Notifications
+{
+ public interface INotificationService
+ {
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ /// <summary>
+ /// Sends the notification.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SendNotification(UserNotification request, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Determines whether [is enabled for user] [the specified user identifier].
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <returns><c>true</c> if [is enabled for user] [the specified user identifier]; otherwise, <c>false</c>.</returns>
+ bool IsEnabledForUser(User user);
+ }
+}
diff --git a/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs b/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs
new file mode 100644
index 000000000..bf92aae2d
--- /dev/null
+++ b/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs
@@ -0,0 +1,14 @@
+using MediaBrowser.Model.Notifications;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Notifications
+{
+ public interface INotificationTypeFactory
+ {
+ /// <summary>
+ /// Gets the notification types.
+ /// </summary>
+ /// <returns>IEnumerable{NotificationTypeInfo}.</returns>
+ IEnumerable<NotificationTypeInfo> GetNotificationTypes();
+ }
+}
diff --git a/MediaBrowser.Controller/Notifications/UserNotification.cs b/MediaBrowser.Controller/Notifications/UserNotification.cs
new file mode 100644
index 000000000..d035a3995
--- /dev/null
+++ b/MediaBrowser.Controller/Notifications/UserNotification.cs
@@ -0,0 +1,21 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Notifications;
+using System;
+
+namespace MediaBrowser.Controller.Notifications
+{
+ public class UserNotification
+ {
+ public string Name { get; set; }
+
+ public string Description { get; set; }
+
+ public string Url { get; set; }
+
+ public NotificationLevel Level { get; set; }
+
+ public DateTime Date { get; set; }
+
+ public User User { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs b/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs
new file mode 100644
index 000000000..25aba6bd9
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Threading;
+
+namespace MediaBrowser.Controller.Persistence
+{
+ /// <summary>
+ /// Interface IDisplayPreferencesRepository
+ /// </summary>
+ public interface IDisplayPreferencesRepository : IRepository
+ {
+ /// <summary>
+ /// Saves display preferences for an item
+ /// </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>
+ /// <returns>Task.</returns>
+ void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client,
+ CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Saves all display preferences for a user
+ /// </summary>
+ /// <param name="displayPreferences">The display preferences.</param>
+ /// <param name="userId">The user id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId,
+ CancellationToken cancellationToken);
+ /// <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>
+ DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client);
+
+ /// <summary>
+ /// Gets all display preferences for the given user.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <returns>Task{DisplayPreferences}.</returns>
+ IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId);
+ }
+}
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
new file mode 100644
index 000000000..7905ea1aa
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -0,0 +1,159 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Querying;
+
+namespace MediaBrowser.Controller.Persistence
+{
+ /// <summary>
+ /// Provides an interface to implement an Item repository
+ /// </summary>
+ public interface IItemRepository : IRepository
+ {
+ /// <summary>
+ /// Saves an item
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void SaveItem(BaseItem item, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Deletes the item.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void DeleteItem(Guid id, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Saves the items.
+ /// </summary>
+ /// <param name="items">The items.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void SaveItems(List<BaseItem> items, CancellationToken cancellationToken);
+
+ void SaveImages(BaseItem item);
+
+ /// <summary>
+ /// Retrieves the item.
+ /// </summary>
+ /// <param name="id">The id.</param>
+ /// <returns>BaseItem.</returns>
+ BaseItem RetrieveItem(Guid id);
+
+ /// <summary>
+ /// Gets chapters for an item
+ /// </summary>
+ /// <param name="id"></param>
+ /// <returns></returns>
+ List<ChapterInfo> GetChapters(BaseItem id);
+
+ /// <summary>
+ /// Gets a single chapter for an item
+ /// </summary>
+ /// <param name="id"></param>
+ /// <param name="index"></param>
+ /// <returns></returns>
+ ChapterInfo GetChapter(BaseItem id, int index);
+
+ /// <summary>
+ /// Saves the chapters.
+ /// </summary>
+ void SaveChapters(Guid id, List<ChapterInfo> chapters);
+
+ /// <summary>
+ /// Gets the media streams.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>IEnumerable{MediaStream}.</returns>
+ List<MediaStream> GetMediaStreams(MediaStreamQuery query);
+
+ /// <summary>
+ /// Saves the media streams.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="streams">The streams.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void SaveMediaStreams(Guid id, List<MediaStream> streams, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the item ids.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>IEnumerable&lt;Guid&gt;.</returns>
+ QueryResult<Guid> GetItemIds(InternalItemsQuery query);
+ /// <summary>
+ /// Gets the items.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>QueryResult&lt;BaseItem&gt;.</returns>
+ QueryResult<BaseItem> GetItems(InternalItemsQuery query);
+
+ /// <summary>
+ /// Gets the item ids list.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>List&lt;Guid&gt;.</returns>
+ List<Guid> GetItemIdsList(InternalItemsQuery query);
+
+ /// <summary>
+ /// Gets the people.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>List&lt;PersonInfo&gt;.</returns>
+ List<PersonInfo> GetPeople(InternalPeopleQuery query);
+
+ /// <summary>
+ /// Updates the people.
+ /// </summary>
+ /// <param name="itemId">The item identifier.</param>
+ /// <param name="people">The people.</param>
+ void UpdatePeople(Guid itemId, List<PersonInfo> people);
+
+ /// <summary>
+ /// Gets the people names.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>List&lt;System.String&gt;.</returns>
+ List<string> GetPeopleNames(InternalPeopleQuery query);
+
+ /// <summary>
+ /// Gets the item ids with path.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>QueryResult&lt;Tuple&lt;Guid, System.String&gt;&gt;.</returns>
+ List<Tuple<Guid, string>> GetItemIdsWithPath(InternalItemsQuery query);
+
+ /// <summary>
+ /// Gets the item list.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>List&lt;BaseItem&gt;.</returns>
+ List<BaseItem> GetItemList(InternalItemsQuery query);
+
+ /// <summary>
+ /// Updates the inherited values.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ void UpdateInheritedValues(CancellationToken cancellationToken);
+
+ int GetCount(InternalItemsQuery query);
+
+ QueryResult<Tuple<BaseItem, ItemCounts>> GetGenres(InternalItemsQuery query);
+ QueryResult<Tuple<BaseItem, ItemCounts>> GetMusicGenres(InternalItemsQuery query);
+ QueryResult<Tuple<BaseItem, ItemCounts>> GetGameGenres(InternalItemsQuery query);
+ QueryResult<Tuple<BaseItem, ItemCounts>> GetStudios(InternalItemsQuery query);
+ QueryResult<Tuple<BaseItem, ItemCounts>> GetArtists(InternalItemsQuery query);
+ QueryResult<Tuple<BaseItem, ItemCounts>> GetAlbumArtists(InternalItemsQuery query);
+ QueryResult<Tuple<BaseItem, ItemCounts>> GetAllArtists(InternalItemsQuery query);
+
+ List<string> GetGameGenreNames();
+ List<string> GetMusicGenreNames();
+ List<string> GetStudioNames();
+ List<string> GetGenreNames();
+ List<string> GetAllArtistNames();
+ }
+}
+
diff --git a/MediaBrowser.Controller/Persistence/IRepository.cs b/MediaBrowser.Controller/Persistence/IRepository.cs
new file mode 100644
index 000000000..2340ca646
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/IRepository.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace MediaBrowser.Controller.Persistence
+{
+ /// <summary>
+ /// Provides a base interface for all the repository interfaces
+ /// </summary>
+ public interface IRepository : IDisposable
+ {
+ /// <summary>
+ /// Gets the name of the repository
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+ }
+}
diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
new file mode 100644
index 000000000..5ab3f0943
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs
@@ -0,0 +1,50 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.Threading;
+
+namespace MediaBrowser.Controller.Persistence
+{
+ /// <summary>
+ /// Provides an interface to implement a UserData repository
+ /// </summary>
+ public interface IUserDataRepository : IRepository
+ {
+ /// <summary>
+ /// Saves the user data.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="key">The key.</param>
+ /// <param name="userData">The user data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the user data.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="key">The key.</param>
+ /// <returns>Task{UserItemData}.</returns>
+ UserItemData GetUserData(long userId, string key);
+
+ UserItemData GetUserData(long userId, List<string> keys);
+
+ /// <summary>
+ /// Return all user data associated with the given user
+ /// </summary>
+ /// <param name="userId"></param>
+ /// <returns></returns>
+ List<UserItemData> GetAllUserData(long userId);
+
+ /// <summary>
+ /// Save all user data associated with the given user
+ /// </summary>
+ /// <param name="userId"></param>
+ /// <param name="userData"></param>
+ /// <param name="cancellationToken"></param>
+ /// <returns></returns>
+ void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken);
+
+ }
+}
diff --git a/MediaBrowser.Controller/Persistence/IUserRepository.cs b/MediaBrowser.Controller/Persistence/IUserRepository.cs
new file mode 100644
index 000000000..2817c4255
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/IUserRepository.cs
@@ -0,0 +1,29 @@
+using MediaBrowser.Controller.Entities;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace MediaBrowser.Controller.Persistence
+{
+ /// <summary>
+ /// Provides an interface to implement a User repository
+ /// </summary>
+ public interface IUserRepository : IRepository
+ {
+ /// <summary>
+ /// Deletes the user.
+ /// </summary>
+ /// <param name="user">The user.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ void DeleteUser(User user);
+
+ /// <summary>
+ /// Retrieves all users.
+ /// </summary>
+ /// <returns>IEnumerable{User}.</returns>
+ List<User> RetrieveAllUsers();
+
+ void CreateUser(User user);
+ void UpdateUser(User user);
+ }
+}
diff --git a/MediaBrowser.Controller/Persistence/MediaStreamQuery.cs b/MediaBrowser.Controller/Persistence/MediaStreamQuery.cs
new file mode 100644
index 000000000..10985f57d
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/MediaStreamQuery.cs
@@ -0,0 +1,26 @@
+using MediaBrowser.Model.Entities;
+using System;
+
+namespace MediaBrowser.Controller.Persistence
+{
+ public class MediaStreamQuery
+ {
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ public MediaStreamType? Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index.
+ /// </summary>
+ /// <value>The index.</value>
+ public int? Index { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item identifier.
+ /// </summary>
+ /// <value>The item identifier.</value>
+ public Guid ItemId { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
new file mode 100644
index 000000000..5e790111d
--- /dev/null
+++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
@@ -0,0 +1,59 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Playlists;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using System;
+
+
+namespace MediaBrowser.Controller.Playlists
+{
+ public interface IPlaylistManager
+ {
+ /// <summary>
+ /// Gets the playlists.
+ /// </summary>
+ /// <param name="userId">The user identifier.</param>
+ /// <returns>IEnumerable&lt;Playlist&gt;.</returns>
+ IEnumerable<Playlist> GetPlaylists(Guid userId);
+
+ /// <summary>
+ /// Creates the playlist.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <returns>Task&lt;Playlist&gt;.</returns>
+ Task<PlaylistCreationResult> CreatePlaylist(PlaylistCreationRequest options);
+
+ /// <summary>
+ /// Adds to playlist.
+ /// </summary>
+ /// <param name="playlistId">The playlist identifier.</param>
+ /// <param name="itemIds">The item ids.</param>
+ /// <param name="userId">The user identifier.</param>
+ /// <returns>Task.</returns>
+ void AddToPlaylist(string playlistId, IEnumerable<Guid> itemIds, Guid userId);
+
+ /// <summary>
+ /// Removes from playlist.
+ /// </summary>
+ /// <param name="playlistId">The playlist identifier.</param>
+ /// <param name="entryIds">The entry ids.</param>
+ /// <returns>Task.</returns>
+ void RemoveFromPlaylist(string playlistId, IEnumerable<string> entryIds);
+
+ /// <summary>
+ /// Gets the playlists folder.
+ /// </summary>
+ /// <param name="userId">The user identifier.</param>
+ /// <returns>Folder.</returns>
+ Folder GetPlaylistsFolder(Guid userId);
+
+ /// <summary>
+ /// Moves the item.
+ /// </summary>
+ /// <param name="playlistId">The playlist identifier.</param>
+ /// <param name="entryId">The entry identifier.</param>
+ /// <param name="newIndex">The new index.</param>
+ /// <returns>Task.</returns>
+ void MoveItem(string playlistId, string entryId, int newIndex);
+ }
+}
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
new file mode 100644
index 000000000..78614340a
--- /dev/null
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -0,0 +1,315 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.Serialization;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Providers;
+using System.Threading;
+
+namespace MediaBrowser.Controller.Playlists
+{
+ public class Playlist : Folder, IHasShares
+ {
+ public static string[] SupportedExtensions = new string[] {
+
+ ".m3u",
+ ".m3u8",
+ ".pls",
+ ".wpl",
+ ".zpl"
+ };
+
+ public Guid OwnerUserId { get; set; }
+
+ public Share[] Shares { get; set; }
+
+ public Playlist()
+ {
+ Shares = new Share[] { };
+ }
+
+ [IgnoreDataMember]
+ public bool IsFile
+ {
+ get
+ {
+ return IsPlaylistFile(Path);
+ }
+ }
+
+ public static bool IsPlaylistFile(string path)
+ {
+ return System.IO.Path.HasExtension(path);
+ }
+
+ [IgnoreDataMember]
+ public override string ContainingFolderPath
+ {
+ get
+ {
+ var path = Path;
+
+ if (IsPlaylistFile(path))
+ {
+ return FileSystem.GetDirectoryName(path);
+ }
+
+ return path;
+ }
+ }
+
+ [IgnoreDataMember]
+ protected override bool FilterLinkedChildrenPerUser
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsInheritedParentImages
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsPlayedStatus
+ {
+ get
+ {
+ return string.Equals(MediaType, "Video", StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool AlwaysScanInternalMetadataPath
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ [IgnoreDataMember]
+ public override bool SupportsCumulativeRunTimeTicks
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ public override double GetDefaultPrimaryImageAspectRatio()
+ {
+ return 1;
+ }
+
+ public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
+ {
+ return true;
+ }
+
+ public override bool IsSaveLocalMetadataEnabled()
+ {
+ return true;
+ }
+
+ protected override List<BaseItem> LoadChildren()
+ {
+ // Save a trip to the database
+ return new List<BaseItem>();
+ }
+
+ protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
+ {
+ return Task.FromResult(true);
+ }
+
+ public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
+ {
+ return GetPlayableItems(user, query);
+ }
+
+ protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
+ {
+ return new List<BaseItem>();
+ }
+
+ public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
+ {
+ return GetPlayableItems(user, query);
+ }
+
+ public IEnumerable<Tuple<LinkedChild, BaseItem>> GetManageableItems()
+ {
+ return GetLinkedChildrenInfos();
+ }
+
+ private List<BaseItem> GetPlayableItems(User user, InternalItemsQuery query)
+ {
+ if (query == null)
+ {
+ query = new InternalItemsQuery(user);
+ }
+
+ query.IsFolder = false;
+
+ return base.GetChildren(user, true, query);
+ }
+
+ public static List<BaseItem> GetPlaylistItems(string playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
+ {
+ if (user != null)
+ {
+ inputItems = inputItems.Where(i => i.IsVisible(user));
+ }
+
+ var list = new List<BaseItem>();
+
+ foreach (var item in inputItems)
+ {
+ var playlistItems = GetPlaylistItems(item, user, playlistMediaType, options);
+ list.AddRange(playlistItems);
+ }
+
+ return list;
+ }
+
+ private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, string mediaType, DtoOptions options)
+ {
+ var musicGenre = item as MusicGenre;
+ if (musicGenre != null)
+ {
+ return LibraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ Recursive = true,
+ IncludeItemTypes = new[] { typeof(Audio).Name },
+ GenreIds = new[] { musicGenre.Id },
+ OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
+ DtoOptions = options
+ });
+ }
+
+ var musicArtist = item as MusicArtist;
+ if (musicArtist != null)
+ {
+ return LibraryManager.GetItemList(new InternalItemsQuery(user)
+ {
+ Recursive = true,
+ IncludeItemTypes = new[] { typeof(Audio).Name },
+ ArtistIds = new[] { musicArtist.Id },
+ OrderBy = new[] { ItemSortBy.AlbumArtist, ItemSortBy.Album, ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
+ DtoOptions = options
+ });
+ }
+
+ var folder = item as Folder;
+ if (folder != null)
+ {
+ var query = new InternalItemsQuery(user)
+ {
+ Recursive = true,
+ IsFolder = false,
+ OrderBy = new[] { ItemSortBy.SortName }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Ascending)).ToArray(),
+ MediaTypes = new[] { mediaType },
+ EnableTotalRecordCount = false,
+ DtoOptions = options
+ };
+
+ return folder.GetItemList(query);
+ }
+
+ return new[] { item };
+ }
+
+ [IgnoreDataMember]
+ public override bool IsPreSorted
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ public string PlaylistMediaType { get; set; }
+
+ [IgnoreDataMember]
+ public override string MediaType
+ {
+ get
+ {
+ return PlaylistMediaType;
+ }
+ }
+
+ public void SetMediaType(string value)
+ {
+ PlaylistMediaType = value;
+ }
+
+ [IgnoreDataMember]
+ private bool IsSharedItem
+ {
+ get
+ {
+ var path = Path;
+
+ if (string.IsNullOrEmpty(path))
+ {
+ return false;
+ }
+
+ return FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, path);
+ }
+ }
+
+ public override bool IsVisible(User user)
+ {
+ if (!IsSharedItem)
+ {
+ return base.IsVisible(user);
+ }
+
+ if (user.Id == OwnerUserId)
+ {
+ return true;
+ }
+
+ var shares = Shares;
+ if (shares.Length == 0)
+ {
+ return base.IsVisible(user);
+ }
+
+ var userId = user.Id.ToString("N");
+ foreach (var share in shares)
+ {
+ if (string.Equals(share.UserId, userId, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public override bool IsVisibleStandalone(User user)
+ {
+ if (!IsSharedItem)
+ {
+ return base.IsVisibleStandalone(user);
+ }
+
+ return IsVisible(user);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs b/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs
new file mode 100644
index 000000000..d294107d7
--- /dev/null
+++ b/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs
@@ -0,0 +1,20 @@
+using System.IO;
+using System.Reflection;
+
+namespace MediaBrowser.Controller.Plugins
+{
+ public interface ILocalizablePlugin
+ {
+ Stream GetDictionary(string culture);
+ }
+
+ public static class LocalizablePluginHelper
+ {
+ public static Stream GetDictionary(Assembly assembly, string manifestPrefix, string culture)
+ {
+ // Find all dictionaries using GetManifestResourceNames, start start with the prefix
+ // Return the one for the culture if exists, otherwise return the default
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs b/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs
new file mode 100644
index 000000000..5feaf798c
--- /dev/null
+++ b/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs
@@ -0,0 +1,50 @@
+using MediaBrowser.Common.Plugins;
+using System.IO;
+
+namespace MediaBrowser.Controller.Plugins
+{
+ /// <summary>
+ /// Interface IConfigurationPage
+ /// </summary>
+ public interface IPluginConfigurationPage
+ {
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ /// <summary>
+ /// Gets the type of the configuration page.
+ /// </summary>
+ /// <value>The type of the configuration page.</value>
+ ConfigurationPageType ConfigurationPageType { get; }
+
+ /// <summary>
+ /// Gets the plugin.
+ /// </summary>
+ /// <value>The plugin.</value>
+ IPlugin Plugin { get; }
+
+ /// <summary>
+ /// Gets the HTML stream.
+ /// </summary>
+ /// <returns>Stream.</returns>
+ Stream GetHtmlStream();
+ }
+
+ /// <summary>
+ /// Enum ConfigurationPageType
+ /// </summary>
+ public enum ConfigurationPageType
+ {
+ /// <summary>
+ /// The plugin configuration
+ /// </summary>
+ PluginConfiguration,
+ /// <summary>
+ /// The none
+ /// </summary>
+ None
+ }
+}
diff --git a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs
new file mode 100644
index 000000000..9ad829e45
--- /dev/null
+++ b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace MediaBrowser.Controller.Plugins
+{
+ /// <summary>
+ /// Interface IServerEntryPoint
+ /// </summary>
+ public interface IServerEntryPoint : IDisposable
+ {
+ /// <summary>
+ /// Runs this instance.
+ /// </summary>
+ void Run();
+ }
+
+ public interface IRunBeforeStartup
+ {
+
+ }
+}
diff --git a/MediaBrowser.Controller/Properties/AssemblyInfo.cs b/MediaBrowser.Controller/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..844b93f37
--- /dev/null
+++ b/MediaBrowser.Controller/Properties/AssemblyInfo.cs
@@ -0,0 +1,27 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("MediaBrowser.Controller")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.Controller")]
+[assembly: AssemblyCopyright("Copyright © 2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+// \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/AlbumInfo.cs b/MediaBrowser.Controller/Providers/AlbumInfo.cs
new file mode 100644
index 000000000..74feb4ea2
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/AlbumInfo.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public class AlbumInfo : ItemLookupInfo
+ {
+ /// <summary>
+ /// Gets or sets the album artist.
+ /// </summary>
+ /// <value>The album artist.</value>
+ public string[] AlbumArtists { get; set; }
+
+ /// <summary>
+ /// Gets or sets the artist provider ids.
+ /// </summary>
+ /// <value>The artist provider ids.</value>
+ public Dictionary<string, string> ArtistProviderIds { get; set; }
+ public List<SongInfo> SongInfos { get; set; }
+
+ public AlbumInfo()
+ {
+ ArtistProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ SongInfos = new List<SongInfo>();
+ AlbumArtists = EmptyStringArray;
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/ArtistInfo.cs b/MediaBrowser.Controller/Providers/ArtistInfo.cs
new file mode 100644
index 000000000..8a4abd5c6
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/ArtistInfo.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public class ArtistInfo : ItemLookupInfo
+ {
+ public List<SongInfo> SongInfos { get; set; }
+
+ public ArtistInfo()
+ {
+ SongInfos = new List<SongInfo>();
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/BookInfo.cs b/MediaBrowser.Controller/Providers/BookInfo.cs
new file mode 100644
index 000000000..52519bcb0
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/BookInfo.cs
@@ -0,0 +1,7 @@
+namespace MediaBrowser.Controller.Providers
+{
+ public class BookInfo : ItemLookupInfo
+ {
+ public string SeriesName { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/BoxSetInfo.cs b/MediaBrowser.Controller/Providers/BoxSetInfo.cs
new file mode 100644
index 000000000..f604231de
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/BoxSetInfo.cs
@@ -0,0 +1,7 @@
+namespace MediaBrowser.Controller.Providers
+{
+ public class BoxSetInfo : ItemLookupInfo
+ {
+
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs
new file mode 100644
index 000000000..65192671e
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/DirectoryService.cs
@@ -0,0 +1,106 @@
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public class DirectoryService : IDirectoryService
+ {
+ private readonly ILogger _logger;
+ private readonly IFileSystem _fileSystem;
+
+ private readonly Dictionary<string, FileSystemMetadata[]> _cache = new Dictionary<string, FileSystemMetadata[]>(StringComparer.OrdinalIgnoreCase);
+
+ private readonly Dictionary<string, FileSystemMetadata> _fileCache = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
+
+ private readonly Dictionary<string, List<string>> _filePathCache = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
+
+ public DirectoryService(ILogger logger, IFileSystem fileSystem)
+ {
+ _logger = logger;
+ _fileSystem = fileSystem;
+ }
+
+ public DirectoryService(IFileSystem fileSystem)
+ : this(new NullLogger(), fileSystem)
+ {
+ }
+
+ public FileSystemMetadata[] GetFileSystemEntries(string path)
+ {
+ FileSystemMetadata[] entries;
+
+ if (!_cache.TryGetValue(path, out entries))
+ {
+ //_logger.Debug("Getting files for " + path);
+
+ entries = _fileSystem.GetFileSystemEntries(path).ToArray();
+
+ //_cache.TryAdd(path, entries);
+ _cache[path] = entries;
+ }
+
+ return entries;
+ }
+
+ public List<FileSystemMetadata> GetFiles(string path)
+ {
+ var list = new List<FileSystemMetadata>();
+ var items = GetFileSystemEntries(path);
+ foreach (var item in items)
+ {
+ if (!item.IsDirectory)
+ {
+ list.Add(item);
+ }
+ }
+ return list;
+ }
+
+ public FileSystemMetadata GetFile(string path)
+ {
+ FileSystemMetadata file;
+ if (!_fileCache.TryGetValue(path, out file))
+ {
+ file = _fileSystem.GetFileInfo(path);
+
+ if (file != null && file.Exists)
+ {
+ //_fileCache.TryAdd(path, file);
+ _fileCache[path] = file;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ return file;
+ //return _fileSystem.GetFileInfo(path);
+ }
+
+ public List<string> GetFilePaths(string path)
+ {
+ return GetFilePaths(path, false);
+ }
+
+ public List<string> GetFilePaths(string path, bool clearCache)
+ {
+ List<string> result;
+ if (clearCache || !_filePathCache.TryGetValue(path, out result))
+ {
+ result = _fileSystem.GetFilePaths(path).ToList();
+
+ _filePathCache[path] = result;
+ }
+
+ return result;
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/DynamicImageInfo.cs b/MediaBrowser.Controller/Providers/DynamicImageInfo.cs
new file mode 100644
index 000000000..14b4c6afb
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/DynamicImageInfo.cs
@@ -0,0 +1,10 @@
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public class DynamicImageInfo
+ {
+ public string ImageId { get; set; }
+ public ImageType Type { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs
new file mode 100644
index 000000000..d19a28a24
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs
@@ -0,0 +1,36 @@
+using System;
+using System.IO;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.MediaInfo;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public class DynamicImageResponse
+ {
+ public string Path { get; set; }
+ public MediaProtocol Protocol { get; set; }
+ public Stream Stream { get; set; }
+ public ImageFormat Format { get; set; }
+ public bool HasImage { get; set; }
+
+ public void SetFormatFromMimeType(string mimeType)
+ {
+ if (mimeType.EndsWith("gif", StringComparison.OrdinalIgnoreCase))
+ {
+ Format = ImageFormat.Gif;
+ }
+ else if (mimeType.EndsWith("bmp", StringComparison.OrdinalIgnoreCase))
+ {
+ Format = ImageFormat.Bmp;
+ }
+ else if (mimeType.EndsWith("png", StringComparison.OrdinalIgnoreCase))
+ {
+ Format = ImageFormat.Png;
+ }
+ else
+ {
+ Format = ImageFormat.Jpg;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs
new file mode 100644
index 000000000..4eafe0e0e
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public class EpisodeInfo : ItemLookupInfo
+ {
+ public Dictionary<string, string> SeriesProviderIds { get; set; }
+
+ public int? IndexNumberEnd { get; set; }
+
+ public bool IsMissingEpisode { get; set; }
+ public string SeriesDisplayOrder { get; set; }
+
+ public EpisodeInfo()
+ {
+ SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/ExtraInfo.cs b/MediaBrowser.Controller/Providers/ExtraInfo.cs
new file mode 100644
index 000000000..1fbe6e93a
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/ExtraInfo.cs
@@ -0,0 +1,15 @@
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public class ExtraInfo
+ {
+ public string Path { get; set; }
+
+ public LocationType LocationType { get; set; }
+
+ public bool IsDownloadable { get; set; }
+
+ public ExtraType ExtraType { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/ExtraSource.cs b/MediaBrowser.Controller/Providers/ExtraSource.cs
new file mode 100644
index 000000000..901af60f8
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/ExtraSource.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Controller.Providers
+{
+ public enum ExtraSource
+ {
+ Local = 1,
+ Metadata = 2,
+ Remote = 3
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/GameInfo.cs b/MediaBrowser.Controller/Providers/GameInfo.cs
new file mode 100644
index 000000000..771cf6cec
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/GameInfo.cs
@@ -0,0 +1,11 @@
+namespace MediaBrowser.Controller.Providers
+{
+ public class GameInfo : ItemLookupInfo
+ {
+ /// <summary>
+ /// Gets or sets the game system.
+ /// </summary>
+ /// <value>The game system.</value>
+ public string GameSystem { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/GameSystemInfo.cs b/MediaBrowser.Controller/Providers/GameSystemInfo.cs
new file mode 100644
index 000000000..efe2635cd
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/GameSystemInfo.cs
@@ -0,0 +1,11 @@
+namespace MediaBrowser.Controller.Providers
+{
+ public class GameSystemInfo : ItemLookupInfo
+ {
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ public string Path { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs b/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs
new file mode 100644
index 000000000..af1838d74
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs
@@ -0,0 +1,24 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public interface ICustomMetadataProvider : IMetadataProvider
+ {
+ }
+
+ public interface ICustomMetadataProvider<TItemType> : IMetadataProvider<TItemType>, ICustomMetadataProvider
+ where TItemType : BaseItem
+ {
+ /// <summary>
+ /// Fetches the asynchronous.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="options">The options.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{ItemUpdateType}.</returns>
+ Task<ItemUpdateType> FetchAsync(TItemType item, MetadataRefreshOptions options, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs
new file mode 100644
index 000000000..0b4574f6e
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using MediaBrowser.Model.IO;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public interface IDirectoryService
+ {
+ FileSystemMetadata[] GetFileSystemEntries(string path);
+ List<FileSystemMetadata> GetFiles(string path);
+ FileSystemMetadata GetFile(string path);
+
+ List<string> GetFilePaths(string path);
+ List<string> GetFilePaths(string path, bool clearCache);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/IDynamicImageProvider.cs b/MediaBrowser.Controller/Providers/IDynamicImageProvider.cs
new file mode 100644
index 000000000..3e9127fc4
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IDynamicImageProvider.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public interface IDynamicImageProvider : IImageProvider
+ {
+ /// <summary>
+ /// Gets the supported images.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>IEnumerable{ImageType}.</returns>
+ IEnumerable<ImageType> GetSupportedImages(BaseItem item);
+
+ /// <summary>
+ /// Gets the image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{DynamicImageResponse}.</returns>
+ Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/IExternalId.cs b/MediaBrowser.Controller/Providers/IExternalId.cs
new file mode 100644
index 000000000..946f28199
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IExternalId.cs
@@ -0,0 +1,15 @@
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public interface IExternalId
+ {
+ string Name { get; }
+
+ string Key { get; }
+
+ string UrlFormatString { get; }
+
+ bool Supports(IHasProviderIds item);
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/IExtrasProvider.cs b/MediaBrowser.Controller/Providers/IExtrasProvider.cs
new file mode 100644
index 000000000..58775ccac
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IExtrasProvider.cs
@@ -0,0 +1,20 @@
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public interface IExtrasProvider
+ {
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+ bool Supports(BaseItem item);
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/IForcedProvider.cs b/MediaBrowser.Controller/Providers/IForcedProvider.cs
new file mode 100644
index 000000000..9e35b00ad
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IForcedProvider.cs
@@ -0,0 +1,10 @@
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// This is a marker interface that will cause a provider to run even if IsLocked=true
+ /// </summary>
+ public interface IForcedProvider
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/IHasItemChangeMonitor.cs b/MediaBrowser.Controller/Providers/IHasItemChangeMonitor.cs
new file mode 100644
index 000000000..7cc05bf67
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IHasItemChangeMonitor.cs
@@ -0,0 +1,15 @@
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public interface IHasItemChangeMonitor
+ {
+ /// <summary>
+ /// Determines whether the specified item has changed.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="directoryService">The directory service.</param>
+ /// <returns><c>true</c> if the specified item has changed; otherwise, <c>false</c>.</returns>
+ bool HasChanged(BaseItem item, IDirectoryService directoryService);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/IHasLookupInfo.cs b/MediaBrowser.Controller/Providers/IHasLookupInfo.cs
new file mode 100644
index 000000000..afce49852
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IHasLookupInfo.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Controller.Providers
+{
+ public interface IHasLookupInfo<out TLookupInfoType>
+ where TLookupInfoType : ItemLookupInfo, new()
+ {
+ TLookupInfoType GetLookupInfo();
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/IHasOrder.cs b/MediaBrowser.Controller/Providers/IHasOrder.cs
new file mode 100644
index 000000000..cb5298dd3
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IHasOrder.cs
@@ -0,0 +1,7 @@
+namespace MediaBrowser.Controller.Providers
+{
+ public interface IHasOrder
+ {
+ int Order { get; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/IImageEnhancer.cs b/MediaBrowser.Controller/Providers/IImageEnhancer.cs
new file mode 100644
index 000000000..c8ea25335
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IImageEnhancer.cs
@@ -0,0 +1,61 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Entities;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public interface IImageEnhancer
+ {
+ /// <summary>
+ /// Return true only if the given image for the given item will be enhanced by this enhancer.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <returns><c>true</c> if this enhancer will enhance the supplied image for the supplied item, <c>false</c> otherwise</returns>
+ bool Supports(BaseItem item, ImageType imageType);
+
+ /// <summary>
+ /// Gets the priority or order in which this enhancer should be run.
+ /// </summary>
+ /// <value>The priority.</value>
+ MetadataProviderPriority Priority { get; }
+
+ /// <summary>
+ /// Return a key incorporating all configuration information related to this item
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <returns>Cache key relating to the current state of this item and configuration</returns>
+ string GetConfigurationCacheKey(BaseItem item, ImageType imageType);
+
+ /// <summary>
+ /// Gets the size of the enhanced image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <param name="originalImageSize">Size of the original image.</param>
+ /// <returns>ImageSize.</returns>
+ ImageSize GetEnhancedImageSize(BaseItem item, ImageType imageType, int imageIndex, ImageSize originalImageSize);
+
+ EnhancedImageInfo GetEnhancedImageInfo(BaseItem item, string inputFile, ImageType imageType, int imageIndex);
+
+ /// <summary>
+ /// Enhances the image async.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="inputFile">The input file.</param>
+ /// <param name="outputFile">The output file.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <returns>Task{Image}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ Task EnhanceImageAsync(BaseItem item, string inputFile, string outputFile, ImageType imageType, int imageIndex);
+ }
+
+ public class EnhancedImageInfo
+ {
+ public bool RequiresTransparency { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/IImageProvider.cs b/MediaBrowser.Controller/Providers/IImageProvider.cs
new file mode 100644
index 000000000..ac857b2aa
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IImageProvider.cs
@@ -0,0 +1,23 @@
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Interface IImageProvider
+ /// </summary>
+ public interface IImageProvider
+ {
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ bool Supports(BaseItem item);
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs b/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs
new file mode 100644
index 000000000..96e154dad
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public interface ILocalImageFileProvider : ILocalImageProvider
+ {
+ List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs
new file mode 100644
index 000000000..1027a4cb2
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// This is just a marker interface
+ /// </summary>
+ public interface ILocalImageProvider : IImageProvider
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs b/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs
new file mode 100644
index 000000000..fc4cca19c
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs
@@ -0,0 +1,25 @@
+using MediaBrowser.Controller.Entities;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public interface ILocalMetadataProvider : IMetadataProvider
+ {
+ }
+
+ public interface ILocalMetadataProvider<TItemType> : IMetadataProvider<TItemType>, ILocalMetadataProvider
+ where TItemType : BaseItem
+ {
+ /// <summary>
+ /// Gets the metadata.
+ /// </summary>
+ /// <param name="info">The information.</param>
+ /// <param name="directoryService">The directory service.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{MetadataResult{`0}}.</returns>
+ Task<MetadataResult<TItemType>> GetMetadata(ItemInfo info,
+ IDirectoryService directoryService,
+ CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/IMetadataProvider.cs b/MediaBrowser.Controller/Providers/IMetadataProvider.cs
new file mode 100644
index 000000000..7da590193
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IMetadataProvider.cs
@@ -0,0 +1,21 @@
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Marker interface
+ /// </summary>
+ public interface IMetadataProvider
+ {
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+ }
+
+ public interface IMetadataProvider<TItemType> : IMetadataProvider
+ where TItemType : BaseItem
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/IMetadataService.cs b/MediaBrowser.Controller/Providers/IMetadataService.cs
new file mode 100644
index 000000000..1c9c4b71a
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IMetadataService.cs
@@ -0,0 +1,34 @@
+using System;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public interface IMetadataService
+ {
+ /// <summary>
+ /// Determines whether this instance can refresh the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if this instance can refresh the specified item; otherwise, <c>false</c>.</returns>
+ bool CanRefresh(BaseItem item);
+ bool CanRefreshPrimary(Type type);
+
+ /// <summary>
+ /// Refreshes the metadata.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="refreshOptions">The options.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the order.
+ /// </summary>
+ /// <value>The order.</value>
+ int Order { get; }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs b/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs
new file mode 100644
index 000000000..608674905
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs
@@ -0,0 +1,7 @@
+namespace MediaBrowser.Controller.Providers
+{
+ public interface IPreRefreshProvider : ICustomMetadataProvider
+ {
+
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
new file mode 100644
index 000000000..f1930ee2f
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -0,0 +1,178 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Events;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Interface IProviderManager
+ /// </summary>
+ public interface IProviderManager
+ {
+ /// <summary>
+ /// Queues the refresh.
+ /// </summary>
+ void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority);
+
+ /// <summary>
+ /// Refreshes the full item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="options">The options.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task RefreshFullItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Refreshes the metadata.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="options">The options.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task<ItemUpdateType> RefreshSingleItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Saves the image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="url">The URL.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SaveImage(BaseItem item, string url, ImageType type, int? imageIndex, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Saves the image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="source">The source.</param>
+ /// <param name="mimeType">Type of the MIME.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Saves the image.
+ /// </summary>
+ /// <returns>Task.</returns>
+ Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Adds the metadata providers.
+ /// </summary>
+ void AddParts(IEnumerable<IImageProvider> imageProviders, IEnumerable<IMetadataService> metadataServices, IEnumerable<IMetadataProvider> metadataProviders,
+ IEnumerable<IMetadataSaver> savers,
+ IEnumerable<IExternalId> externalIds);
+
+ /// <summary>
+ /// Gets the available remote images.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="query">The query.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
+ Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, RemoteImageQuery query, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the image providers.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>IEnumerable{ImageProviderInfo}.</returns>
+ IEnumerable<ImageProviderInfo> GetRemoteImageProviderInfo(BaseItem item);
+
+ /// <summary>
+ /// Gets all metadata plugins.
+ /// </summary>
+ /// <returns>IEnumerable{MetadataPlugin}.</returns>
+ MetadataPluginSummary[] GetAllMetadataPlugins();
+
+ /// <summary>
+ /// Gets the external urls.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>IEnumerable{ExternalUrl}.</returns>
+ IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item);
+
+ /// <summary>
+ /// Gets the external identifier infos.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>IEnumerable{ExternalIdInfo}.</returns>
+ IEnumerable<ExternalIdInfo> GetExternalIdInfos(IHasProviderIds item);
+
+ /// <summary>
+ /// Saves the metadata.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="updateType">Type of the update.</param>
+ /// <returns>Task.</returns>
+ void SaveMetadata(BaseItem item, ItemUpdateType updateType);
+
+ /// <summary>
+ /// Saves the metadata.
+ /// </summary>
+ void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers);
+
+ /// <summary>
+ /// Gets the metadata options.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>MetadataOptions.</returns>
+ MetadataOptions GetMetadataOptions(BaseItem item);
+
+ /// <summary>
+ /// Gets the remote search results.
+ /// </summary>
+ /// <typeparam name="TItemType">The type of the t item type.</typeparam>
+ /// <typeparam name="TLookupType">The type of the t lookup type.</typeparam>
+ /// <param name="searchInfo">The search information.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{SearchResult{``1}}}.</returns>
+ Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(
+ RemoteSearchQuery<TLookupType> searchInfo,
+ CancellationToken cancellationToken)
+ where TItemType : BaseItem, new()
+ where TLookupType : ItemLookupInfo;
+
+ /// <summary>
+ /// Gets the search image.
+ /// </summary>
+ /// <param name="providerName">Name of the provider.</param>
+ /// <param name="url">The URL.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{HttpResponseInfo}.</returns>
+ Task<HttpResponseInfo> GetSearchImage(string providerName, string url, CancellationToken cancellationToken);
+
+ Dictionary<Guid, Guid> GetRefreshQueue();
+
+ void OnRefreshStart(BaseItem item);
+ void OnRefreshProgress(BaseItem item, double progress);
+ void OnRefreshComplete(BaseItem item);
+
+ double? GetRefreshProgress(Guid id);
+
+ event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted;
+ event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted;
+ event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress;
+ }
+
+ public enum RefreshPriority
+ {
+ High = 0,
+ Normal = 1,
+ Low = 2
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs
new file mode 100644
index 000000000..5db5ddbb2
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs
@@ -0,0 +1,39 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Interface IImageProvider
+ /// </summary>
+ public interface IRemoteImageProvider : IImageProvider
+ {
+ /// <summary>
+ /// Gets the supported images.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>IEnumerable{ImageType}.</returns>
+ IEnumerable<ImageType> GetSupportedImages(BaseItem item);
+
+ /// <summary>
+ /// Gets the images.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
+ Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the image response.
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{HttpResponseInfo}.</returns>
+ Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs b/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs
new file mode 100644
index 000000000..695d488ed
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs
@@ -0,0 +1,25 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Providers;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public interface IRemoteMetadataProvider : IMetadataProvider
+ {
+ }
+
+ public interface IRemoteMetadataProvider<TItemType, in TLookupInfoType> : IMetadataProvider<TItemType>, IRemoteMetadataProvider, IRemoteSearchProvider<TLookupInfoType>
+ where TItemType : BaseItem, IHasLookupInfo<TLookupInfoType>
+ where TLookupInfoType : ItemLookupInfo, new()
+ {
+ Task<MetadataResult<TItemType>> GetMetadata(TLookupInfoType info, CancellationToken cancellationToken);
+ }
+
+ public interface IRemoteSearchProvider<in TLookupInfoType> : IRemoteSearchProvider
+ where TLookupInfoType : ItemLookupInfo
+ {
+ Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TLookupInfoType searchInfo, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs b/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs
new file mode 100644
index 000000000..0077def42
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs
@@ -0,0 +1,17 @@
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public interface IRemoteSearchProvider : IMetadataProvider
+ {
+ /// <summary>
+ /// Gets the image response.
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{HttpResponseInfo}.</returns>
+ Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
new file mode 100644
index 000000000..942d25071
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public class ImageRefreshOptions
+ {
+ public MetadataRefreshMode ImageRefreshMode { get; set; }
+ public IDirectoryService DirectoryService { get; private set; }
+
+ public bool ReplaceAllImages { get; set; }
+
+ public ImageType[] ReplaceImages { get; set; }
+ public bool IsAutomated { get; set; }
+
+ public ImageRefreshOptions(IDirectoryService directoryService)
+ {
+ ImageRefreshMode = MetadataRefreshMode.Default;
+ DirectoryService = directoryService;
+
+ ReplaceImages = new ImageType[] { };
+ IsAutomated = true;
+ }
+
+ public bool IsReplacingImage(ImageType type)
+ {
+ return ImageRefreshMode == MetadataRefreshMode.FullRefresh &&
+ (ReplaceAllImages || ReplaceImages.Contains(type));
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/ItemInfo.cs b/MediaBrowser.Controller/Providers/ItemInfo.cs
new file mode 100644
index 000000000..76adfe8a2
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/ItemInfo.cs
@@ -0,0 +1,32 @@
+using System;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public class ItemInfo
+ {
+ public ItemInfo(BaseItem item)
+ {
+ Path = item.Path;
+ ContainingFolderPath = item.ContainingFolderPath;
+ IsInMixedFolder = item.IsInMixedFolder;
+
+ var video = item as Video;
+ if (video != null)
+ {
+ VideoType = video.VideoType;
+ IsPlaceHolder = video.IsPlaceHolder;
+ }
+
+ ItemType = item.GetType();
+ }
+
+ public Type ItemType { get; set; }
+ public string Path { get; set; }
+ public string ContainingFolderPath { get; set; }
+ public VideoType VideoType { get; set; }
+ public bool IsInMixedFolder { get; set; }
+ public bool IsPlaceHolder { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
new file mode 100644
index 000000000..98122e776
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs
@@ -0,0 +1,47 @@
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public class ItemLookupInfo : IHasProviderIds
+ {
+ protected static string[] EmptyStringArray = new string[] { };
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the metadata language.
+ /// </summary>
+ /// <value>The metadata language.</value>
+ public string MetadataLanguage { get; set; }
+ /// <summary>
+ /// Gets or sets the metadata country code.
+ /// </summary>
+ /// <value>The metadata country code.</value>
+ public string MetadataCountryCode { get; set; }
+ /// <summary>
+ /// Gets or sets the provider ids.
+ /// </summary>
+ /// <value>The provider ids.</value>
+ public Dictionary<string, string> ProviderIds { get; set; }
+ /// <summary>
+ /// Gets or sets the year.
+ /// </summary>
+ /// <value>The year.</value>
+ public int? Year { get; set; }
+ public int? IndexNumber { get; set; }
+ public int? ParentIndexNumber { get; set; }
+ public DateTime? PremiereDate { get; set; }
+ public bool IsAutomated { get; set; }
+
+ public ItemLookupInfo()
+ {
+ IsAutomated = true;
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/LocalImageInfo.cs b/MediaBrowser.Controller/Providers/LocalImageInfo.cs
new file mode 100644
index 000000000..5e6efe9f6
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/LocalImageInfo.cs
@@ -0,0 +1,13 @@
+
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public class LocalImageInfo
+ {
+ public FileSystemMetadata FileInfo { get; set; }
+ public ImageType Type { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs b/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs
new file mode 100644
index 000000000..d01261866
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs
@@ -0,0 +1,40 @@
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Determines when a provider should execute, relative to others
+ /// </summary>
+ public enum MetadataProviderPriority
+ {
+ // Run this provider at the beginning
+ /// <summary>
+ /// The first
+ /// </summary>
+ First = 1,
+
+ // Run this provider after all first priority providers
+ /// <summary>
+ /// The second
+ /// </summary>
+ Second = 2,
+
+ // Run this provider after all second priority providers
+ /// <summary>
+ /// The third
+ /// </summary>
+ Third = 3,
+
+ /// <summary>
+ /// The fourth
+ /// </summary>
+ Fourth = 4,
+
+ Fifth = 5,
+
+ // Run this provider last
+ /// <summary>
+ /// The last
+ /// </summary>
+ Last = 999
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs b/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs
new file mode 100644
index 000000000..56492006a
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs
@@ -0,0 +1,25 @@
+namespace MediaBrowser.Controller.Providers
+{
+ public enum MetadataRefreshMode
+ {
+ /// <summary>
+ /// The none
+ /// </summary>
+ None = 0,
+
+ /// <summary>
+ /// The validation only
+ /// </summary>
+ ValidationOnly = 1,
+
+ /// <summary>
+ /// Providers will be executed based on default rules
+ /// </summary>
+ Default = 2,
+
+ /// <summary>
+ /// All providers will be executed to search for new metadata
+ /// </summary>
+ FullRefresh = 3
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
new file mode 100644
index 000000000..3e34075a6
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public class MetadataRefreshOptions : ImageRefreshOptions
+ {
+ /// <summary>
+ /// When paired with MetadataRefreshMode=FullRefresh, all existing data will be overwritten with new data from the providers.
+ /// </summary>
+ public bool ReplaceAllMetadata { get; set; }
+
+ public MetadataRefreshMode MetadataRefreshMode { get; set; }
+ public RemoteSearchResult SearchResult { get; set; }
+
+ public string[] RefreshPaths { get; set; }
+
+ public bool ForceSave { get; set; }
+ public bool EnableRemoteContentProbe { get; set; }
+
+ public MetadataRefreshOptions(IFileSystem fileSystem)
+ : this(new DirectoryService(new NullLogger(), fileSystem))
+ {
+ }
+
+ public MetadataRefreshOptions(IDirectoryService directoryService)
+ : base(directoryService)
+ {
+ MetadataRefreshMode = MetadataRefreshMode.Default;
+ }
+
+ public MetadataRefreshOptions(MetadataRefreshOptions copy)
+ : base(copy.DirectoryService)
+ {
+ MetadataRefreshMode = copy.MetadataRefreshMode;
+ ForceSave = copy.ForceSave;
+ ReplaceAllMetadata = copy.ReplaceAllMetadata;
+ EnableRemoteContentProbe = copy.EnableRemoteContentProbe;
+
+ ImageRefreshMode = copy.ImageRefreshMode;
+ ReplaceAllImages = copy.ReplaceAllImages;
+ ReplaceImages = copy.ReplaceImages;
+ SearchResult = copy.SearchResult;
+
+ if (copy.RefreshPaths != null && copy.RefreshPaths.Length > 0)
+ {
+ if (RefreshPaths == null)
+ {
+ RefreshPaths = new string[] { };
+ }
+
+ RefreshPaths = copy.RefreshPaths.ToArray();
+ }
+ }
+
+ public bool RefreshItem(BaseItem item)
+ {
+ if (RefreshPaths != null && RefreshPaths.Length > 0)
+ {
+ return RefreshPaths.Contains(item.Path ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs
new file mode 100644
index 000000000..f35d41ca4
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MetadataResult.cs
@@ -0,0 +1,77 @@
+using MediaBrowser.Controller.Entities;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public class MetadataResult<T>
+ {
+ public List<LocalImageInfo> Images { get; set; }
+ public List<UserItemData> UserDataList { get; set; }
+
+ public MetadataResult()
+ {
+ Images = new List<LocalImageInfo>();
+ ResultLanguage = "en";
+ }
+
+ public List<PersonInfo> People { get; set; }
+
+ public bool HasMetadata { get; set; }
+ public T Item { get; set; }
+ public string ResultLanguage { get; set; }
+ public string Provider { get; set; }
+ public bool QueriedById { get; set; }
+ public void AddPerson(PersonInfo p)
+ {
+ if (People == null)
+ {
+ People = new List<PersonInfo>();
+ }
+
+ PeopleHelper.AddPerson(People, p);
+ }
+
+ /// <summary>
+ /// Not only does this clear, but initializes the list so that services can differentiate between a null list and zero people
+ /// </summary>
+ public void ResetPeople()
+ {
+ if (People == null)
+ {
+ People = new List<PersonInfo>();
+ }
+ People.Clear();
+ }
+
+ public UserItemData GetOrAddUserData(string userId)
+ {
+ if (UserDataList == null)
+ {
+ UserDataList = new List<UserItemData>();
+ }
+
+ UserItemData userData = null;
+
+ foreach (var i in UserDataList)
+ {
+ if (string.Equals(userId, i.UserId.ToString("N"), StringComparison.OrdinalIgnoreCase))
+ {
+ userData = i;
+ }
+ }
+
+ if (userData == null)
+ {
+ userData = new UserItemData()
+ {
+ UserId = new Guid(userId)
+ };
+
+ UserDataList.Add(userData);
+ }
+
+ return userData;
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/MovieInfo.cs b/MediaBrowser.Controller/Providers/MovieInfo.cs
new file mode 100644
index 000000000..198336fc0
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MovieInfo.cs
@@ -0,0 +1,7 @@
+namespace MediaBrowser.Controller.Providers
+{
+ public class MovieInfo : ItemLookupInfo
+ {
+
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/MusicVideoInfo.cs b/MediaBrowser.Controller/Providers/MusicVideoInfo.cs
new file mode 100644
index 000000000..6e12405f7
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MusicVideoInfo.cs
@@ -0,0 +1,7 @@
+namespace MediaBrowser.Controller.Providers
+{
+ public class MusicVideoInfo : ItemLookupInfo
+ {
+ public string[] Artists { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/PersonLookupInfo.cs b/MediaBrowser.Controller/Providers/PersonLookupInfo.cs
new file mode 100644
index 000000000..db4dacb0b
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/PersonLookupInfo.cs
@@ -0,0 +1,7 @@
+namespace MediaBrowser.Controller.Providers
+{
+ public class PersonLookupInfo : ItemLookupInfo
+ {
+
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs
new file mode 100644
index 000000000..77cf9e255
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs
@@ -0,0 +1,21 @@
+namespace MediaBrowser.Controller.Providers
+{
+ public class RemoteSearchQuery<T>
+ where T : ItemLookupInfo
+ {
+ public T SearchInfo { get; set; }
+
+ public string ItemId { get; set; }
+
+ /// <summary>
+ /// If set will only search within the given provider
+ /// </summary>
+ public string SearchProviderName { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [include disabled providers].
+ /// </summary>
+ /// <value><c>true</c> if [include disabled providers]; otherwise, <c>false</c>.</value>
+ public bool IncludeDisabledProviders { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/SeasonInfo.cs b/MediaBrowser.Controller/Providers/SeasonInfo.cs
new file mode 100644
index 000000000..31af268b8
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/SeasonInfo.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Providers
+{
+ public class SeasonInfo : ItemLookupInfo
+ {
+ public Dictionary<string, string> SeriesProviderIds { get; set; }
+
+ public SeasonInfo()
+ {
+ SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/SeriesInfo.cs b/MediaBrowser.Controller/Providers/SeriesInfo.cs
new file mode 100644
index 000000000..0b1361757
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/SeriesInfo.cs
@@ -0,0 +1,6 @@
+namespace MediaBrowser.Controller.Providers
+{
+ public class SeriesInfo : ItemLookupInfo
+ {
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/SongInfo.cs b/MediaBrowser.Controller/Providers/SongInfo.cs
new file mode 100644
index 000000000..e3a6f5d37
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/SongInfo.cs
@@ -0,0 +1,16 @@
+
+namespace MediaBrowser.Controller.Providers
+{
+ public class SongInfo : ItemLookupInfo
+ {
+ public string[] AlbumArtists { get; set; }
+ public string Album { get; set; }
+ public string[] Artists { get; set; }
+
+ public SongInfo()
+ {
+ Artists = EmptyStringArray;
+ AlbumArtists = EmptyStringArray;
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/TrailerInfo.cs b/MediaBrowser.Controller/Providers/TrailerInfo.cs
new file mode 100644
index 000000000..ea8377adf
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/TrailerInfo.cs
@@ -0,0 +1,6 @@
+namespace MediaBrowser.Controller.Providers
+{
+ public class TrailerInfo : ItemLookupInfo
+ {
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Providers/VideoContentType.cs b/MediaBrowser.Controller/Providers/VideoContentType.cs
new file mode 100644
index 000000000..903c77612
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/VideoContentType.cs
@@ -0,0 +1,19 @@
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Enum VideoContentType
+ /// </summary>
+ public enum VideoContentType
+ {
+ /// <summary>
+ /// The episode
+ /// </summary>
+ Episode = 0,
+
+ /// <summary>
+ /// The movie
+ /// </summary>
+ Movie = 1
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs b/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs
new file mode 100644
index 000000000..fc5157d5f
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/BaseItemResolver.cs
@@ -0,0 +1,61 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ /// <summary>
+ /// Class ItemResolver
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public abstract class ItemResolver<T> : IItemResolver
+ where T : BaseItem, new()
+ {
+ /// <summary>
+ /// Resolves the specified args.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>`0.</returns>
+ protected virtual T Resolve(ItemResolveArgs args)
+ {
+ return null;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public virtual ResolverPriority Priority
+ {
+ get
+ {
+ return ResolverPriority.First;
+ }
+ }
+
+ /// <summary>
+ /// Sets initial values on the newly resolved item
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="args">The args.</param>
+ protected virtual void SetInitialItemValues(T item, ItemResolveArgs args)
+ {
+ }
+
+ /// <summary>
+ /// Resolves the path.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>BaseItem.</returns>
+ BaseItem IItemResolver.ResolvePath(ItemResolveArgs args)
+ {
+ var item = Resolve(args);
+
+ if (item != null)
+ {
+ SetInitialItemValues(item, args);
+ }
+
+ return item;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
new file mode 100644
index 000000000..3af5d5f7f
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
@@ -0,0 +1,48 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using System.Collections.Generic;
+
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ /// <summary>
+ /// Interface IItemResolver
+ /// </summary>
+ public interface IItemResolver
+ {
+ /// <summary>
+ /// Resolves the path.
+ /// </summary>
+ /// <param name="args">The args.</param>
+ /// <returns>BaseItem.</returns>
+ BaseItem ResolvePath(ItemResolveArgs args);
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ ResolverPriority Priority { get; }
+ }
+
+ public interface IMultiItemResolver
+ {
+ MultiItemResolverResult ResolveMultiple(Folder parent,
+ List<FileSystemMetadata> files,
+ string collectionType,
+ IDirectoryService directoryService);
+ }
+
+ public class MultiItemResolverResult
+ {
+ public List<BaseItem> Items { get; set; }
+ public List<FileSystemMetadata> ExtraFiles { get; set; }
+
+ public MultiItemResolverResult()
+ {
+ Items = new List<BaseItem>();
+ ExtraFiles = new List<FileSystemMetadata>();
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs
new file mode 100644
index 000000000..25537193a
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs
@@ -0,0 +1,15 @@
+
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.IO;
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ /// <summary>
+ /// Provides a base "rule" that anyone can use to have paths ignored by the resolver
+ /// </summary>
+ public interface IResolverIgnoreRule
+ {
+ bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent);
+ }
+}
diff --git a/MediaBrowser.Controller/Resolvers/ResolverPriority.cs b/MediaBrowser.Controller/Resolvers/ResolverPriority.cs
new file mode 100644
index 000000000..df5edeb05
--- /dev/null
+++ b/MediaBrowser.Controller/Resolvers/ResolverPriority.cs
@@ -0,0 +1,27 @@
+
+namespace MediaBrowser.Controller.Resolvers
+{
+ /// <summary>
+ /// Enum ResolverPriority
+ /// </summary>
+ public enum ResolverPriority
+ {
+ /// <summary>
+ /// The first
+ /// </summary>
+ First = 1,
+ /// <summary>
+ /// The second
+ /// </summary>
+ Second = 2,
+ /// <summary>
+ /// The third
+ /// </summary>
+ Third = 3,
+ Fourth = 4,
+ /// <summary>
+ /// The last
+ /// </summary>
+ Last = 5
+ }
+}
diff --git a/MediaBrowser.Controller/Security/AuthenticationInfo.cs b/MediaBrowser.Controller/Security/AuthenticationInfo.cs
new file mode 100644
index 000000000..c75bf89e4
--- /dev/null
+++ b/MediaBrowser.Controller/Security/AuthenticationInfo.cs
@@ -0,0 +1,70 @@
+using System;
+
+namespace MediaBrowser.Controller.Security
+{
+ public class AuthenticationInfo
+ {
+ /// <summary>
+ /// Gets or sets the identifier.
+ /// </summary>
+ /// <value>The identifier.</value>
+ public long Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the access token.
+ /// </summary>
+ /// <value>The access token.</value>
+ public string AccessToken { get; set; }
+
+ /// <summary>
+ /// Gets or sets the device identifier.
+ /// </summary>
+ /// <value>The device identifier.</value>
+ public string DeviceId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the application.
+ /// </summary>
+ /// <value>The name of the application.</value>
+ public string AppName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the application version.
+ /// </summary>
+ /// <value>The application version.</value>
+ public string AppVersion { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the device.
+ /// </summary>
+ /// <value>The name of the device.</value>
+ public string DeviceName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user identifier.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is active.
+ /// </summary>
+ /// <value><c>true</c> if this instance is active; otherwise, <c>false</c>.</value>
+ public bool IsActive { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date created.
+ /// </summary>
+ /// <value>The date created.</value>
+ public DateTime DateCreated { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date revoked.
+ /// </summary>
+ /// <value>The date revoked.</value>
+ public DateTime? DateRevoked { get; set; }
+
+ public DateTime DateLastActivity { get; set; }
+ public string UserName { get; set;}
+ }
+}
diff --git a/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs b/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs
new file mode 100644
index 000000000..125534c46
--- /dev/null
+++ b/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs
@@ -0,0 +1,49 @@
+using System;
+
+namespace MediaBrowser.Controller.Security
+{
+ public class AuthenticationInfoQuery
+ {
+ /// <summary>
+ /// Gets or sets the device identifier.
+ /// </summary>
+ /// <value>The device identifier.</value>
+ public string DeviceId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user identifier.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the access token.
+ /// </summary>
+ /// <value>The access token.</value>
+ public string AccessToken { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is active.
+ /// </summary>
+ /// <value><c>null</c> if [is active] contains no value, <c>true</c> if [is active]; otherwise, <c>false</c>.</value>
+ public bool? IsActive { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has user.
+ /// </summary>
+ /// <value><c>null</c> if [has user] contains no value, <c>true</c> if [has user]; otherwise, <c>false</c>.</value>
+ public bool? HasUser { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start index.
+ /// </summary>
+ /// <value>The start index.</value>
+ public int? StartIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the limit.
+ /// </summary>
+ /// <value>The limit.</value>
+ public int? Limit { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs b/MediaBrowser.Controller/Security/IAuthenticationRepository.cs
new file mode 100644
index 000000000..2843c6b73
--- /dev/null
+++ b/MediaBrowser.Controller/Security/IAuthenticationRepository.cs
@@ -0,0 +1,37 @@
+using MediaBrowser.Model.Devices;
+using MediaBrowser.Model.Querying;
+using System.Threading;
+
+namespace MediaBrowser.Controller.Security
+{
+ public interface IAuthenticationRepository
+ {
+ /// <summary>
+ /// Creates the specified information.
+ /// </summary>
+ /// <param name="info">The information.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ void Create(AuthenticationInfo info);
+
+ /// <summary>
+ /// Updates the specified information.
+ /// </summary>
+ /// <param name="info">The information.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ void Update(AuthenticationInfo info);
+
+ /// <summary>
+ /// Gets the specified query.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>QueryResult{AuthenticationInfo}.</returns>
+ QueryResult<AuthenticationInfo> Get(AuthenticationInfoQuery query);
+
+ void Delete(AuthenticationInfo info);
+
+ DeviceOptions GetDeviceOptions(string deviceId);
+ void UpdateDeviceOptions(string deviceId, DeviceOptions options);
+ }
+}
diff --git a/MediaBrowser.Controller/Security/IEncryptionManager.cs b/MediaBrowser.Controller/Security/IEncryptionManager.cs
new file mode 100644
index 000000000..bb4f77d83
--- /dev/null
+++ b/MediaBrowser.Controller/Security/IEncryptionManager.cs
@@ -0,0 +1,20 @@
+
+namespace MediaBrowser.Controller.Security
+{
+ public interface IEncryptionManager
+ {
+ /// <summary>
+ /// Encrypts the string.
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <returns>System.String.</returns>
+ string EncryptString(string value);
+
+ /// <summary>
+ /// Decrypts the string.
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <returns>System.String.</returns>
+ string DecryptString(string value);
+ }
+}
diff --git a/MediaBrowser.Controller/Session/AuthenticationRequest.cs b/MediaBrowser.Controller/Session/AuthenticationRequest.cs
new file mode 100644
index 000000000..eb64db8c3
--- /dev/null
+++ b/MediaBrowser.Controller/Session/AuthenticationRequest.cs
@@ -0,0 +1,19 @@
+using System;
+
+
+namespace MediaBrowser.Controller.Session
+{
+ public class AuthenticationRequest
+ {
+ public string Username { get; set; }
+ public Guid UserId { get; set; }
+ public string Password { get; set; }
+ public string PasswordSha1 { get; set; }
+ public string PasswordMd5 { get; set; }
+ public string App { get; set; }
+ public string AppVersion { get; set; }
+ public string DeviceId { get; set; }
+ public string DeviceName { get; set; }
+ public string RemoteEndPoint { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs
new file mode 100644
index 000000000..e1d3a7ee6
--- /dev/null
+++ b/MediaBrowser.Controller/Session/ISessionController.cs
@@ -0,0 +1,25 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Session
+{
+ public interface ISessionController
+ {
+ /// <summary>
+ /// Gets a value indicating whether this instance is session active.
+ /// </summary>
+ /// <value><c>true</c> if this instance is session active; otherwise, <c>false</c>.</value>
+ bool IsSessionActive { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether [supports media remote control].
+ /// </summary>
+ /// <value><c>true</c> if [supports media remote control]; otherwise, <c>false</c>.</value>
+ bool SupportsMediaControl { get; }
+
+ /// <summary>
+ /// Sends the message.
+ /// </summary>
+ Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
new file mode 100644
index 000000000..b7719e556
--- /dev/null
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -0,0 +1,328 @@
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Security;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.Users;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Session
+{
+ /// <summary>
+ /// Interface ISessionManager
+ /// </summary>
+ public interface ISessionManager
+ {
+ /// <summary>
+ /// Occurs when [playback start].
+ /// </summary>
+ event EventHandler<PlaybackProgressEventArgs> PlaybackStart;
+
+ /// <summary>
+ /// Occurs when [playback progress].
+ /// </summary>
+ event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
+
+ /// <summary>
+ /// Occurs when [playback stopped].
+ /// </summary>
+ event EventHandler<PlaybackStopEventArgs> PlaybackStopped;
+
+ /// <summary>
+ /// Occurs when [session started].
+ /// </summary>
+ event EventHandler<SessionEventArgs> SessionStarted;
+
+ /// <summary>
+ /// Occurs when [session ended].
+ /// </summary>
+ event EventHandler<SessionEventArgs> SessionEnded;
+
+ event EventHandler<SessionEventArgs> SessionActivity;
+
+ /// <summary>
+ /// Occurs when [capabilities changed].
+ /// </summary>
+ event EventHandler<SessionEventArgs> CapabilitiesChanged;
+
+ /// <summary>
+ /// Occurs when [authentication failed].
+ /// </summary>
+ event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed;
+
+ /// <summary>
+ /// Occurs when [authentication succeeded].
+ /// </summary>
+ event EventHandler<GenericEventArgs<AuthenticationResult>> AuthenticationSucceeded;
+
+ /// <summary>
+ /// Gets the sessions.
+ /// </summary>
+ /// <value>The sessions.</value>
+ IEnumerable<SessionInfo> Sessions { get; }
+
+ /// <summary>
+ /// Logs the user activity.
+ /// </summary>
+ /// <param name="appName">Type of the client.</param>
+ /// <param name="appVersion">The app version.</param>
+ /// <param name="deviceId">The device id.</param>
+ /// <param name="deviceName">Name of the device.</param>
+ /// <param name="remoteEndPoint">The remote end point.</param>
+ /// <param name="user">The user.</param>
+ SessionInfo LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user);
+
+ void UpdateDeviceName(string sessionId, string reportedDeviceName);
+
+ /// <summary>
+ /// Used to report that playback has started for an item
+ /// </summary>
+ /// <param name="info">The info.</param>
+ /// <returns>Task.</returns>
+ Task OnPlaybackStart(PlaybackStartInfo info);
+
+ /// <summary>
+ /// Used to report playback progress for an item
+ /// </summary>
+ /// <param name="info">The info.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ Task OnPlaybackProgress(PlaybackProgressInfo info);
+
+ Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated);
+
+ /// <summary>
+ /// Used to report that playback has ended for an item
+ /// </summary>
+ /// <param name="info">The info.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ Task OnPlaybackStopped(PlaybackStopInfo info);
+
+ /// <summary>
+ /// Reports the session ended.
+ /// </summary>
+ /// <param name="sessionId">The session identifier.</param>
+ /// <returns>Task.</returns>
+ void ReportSessionEnded(string sessionId);
+
+ /// <summary>
+ /// Sends the general command.
+ /// </summary>
+ /// <param name="controllingSessionId">The controlling session identifier.</param>
+ /// <param name="sessionId">The session identifier.</param>
+ /// <param name="command">The command.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Sends the message command.
+ /// </summary>
+ /// <param name="controllingSessionId">The controlling session identifier.</param>
+ /// <param name="sessionId">The session id.</param>
+ /// <param name="command">The command.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Sends the play command.
+ /// </summary>
+ /// <param name="controllingSessionId">The controlling session identifier.</param>
+ /// <param name="sessionId">The session id.</param>
+ /// <param name="command">The command.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Sends the browse command.
+ /// </summary>
+ /// <param name="controllingSessionId">The controlling session identifier.</param>
+ /// <param name="sessionId">The session id.</param>
+ /// <param name="command">The command.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Sends the playstate command.
+ /// </summary>
+ /// <param name="controllingSessionId">The controlling session identifier.</param>
+ /// <param name="sessionId">The session id.</param>
+ /// <param name="command">The command.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Sends the message to admin sessions.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="name">The name.</param>
+ /// <param name="data">The data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Sends the message to user sessions.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <returns>Task.</returns>
+ Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken);
+
+ Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Sends the message to user device sessions.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="deviceId">The device identifier.</param>
+ /// <param name="name">The name.</param>
+ /// <param name="data">The data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Sends the restart required message.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SendRestartRequiredNotification(CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Sends the server shutdown notification.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SendServerShutdownNotification(CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Sends the server restart notification.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task SendServerRestartNotification(CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Adds the additional user.
+ /// </summary>
+ /// <param name="sessionId">The session identifier.</param>
+ /// <param name="userId">The user identifier.</param>
+ void AddAdditionalUser(string sessionId, Guid userId);
+
+ /// <summary>
+ /// Removes the additional user.
+ /// </summary>
+ /// <param name="sessionId">The session identifier.</param>
+ /// <param name="userId">The user identifier.</param>
+ void RemoveAdditionalUser(string sessionId, Guid userId);
+
+ /// <summary>
+ /// Reports the now viewing item.
+ /// </summary>
+ /// <param name="sessionId">The session identifier.</param>
+ /// <param name="itemId">The item identifier.</param>
+ void ReportNowViewingItem(string sessionId, string itemId);
+
+ /// <summary>
+ /// Reports the now viewing item.
+ /// </summary>
+ /// <param name="sessionId">The session identifier.</param>
+ /// <param name="item">The item.</param>
+ void ReportNowViewingItem(string sessionId, BaseItemDto item);
+
+ /// <summary>
+ /// Authenticates the new session.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <returns>Task{SessionInfo}.</returns>
+ Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request);
+
+ /// <summary>
+ /// Creates the new session.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <returns>Task&lt;AuthenticationResult&gt;.</returns>
+ Task<AuthenticationResult> CreateNewSession(AuthenticationRequest request);
+
+ /// <summary>
+ /// Reports the capabilities.
+ /// </summary>
+ /// <param name="sessionId">The session identifier.</param>
+ /// <param name="capabilities">The capabilities.</param>
+ void ReportCapabilities(string sessionId, ClientCapabilities capabilities);
+
+ /// <summary>
+ /// Reports the transcoding information.
+ /// </summary>
+ /// <param name="deviceId">The device identifier.</param>
+ /// <param name="info">The information.</param>
+ void ReportTranscodingInfo(string deviceId, TranscodingInfo info);
+
+ /// <summary>
+ /// Clears the transcoding information.
+ /// </summary>
+ /// <param name="deviceId">The device identifier.</param>
+ void ClearTranscodingInfo(string deviceId);
+
+ /// <summary>
+ /// Gets the session.
+ /// </summary>
+ /// <param name="deviceId">The device identifier.</param>
+ /// <param name="client">The client.</param>
+ /// <param name="version">The version.</param>
+ /// <returns>SessionInfo.</returns>
+ SessionInfo GetSession(string deviceId, string client, string version);
+
+ /// <summary>
+ /// Gets the session by authentication token.
+ /// </summary>
+ /// <param name="token">The token.</param>
+ /// <param name="deviceId">The device identifier.</param>
+ /// <param name="remoteEndpoint">The remote endpoint.</param>
+ /// <returns>SessionInfo.</returns>
+ SessionInfo GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint);
+
+ /// <summary>
+ /// Gets the session by authentication token.
+ /// </summary>
+ /// <param name="info">The information.</param>
+ /// <param name="deviceId">The device identifier.</param>
+ /// <param name="remoteEndpoint">The remote endpoint.</param>
+ /// <param name="appVersion">The application version.</param>
+ /// <returns>Task&lt;SessionInfo&gt;.</returns>
+ SessionInfo GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion);
+
+ /// <summary>
+ /// Logouts the specified access token.
+ /// </summary>
+ /// <param name="accessToken">The access token.</param>
+ /// <returns>Task.</returns>
+ void Logout(string accessToken);
+ void Logout(AuthenticationInfo accessToken);
+
+ /// <summary>
+ /// Revokes the user tokens.
+ /// </summary>
+ /// <returns>Task.</returns>
+ void RevokeUserTokens(Guid userId, string currentAccessToken);
+
+ /// <summary>
+ /// Revokes the token.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <returns>Task.</returns>
+ void RevokeToken(string id);
+
+ void CloseIfNeeded(SessionInfo session);
+ }
+}
diff --git a/MediaBrowser.Controller/Session/SessionEventArgs.cs b/MediaBrowser.Controller/Session/SessionEventArgs.cs
new file mode 100644
index 000000000..96daa6ec9
--- /dev/null
+++ b/MediaBrowser.Controller/Session/SessionEventArgs.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace MediaBrowser.Controller.Session
+{
+ public class SessionEventArgs : EventArgs
+ {
+ public SessionInfo SessionInfo { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
new file mode 100644
index 000000000..869d3fcb0
--- /dev/null
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -0,0 +1,396 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.Threading;
+using System.Linq;
+using System;
+
+namespace MediaBrowser.Controller.Session
+{
+ /// <summary>
+ /// Class SessionInfo
+ /// </summary>
+ public class SessionInfo : IDisposable
+ {
+ private ISessionManager _sessionManager;
+ private readonly ILogger _logger;
+
+ public SessionInfo(ISessionManager sessionManager, ILogger logger)
+ {
+ _sessionManager = sessionManager;
+ _logger = logger;
+
+ AdditionalUsers = new SessionUserInfo[] { };
+ PlayState = new PlayerStateInfo();
+ SessionControllers = new ISessionController[] { };
+ }
+
+ public PlayerStateInfo PlayState { get; set; }
+
+ public SessionUserInfo[] AdditionalUsers { get; set; }
+
+ public ClientCapabilities Capabilities { get; set; }
+
+ /// <summary>
+ /// Gets or sets the remote end point.
+ /// </summary>
+ /// <value>The remote end point.</value>
+ public string RemoteEndPoint { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playable media types.
+ /// </summary>
+ /// <value>The playable media types.</value>
+ public string[] PlayableMediaTypes
+ {
+ get
+ {
+ if (Capabilities == null)
+ {
+ return new string[] {};
+ }
+ return Capabilities.PlayableMediaTypes;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ /// <value>The user id.</value>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the username.
+ /// </summary>
+ /// <value>The username.</value>
+ public string UserName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the client.
+ /// </summary>
+ /// <value>The type of the client.</value>
+ public string Client { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last activity date.
+ /// </summary>
+ /// <value>The last activity date.</value>
+ public DateTime LastActivityDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last playback check in.
+ /// </summary>
+ /// <value>The last playback check in.</value>
+ public DateTime LastPlaybackCheckIn { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the device.
+ /// </summary>
+ /// <value>The name of the device.</value>
+ public string DeviceName { get; set; }
+
+ public string DeviceType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the now playing item.
+ /// </summary>
+ /// <value>The now playing item.</value>
+ public BaseItemDto NowPlayingItem { get; set; }
+
+ public BaseItem FullNowPlayingItem { get; set; }
+
+ /// <summary>
+ /// Gets or sets the device id.
+ /// </summary>
+ /// <value>The device id.</value>
+ public string DeviceId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the application version.
+ /// </summary>
+ /// <value>The application version.</value>
+ public string ApplicationVersion { get; set; }
+
+ /// <summary>
+ /// Gets or sets the session controller.
+ /// </summary>
+ /// <value>The session controller.</value>
+ [IgnoreDataMember]
+ public ISessionController[] SessionControllers { get; set; }
+
+ /// <summary>
+ /// Gets or sets the application icon URL.
+ /// </summary>
+ /// <value>The application icon URL.</value>
+ public string AppIconUrl { get; set; }
+
+ /// <summary>
+ /// Gets or sets the supported commands.
+ /// </summary>
+ /// <value>The supported commands.</value>
+ public string[] SupportedCommands
+ {
+ get
+ {
+ if (Capabilities == null)
+ {
+ return new string[] {};
+ }
+ return Capabilities.SupportedCommands;
+ }
+ }
+
+ public TranscodingInfo TranscodingInfo { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is active.
+ /// </summary>
+ /// <value><c>true</c> if this instance is active; otherwise, <c>false</c>.</value>
+ public bool IsActive
+ {
+ get
+ {
+ var controllers = SessionControllers;
+ foreach (var controller in controllers)
+ {
+ if (controller.IsSessionActive)
+ {
+ return true;
+ }
+ }
+ if (controllers.Length > 0)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ public bool SupportsMediaControl
+ {
+ get
+ {
+ if (Capabilities == null || !Capabilities.SupportsMediaControl)
+ {
+ return false;
+ }
+
+ var controllers = SessionControllers;
+ foreach (var controller in controllers)
+ {
+ if (controller.SupportsMediaControl)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ public bool SupportsRemoteControl
+ {
+ get
+ {
+ if (Capabilities == null || !Capabilities.SupportsMediaControl)
+ {
+ return false;
+ }
+
+ var controllers = SessionControllers;
+ foreach (var controller in controllers)
+ {
+ if (controller.SupportsMediaControl)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
+ {
+ var controllers = SessionControllers.ToList();
+ foreach (var controller in controllers)
+ {
+ if (controller is T)
+ {
+ return new Tuple<ISessionController, bool>(controller, false);
+ }
+ }
+
+ var newController = factory(this);
+ _logger.Debug("Creating new {0}", newController.GetType().Name);
+ controllers.Add(newController);
+
+ SessionControllers = controllers.ToArray();
+ return new Tuple<ISessionController, bool>(newController, true);
+ }
+
+ public void AddController(ISessionController controller)
+ {
+ var controllers = SessionControllers.ToList();
+ controllers.Add(controller);
+ SessionControllers = controllers.ToArray();
+ }
+
+ public bool ContainsUser(string userId)
+ {
+ return ContainsUser(new Guid(userId));
+ }
+
+ public bool ContainsUser(Guid userId)
+ {
+ if (UserId.Equals(userId))
+ {
+ return true;
+ }
+
+ foreach (var additionalUser in AdditionalUsers)
+ {
+ if (userId.Equals(userId))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private readonly object _progressLock = new object();
+ private ITimer _progressTimer;
+ private PlaybackProgressInfo _lastProgressInfo;
+
+ public void StartAutomaticProgress(ITimerFactory timerFactory, PlaybackProgressInfo progressInfo)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ lock (_progressLock)
+ {
+ _lastProgressInfo = progressInfo;
+
+ if (_progressTimer == null)
+ {
+ _progressTimer = timerFactory.Create(OnProgressTimerCallback, null, 1000, 1000);
+ }
+ else
+ {
+ _progressTimer.Change(1000, 1000);
+ }
+ }
+ }
+
+ // 1 second
+ private const long ProgressIncrement = 10000000;
+
+ private async void OnProgressTimerCallback(object state)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ var progressInfo = _lastProgressInfo;
+ if (progressInfo == null)
+ {
+ return;
+ }
+ if (progressInfo.IsPaused)
+ {
+ return;
+ }
+
+ var positionTicks = progressInfo.PositionTicks ?? 0;
+ if (positionTicks < 0)
+ {
+ positionTicks = 0;
+ }
+
+ var newPositionTicks = positionTicks + ProgressIncrement;
+ var item = progressInfo.Item;
+ long? runtimeTicks = item == null ? null : item.RunTimeTicks;
+
+ // Don't report beyond the runtime
+ if (runtimeTicks.HasValue && newPositionTicks >= runtimeTicks.Value)
+ {
+ return;
+ }
+
+ progressInfo.PositionTicks = newPositionTicks;
+
+ try
+ {
+ await _sessionManager.OnPlaybackProgress(progressInfo, true).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error reporting playback progress", ex);
+ }
+ }
+
+ public void StopAutomaticProgress()
+ {
+ lock (_progressLock)
+ {
+ if (_progressTimer != null)
+ {
+ _progressTimer.Dispose();
+ _progressTimer = null;
+ }
+ _lastProgressInfo = null;
+ }
+ }
+
+ private bool _disposed = false;
+
+ public void Dispose()
+ {
+ _disposed = true;
+
+ StopAutomaticProgress();
+
+ var controllers = SessionControllers.ToList();
+ SessionControllers = new ISessionController[] { };
+
+ foreach (var controller in controllers)
+ {
+ var disposable = controller as IDisposable;
+
+ if (disposable != null)
+ {
+ _logger.Debug("Disposing session controller {0}", disposable.GetType().Name);
+
+ try
+ {
+ disposable.Dispose();
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error disposing session controller", ex);
+ }
+ }
+ }
+
+ _sessionManager = null;
+ }
+
+ public QueueItem[] NowPlayingQueue { get; set; }
+ public bool HasCustomDeviceName { get; set; }
+ public string PlaylistItemId { get; set; }
+ public string ServerId { get; set; }
+ public string UserPrimaryImageTag { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs
new file mode 100644
index 000000000..6d0b95bcb
--- /dev/null
+++ b/MediaBrowser.Controller/Sorting/IBaseItemComparer.cs
@@ -0,0 +1,17 @@
+using MediaBrowser.Controller.Entities;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Sorting
+{
+ /// <summary>
+ /// Interface IBaseItemComparer
+ /// </summary>
+ public interface IBaseItemComparer : IComparer<BaseItem>
+ {
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+ }
+}
diff --git a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
new file mode 100644
index 000000000..915d4854b
--- /dev/null
+++ b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs
@@ -0,0 +1,29 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+
+namespace MediaBrowser.Controller.Sorting
+{
+ /// <summary>
+ /// Represents a BaseItem comparer that requires a User to perform it's comparison
+ /// </summary>
+ public interface IUserBaseItemComparer : IBaseItemComparer
+ {
+ /// <summary>
+ /// Gets or sets the user.
+ /// </summary>
+ /// <value>The user.</value>
+ User User { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user manager.
+ /// </summary>
+ /// <value>The user manager.</value>
+ IUserManager UserManager { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user data repository.
+ /// </summary>
+ /// <value>The user data repository.</value>
+ IUserDataManager UserDataRepository { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs
new file mode 100644
index 000000000..ec8ee5a11
--- /dev/null
+++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace MediaBrowser.Controller.Sorting
+{
+ public static class SortExtensions
+ {
+ public static IEnumerable<T> OrderByString<T>(this IEnumerable<T> list, Func<T, string> getName)
+ {
+ return list.OrderBy(getName, new AlphanumComparator());
+ }
+
+ public static IEnumerable<T> OrderByStringDescending<T>(this IEnumerable<T> list, Func<T, string> getName)
+ {
+ return list.OrderByDescending(getName, new AlphanumComparator());
+ }
+
+ public static IOrderedEnumerable<T> ThenByString<T>(this IOrderedEnumerable<T> list, Func<T, string> getName)
+ {
+ return list.ThenBy(getName, new AlphanumComparator());
+ }
+
+ public static IOrderedEnumerable<T> ThenByStringDescending<T>(this IOrderedEnumerable<T> list, Func<T, string> getName)
+ {
+ return list.ThenByDescending(getName, new AlphanumComparator());
+ }
+
+ private class AlphanumComparator : IComparer<string>
+ {
+ private enum ChunkType { Alphanumeric, Numeric };
+
+ private static bool InChunk(char ch, char otherCh)
+ {
+ var type = ChunkType.Alphanumeric;
+
+ if (char.IsDigit(otherCh))
+ {
+ type = ChunkType.Numeric;
+ }
+
+ if ((type == ChunkType.Alphanumeric && char.IsDigit(ch))
+ || (type == ChunkType.Numeric && !char.IsDigit(ch)))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static int CompareValues(string s1, string s2)
+ {
+ if (s1 == null || s2 == null)
+ {
+ return 0;
+ }
+
+ int thisMarker = 0, thisNumericChunk = 0;
+ int thatMarker = 0, thatNumericChunk = 0;
+
+ while ((thisMarker < s1.Length) || (thatMarker < s2.Length))
+ {
+ if (thisMarker >= s1.Length)
+ {
+ return -1;
+ }
+ else if (thatMarker >= s2.Length)
+ {
+ return 1;
+ }
+ char thisCh = s1[thisMarker];
+ char thatCh = s2[thatMarker];
+
+ StringBuilder thisChunk = new StringBuilder();
+ StringBuilder thatChunk = new StringBuilder();
+
+ while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || InChunk(thisCh, thisChunk[0])))
+ {
+ thisChunk.Append(thisCh);
+ thisMarker++;
+
+ if (thisMarker < s1.Length)
+ {
+ thisCh = s1[thisMarker];
+ }
+ }
+
+ while ((thatMarker < s2.Length) && (thatChunk.Length == 0 || InChunk(thatCh, thatChunk[0])))
+ {
+ thatChunk.Append(thatCh);
+ thatMarker++;
+
+ if (thatMarker < s2.Length)
+ {
+ thatCh = s2[thatMarker];
+ }
+ }
+
+ int result = 0;
+ // If both chunks contain numeric characters, sort them numerically
+ if (char.IsDigit(thisChunk[0]) && char.IsDigit(thatChunk[0]))
+ {
+ if (!int.TryParse(thisChunk.ToString(), out thisNumericChunk))
+ {
+ return 0;
+ }
+ if (!int.TryParse(thatChunk.ToString(), out thatNumericChunk))
+ {
+ return 0;
+ }
+
+ if (thisNumericChunk < thatNumericChunk)
+ {
+ result = -1;
+ }
+
+ if (thisNumericChunk > thatNumericChunk)
+ {
+ result = 1;
+ }
+ }
+ else
+ {
+ result = thisChunk.ToString().CompareTo(thatChunk.ToString());
+ }
+
+ if (result != 0)
+ {
+ return result;
+ }
+ }
+
+ return 0;
+ }
+
+ public int Compare(string x, string y)
+ {
+ return CompareValues(x, y);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Sorting/SortHelper.cs b/MediaBrowser.Controller/Sorting/SortHelper.cs
new file mode 100644
index 000000000..3456b9b04
--- /dev/null
+++ b/MediaBrowser.Controller/Sorting/SortHelper.cs
@@ -0,0 +1,25 @@
+namespace MediaBrowser.Controller.Sorting
+{
+ public static class SortHelper
+ {
+ private enum ChunkType { Alphanumeric, Numeric };
+
+ public static bool InChunk(char ch, char otherCh)
+ {
+ var type = ChunkType.Alphanumeric;
+
+ if (char.IsDigit(otherCh))
+ {
+ type = ChunkType.Numeric;
+ }
+
+ if ((type == ChunkType.Alphanumeric && char.IsDigit(ch))
+ || (type == ChunkType.Numeric && !char.IsDigit(ch)))
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
new file mode 100644
index 000000000..e41826be5
--- /dev/null
+++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
@@ -0,0 +1,74 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Providers;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Configuration;
+
+namespace MediaBrowser.Controller.Subtitles
+{
+ public interface ISubtitleManager
+ {
+ /// <summary>
+ /// Occurs when [subtitle download failure].
+ /// </summary>
+ event EventHandler<SubtitleDownloadFailureEventArgs> SubtitleDownloadFailure;
+
+ /// <summary>
+ /// Occurs when [subtitles downloaded].
+ /// </summary>
+ event EventHandler<SubtitleDownloadEventArgs> SubtitlesDownloaded;
+
+ /// <summary>
+ /// Adds the parts.
+ /// </summary>
+ /// <param name="subtitleProviders">The subtitle providers.</param>
+ void AddParts(IEnumerable<ISubtitleProvider> subtitleProviders);
+
+ /// <summary>
+ /// Searches the subtitles.
+ /// </summary>
+ Task<RemoteSubtitleInfo[]> SearchSubtitles(Video video,
+ string language,
+ bool? isPerfectMatch,
+ CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Searches the subtitles.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{RemoteSubtitleInfo}}.</returns>
+ Task<RemoteSubtitleInfo[]> SearchSubtitles(SubtitleSearchRequest request,
+ CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Downloads the subtitles.
+ /// </summary>
+ Task DownloadSubtitles(Video video, string subtitleId, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Downloads the subtitles.
+ /// </summary>
+ Task DownloadSubtitles(Video video, LibraryOptions libraryOptions, string subtitleId, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the remote subtitles.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{SubtitleResponse}.</returns>
+ Task<SubtitleResponse> GetRemoteSubtitles(string id, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Deletes the subtitles.
+ /// </summary>
+ Task DeleteSubtitles(BaseItem item, int index);
+
+ /// <summary>
+ /// Gets the providers.
+ /// </summary>
+ SubtitleProviderInfo[] GetSupportedProviders(BaseItem item);
+ }
+}
diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs b/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs
new file mode 100644
index 000000000..2502d685d
--- /dev/null
+++ b/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs
@@ -0,0 +1,40 @@
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Providers;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Subtitles
+{
+ public interface ISubtitleProvider
+ {
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ /// <summary>
+ /// Gets the supported media types.
+ /// </summary>
+ /// <value>The supported media types.</value>
+ IEnumerable<VideoContentType> SupportedMediaTypes { get; }
+
+ /// <summary>
+ /// Searches the subtitles.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{IEnumerable{RemoteSubtitleInfo}}.</returns>
+ Task<IEnumerable<RemoteSubtitleInfo>> Search(SubtitleSearchRequest request, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the subtitles.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{SubtitleResponse}.</returns>
+ Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Subtitles/SubtitleDownloadEventArgs.cs b/MediaBrowser.Controller/Subtitles/SubtitleDownloadEventArgs.cs
new file mode 100644
index 000000000..1d204f2cb
--- /dev/null
+++ b/MediaBrowser.Controller/Subtitles/SubtitleDownloadEventArgs.cs
@@ -0,0 +1,27 @@
+using System;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Subtitles
+{
+ public class SubtitleDownloadEventArgs
+ {
+ public BaseItem Item { get; set; }
+
+ public string Format { get; set; }
+
+ public string Language { get; set; }
+
+ public bool IsForced { get; set; }
+
+ public string Provider { get; set; }
+ }
+
+ public class SubtitleDownloadFailureEventArgs
+ {
+ public BaseItem Item { get; set; }
+
+ public string Provider { get; set; }
+
+ public Exception Exception { get; set; }
+ }
+}
diff --git a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs
new file mode 100644
index 000000000..e2f6dfc97
--- /dev/null
+++ b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs
@@ -0,0 +1,12 @@
+using System.IO;
+
+namespace MediaBrowser.Controller.Subtitles
+{
+ public class SubtitleResponse
+ {
+ public string Language { get; set; }
+ public string Format { get; set; }
+ public bool IsForced { get; set; }
+ public Stream Stream { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs
new file mode 100644
index 000000000..84bf28c05
--- /dev/null
+++ b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs
@@ -0,0 +1,39 @@
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Subtitles
+{
+ public class SubtitleSearchRequest : IHasProviderIds
+ {
+ public string Language { get; set; }
+ public string TwoLetterISOLanguageName { get; set; }
+
+ public VideoContentType ContentType { get; set; }
+
+ public string MediaPath { get; set; }
+ public string SeriesName { get; set; }
+ public string Name { get; set; }
+ public int? IndexNumber { get; set; }
+ public int? IndexNumberEnd { get; set; }
+ public int? ParentIndexNumber { get; set; }
+ public int? ProductionYear { get; set; }
+ public long? RuntimeTicks { get; set; }
+ public bool IsPerfectMatch { get; set; }
+ public Dictionary<string, string> ProviderIds { get; set; }
+
+ public bool SearchAllProviders { get; set; }
+ public string[] DisabledSubtitleFetchers { get; set; }
+ public string[] SubtitleFetcherOrder { get; set; }
+
+ public SubtitleSearchRequest()
+ {
+ SearchAllProviders = true;
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+ DisabledSubtitleFetchers = new string[] {};
+ SubtitleFetcherOrder = new string[] {};
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs b/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs
new file mode 100644
index 000000000..cf868a381
--- /dev/null
+++ b/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs
@@ -0,0 +1,18 @@
+using MediaBrowser.Model.Sync;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Sync
+{
+ public interface IHasDynamicAccess
+ {
+ /// <summary>
+ /// Gets the synced file information.
+ /// </summary>
+ /// <param name="id">The identifier.</param>
+ /// <param name="target">The target.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task&lt;SyncedFileInfo&gt;.</returns>
+ Task<SyncedFileInfo> GetSyncedFileInfo(string id, SyncTarget target, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs b/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs
new file mode 100644
index 000000000..aeb7a3bff
--- /dev/null
+++ b/MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs
@@ -0,0 +1,10 @@
+
+namespace MediaBrowser.Controller.Sync
+{
+ /// <summary>
+ /// A marker interface
+ /// </summary>
+ public interface IRemoteSyncProvider
+ {
+ }
+}
diff --git a/MediaBrowser.Controller/Sync/IServerSyncProvider.cs b/MediaBrowser.Controller/Sync/IServerSyncProvider.cs
new file mode 100644
index 000000000..335fea296
--- /dev/null
+++ b/MediaBrowser.Controller/Sync/IServerSyncProvider.cs
@@ -0,0 +1,29 @@
+using MediaBrowser.Model.Querying;
+using MediaBrowser.Model.Sync;
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Sync
+{
+ public interface IServerSyncProvider : ISyncProvider
+ {
+ /// <summary>
+ /// Transfers the file.
+ /// </summary>
+ Task<SyncedFileInfo> SendFile(SyncJob syncJob, string originalMediaPath, Stream inputStream, bool isMedia, string[] outputPathParts, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
+
+ Task<QueryResult<FileSystemMetadata>> GetFiles(string[] directoryPathParts, SyncTarget target, CancellationToken cancellationToken);
+ }
+
+ public interface ISupportsDirectCopy
+ {
+ /// <summary>
+ /// Sends the file.
+ /// </summary>
+ Task<SyncedFileInfo> SendFile(SyncJob syncJob, string originalMediaPath, string inputPath, bool isMedia, string[] outputPathParts, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/Sync/ISyncProvider.cs b/MediaBrowser.Controller/Sync/ISyncProvider.cs
new file mode 100644
index 000000000..0b2fcc95e
--- /dev/null
+++ b/MediaBrowser.Controller/Sync/ISyncProvider.cs
@@ -0,0 +1,27 @@
+using MediaBrowser.Model.Sync;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Sync
+{
+ public interface ISyncProvider
+ {
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ /// <summary>
+ /// Gets the synchronize targets.
+ /// </summary>
+ /// <param name="userId">The user identifier.</param>
+ /// <returns>IEnumerable&lt;SyncTarget&gt;.</returns>
+ List<SyncTarget> GetSyncTargets(string userId);
+
+ /// <summary>
+ /// Gets all synchronize targets.
+ /// </summary>
+ /// <returns>IEnumerable&lt;SyncTarget&gt;.</returns>
+ List<SyncTarget> GetAllSyncTargets();
+ }
+}
diff --git a/MediaBrowser.Controller/Sync/SyncedFileInfo.cs b/MediaBrowser.Controller/Sync/SyncedFileInfo.cs
new file mode 100644
index 000000000..1c87551f1
--- /dev/null
+++ b/MediaBrowser.Controller/Sync/SyncedFileInfo.cs
@@ -0,0 +1,35 @@
+using MediaBrowser.Model.MediaInfo;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Sync
+{
+ public class SyncedFileInfo
+ {
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ public string Path { get; set; }
+ public string[] PathParts { get; set; }
+ /// <summary>
+ /// Gets or sets the protocol.
+ /// </summary>
+ /// <value>The protocol.</value>
+ public MediaProtocol Protocol { get; set; }
+ /// <summary>
+ /// Gets or sets the required HTTP headers.
+ /// </summary>
+ /// <value>The required HTTP headers.</value>
+ public Dictionary<string, string> RequiredHttpHeaders { get; set; }
+ /// <summary>
+ /// Gets or sets the identifier.
+ /// </summary>
+ /// <value>The identifier.</value>
+ public string Id { get; set; }
+
+ public SyncedFileInfo()
+ {
+ RequiredHttpHeaders = new Dictionary<string, string>();
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/TV/ITVSeriesManager.cs b/MediaBrowser.Controller/TV/ITVSeriesManager.cs
new file mode 100644
index 000000000..56e06bcfa
--- /dev/null
+++ b/MediaBrowser.Controller/TV/ITVSeriesManager.cs
@@ -0,0 +1,20 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Querying;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Dto;
+
+namespace MediaBrowser.Controller.TV
+{
+ public interface ITVSeriesManager
+ {
+ /// <summary>
+ /// Gets the next up.
+ /// </summary>
+ QueryResult<BaseItem> GetNextUp(NextUpQuery query, DtoOptions options);
+
+ /// <summary>
+ /// Gets the next up.
+ /// </summary>
+ QueryResult<BaseItem> GetNextUp(NextUpQuery request, BaseItem[] parentsFolders, DtoOptions options);
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs
index 945e20dd0..294181fcc 100644
--- a/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs
@@ -69,7 +69,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
DisposeLiveStream();
DisposeLogStream();
- DisposeIsoMount();
}
private void DisposeLogStream()
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 31c7d20b2..64372c072 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -727,11 +727,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
var encodinghelper = new EncodingHelper(this, FileSystem, SubtitleEncoder());
if (videoStream != null)
{
+ /* fix
var decoder = encodinghelper.GetHardwareAcceleratedVideoDecoder(VideoType.VideoFile, videoStream, GetEncodingOptions());
if (!string.IsNullOrWhiteSpace(decoder))
{
args = decoder + " " + args;
}
+ */
}
if (!string.IsNullOrWhiteSpace(container))
@@ -847,11 +849,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
var encodinghelper = new EncodingHelper(this, FileSystem, SubtitleEncoder());
if (videoStream != null)
{
+ /* fix
var decoder = encodinghelper.GetHardwareAcceleratedVideoDecoder(VideoType.VideoFile, videoStream, GetEncodingOptions());
if (!string.IsNullOrWhiteSpace(decoder))
{
args = decoder + " " + args;
}
+ */
}
if (!string.IsNullOrWhiteSpace(container))
@@ -1145,4 +1149,4 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
}
}
-} \ No newline at end of file
+}
diff --git a/MediaBrowser.Model/Activity/ActivityLogEntry.cs b/MediaBrowser.Model/Activity/ActivityLogEntry.cs
new file mode 100644
index 000000000..1f4bff10d
--- /dev/null
+++ b/MediaBrowser.Model/Activity/ActivityLogEntry.cs
@@ -0,0 +1,68 @@
+using MediaBrowser.Model.Logging;
+using System;
+
+namespace MediaBrowser.Model.Activity
+{
+ public class ActivityLogEntry
+ {
+ /// <summary>
+ /// Gets or sets the identifier.
+ /// </summary>
+ /// <value>The identifier.</value>
+ public long Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the overview.
+ /// </summary>
+ /// <value>The overview.</value>
+ public string Overview { get; set; }
+
+ /// <summary>
+ /// Gets or sets the short overview.
+ /// </summary>
+ /// <value>The short overview.</value>
+ public string ShortOverview { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ public string Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item identifier.
+ /// </summary>
+ /// <value>The item identifier.</value>
+ public string ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date.
+ /// </summary>
+ /// <value>The date.</value>
+ public DateTime Date { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user identifier.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user primary image tag.
+ /// </summary>
+ /// <value>The user primary image tag.</value>
+ public string UserPrimaryImageTag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the log severity.
+ /// </summary>
+ /// <value>The log severity.</value>
+ public LogSeverity Severity { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Activity/IActivityManager.cs b/MediaBrowser.Model/Activity/IActivityManager.cs
new file mode 100644
index 000000000..7fff26987
--- /dev/null
+++ b/MediaBrowser.Model/Activity/IActivityManager.cs
@@ -0,0 +1,17 @@
+using System;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Querying;
+
+namespace MediaBrowser.Model.Activity
+{
+ public interface IActivityManager
+ {
+ event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
+
+ void Create(ActivityLogEntry entry);
+
+ QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit);
+
+ QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? x, int? y);
+ }
+}
diff --git a/MediaBrowser.Model/Activity/IActivityRepository.cs b/MediaBrowser.Model/Activity/IActivityRepository.cs
new file mode 100644
index 000000000..5a2c52400
--- /dev/null
+++ b/MediaBrowser.Model/Activity/IActivityRepository.cs
@@ -0,0 +1,12 @@
+using System;
+using MediaBrowser.Model.Querying;
+
+namespace MediaBrowser.Model.Activity
+{
+ public interface IActivityRepository
+ {
+ void Create(ActivityLogEntry entry);
+
+ QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? z, int? startIndex, int? limit);
+ }
+}
diff --git a/MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs b/MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs
new file mode 100644
index 000000000..e2f780605
--- /dev/null
+++ b/MediaBrowser.Model/ApiClient/ServerDiscoveryInfo.cs
@@ -0,0 +1,27 @@
+
+namespace MediaBrowser.Model.ApiClient
+{
+ public class ServerDiscoveryInfo
+ {
+ /// <summary>
+ /// Gets or sets the address.
+ /// </summary>
+ /// <value>The address.</value>
+ public string Address { get; set; }
+ /// <summary>
+ /// Gets or sets the server identifier.
+ /// </summary>
+ /// <value>The server identifier.</value>
+ public string Id { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the endpoint address.
+ /// </summary>
+ /// <value>The endpoint address.</value>
+ public string EndpointAddress { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Branding/BrandingOptions.cs b/MediaBrowser.Model/Branding/BrandingOptions.cs
new file mode 100644
index 000000000..3b207d345
--- /dev/null
+++ b/MediaBrowser.Model/Branding/BrandingOptions.cs
@@ -0,0 +1,17 @@
+
+namespace MediaBrowser.Model.Branding
+{
+ public class BrandingOptions
+ {
+ /// <summary>
+ /// Gets or sets the login disclaimer.
+ /// </summary>
+ /// <value>The login disclaimer.</value>
+ public string LoginDisclaimer { get; set; }
+ /// <summary>
+ /// Gets or sets the custom CSS.
+ /// </summary>
+ /// <value>The custom CSS.</value>
+ public string CustomCss { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Channels/ChannelFeatures.cs b/MediaBrowser.Model/Channels/ChannelFeatures.cs
new file mode 100644
index 000000000..39b40cabc
--- /dev/null
+++ b/MediaBrowser.Model/Channels/ChannelFeatures.cs
@@ -0,0 +1,85 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Channels
+{
+ public class ChannelFeatures
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the identifier.
+ /// </summary>
+ /// <value>The identifier.</value>
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance can search.
+ /// </summary>
+ /// <value><c>true</c> if this instance can search; otherwise, <c>false</c>.</value>
+ public bool CanSearch { get; set; }
+
+ /// <summary>
+ /// Gets or sets the media types.
+ /// </summary>
+ /// <value>The media types.</value>
+ public ChannelMediaType[] MediaTypes { get; set; }
+
+ /// <summary>
+ /// Gets or sets the content types.
+ /// </summary>
+ /// <value>The content types.</value>
+ public ChannelMediaContentType[] ContentTypes { get; set; }
+
+ /// <summary>
+ /// Represents the maximum number of records the channel allows retrieving at a time
+ /// </summary>
+ public int? MaxPageSize { get; set; }
+
+ /// <summary>
+ /// Gets or sets the automatic refresh levels.
+ /// </summary>
+ /// <value>The automatic refresh levels.</value>
+ public int? AutoRefreshLevels { get; set; }
+
+ /// <summary>
+ /// Gets or sets the default sort orders.
+ /// </summary>
+ /// <value>The default sort orders.</value>
+ public ChannelItemSortField[] DefaultSortFields { get; set; }
+
+ /// <summary>
+ /// Indicates if a sort ascending/descending toggle is supported or not.
+ /// </summary>
+ public bool SupportsSortOrderToggle { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [supports latest media].
+ /// </summary>
+ /// <value><c>true</c> if [supports latest media]; otherwise, <c>false</c>.</value>
+ public bool SupportsLatestMedia { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance can filter.
+ /// </summary>
+ /// <value><c>true</c> if this instance can filter; otherwise, <c>false</c>.</value>
+ public bool CanFilter { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [supports content downloading].
+ /// </summary>
+ /// <value><c>true</c> if [supports content downloading]; otherwise, <c>false</c>.</value>
+ public bool SupportsContentDownloading { get; set; }
+
+ public ChannelFeatures()
+ {
+ MediaTypes = new ChannelMediaType[] { };
+ ContentTypes = new ChannelMediaContentType[] { };
+
+ DefaultSortFields = new ChannelItemSortField[] { };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Channels/ChannelFolderType.cs b/MediaBrowser.Model/Channels/ChannelFolderType.cs
new file mode 100644
index 000000000..7c97afd02
--- /dev/null
+++ b/MediaBrowser.Model/Channels/ChannelFolderType.cs
@@ -0,0 +1,17 @@
+namespace MediaBrowser.Model.Channels
+{
+ public enum ChannelFolderType
+ {
+ Container = 0,
+
+ MusicAlbum = 1,
+
+ PhotoAlbum = 2,
+
+ MusicArtist = 3,
+
+ Series = 4,
+
+ Season = 5
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Channels/ChannelInfo.cs b/MediaBrowser.Model/Channels/ChannelInfo.cs
new file mode 100644
index 000000000..36e3c17d9
--- /dev/null
+++ b/MediaBrowser.Model/Channels/ChannelInfo.cs
@@ -0,0 +1,30 @@
+
+namespace MediaBrowser.Model.Channels
+{
+ public class ChannelInfo
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the identifier.
+ /// </summary>
+ /// <value>The identifier.</value>
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the home page URL.
+ /// </summary>
+ /// <value>The home page URL.</value>
+ public string HomePageUrl { get; set; }
+
+ /// <summary>
+ /// Gets or sets the features.
+ /// </summary>
+ /// <value>The features.</value>
+ public ChannelFeatures Features { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Channels/ChannelItemSortField.cs b/MediaBrowser.Model/Channels/ChannelItemSortField.cs
new file mode 100644
index 000000000..6b5015b77
--- /dev/null
+++ b/MediaBrowser.Model/Channels/ChannelItemSortField.cs
@@ -0,0 +1,13 @@
+namespace MediaBrowser.Model.Channels
+{
+ public enum ChannelItemSortField
+ {
+ Name = 0,
+ CommunityRating = 1,
+ PremiereDate = 2,
+ DateCreated = 3,
+ Runtime = 4,
+ PlayCount = 5,
+ CommunityPlayCount = 6
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Channels/ChannelMediaContentType.cs b/MediaBrowser.Model/Channels/ChannelMediaContentType.cs
new file mode 100644
index 000000000..efb5021c0
--- /dev/null
+++ b/MediaBrowser.Model/Channels/ChannelMediaContentType.cs
@@ -0,0 +1,23 @@
+namespace MediaBrowser.Model.Channels
+{
+ public enum ChannelMediaContentType
+ {
+ Clip = 0,
+
+ Podcast = 1,
+
+ Trailer = 2,
+
+ Movie = 3,
+
+ Episode = 4,
+
+ Song = 5,
+
+ MovieExtra = 6,
+
+ TvExtra = 7,
+
+ GameExtra = 8
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Channels/ChannelMediaType.cs b/MediaBrowser.Model/Channels/ChannelMediaType.cs
new file mode 100644
index 000000000..102cb6644
--- /dev/null
+++ b/MediaBrowser.Model/Channels/ChannelMediaType.cs
@@ -0,0 +1,11 @@
+namespace MediaBrowser.Model.Channels
+{
+ public enum ChannelMediaType
+ {
+ Audio = 0,
+
+ Video = 1,
+
+ Photo = 2
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Channels/ChannelQuery.cs b/MediaBrowser.Model/Channels/ChannelQuery.cs
new file mode 100644
index 000000000..6b5e95d69
--- /dev/null
+++ b/MediaBrowser.Model/Channels/ChannelQuery.cs
@@ -0,0 +1,52 @@
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace MediaBrowser.Model.Channels
+{
+ public class ChannelQuery
+ {
+ /// <summary>
+ /// Fields to return within the items, in addition to basic information
+ /// </summary>
+ /// <value>The fields.</value>
+ public ItemFields[] Fields { get; set; }
+ public bool? EnableImages { get; set; }
+ public int? ImageTypeLimit { get; set; }
+ public ImageType[] EnableImageTypes { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user identifier.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Skips over a given number of items within the results. Use for paging.
+ /// </summary>
+ /// <value>The start index.</value>
+ public int? StartIndex { get; set; }
+
+ /// <summary>
+ /// The maximum number of items to return
+ /// </summary>
+ /// <value>The limit.</value>
+ public int? Limit { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [supports latest items].
+ /// </summary>
+ /// <value><c>true</c> if [supports latest items]; otherwise, <c>false</c>.</value>
+ public bool? SupportsLatestItems { get; set; }
+
+ public bool? SupportsMediaDeletion { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is favorite.
+ /// </summary>
+ /// <value><c>null</c> if [is favorite] contains no value, <c>true</c> if [is favorite]; otherwise, <c>false</c>.</value>
+ public bool? IsFavorite { get; set; }
+ public bool? IsRecordingsFolder { get; set; }
+ public bool RefreshLatestChannelItems { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Collections/CollectionCreationResult.cs b/MediaBrowser.Model/Collections/CollectionCreationResult.cs
new file mode 100644
index 000000000..689625ac3
--- /dev/null
+++ b/MediaBrowser.Model/Collections/CollectionCreationResult.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace MediaBrowser.Model.Collections
+{
+ public class CollectionCreationResult
+ {
+ public Guid Id { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/AccessSchedule.cs b/MediaBrowser.Model/Configuration/AccessSchedule.cs
new file mode 100644
index 000000000..3a66cf5bb
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/AccessSchedule.cs
@@ -0,0 +1,22 @@
+
+namespace MediaBrowser.Model.Configuration
+{
+ public class AccessSchedule
+ {
+ /// <summary>
+ /// Gets or sets the day of week.
+ /// </summary>
+ /// <value>The day of week.</value>
+ public DynamicDayOfWeek DayOfWeek { get; set; }
+ /// <summary>
+ /// Gets or sets the start hour.
+ /// </summary>
+ /// <value>The start hour.</value>
+ public double StartHour { get; set; }
+ /// <summary>
+ /// Gets or sets the end hour.
+ /// </summary>
+ /// <value>The end hour.</value>
+ public double EndHour { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs
new file mode 100644
index 000000000..b5b0101cb
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/BaseApplicationConfiguration.cs
@@ -0,0 +1,57 @@
+using MediaBrowser.Model.Updates;
+
+namespace MediaBrowser.Model.Configuration
+{
+ /// <summary>
+ /// Serves as a common base class for the Server and UI application Configurations
+ /// ProtoInclude tells Protobuf about subclasses,
+ /// The number 50 can be any number, so long as it doesn't clash with any of the ProtoMember numbers either here or in subclasses.
+ /// </summary>
+ public class BaseApplicationConfiguration
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether [enable debug level logging].
+ /// </summary>
+ /// <value><c>true</c> if [enable debug level logging]; otherwise, <c>false</c>.</value>
+ public bool EnableDebugLevelLogging { get; set; }
+
+ /// <summary>
+ /// Enable automatically and silently updating of the application
+ /// </summary>
+ /// <value><c>true</c> if [enable auto update]; otherwise, <c>false</c>.</value>
+ public bool EnableAutoUpdate { get; set; }
+
+ /// <summary>
+ /// The number of days we should retain log files
+ /// </summary>
+ /// <value>The log file retention days.</value>
+ public int LogFileRetentionDays { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [run at startup].
+ /// </summary>
+ /// <value><c>true</c> if [run at startup]; otherwise, <c>false</c>.</value>
+ public bool RunAtStartup { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is first run.
+ /// </summary>
+ /// <value><c>true</c> if this instance is first run; otherwise, <c>false</c>.</value>
+ public bool IsStartupWizardCompleted { get; set; }
+
+ /// <summary>
+ /// Gets or sets the cache path.
+ /// </summary>
+ /// <value>The cache path.</value>
+ public string CachePath { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BaseApplicationConfiguration" /> class.
+ /// </summary>
+ public BaseApplicationConfiguration()
+ {
+ EnableAutoUpdate = true;
+ LogFileRetentionDays = 3;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/DynamicDayOfWeek.cs b/MediaBrowser.Model/Configuration/DynamicDayOfWeek.cs
new file mode 100644
index 000000000..1c7de11fd
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/DynamicDayOfWeek.cs
@@ -0,0 +1,17 @@
+
+namespace MediaBrowser.Model.Configuration
+{
+ public enum DynamicDayOfWeek
+ {
+ Sunday = 0,
+ Monday = 1,
+ Tuesday = 2,
+ Wednesday = 3,
+ Thursday = 4,
+ Friday = 5,
+ Saturday = 6,
+ Everyday = 7,
+ Weekday = 8,
+ Weekend = 9
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
new file mode 100644
index 000000000..fbc5e1b37
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -0,0 +1,36 @@
+
+namespace MediaBrowser.Model.Configuration
+{
+ public class EncodingOptions
+ {
+ public int EncodingThreadCount { get; set; }
+ public string TranscodingTempPath { get; set; }
+ public double DownMixAudioBoost { get; set; }
+ public bool EnableThrottling { get; set; }
+ public int ThrottleDelaySeconds { get; set; }
+ public string HardwareAccelerationType { get; set; }
+ public string EncoderAppPath { get; set; }
+ public string VaapiDevice { get; set; }
+ public int H264Crf { get; set; }
+ public string H264Preset { get; set; }
+ public string DeinterlaceMethod { get; set; }
+ public bool EnableHardwareEncoding { get; set; }
+ public bool EnableSubtitleExtraction { get; set; }
+
+ public string[] HardwareDecodingCodecs { get; set; }
+
+ public EncodingOptions()
+ {
+ DownMixAudioBoost = 2;
+ EnableThrottling = true;
+ ThrottleDelaySeconds = 180;
+ EncodingThreadCount = -1;
+ // This is a DRM device that is almost guaranteed to be there on every intel platform, plus it's the default one in ffmpeg if you don't specify anything
+ VaapiDevice = "/dev/dri/renderD128";
+ H264Crf = 23;
+ EnableHardwareEncoding = true;
+ EnableSubtitleExtraction = true;
+ HardwareDecodingCodecs = new string[] { "h264", "vc1" };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/FanartOptions.cs b/MediaBrowser.Model/Configuration/FanartOptions.cs
new file mode 100644
index 000000000..6924b25d7
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/FanartOptions.cs
@@ -0,0 +1,12 @@
+
+namespace MediaBrowser.Model.Configuration
+{
+ public class FanartOptions
+ {
+ /// <summary>
+ /// Gets or sets the user API key.
+ /// </summary>
+ /// <value>The user API key.</value>
+ public string UserApiKey { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/ImageOption.cs b/MediaBrowser.Model/Configuration/ImageOption.cs
new file mode 100644
index 000000000..ade0af83e
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/ImageOption.cs
@@ -0,0 +1,29 @@
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.Configuration
+{
+ public class ImageOption
+ {
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ public ImageType Type { get; set; }
+ /// <summary>
+ /// Gets or sets the limit.
+ /// </summary>
+ /// <value>The limit.</value>
+ public int Limit { get; set; }
+
+ /// <summary>
+ /// Gets or sets the minimum width.
+ /// </summary>
+ /// <value>The minimum width.</value>
+ public int MinWidth { get; set; }
+
+ public ImageOption()
+ {
+ Limit = 1;
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Configuration/ImageSavingConvention.cs b/MediaBrowser.Model/Configuration/ImageSavingConvention.cs
new file mode 100644
index 000000000..611678e67
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/ImageSavingConvention.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Model.Configuration
+{
+ public enum ImageSavingConvention
+ {
+ Legacy,
+ Compatible
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
new file mode 100644
index 000000000..a271d43af
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -0,0 +1,444 @@
+using System;
+using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Configuration
+{
+ public class LibraryOptions
+ {
+ public bool EnableArchiveMediaFiles { get; set; }
+ public bool EnablePhotos { get; set; }
+ public bool EnableRealtimeMonitor { get; set; }
+ public bool EnableChapterImageExtraction { get; set; }
+ public bool ExtractChapterImagesDuringLibraryScan { get; set; }
+ public bool DownloadImagesInAdvance { get; set; }
+ public MediaPathInfo[] PathInfos { get; set; }
+
+ public bool SaveLocalMetadata { get; set; }
+ public bool EnableInternetProviders { get; set; }
+ public bool ImportMissingEpisodes { get; set; }
+ public bool EnableAutomaticSeriesGrouping { get; set; }
+ public bool EnableEmbeddedTitles { get; set; }
+
+ public int AutomaticRefreshIntervalDays { get; set; }
+
+ /// <summary>
+ /// Gets or sets the preferred metadata language.
+ /// </summary>
+ /// <value>The preferred metadata language.</value>
+ public string PreferredMetadataLanguage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the metadata country code.
+ /// </summary>
+ /// <value>The metadata country code.</value>
+ public string MetadataCountryCode { get; set; }
+
+ public string SeasonZeroDisplayName { get; set; }
+ public string[] MetadataSavers { get; set; }
+ public string[] DisabledLocalMetadataReaders { get; set; }
+ public string[] LocalMetadataReaderOrder { get; set; }
+
+ public string[] DisabledSubtitleFetchers { get; set; }
+ public string[] SubtitleFetcherOrder { get; set; }
+
+ public bool SkipSubtitlesIfEmbeddedSubtitlesPresent { get; set; }
+ public bool SkipSubtitlesIfAudioTrackMatches { get; set; }
+ public string[] SubtitleDownloadLanguages { get; set; }
+ public bool RequirePerfectSubtitleMatch { get; set; }
+ public bool SaveSubtitlesWithMedia { get; set; }
+
+ public TypeOptions[] TypeOptions { get; set; }
+
+ public TypeOptions GetTypeOptions(string type)
+ {
+ foreach (var options in TypeOptions)
+ {
+ if (string.Equals(options.Type, type, StringComparison.OrdinalIgnoreCase))
+ {
+ return options;
+ }
+ }
+
+ return null;
+ }
+
+ public LibraryOptions()
+ {
+ TypeOptions = new TypeOptions[] { };
+ DisabledSubtitleFetchers = new string[] { };
+ SubtitleFetcherOrder = new string[] { };
+ DisabledLocalMetadataReaders = new string[] { };
+
+ SkipSubtitlesIfAudioTrackMatches = true;
+ RequirePerfectSubtitleMatch = true;
+
+ EnablePhotos = true;
+ SaveSubtitlesWithMedia = true;
+ EnableRealtimeMonitor = true;
+ PathInfos = new MediaPathInfo[] { };
+ EnableInternetProviders = true;
+ EnableAutomaticSeriesGrouping = true;
+ SeasonZeroDisplayName = "Specials";
+ }
+ }
+
+ public class MediaPathInfo
+ {
+ public string Path { get; set; }
+ public string NetworkPath { get; set; }
+ }
+
+ public class TypeOptions
+ {
+ public string Type { get; set; }
+ public string[] MetadataFetchers { get; set; }
+ public string[] MetadataFetcherOrder { get; set; }
+
+ public string[] ImageFetchers { get; set; }
+ public string[] ImageFetcherOrder { get; set; }
+ public ImageOption[] ImageOptions { get; set; }
+
+ public ImageOption GetImageOptions(ImageType type)
+ {
+ foreach (ImageOption i in ImageOptions)
+ {
+ if (i.Type == type)
+ {
+ return i;
+ }
+ }
+
+ ImageOption[] options;
+ if (DefaultImageOptions.TryGetValue(Type, out options))
+ {
+ foreach (ImageOption i in options)
+ {
+ if (i.Type == type)
+ {
+ return i;
+ }
+ }
+ }
+
+ return DefaultInstance;
+ }
+
+ public int GetLimit(ImageType type)
+ {
+ return GetImageOptions(type).Limit;
+ }
+
+ public int GetMinWidth(ImageType type)
+ {
+ return GetImageOptions(type).MinWidth;
+ }
+
+ public bool IsEnabled(ImageType type)
+ {
+ return GetLimit(type) > 0;
+ }
+
+ public TypeOptions()
+ {
+ MetadataFetchers = new string[] { };
+ MetadataFetcherOrder = new string[] { };
+ ImageFetchers = new string[] { };
+ ImageFetcherOrder = new string[] { };
+ ImageOptions = new ImageOption[] { };
+ }
+
+ public static Dictionary<string, ImageOption[]> DefaultImageOptions = new Dictionary<string, ImageOption[]>
+ {
+ {
+ "Movie", new []
+ {
+ new ImageOption
+ {
+ Limit = 1,
+ MinWidth = 1280,
+ Type = ImageType.Backdrop
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Art
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Disc
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Primary
+ },
+
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Banner
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Thumb
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Logo
+ }
+ }
+ },
+ {
+ "MusicVideo", new []
+ {
+ new ImageOption
+ {
+ Limit = 1,
+ MinWidth = 1280,
+ Type = ImageType.Backdrop
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Art
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Disc
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Primary
+ },
+
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Banner
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Thumb
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Logo
+ }
+ }
+ },
+ {
+ "Series", new []
+ {
+ new ImageOption
+ {
+ Limit = 1,
+ MinWidth = 1280,
+ Type = ImageType.Backdrop
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Art
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Primary
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Banner
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Thumb
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Logo
+ }
+ }
+ },
+ {
+ "MusicAlbum", new []
+ {
+ new ImageOption
+ {
+ Limit = 0,
+ MinWidth = 1280,
+ Type = ImageType.Backdrop
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Disc
+ }
+ }
+ },
+ {
+ "MusicArtist", new []
+ {
+ new ImageOption
+ {
+ Limit = 1,
+ MinWidth = 1280,
+ Type = ImageType.Backdrop
+ },
+
+ // Don't download this by default
+ // They do look great, but most artists won't have them, which means a banner view isn't really possible
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Banner
+ },
+
+ // Don't download this by default
+ // Generally not used
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Art
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Logo
+ }
+ }
+ },
+ {
+ "BoxSet", new []
+ {
+ new ImageOption
+ {
+ Limit = 1,
+ MinWidth = 1280,
+ Type = ImageType.Backdrop
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Primary
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Thumb
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Logo
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Art
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Disc
+ },
+
+ // Don't download this by default as it's rarely used.
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Banner
+ }
+ }
+ },
+ {
+ "Season", new []
+ {
+ new ImageOption
+ {
+ Limit = 0,
+ MinWidth = 1280,
+ Type = ImageType.Backdrop
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Primary
+ },
+
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Banner
+ },
+
+ new ImageOption
+ {
+ Limit = 0,
+ Type = ImageType.Thumb
+ }
+ }
+ },
+ {
+ "Episode", new []
+ {
+ new ImageOption
+ {
+ Limit = 0,
+ MinWidth = 1280,
+ Type = ImageType.Backdrop
+ },
+
+ new ImageOption
+ {
+ Limit = 1,
+ Type = ImageType.Primary
+ }
+ }
+ }
+ };
+
+ public static ImageOption DefaultInstance = new ImageOption();
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/MetadataConfiguration.cs b/MediaBrowser.Model/Configuration/MetadataConfiguration.cs
new file mode 100644
index 000000000..d1658e5d6
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/MetadataConfiguration.cs
@@ -0,0 +1,13 @@
+
+namespace MediaBrowser.Model.Configuration
+{
+ public class MetadataConfiguration
+ {
+ public bool UseFileCreationTimeForDateAdded { get; set; }
+
+ public MetadataConfiguration()
+ {
+ UseFileCreationTimeForDateAdded = true;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/MetadataOptions.cs b/MediaBrowser.Model/Configuration/MetadataOptions.cs
new file mode 100644
index 000000000..26cfee085
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/MetadataOptions.cs
@@ -0,0 +1,38 @@
+using MediaBrowser.Model.Extensions;
+using System;
+
+namespace MediaBrowser.Model.Configuration
+{
+ /// <summary>
+ /// Class MetadataOptions.
+ /// </summary>
+ public class MetadataOptions
+ {
+ public string ItemType { get; set; }
+
+ public string[] DisabledMetadataSavers { get; set; }
+ public string[] LocalMetadataReaderOrder { get; set; }
+
+ public string[] DisabledMetadataFetchers { get; set; }
+ public string[] MetadataFetcherOrder { get; set; }
+
+ public string[] DisabledImageFetchers { get; set; }
+ public string[] ImageFetcherOrder { get; set; }
+
+ public MetadataOptions()
+ {
+ DisabledMetadataSavers = new string[] { };
+ LocalMetadataReaderOrder = new string[] { };
+
+ DisabledMetadataFetchers = new string[] { };
+ MetadataFetcherOrder = new string[] { };
+ DisabledImageFetchers = new string[] { };
+ ImageFetcherOrder = new string[] { };
+ }
+
+ public bool IsMetadataSaverEnabled(string name)
+ {
+ return !ListHelper.ContainsIgnoreCase(DisabledMetadataSavers, name);
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/MetadataPlugin.cs b/MediaBrowser.Model/Configuration/MetadataPlugin.cs
new file mode 100644
index 000000000..f3e0ce106
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/MetadataPlugin.cs
@@ -0,0 +1,17 @@
+namespace MediaBrowser.Model.Configuration
+{
+ public class MetadataPlugin
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ public MetadataPluginType Type { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/MetadataPluginSummary.cs b/MediaBrowser.Model/Configuration/MetadataPluginSummary.cs
new file mode 100644
index 000000000..80142cf43
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/MetadataPluginSummary.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.Configuration
+{
+ public class MetadataPluginSummary
+ {
+ /// <summary>
+ /// Gets or sets the type of the item.
+ /// </summary>
+ /// <value>The type of the item.</value>
+ public string ItemType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the plugins.
+ /// </summary>
+ /// <value>The plugins.</value>
+ public MetadataPlugin[] Plugins { get; set; }
+
+ /// <summary>
+ /// Gets or sets the supported image types.
+ /// </summary>
+ /// <value>The supported image types.</value>
+ public ImageType[] SupportedImageTypes { get; set; }
+
+ public MetadataPluginSummary()
+ {
+ SupportedImageTypes = new ImageType[] { };
+ Plugins = new MetadataPlugin[] { };
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Configuration/MetadataPluginType.cs b/MediaBrowser.Model/Configuration/MetadataPluginType.cs
new file mode 100644
index 000000000..5ba0b395e
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/MetadataPluginType.cs
@@ -0,0 +1,16 @@
+namespace MediaBrowser.Model.Configuration
+{
+ /// <summary>
+ /// Enum MetadataPluginType
+ /// </summary>
+ public enum MetadataPluginType
+ {
+ LocalImageProvider,
+ ImageFetcher,
+ ImageSaver,
+ LocalMetadataProvider,
+ MetadataFetcher,
+ MetadataSaver,
+ SubtitleFetcher
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
new file mode 100644
index 000000000..58a839167
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -0,0 +1,313 @@
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using System;
+
+namespace MediaBrowser.Model.Configuration
+{
+ /// <summary>
+ /// Represents the server configuration.
+ /// </summary>
+ public class ServerConfiguration : BaseApplicationConfiguration
+ {
+ public const int DefaultHttpPort = 8096;
+ public const int DefaultHttpsPort = 8920;
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [enable u pn p].
+ /// </summary>
+ /// <value><c>true</c> if [enable u pn p]; otherwise, <c>false</c>.</value>
+ public bool EnableUPnP { get; set; }
+
+ /// <summary>
+ /// Gets or sets the public mapped port.
+ /// </summary>
+ /// <value>The public mapped port.</value>
+ public int PublicPort { get; set; }
+
+ /// <summary>
+ /// Gets or sets the public HTTPS port.
+ /// </summary>
+ /// <value>The public HTTPS port.</value>
+ public int PublicHttpsPort { get; set; }
+
+ /// <summary>
+ /// Gets or sets the HTTP server port number.
+ /// </summary>
+ /// <value>The HTTP server port number.</value>
+ public int HttpServerPortNumber { get; set; }
+
+ /// <summary>
+ /// Gets or sets the HTTPS server port number.
+ /// </summary>
+ /// <value>The HTTPS server port number.</value>
+ public int HttpsPortNumber { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [use HTTPS].
+ /// </summary>
+ /// <value><c>true</c> if [use HTTPS]; otherwise, <c>false</c>.</value>
+ public bool EnableHttps { get; set; }
+ public bool EnableNormalizedItemByNameIds { get; set; }
+
+ /// <summary>
+ /// Gets or sets the value pointing to the file system where the ssl certiifcate is located..
+ /// </summary>
+ /// <value>The value pointing to the file system where the ssl certiifcate is located..</value>
+ public string CertificatePath { get; set; }
+ public string CertificatePassword { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is port authorized.
+ /// </summary>
+ /// <value><c>true</c> if this instance is port authorized; otherwise, <c>false</c>.</value>
+ public bool IsPortAuthorized { get; set; }
+
+ public bool AutoRunWebApp { get; set; }
+ public bool EnableRemoteAccess { get; set; }
+ public bool CameraUploadUpgraded { get; set; }
+ public bool CollectionsUpgraded { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [enable case sensitive item ids].
+ /// </summary>
+ /// <value><c>true</c> if [enable case sensitive item ids]; otherwise, <c>false</c>.</value>
+ public bool EnableCaseSensitiveItemIds { get; set; }
+
+ public bool DisableLiveTvChannelUserDataName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the metadata path.
+ /// </summary>
+ /// <value>The metadata path.</value>
+ public string MetadataPath { get; set; }
+ public string MetadataNetworkPath { get; set; }
+
+ /// <summary>
+ /// Gets or sets the preferred metadata language.
+ /// </summary>
+ /// <value>The preferred metadata language.</value>
+ public string PreferredMetadataLanguage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the metadata country code.
+ /// </summary>
+ /// <value>The metadata country code.</value>
+ public string MetadataCountryCode { get; set; }
+
+ /// <summary>
+ /// Characters to be replaced with a ' ' in strings to create a sort name
+ /// </summary>
+ /// <value>The sort replace characters.</value>
+ public string[] SortReplaceCharacters { get; set; }
+
+ /// <summary>
+ /// Characters to be removed from strings to create a sort name
+ /// </summary>
+ /// <value>The sort remove characters.</value>
+ public string[] SortRemoveCharacters { get; set; }
+
+ /// <summary>
+ /// Words to be removed from strings to create a sort name
+ /// </summary>
+ /// <value>The sort remove words.</value>
+ public string[] SortRemoveWords { get; set; }
+
+ /// <summary>
+ /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated.
+ /// </summary>
+ /// <value>The min resume PCT.</value>
+ public int MinResumePct { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched.
+ /// </summary>
+ /// <value>The max resume PCT.</value>
+ public int MaxResumePct { get; set; }
+
+ /// <summary>
+ /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates..
+ /// </summary>
+ /// <value>The min resume duration seconds.</value>
+ public int MinResumeDurationSeconds { get; set; }
+
+ /// <summary>
+ /// The delay in seconds that we will wait after a file system change to try and discover what has been added/removed
+ /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several
+ /// different directories and files.
+ /// </summary>
+ /// <value>The file watcher delay.</value>
+ public int LibraryMonitorDelay { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [enable dashboard response caching].
+ /// Allows potential contributors without visual studio to modify production dashboard code and test changes.
+ /// </summary>
+ /// <value><c>true</c> if [enable dashboard response caching]; otherwise, <c>false</c>.</value>
+ public bool EnableDashboardResponseCaching { get; set; }
+
+ /// <summary>
+ /// Allows the dashboard to be served from a custom path.
+ /// </summary>
+ /// <value>The dashboard source path.</value>
+ public string DashboardSourcePath { get; set; }
+
+ /// <summary>
+ /// Gets or sets the image saving convention.
+ /// </summary>
+ /// <value>The image saving convention.</value>
+ public ImageSavingConvention ImageSavingConvention { get; set; }
+
+ public MetadataOptions[] MetadataOptions { get; set; }
+
+ public bool EnableAutomaticRestart { get; set; }
+ public bool SkipDeserializationForBasicTypes { get; set; }
+
+ public string ServerName { get; set; }
+ public string WanDdns { get; set; }
+
+ public string UICulture { get; set; }
+
+ public bool SaveMetadataHidden { get; set; }
+
+ public NameValuePair[] ContentTypes { get; set; }
+
+ public int RemoteClientBitrateLimit { get; set; }
+
+ public int SchemaVersion { get; set; }
+
+ public bool EnableAnonymousUsageReporting { get; set; }
+ public bool EnableFolderView { get; set; }
+ public bool EnableGroupingIntoCollections { get; set; }
+ public bool DisplaySpecialsWithinSeasons { get; set; }
+ public string[] LocalNetworkSubnets { get; set; }
+ public string[] LocalNetworkAddresses { get; set; }
+ public string[] CodecsUsed { get; set; }
+ public bool EnableExternalContentInSuggestions { get; set; }
+ public bool RequireHttps { get; set; }
+ public bool IsBehindProxy { get; set; }
+ public bool EnableNewOmdbSupport { get; set; }
+
+ public string[] RemoteIPFilter { get; set; }
+ public bool IsRemoteIPFilterBlacklist { get; set; }
+
+ public int ImageExtractionTimeoutMs { get; set; }
+
+ public PathSubstitution[] PathSubstitutions { get; set; }
+ public bool EnableSimpleArtistDetection { get; set; }
+
+ public string[] UninstalledPlugins { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
+ /// </summary>
+ public ServerConfiguration()
+ {
+ UninstalledPlugins = new string[] { };
+ RemoteIPFilter = new string[] { };
+ LocalNetworkSubnets = new string[] { };
+ LocalNetworkAddresses = new string[] { };
+ CodecsUsed = new string[] { };
+ ImageExtractionTimeoutMs = 0;
+ PathSubstitutions = new PathSubstitution[] { };
+ EnableSimpleArtistDetection = true;
+
+ DisplaySpecialsWithinSeasons = true;
+ EnableExternalContentInSuggestions = true;
+
+ ImageSavingConvention = ImageSavingConvention.Compatible;
+ PublicPort = DefaultHttpPort;
+ PublicHttpsPort = DefaultHttpsPort;
+ HttpServerPortNumber = DefaultHttpPort;
+ HttpsPortNumber = DefaultHttpsPort;
+ EnableHttps = true;
+ EnableDashboardResponseCaching = true;
+ EnableAnonymousUsageReporting = true;
+ EnableCaseSensitiveItemIds = true;
+
+ EnableAutomaticRestart = true;
+ AutoRunWebApp = true;
+ EnableRemoteAccess = true;
+
+ EnableUPnP = true;
+ MinResumePct = 5;
+ MaxResumePct = 90;
+
+ // 5 minutes
+ MinResumeDurationSeconds = 300;
+
+ LibraryMonitorDelay = 60;
+
+ ContentTypes = new NameValuePair[] { };
+
+ PreferredMetadataLanguage = "en";
+ MetadataCountryCode = "US";
+
+ SortReplaceCharacters = new[] { ".", "+", "%" };
+ SortRemoveCharacters = new[] { ",", "&", "-", "{", "}", "'" };
+ SortRemoveWords = new[] { "the", "a", "an" };
+
+ UICulture = "en-us";
+
+ MetadataOptions = new[]
+ {
+ new MetadataOptions {ItemType = "Book"},
+
+ new MetadataOptions
+ {
+ ItemType = "Movie"
+ },
+
+ new MetadataOptions
+ {
+ ItemType = "MusicVideo",
+ DisabledMetadataFetchers = new []{ "The Open Movie Database" },
+ DisabledImageFetchers = new []{ "The Open Movie Database", "FanArt" }
+ },
+
+ new MetadataOptions
+ {
+ ItemType = "Series",
+ DisabledMetadataFetchers = new []{ "TheMovieDb" },
+ DisabledImageFetchers = new []{ "TheMovieDb" }
+ },
+
+ new MetadataOptions
+ {
+ ItemType = "MusicAlbum",
+ DisabledMetadataFetchers = new []{ "TheAudioDB" }
+ },
+
+ new MetadataOptions
+ {
+ ItemType = "MusicArtist",
+ DisabledMetadataFetchers = new []{ "TheAudioDB" }
+ },
+
+ new MetadataOptions
+ {
+ ItemType = "BoxSet"
+ },
+
+ new MetadataOptions
+ {
+ ItemType = "Season",
+ DisabledMetadataFetchers = new []{ "TheMovieDb" },
+ DisabledImageFetchers = new [] { "FanArt" }
+ },
+
+ new MetadataOptions
+ {
+ ItemType = "Episode",
+ DisabledMetadataFetchers = new []{ "The Open Movie Database", "TheMovieDb" },
+ DisabledImageFetchers = new []{ "The Open Movie Database", "TheMovieDb" }
+ }
+ };
+ }
+ }
+
+ public class PathSubstitution
+ {
+ public string From { get; set; }
+ public string To { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs b/MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs
new file mode 100644
index 000000000..fbee912d9
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs
@@ -0,0 +1,11 @@
+namespace MediaBrowser.Model.Configuration
+{
+ public enum SubtitlePlaybackMode
+ {
+ Default = 0,
+ Always = 1,
+ OnlyForced = 2,
+ None = 3,
+ Smart = 4
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Configuration/UnratedItem.cs b/MediaBrowser.Model/Configuration/UnratedItem.cs
new file mode 100644
index 000000000..1082d684b
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/UnratedItem.cs
@@ -0,0 +1,16 @@
+namespace MediaBrowser.Model.Configuration
+{
+ public enum UnratedItem
+ {
+ Movie,
+ Trailer,
+ Series,
+ Music,
+ Game,
+ Book,
+ LiveTvChannel,
+ LiveTvProgram,
+ ChannelContent,
+ Other
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs
new file mode 100644
index 000000000..34b0be095
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs
@@ -0,0 +1,66 @@
+using System;
+
+namespace MediaBrowser.Model.Configuration
+{
+ /// <summary>
+ /// Class UserConfiguration
+ /// </summary>
+ public class UserConfiguration
+ {
+ /// <summary>
+ /// Gets or sets the audio language preference.
+ /// </summary>
+ /// <value>The audio language preference.</value>
+ public string AudioLanguagePreference { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [play default audio track].
+ /// </summary>
+ /// <value><c>true</c> if [play default audio track]; otherwise, <c>false</c>.</value>
+ public bool PlayDefaultAudioTrack { get; set; }
+
+ /// <summary>
+ /// Gets or sets the subtitle language preference.
+ /// </summary>
+ /// <value>The subtitle language preference.</value>
+ public string SubtitleLanguagePreference { get; set; }
+
+ public bool DisplayMissingEpisodes { get; set; }
+
+ public string[] GroupedFolders { get; set; }
+
+ public SubtitlePlaybackMode SubtitleMode { get; set; }
+ public bool DisplayCollectionsView { get; set; }
+
+ public bool EnableLocalPassword { get; set; }
+
+ public string[] OrderedViews { get; set; }
+
+ public string[] LatestItemsExcludes { get; set; }
+ public string[] MyMediaExcludes { get; set; }
+
+ public bool HidePlayedInLatest { get; set; }
+
+ public bool RememberAudioSelections { get; set; }
+ public bool RememberSubtitleSelections { get; set; }
+ public bool EnableNextEpisodeAutoPlay { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserConfiguration" /> class.
+ /// </summary>
+ public UserConfiguration()
+ {
+ EnableNextEpisodeAutoPlay = true;
+ RememberAudioSelections = true;
+ RememberSubtitleSelections = true;
+
+ HidePlayedInLatest = true;
+ PlayDefaultAudioTrack = true;
+
+ LatestItemsExcludes = new string[] {};
+ OrderedViews = new string[] {};
+ MyMediaExcludes = new string[] {};
+ GroupedFolders = new string[] {};
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs b/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs
new file mode 100644
index 000000000..de8a59a3d
--- /dev/null
+++ b/MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs
@@ -0,0 +1,23 @@
+
+namespace MediaBrowser.Model.Configuration
+{
+ public class XbmcMetadataOptions
+ {
+ public string UserId { get; set; }
+
+ public string ReleaseDateFormat { get; set; }
+
+ public bool SaveImagePathsInNfo { get; set; }
+ public bool EnablePathSubstitution { get; set; }
+
+ public bool EnableExtraThumbsDuplication { get; set; }
+
+ public XbmcMetadataOptions()
+ {
+ ReleaseDateFormat = "yyyy-MM-dd";
+
+ SaveImagePathsInNfo = true;
+ EnablePathSubstitution = true;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Connect/ConnectAuthenticationExchangeResult.cs b/MediaBrowser.Model/Connect/ConnectAuthenticationExchangeResult.cs
new file mode 100644
index 000000000..c60224045
--- /dev/null
+++ b/MediaBrowser.Model/Connect/ConnectAuthenticationExchangeResult.cs
@@ -0,0 +1,17 @@
+
+namespace MediaBrowser.Model.Connect
+{
+ public class ConnectAuthenticationExchangeResult
+ {
+ /// <summary>
+ /// Gets or sets the local user identifier.
+ /// </summary>
+ /// <value>The local user identifier.</value>
+ public string LocalUserId { get; set; }
+ /// <summary>
+ /// Gets or sets the access token.
+ /// </summary>
+ /// <value>The access token.</value>
+ public string AccessToken { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Connect/ConnectAuthenticationResult.cs b/MediaBrowser.Model/Connect/ConnectAuthenticationResult.cs
new file mode 100644
index 000000000..0df035e57
--- /dev/null
+++ b/MediaBrowser.Model/Connect/ConnectAuthenticationResult.cs
@@ -0,0 +1,17 @@
+
+namespace MediaBrowser.Model.Connect
+{
+ public class ConnectAuthenticationResult
+ {
+ /// <summary>
+ /// Gets or sets the user.
+ /// </summary>
+ /// <value>The user.</value>
+ public ConnectUser User { get; set; }
+ /// <summary>
+ /// Gets or sets the access token.
+ /// </summary>
+ /// <value>The access token.</value>
+ public string AccessToken { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Connect/ConnectAuthorization.cs b/MediaBrowser.Model/Connect/ConnectAuthorization.cs
new file mode 100644
index 000000000..84dbf5c36
--- /dev/null
+++ b/MediaBrowser.Model/Connect/ConnectAuthorization.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace MediaBrowser.Model.Connect
+{
+ public class ConnectAuthorization
+ {
+ public string ConnectUserId { get; set; }
+ public string UserName { get; set; }
+ public string ImageUrl { get; set; }
+ public string Id { get; set; }
+ public string[] EnabledLibraries { get; set; }
+ public bool EnableLiveTv { get; set; }
+ public string[] EnabledChannels { get; set; }
+
+ public ConnectAuthorization()
+ {
+ EnabledLibraries = new string[] {};
+ EnabledChannels = new string[] {};
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Connect/ConnectAuthorizationRequest.cs b/MediaBrowser.Model/Connect/ConnectAuthorizationRequest.cs
new file mode 100644
index 000000000..ba4e9f5cf
--- /dev/null
+++ b/MediaBrowser.Model/Connect/ConnectAuthorizationRequest.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace MediaBrowser.Model.Connect
+{
+ public class ConnectAuthorizationRequest
+ {
+ public string SendingUserId { get; set; }
+ public string ConnectUserName { get; set; }
+ public string[] EnabledLibraries { get; set; }
+ public bool EnableLiveTv { get; set; }
+ public string[] EnabledChannels { get; set; }
+
+ public ConnectAuthorizationRequest()
+ {
+ EnabledLibraries = new string[] {};
+ EnabledChannels = new string[] {};
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Connect/ConnectUser.cs b/MediaBrowser.Model/Connect/ConnectUser.cs
new file mode 100644
index 000000000..da290da12
--- /dev/null
+++ b/MediaBrowser.Model/Connect/ConnectUser.cs
@@ -0,0 +1,12 @@
+
+namespace MediaBrowser.Model.Connect
+{
+ public class ConnectUser
+ {
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public string Email { get; set; }
+ public bool IsActive { get; set; }
+ public string ImageUrl { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Connect/ConnectUserQuery.cs b/MediaBrowser.Model/Connect/ConnectUserQuery.cs
new file mode 100644
index 000000000..a7dc649a8
--- /dev/null
+++ b/MediaBrowser.Model/Connect/ConnectUserQuery.cs
@@ -0,0 +1,11 @@
+
+namespace MediaBrowser.Model.Connect
+{
+ public class ConnectUserQuery
+ {
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public string Email { get; set; }
+ public string NameOrEmail { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Connect/UserLinkType.cs b/MediaBrowser.Model/Connect/UserLinkType.cs
new file mode 100644
index 000000000..4ac5bfde1
--- /dev/null
+++ b/MediaBrowser.Model/Connect/UserLinkType.cs
@@ -0,0 +1,15 @@
+
+namespace MediaBrowser.Model.Connect
+{
+ public enum UserLinkType
+ {
+ /// <summary>
+ /// The linked user
+ /// </summary>
+ LinkedUser = 0,
+ /// <summary>
+ /// The guest
+ /// </summary>
+ Guest = 1
+ }
+}
diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs
new file mode 100644
index 000000000..7a82dee52
--- /dev/null
+++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs
@@ -0,0 +1,13 @@
+using System;
+using System.IO;
+
+namespace MediaBrowser.Model.Cryptography
+{
+ public interface ICryptoProvider
+ {
+ Guid GetMD5(string str);
+ byte[] ComputeMD5(Stream str);
+ byte[] ComputeMD5(byte[] bytes);
+ byte[] ComputeSHA1(byte[] bytes);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Devices/ContentUploadHistory.cs b/MediaBrowser.Model/Devices/ContentUploadHistory.cs
new file mode 100644
index 000000000..2b344df24
--- /dev/null
+++ b/MediaBrowser.Model/Devices/ContentUploadHistory.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Devices
+{
+ public class ContentUploadHistory
+ {
+ public string DeviceId { get; set; }
+ public LocalFileInfo[] FilesUploaded { get; set; }
+
+ public ContentUploadHistory()
+ {
+ FilesUploaded = new LocalFileInfo[] { };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Devices/DeviceInfo.cs b/MediaBrowser.Model/Devices/DeviceInfo.cs
new file mode 100644
index 000000000..590dc5eb3
--- /dev/null
+++ b/MediaBrowser.Model/Devices/DeviceInfo.cs
@@ -0,0 +1,69 @@
+using MediaBrowser.Model.Session;
+using System;
+
+namespace MediaBrowser.Model.Devices
+{
+ public class DeviceInfo
+ {
+ /// <summary>
+ /// Gets or sets the name of the reported.
+ /// </summary>
+ /// <value>The name of the reported.</value>
+ public string ReportedName { get; set; }
+ /// <summary>
+ /// Gets or sets the name of the custom.
+ /// </summary>
+ /// <value>The name of the custom.</value>
+ public string CustomName { get; set; }
+ /// <summary>
+ /// Gets or sets the camera upload path.
+ /// </summary>
+ /// <value>The camera upload path.</value>
+ public string CameraUploadPath { get; set; }
+
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the identifier.
+ /// </summary>
+ /// <value>The identifier.</value>
+ public string Id { get; set; }
+ /// <summary>
+ /// Gets or sets the last name of the user.
+ /// </summary>
+ /// <value>The last name of the user.</value>
+ public string LastUserName { get; set; }
+ /// <summary>
+ /// Gets or sets the name of the application.
+ /// </summary>
+ /// <value>The name of the application.</value>
+ public string AppName { get; set; }
+ /// <summary>
+ /// Gets or sets the application version.
+ /// </summary>
+ /// <value>The application version.</value>
+ public string AppVersion { get; set; }
+ /// <summary>
+ /// Gets or sets the last user identifier.
+ /// </summary>
+ /// <value>The last user identifier.</value>
+ public Guid LastUserId { get; set; }
+ /// <summary>
+ /// Gets or sets the date last modified.
+ /// </summary>
+ /// <value>The date last modified.</value>
+ public DateTime DateLastActivity { get; set; }
+ /// <summary>
+ /// Gets or sets the capabilities.
+ /// </summary>
+ /// <value>The capabilities.</value>
+ public ClientCapabilities Capabilities { get; set; }
+
+ public DeviceInfo()
+ {
+ Capabilities = new ClientCapabilities();
+ }
+
+ public string IconUrl { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Devices/DeviceQuery.cs b/MediaBrowser.Model/Devices/DeviceQuery.cs
new file mode 100644
index 000000000..9ceea1ea8
--- /dev/null
+++ b/MediaBrowser.Model/Devices/DeviceQuery.cs
@@ -0,0 +1,22 @@
+
+namespace MediaBrowser.Model.Devices
+{
+ public class DeviceQuery
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether [supports unique identifier].
+ /// </summary>
+ /// <value><c>null</c> if [supports unique identifier] contains no value, <c>true</c> if [supports unique identifier]; otherwise, <c>false</c>.</value>
+ public bool? SupportsPersistentIdentifier { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether [supports synchronize].
+ /// </summary>
+ /// <value><c>null</c> if [supports synchronize] contains no value, <c>true</c> if [supports synchronize]; otherwise, <c>false</c>.</value>
+ public bool? SupportsSync { get; set; }
+ /// <summary>
+ /// Gets or sets the user identifier.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public string UserId { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Devices/DevicesOptions.cs b/MediaBrowser.Model/Devices/DevicesOptions.cs
new file mode 100644
index 000000000..5bcdd1a82
--- /dev/null
+++ b/MediaBrowser.Model/Devices/DevicesOptions.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace MediaBrowser.Model.Devices
+{
+ public class DevicesOptions
+ {
+ public string[] EnabledCameraUploadDevices { get; set; }
+ public string CameraUploadPath { get; set; }
+ public bool EnableCameraUploadSubfolders { get; set; }
+
+ public DevicesOptions()
+ {
+ EnabledCameraUploadDevices = new string[] {};
+ }
+ }
+
+ public class DeviceOptions
+ {
+ public string[] EnabledCameraUploadDevices { get; set; }
+ public string CameraUploadPath { get; set; }
+ public bool EnableCameraUploadSubfolders { get; set; }
+ public string CustomName { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Devices/LocalFileInfo.cs b/MediaBrowser.Model/Devices/LocalFileInfo.cs
new file mode 100644
index 000000000..e7a78bf8b
--- /dev/null
+++ b/MediaBrowser.Model/Devices/LocalFileInfo.cs
@@ -0,0 +1,11 @@
+
+namespace MediaBrowser.Model.Devices
+{
+ public class LocalFileInfo
+ {
+ public string Name { get; set; }
+ public string Id { get; set; }
+ public string Album { get; set; }
+ public string MimeType { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Diagnostics/IProcess.cs b/MediaBrowser.Model/Diagnostics/IProcess.cs
new file mode 100644
index 000000000..7cd26af00
--- /dev/null
+++ b/MediaBrowser.Model/Diagnostics/IProcess.cs
@@ -0,0 +1,21 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Model.Diagnostics
+{
+ public interface IProcess : IDisposable
+ {
+ event EventHandler Exited;
+
+ void Kill();
+ bool WaitForExit(int timeMs);
+ Task<bool> WaitForExitAsync(int timeMs);
+ int ExitCode { get; }
+ void Start();
+ StreamWriter StandardInput { get; }
+ StreamReader StandardError { get; }
+ StreamReader StandardOutput { get; }
+ ProcessOptions StartInfo { get; }
+ }
+}
diff --git a/MediaBrowser.Model/Diagnostics/IProcessFactory.cs b/MediaBrowser.Model/Diagnostics/IProcessFactory.cs
new file mode 100644
index 000000000..a60c4b4fb
--- /dev/null
+++ b/MediaBrowser.Model/Diagnostics/IProcessFactory.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace MediaBrowser.Model.Diagnostics
+{
+ public interface IProcessFactory
+ {
+ IProcess Create(ProcessOptions options);
+ }
+
+ public class ProcessOptions
+ {
+ public String FileName { get; set; }
+ public String Arguments { get; set; }
+ public String WorkingDirectory { get; set; }
+ public bool CreateNoWindow { get; set; }
+ public bool UseShellExecute { get; set; }
+ public bool EnableRaisingEvents { get; set; }
+ public bool ErrorDialog { get; set; }
+ public bool RedirectStandardError { get; set; }
+ public bool RedirectStandardInput { get; set; }
+ public bool RedirectStandardOutput { get; set; }
+ public bool IsHidden { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/AudioOptions.cs b/MediaBrowser.Model/Dlna/AudioOptions.cs
new file mode 100644
index 000000000..189f64635
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/AudioOptions.cs
@@ -0,0 +1,87 @@
+using MediaBrowser.Model.Dto;
+using System.Collections.Generic;
+using System;
+
+namespace MediaBrowser.Model.Dlna
+{
+ /// <summary>
+ /// Class AudioOptions.
+ /// </summary>
+ public class AudioOptions
+ {
+ public AudioOptions()
+ {
+ Context = EncodingContext.Streaming;
+
+ EnableDirectPlay = true;
+ EnableDirectStream = true;
+ }
+
+ public bool EnableDirectPlay { get; set; }
+ public bool EnableDirectStream { get; set; }
+ public bool ForceDirectPlay { get; set; }
+ public bool ForceDirectStream { get; set; }
+
+ public Guid ItemId { get; set; }
+ public MediaSourceInfo[] MediaSources { get; set; }
+ public DeviceProfile Profile { get; set; }
+
+ /// <summary>
+ /// Optional. Only needed if a specific AudioStreamIndex or SubtitleStreamIndex are requested.
+ /// </summary>
+ public string MediaSourceId { get; set; }
+
+ public string DeviceId { get; set; }
+
+ /// <summary>
+ /// Allows an override of supported number of audio channels
+ /// Example: DeviceProfile supports five channel, but user only has stereo speakers
+ /// </summary>
+ public int? MaxAudioChannels { get; set; }
+
+ /// <summary>
+ /// The application's configured quality setting
+ /// </summary>
+ public long? MaxBitrate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the context.
+ /// </summary>
+ /// <value>The context.</value>
+ public EncodingContext Context { get; set; }
+
+ /// <summary>
+ /// Gets or sets the audio transcoding bitrate.
+ /// </summary>
+ /// <value>The audio transcoding bitrate.</value>
+ public int? AudioTranscodingBitrate { get; set; }
+
+ /// <summary>
+ /// Gets the maximum bitrate.
+ /// </summary>
+ /// <returns>System.Nullable&lt;System.Int32&gt;.</returns>
+ public long? GetMaxBitrate(bool isAudio)
+ {
+ if (MaxBitrate.HasValue)
+ {
+ return MaxBitrate;
+ }
+
+ if (Profile != null)
+ {
+ if (Context == EncodingContext.Static)
+ {
+ if (isAudio && Profile.MaxStaticMusicBitrate.HasValue)
+ {
+ return Profile.MaxStaticMusicBitrate;
+ }
+ return Profile.MaxStaticBitrate;
+ }
+
+ return Profile.MaxStreamingBitrate;
+ }
+
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/CodecProfile.cs b/MediaBrowser.Model/Dlna/CodecProfile.cs
new file mode 100644
index 000000000..6d143962d
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/CodecProfile.cs
@@ -0,0 +1,68 @@
+using MediaBrowser.Model.Extensions;
+using System.Collections.Generic;
+using System.Xml.Serialization;
+using MediaBrowser.Model.Dlna;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class CodecProfile
+ {
+ [XmlAttribute("type")]
+ public CodecType Type { get; set; }
+
+ public ProfileCondition[] Conditions { get; set; }
+
+ public ProfileCondition[] ApplyConditions { get; set; }
+
+ [XmlAttribute("codec")]
+ public string Codec { get; set; }
+
+ [XmlAttribute("container")]
+ public string Container { get; set; }
+
+ public CodecProfile()
+ {
+ Conditions = new ProfileCondition[] {};
+ ApplyConditions = new ProfileCondition[] { };
+ }
+
+ public string[] GetCodecs()
+ {
+ return ContainerProfile.SplitValue(Codec);
+ }
+
+ private bool ContainsContainer(string container)
+ {
+ return ContainerProfile.ContainsContainer(Container, container);
+ }
+
+ public bool ContainsAnyCodec(string codec, string container)
+ {
+ return ContainsAnyCodec(ContainerProfile.SplitValue(codec), container);
+ }
+
+ public bool ContainsAnyCodec(string[] codec, string container)
+ {
+ if (!ContainsContainer(container))
+ {
+ return false;
+ }
+
+ var codecs = GetCodecs();
+ if (codecs.Length == 0)
+ {
+ return true;
+ }
+
+ foreach (var val in codec)
+ {
+ if (ListHelper.ContainsIgnoreCase(codecs, val))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/CodecType.cs b/MediaBrowser.Model/Dlna/CodecType.cs
new file mode 100644
index 000000000..415cae7ac
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/CodecType.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Model.Dlna
+{
+ public enum CodecType
+ {
+ Video = 0,
+ VideoAudio = 1,
+ Audio = 2
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
new file mode 100644
index 000000000..a550ee982
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs
@@ -0,0 +1,284 @@
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.MediaInfo;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class ConditionProcessor
+ {
+ public bool IsVideoConditionSatisfied(ProfileCondition condition,
+ int? width,
+ int? height,
+ int? videoBitDepth,
+ int? videoBitrate,
+ string videoProfile,
+ double? videoLevel,
+ float? videoFramerate,
+ int? packetLength,
+ TransportStreamTimestamp? timestamp,
+ bool? isAnamorphic,
+ bool? isInterlaced,
+ int? refFrames,
+ int? numVideoStreams,
+ int? numAudioStreams,
+ string videoCodecTag,
+ bool? isAvc )
+ {
+ switch (condition.Property)
+ {
+ case ProfileConditionValue.IsInterlaced:
+ return IsConditionSatisfied(condition, isInterlaced);
+ case ProfileConditionValue.IsAnamorphic:
+ return IsConditionSatisfied(condition, isAnamorphic);
+ case ProfileConditionValue.IsAvc:
+ return IsConditionSatisfied(condition, isAvc);
+ case ProfileConditionValue.VideoFramerate:
+ return IsConditionSatisfied(condition, videoFramerate);
+ case ProfileConditionValue.VideoLevel:
+ return IsConditionSatisfied(condition, videoLevel);
+ case ProfileConditionValue.VideoProfile:
+ return IsConditionSatisfied(condition, videoProfile);
+ case ProfileConditionValue.VideoCodecTag:
+ return IsConditionSatisfied(condition, videoCodecTag);
+ case ProfileConditionValue.PacketLength:
+ return IsConditionSatisfied(condition, packetLength);
+ case ProfileConditionValue.VideoBitDepth:
+ return IsConditionSatisfied(condition, videoBitDepth);
+ case ProfileConditionValue.VideoBitrate:
+ return IsConditionSatisfied(condition, videoBitrate);
+ case ProfileConditionValue.Height:
+ return IsConditionSatisfied(condition, height);
+ case ProfileConditionValue.Width:
+ return IsConditionSatisfied(condition, width);
+ case ProfileConditionValue.RefFrames:
+ return IsConditionSatisfied(condition, refFrames);
+ case ProfileConditionValue.NumAudioStreams:
+ return IsConditionSatisfied(condition, numAudioStreams);
+ case ProfileConditionValue.NumVideoStreams:
+ return IsConditionSatisfied(condition, numVideoStreams);
+ case ProfileConditionValue.VideoTimestamp:
+ return IsConditionSatisfied(condition, timestamp);
+ default:
+ return true;
+ }
+ }
+
+ public bool IsImageConditionSatisfied(ProfileCondition condition, int? width, int? height)
+ {
+ switch (condition.Property)
+ {
+ case ProfileConditionValue.Height:
+ return IsConditionSatisfied(condition, height);
+ case ProfileConditionValue.Width:
+ return IsConditionSatisfied(condition, width);
+ default:
+ throw new ArgumentException("Unexpected condition on image file: " + condition.Property);
+ }
+ }
+
+ public bool IsAudioConditionSatisfied(ProfileCondition condition, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth)
+ {
+ switch (condition.Property)
+ {
+ case ProfileConditionValue.AudioBitrate:
+ return IsConditionSatisfied(condition, audioBitrate);
+ case ProfileConditionValue.AudioChannels:
+ return IsConditionSatisfied(condition, audioChannels);
+ case ProfileConditionValue.AudioSampleRate:
+ return IsConditionSatisfied(condition, audioSampleRate);
+ case ProfileConditionValue.AudioBitDepth:
+ return IsConditionSatisfied(condition, audioBitDepth);
+ default:
+ throw new ArgumentException("Unexpected condition on audio file: " + condition.Property);
+ }
+ }
+
+ public bool IsVideoAudioConditionSatisfied(ProfileCondition condition,
+ int? audioChannels,
+ int? audioBitrate,
+ int? audioSampleRate,
+ int? audioBitDepth,
+ string audioProfile,
+ bool? isSecondaryTrack)
+ {
+ switch (condition.Property)
+ {
+ case ProfileConditionValue.AudioProfile:
+ return IsConditionSatisfied(condition, audioProfile);
+ case ProfileConditionValue.AudioBitrate:
+ return IsConditionSatisfied(condition, audioBitrate);
+ case ProfileConditionValue.AudioChannels:
+ return IsConditionSatisfied(condition, audioChannels);
+ case ProfileConditionValue.IsSecondaryAudio:
+ return IsConditionSatisfied(condition, isSecondaryTrack);
+ case ProfileConditionValue.AudioSampleRate:
+ return IsConditionSatisfied(condition, audioSampleRate);
+ case ProfileConditionValue.AudioBitDepth:
+ return IsConditionSatisfied(condition, audioBitDepth);
+ default:
+ throw new ArgumentException("Unexpected condition on audio file: " + condition.Property);
+ }
+ }
+
+ private bool IsConditionSatisfied(ProfileCondition condition, int? currentValue)
+ {
+ if (!currentValue.HasValue)
+ {
+ // If the value is unknown, it satisfies if not marked as required
+ return !condition.IsRequired;
+ }
+
+ int expected;
+ if (int.TryParse(condition.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out expected))
+ {
+ switch (condition.Condition)
+ {
+ case ProfileConditionType.Equals:
+ case ProfileConditionType.EqualsAny:
+ return currentValue.Value.Equals(expected);
+ case ProfileConditionType.GreaterThanEqual:
+ return currentValue.Value >= expected;
+ case ProfileConditionType.LessThanEqual:
+ return currentValue.Value <= expected;
+ case ProfileConditionType.NotEquals:
+ return !currentValue.Value.Equals(expected);
+ default:
+ throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition);
+ }
+ }
+
+ return false;
+ }
+
+ private bool IsConditionSatisfied(ProfileCondition condition, string currentValue)
+ {
+ if (string.IsNullOrEmpty(currentValue))
+ {
+ // If the value is unknown, it satisfies if not marked as required
+ return !condition.IsRequired;
+ }
+
+ string expected = condition.Value;
+
+ switch (condition.Condition)
+ {
+ case ProfileConditionType.EqualsAny:
+ {
+ return ListHelper.ContainsIgnoreCase(expected.Split('|'), currentValue);
+ }
+ case ProfileConditionType.Equals:
+ return StringHelper.EqualsIgnoreCase(currentValue, expected);
+ case ProfileConditionType.NotEquals:
+ return !StringHelper.EqualsIgnoreCase(currentValue, expected);
+ default:
+ throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition);
+ }
+ }
+
+ private bool IsConditionSatisfied(ProfileCondition condition, bool? currentValue)
+ {
+ if (!currentValue.HasValue)
+ {
+ // If the value is unknown, it satisfies if not marked as required
+ return !condition.IsRequired;
+ }
+
+ bool expected;
+ if (bool.TryParse(condition.Value, out expected))
+ {
+ switch (condition.Condition)
+ {
+ case ProfileConditionType.Equals:
+ return currentValue.Value == expected;
+ case ProfileConditionType.NotEquals:
+ return currentValue.Value != expected;
+ default:
+ throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition);
+ }
+ }
+
+ return false;
+ }
+
+ private bool IsConditionSatisfied(ProfileCondition condition, float currentValue)
+ {
+ if (currentValue <= 0)
+ {
+ // If the value is unknown, it satisfies if not marked as required
+ return !condition.IsRequired;
+ }
+
+ float expected;
+ if (float.TryParse(condition.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out expected))
+ {
+ switch (condition.Condition)
+ {
+ case ProfileConditionType.Equals:
+ return currentValue.Equals(expected);
+ case ProfileConditionType.GreaterThanEqual:
+ return currentValue >= expected;
+ case ProfileConditionType.LessThanEqual:
+ return currentValue <= expected;
+ case ProfileConditionType.NotEquals:
+ return !currentValue.Equals(expected);
+ default:
+ throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition);
+ }
+ }
+
+ return false;
+ }
+
+ private bool IsConditionSatisfied(ProfileCondition condition, double? currentValue)
+ {
+ if (!currentValue.HasValue)
+ {
+ // If the value is unknown, it satisfies if not marked as required
+ return !condition.IsRequired;
+ }
+
+ double expected;
+ if (double.TryParse(condition.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out expected))
+ {
+ switch (condition.Condition)
+ {
+ case ProfileConditionType.Equals:
+ return currentValue.Value.Equals(expected);
+ case ProfileConditionType.GreaterThanEqual:
+ return currentValue.Value >= expected;
+ case ProfileConditionType.LessThanEqual:
+ return currentValue.Value <= expected;
+ case ProfileConditionType.NotEquals:
+ return !currentValue.Value.Equals(expected);
+ default:
+ throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition);
+ }
+ }
+
+ return false;
+ }
+
+ private bool IsConditionSatisfied(ProfileCondition condition, TransportStreamTimestamp? timestamp)
+ {
+ if (!timestamp.HasValue)
+ {
+ // If the value is unknown, it satisfies if not marked as required
+ return !condition.IsRequired;
+ }
+
+ TransportStreamTimestamp expected = (TransportStreamTimestamp)Enum.Parse(typeof(TransportStreamTimestamp), condition.Value, true);
+
+ switch (condition.Condition)
+ {
+ case ProfileConditionType.Equals:
+ return timestamp == expected;
+ case ProfileConditionType.NotEquals:
+ return timestamp != expected;
+ default:
+ throw new InvalidOperationException("Unexpected ProfileConditionType: " + condition.Condition);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs
new file mode 100644
index 000000000..3fb0682b0
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Xml.Serialization;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class ContainerProfile
+ {
+ [XmlAttribute("type")]
+ public DlnaProfileType Type { get; set; }
+ public ProfileCondition[] Conditions { get; set; }
+
+ [XmlAttribute("container")]
+ public string Container { get; set; }
+
+ public ContainerProfile()
+ {
+ Conditions = new ProfileCondition[] { };
+ }
+
+ public string[] GetContainers()
+ {
+ return SplitValue(Container);
+ }
+
+ private static readonly string[] EmptyStringArray = Array.Empty<string>();
+
+ public static string[] SplitValue(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ return EmptyStringArray;
+ }
+
+ return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+ }
+
+ public bool ContainsContainer(string container)
+ {
+ var containers = GetContainers();
+
+ return ContainsContainer(containers, container);
+ }
+
+ public static bool ContainsContainer(string profileContainers, string inputContainer)
+ {
+ var isNegativeList = false;
+ if (profileContainers != null && profileContainers.StartsWith("-"))
+ {
+ isNegativeList = true;
+ profileContainers = profileContainers.Substring(1);
+ }
+
+ return ContainsContainer(SplitValue(profileContainers), isNegativeList, inputContainer);
+ }
+
+ public static bool ContainsContainer(string[] profileContainers, string inputContainer)
+ {
+ return ContainsContainer(profileContainers, false, inputContainer);
+ }
+
+ public static bool ContainsContainer(string[] profileContainers, bool isNegativeList, string inputContainer)
+ {
+ if (profileContainers.Length == 0)
+ {
+ return true;
+ }
+
+ if (isNegativeList)
+ {
+ var allInputContainers = SplitValue(inputContainer);
+
+ foreach (var container in allInputContainers)
+ {
+ if (ListHelper.ContainsIgnoreCase(profileContainers, container))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ else
+ {
+ var allInputContainers = SplitValue(inputContainer);
+
+ foreach (var container in allInputContainers)
+ {
+ if (ListHelper.ContainsIgnoreCase(profileContainers, container))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
new file mode 100644
index 000000000..966c4a8ce
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
@@ -0,0 +1,236 @@
+using MediaBrowser.Model.MediaInfo;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class ContentFeatureBuilder
+ {
+ private readonly DeviceProfile _profile;
+
+ public ContentFeatureBuilder(DeviceProfile profile)
+ {
+ _profile = profile;
+ }
+
+ public string BuildImageHeader(string container,
+ int? width,
+ int? height,
+ bool isDirectStream,
+ string orgPn = null)
+ {
+ string orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetImageOrgOpValue();
+
+ // 0 = native, 1 = transcoded
+ var orgCi = isDirectStream ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1";
+
+ DlnaFlags flagValue = DlnaFlags.BackgroundTransferMode |
+ DlnaFlags.InteractiveTransferMode |
+ DlnaFlags.DlnaV15;
+
+ string dlnaflags = string.Format(";DLNA.ORG_FLAGS={0}",
+ DlnaMaps.FlagsToString(flagValue));
+
+ ResponseProfile mediaProfile = _profile.GetImageMediaProfile(container,
+ width,
+ height);
+
+ if (string.IsNullOrEmpty(orgPn))
+ {
+ orgPn = mediaProfile == null ? null : mediaProfile.OrgPn;
+ }
+
+ if (string.IsNullOrEmpty(orgPn))
+ {
+ orgPn = GetImageOrgPnValue(container, width, height);
+ }
+
+ string contentFeatures = string.IsNullOrEmpty(orgPn) ? string.Empty : "DLNA.ORG_PN=" + orgPn;
+
+ return (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';');
+ }
+
+ public string BuildAudioHeader(string container,
+ string audioCodec,
+ int? audioBitrate,
+ int? audioSampleRate,
+ int? audioChannels,
+ int? audioBitDepth,
+ bool isDirectStream,
+ long? runtimeTicks,
+ TranscodeSeekInfo transcodeSeekInfo)
+ {
+ // first bit means Time based seek supported, second byte range seek supported (not sure about the order now), so 01 = only byte seek, 10 = time based, 11 = both, 00 = none
+ string orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetOrgOpValue(runtimeTicks > 0, isDirectStream, transcodeSeekInfo);
+
+ // 0 = native, 1 = transcoded
+ string orgCi = isDirectStream ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1";
+
+ DlnaFlags flagValue = DlnaFlags.StreamingTransferMode |
+ DlnaFlags.BackgroundTransferMode |
+ DlnaFlags.InteractiveTransferMode |
+ DlnaFlags.DlnaV15;
+
+ //if (isDirectStream)
+ //{
+ // flagValue = flagValue | DlnaFlags.ByteBasedSeek;
+ //}
+ //else if (runtimeTicks.HasValue)
+ //{
+ // flagValue = flagValue | DlnaFlags.TimeBasedSeek;
+ //}
+
+ string dlnaflags = string.Format(";DLNA.ORG_FLAGS={0}",
+ DlnaMaps.FlagsToString(flagValue));
+
+ ResponseProfile mediaProfile = _profile.GetAudioMediaProfile(container,
+ audioCodec,
+ audioChannels,
+ audioBitrate,
+ audioSampleRate,
+ audioBitDepth);
+
+ string orgPn = mediaProfile == null ? null : mediaProfile.OrgPn;
+
+ if (string.IsNullOrEmpty(orgPn))
+ {
+ orgPn = GetAudioOrgPnValue(container, audioBitrate, audioSampleRate, audioChannels);
+ }
+
+ string contentFeatures = string.IsNullOrEmpty(orgPn) ? string.Empty : "DLNA.ORG_PN=" + orgPn;
+
+ return (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';');
+ }
+
+ public List<string> BuildVideoHeader(string container,
+ string videoCodec,
+ string audioCodec,
+ int? width,
+ int? height,
+ int? bitDepth,
+ int? videoBitrate,
+ TransportStreamTimestamp timestamp,
+ bool isDirectStream,
+ long? runtimeTicks,
+ string videoProfile,
+ double? videoLevel,
+ float? videoFramerate,
+ int? packetLength,
+ TranscodeSeekInfo transcodeSeekInfo,
+ bool? isAnamorphic,
+ bool? isInterlaced,
+ int? refFrames,
+ int? numVideoStreams,
+ int? numAudioStreams,
+ string videoCodecTag,
+ bool? isAvc)
+ {
+ // first bit means Time based seek supported, second byte range seek supported (not sure about the order now), so 01 = only byte seek, 10 = time based, 11 = both, 00 = none
+ string orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetOrgOpValue(runtimeTicks > 0, isDirectStream, transcodeSeekInfo);
+
+ // 0 = native, 1 = transcoded
+ string orgCi = isDirectStream ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1";
+
+ DlnaFlags flagValue = DlnaFlags.StreamingTransferMode |
+ DlnaFlags.BackgroundTransferMode |
+ DlnaFlags.InteractiveTransferMode |
+ DlnaFlags.DlnaV15;
+
+ //if (isDirectStream)
+ //{
+ // flagValue = flagValue | DlnaFlags.ByteBasedSeek;
+ //}
+ //else if (runtimeTicks.HasValue)
+ //{
+ // flagValue = flagValue | DlnaFlags.TimeBasedSeek;
+ //}
+
+ string dlnaflags = string.Format(";DLNA.ORG_FLAGS={0}",
+ DlnaMaps.FlagsToString(flagValue));
+
+ ResponseProfile mediaProfile = _profile.GetVideoMediaProfile(container,
+ audioCodec,
+ videoCodec,
+ width,
+ height,
+ bitDepth,
+ videoBitrate,
+ videoProfile,
+ videoLevel,
+ videoFramerate,
+ packetLength,
+ timestamp,
+ isAnamorphic,
+ isInterlaced,
+ refFrames,
+ numVideoStreams,
+ numAudioStreams,
+ videoCodecTag,
+ isAvc);
+
+ List<string> orgPnValues = new List<string>();
+
+ if (mediaProfile != null && !string.IsNullOrEmpty(mediaProfile.OrgPn))
+ {
+ orgPnValues.AddRange(mediaProfile.OrgPn.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries));
+ }
+ else
+ {
+ foreach (string s in GetVideoOrgPnValue(container, videoCodec, audioCodec, width, height, timestamp))
+ {
+ orgPnValues.Add(s);
+ break;
+ }
+ }
+
+ List<string> contentFeatureList = new List<string>();
+
+ foreach (string orgPn in orgPnValues)
+ {
+ string contentFeatures = string.IsNullOrEmpty(orgPn) ? string.Empty : "DLNA.ORG_PN=" + orgPn;
+
+ var value = (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';');
+
+ contentFeatureList.Add(value);
+ }
+
+ if (orgPnValues.Count == 0)
+ {
+ string contentFeatures = string.Empty;
+
+ var value = (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';');
+
+ contentFeatureList.Add(value);
+ }
+
+ return contentFeatureList;
+ }
+
+ private string GetImageOrgPnValue(string container, int? width, int? height)
+ {
+ MediaFormatProfile? format = new MediaFormatProfileResolver()
+ .ResolveImageFormat(container,
+ width,
+ height);
+
+ return format.HasValue ? format.Value.ToString() : null;
+ }
+
+ private string GetAudioOrgPnValue(string container, int? audioBitrate, int? audioSampleRate, int? audioChannels)
+ {
+ MediaFormatProfile? format = new MediaFormatProfileResolver()
+ .ResolveAudioFormat(container,
+ audioBitrate,
+ audioSampleRate,
+ audioChannels);
+
+ return format.HasValue ? format.Value.ToString() : null;
+ }
+
+ private string[] GetVideoOrgPnValue(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestamp)
+ {
+ return new MediaFormatProfileResolver().ResolveVideoFormat(container, videoCodec, audioCodec, width, height, timestamp);
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/DeviceIdentification.cs b/MediaBrowser.Model/Dlna/DeviceIdentification.cs
new file mode 100644
index 000000000..97f4409da
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/DeviceIdentification.cs
@@ -0,0 +1,61 @@
+namespace MediaBrowser.Model.Dlna
+{
+ public class DeviceIdentification
+ {
+ /// <summary>
+ /// Gets or sets the name of the friendly.
+ /// </summary>
+ /// <value>The name of the friendly.</value>
+ public string FriendlyName { get; set; }
+ /// <summary>
+ /// Gets or sets the model number.
+ /// </summary>
+ /// <value>The model number.</value>
+ public string ModelNumber { get; set; }
+ /// <summary>
+ /// Gets or sets the serial number.
+ /// </summary>
+ /// <value>The serial number.</value>
+ public string SerialNumber { get; set; }
+ /// <summary>
+ /// Gets or sets the name of the model.
+ /// </summary>
+ /// <value>The name of the model.</value>
+ public string ModelName { get; set; }
+ /// <summary>
+ /// Gets or sets the model description.
+ /// </summary>
+ /// <value>The model description.</value>
+ public string ModelDescription { get; set; }
+ /// <summary>
+ /// Gets or sets the device description.
+ /// </summary>
+ /// <value>The device description.</value>
+ public string DeviceDescription { get; set; }
+ /// <summary>
+ /// Gets or sets the model URL.
+ /// </summary>
+ /// <value>The model URL.</value>
+ public string ModelUrl { get; set; }
+ /// <summary>
+ /// Gets or sets the manufacturer.
+ /// </summary>
+ /// <value>The manufacturer.</value>
+ public string Manufacturer { get; set; }
+ /// <summary>
+ /// Gets or sets the manufacturer URL.
+ /// </summary>
+ /// <value>The manufacturer URL.</value>
+ public string ManufacturerUrl { get; set; }
+ /// <summary>
+ /// Gets or sets the headers.
+ /// </summary>
+ /// <value>The headers.</value>
+ public HttpHeaderInfo[] Headers { get; set; }
+
+ public DeviceIdentification()
+ {
+ Headers = new HttpHeaderInfo[] {};
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs
new file mode 100644
index 000000000..4bf7d6b8d
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs
@@ -0,0 +1,327 @@
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.MediaInfo;
+using System.Collections.Generic;
+using System.Xml.Serialization;
+using System;
+
+namespace MediaBrowser.Model.Dlna
+{
+ [XmlRoot("Profile")]
+ public class DeviceProfile
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ [XmlIgnore]
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the identification.
+ /// </summary>
+ /// <value>The identification.</value>
+ public MediaBrowser.Model.Dlna.DeviceIdentification Identification { get; set; }
+
+ public string FriendlyName { get; set; }
+ public string Manufacturer { get; set; }
+ public string ManufacturerUrl { get; set; }
+ public string ModelName { get; set; }
+ public string ModelDescription { get; set; }
+ public string ModelNumber { get; set; }
+ public string ModelUrl { get; set; }
+ public string SerialNumber { get; set; }
+
+ public bool EnableAlbumArtInDidl { get; set; }
+ public bool EnableSingleAlbumArtLimit { get; set; }
+ public bool EnableSingleSubtitleLimit { get; set; }
+
+ public string SupportedMediaTypes { get; set; }
+
+ public string UserId { get; set; }
+
+ public string AlbumArtPn { get; set; }
+
+ public int MaxAlbumArtWidth { get; set; }
+ public int MaxAlbumArtHeight { get; set; }
+
+ public int? MaxIconWidth { get; set; }
+ public int? MaxIconHeight { get; set; }
+
+ public long? MaxStreamingBitrate { get; set; }
+ public long? MaxStaticBitrate { get; set; }
+
+ public int? MusicStreamingTranscodingBitrate { get; set; }
+ public int? MaxStaticMusicBitrate { get; set; }
+
+ /// <summary>
+ /// Controls the content of the aggregationFlags element in the urn:schemas-sonycom:av namespace.
+ /// </summary>
+ public string SonyAggregationFlags { get; set; }
+
+ public string ProtocolInfo { get; set; }
+
+ public int TimelineOffsetSeconds { get; set; }
+ public bool RequiresPlainVideoItems { get; set; }
+ public bool RequiresPlainFolders { get; set; }
+
+ public bool EnableMSMediaReceiverRegistrar { get; set; }
+ public bool IgnoreTranscodeByteRangeRequests { get; set; }
+
+ public XmlAttribute[] XmlRootAttributes { get; set; }
+
+ /// <summary>
+ /// Gets or sets the direct play profiles.
+ /// </summary>
+ /// <value>The direct play profiles.</value>
+ public DirectPlayProfile[] DirectPlayProfiles { get; set; }
+
+ /// <summary>
+ /// Gets or sets the transcoding profiles.
+ /// </summary>
+ /// <value>The transcoding profiles.</value>
+ public TranscodingProfile[] TranscodingProfiles { get; set; }
+
+ public ContainerProfile[] ContainerProfiles { get; set; }
+
+ public CodecProfile[] CodecProfiles { get; set; }
+ public ResponseProfile[] ResponseProfiles { get; set; }
+
+ public SubtitleProfile[] SubtitleProfiles { get; set; }
+
+ public DeviceProfile()
+ {
+ DirectPlayProfiles = new DirectPlayProfile[] { };
+ TranscodingProfiles = new TranscodingProfile[] { };
+ ResponseProfiles = new ResponseProfile[] { };
+ CodecProfiles = new CodecProfile[] { };
+ ContainerProfiles = new ContainerProfile[] { };
+ SubtitleProfiles = Array.Empty<SubtitleProfile>();
+
+ XmlRootAttributes = new XmlAttribute[] { };
+
+ SupportedMediaTypes = "Audio,Photo,Video";
+ MaxStreamingBitrate = 8000000;
+ MaxStaticBitrate = 8000000;
+ MusicStreamingTranscodingBitrate = 128000;
+ }
+
+ public string[] GetSupportedMediaTypes()
+ {
+ return ContainerProfile.SplitValue(SupportedMediaTypes);
+ }
+
+ public TranscodingProfile GetAudioTranscodingProfile(string container, string audioCodec)
+ {
+ container = (container ?? string.Empty).TrimStart('.');
+
+ foreach (var i in TranscodingProfiles)
+ {
+ if (i.Type != MediaBrowser.Model.Dlna.DlnaProfileType.Audio)
+ {
+ continue;
+ }
+
+ if (!StringHelper.EqualsIgnoreCase(container, i.Container))
+ {
+ continue;
+ }
+
+ if (!ListHelper.ContainsIgnoreCase(i.GetAudioCodecs(), audioCodec ?? string.Empty))
+ {
+ continue;
+ }
+
+ return i;
+ }
+ return null;
+ }
+
+ public TranscodingProfile GetVideoTranscodingProfile(string container, string audioCodec, string videoCodec)
+ {
+ container = (container ?? string.Empty).TrimStart('.');
+
+ foreach (var i in TranscodingProfiles)
+ {
+ if (i.Type != MediaBrowser.Model.Dlna.DlnaProfileType.Video)
+ {
+ continue;
+ }
+
+ if (!StringHelper.EqualsIgnoreCase(container, i.Container))
+ {
+ continue;
+ }
+
+ if (!ListHelper.ContainsIgnoreCase(i.GetAudioCodecs(), audioCodec ?? string.Empty))
+ {
+ continue;
+ }
+
+ if (!StringHelper.EqualsIgnoreCase(videoCodec, i.VideoCodec ?? string.Empty))
+ {
+ continue;
+ }
+
+ return i;
+ }
+ return null;
+ }
+
+ public ResponseProfile GetAudioMediaProfile(string container, string audioCodec, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth)
+ {
+ foreach (var i in ResponseProfiles)
+ {
+ if (i.Type != DlnaProfileType.Audio)
+ {
+ continue;
+ }
+
+ if (!ContainerProfile.ContainsContainer(i.GetContainers(), container))
+ {
+ continue;
+ }
+
+ var audioCodecs = i.GetAudioCodecs();
+ if (audioCodecs.Length > 0 && !ListHelper.ContainsIgnoreCase(audioCodecs, audioCodec ?? string.Empty))
+ {
+ continue;
+ }
+
+ var conditionProcessor = new ConditionProcessor();
+
+ var anyOff = false;
+ foreach (ProfileCondition c in i.Conditions)
+ {
+ if (!conditionProcessor.IsAudioConditionSatisfied(GetModelProfileCondition(c), audioChannels, audioBitrate, audioSampleRate, audioBitDepth))
+ {
+ anyOff = true;
+ break;
+ }
+ }
+
+ if (anyOff)
+ {
+ continue;
+ }
+
+ return i;
+ }
+ return null;
+ }
+
+ private ProfileCondition GetModelProfileCondition(ProfileCondition c)
+ {
+ return new ProfileCondition
+ {
+ Condition = c.Condition,
+ IsRequired = c.IsRequired,
+ Property = c.Property,
+ Value = c.Value
+ };
+ }
+
+ public ResponseProfile GetImageMediaProfile(string container, int? width, int? height)
+ {
+ foreach (var i in ResponseProfiles)
+ {
+ if (i.Type != DlnaProfileType.Photo)
+ {
+ continue;
+ }
+
+ if (!ContainerProfile.ContainsContainer(i.GetContainers(), container))
+ {
+ continue;
+ }
+
+ var conditionProcessor = new ConditionProcessor();
+
+ var anyOff = false;
+ foreach (ProfileCondition c in i.Conditions)
+ {
+ if (!conditionProcessor.IsImageConditionSatisfied(GetModelProfileCondition(c), width, height))
+ {
+ anyOff = true;
+ break;
+ }
+ }
+
+ if (anyOff)
+ {
+ continue;
+ }
+
+ return i;
+ }
+ return null;
+ }
+
+ public ResponseProfile GetVideoMediaProfile(string container,
+ string audioCodec,
+ string videoCodec,
+ int? width,
+ int? height,
+ int? bitDepth,
+ int? videoBitrate,
+ string videoProfile,
+ double? videoLevel,
+ float? videoFramerate,
+ int? packetLength,
+ TransportStreamTimestamp timestamp,
+ bool? isAnamorphic,
+ bool? isInterlaced,
+ int? refFrames,
+ int? numVideoStreams,
+ int? numAudioStreams,
+ string videoCodecTag,
+ bool? isAvc)
+ {
+ foreach (var i in ResponseProfiles)
+ {
+ if (i.Type != DlnaProfileType.Video)
+ {
+ continue;
+ }
+
+ if (!ContainerProfile.ContainsContainer(i.GetContainers(), container))
+ {
+ continue;
+ }
+
+ var audioCodecs = i.GetAudioCodecs();
+ if (audioCodecs.Length > 0 && !ListHelper.ContainsIgnoreCase(audioCodecs, audioCodec ?? string.Empty))
+ {
+ continue;
+ }
+
+ var videoCodecs = i.GetVideoCodecs();
+ if (videoCodecs.Length > 0 && !ListHelper.ContainsIgnoreCase(videoCodecs, videoCodec ?? string.Empty))
+ {
+ continue;
+ }
+
+ var conditionProcessor = new ConditionProcessor();
+
+ var anyOff = false;
+ foreach (ProfileCondition c in i.Conditions)
+ {
+ if (!conditionProcessor.IsVideoConditionSatisfied(GetModelProfileCondition(c), width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))
+ {
+ anyOff = true;
+ break;
+ }
+ }
+
+ if (anyOff)
+ {
+ continue;
+ }
+
+ return i;
+ }
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/DeviceProfileInfo.cs b/MediaBrowser.Model/Dlna/DeviceProfileInfo.cs
new file mode 100644
index 000000000..b2afdf292
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/DeviceProfileInfo.cs
@@ -0,0 +1,24 @@
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class DeviceProfileInfo
+ {
+ /// <summary>
+ /// Gets or sets the identifier.
+ /// </summary>
+ /// <value>The identifier.</value>
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ public DeviceProfileType Type { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/DeviceProfileType.cs b/MediaBrowser.Model/Dlna/DeviceProfileType.cs
new file mode 100644
index 000000000..f881a4539
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/DeviceProfileType.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Model.Dlna
+{
+ public enum DeviceProfileType
+ {
+ System = 0,
+ User = 1
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
new file mode 100644
index 000000000..4279b545d
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Xml.Serialization;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class DirectPlayProfile
+ {
+ [XmlAttribute("container")]
+ public string Container { get; set; }
+
+ [XmlAttribute("audioCodec")]
+ public string AudioCodec { get; set; }
+
+ [XmlAttribute("videoCodec")]
+ public string VideoCodec { get; set; }
+
+ [XmlAttribute("type")]
+ public DlnaProfileType Type { get; set; }
+
+ public bool SupportsContainer(string container)
+ {
+ return ContainerProfile.ContainsContainer(Container, container);
+ }
+
+ public bool SupportsVideoCodec(string codec)
+ {
+ return ContainerProfile.ContainsContainer(VideoCodec, codec);
+ }
+
+ public bool SupportsAudioCodec(string codec)
+ {
+ return ContainerProfile.ContainsContainer(AudioCodec, codec);
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/DlnaFlags.cs b/MediaBrowser.Model/Dlna/DlnaFlags.cs
new file mode 100644
index 000000000..b981e8455
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/DlnaFlags.cs
@@ -0,0 +1,48 @@
+using System;
+
+namespace MediaBrowser.Model.Dlna
+{
+ [Flags]
+ public enum DlnaFlags : ulong
+ {
+ /*! <i>Background</i> transfer mode.
+ For use with upload and download transfers to and from the server.
+ The primary difference between \ref DH_TransferMode_Interactive and
+ \ref DH_TransferMode_Bulk is that the latter assumes that the user
+ is not relying on the transfer for immediately rendering the content
+ and there are no issues with causing a buffer overflow if the
+ receiver uses TCP flow control to reduce total throughput.
+ */
+ BackgroundTransferMode = 1 << 22,
+
+ ByteBasedSeek = 1 << 29,
+ ConnectionStall = 1 << 21,
+
+ DlnaV15 = 1 << 20,
+
+ /*! <i>Interactive</i> transfer mode.
+ For best effort transfer of images and non-real-time transfers.
+ URIs with image content usually support \ref DH_TransferMode_Bulk too.
+ The primary difference between \ref DH_TransferMode_Interactive and
+ \ref DH_TransferMode_Bulk is that the former assumes that the
+ transfer is intended for immediate rendering.
+ */
+ InteractiveTransferMode = 1 << 23,
+
+ PlayContainer = 1 << 28,
+ RtspPause = 1 << 25,
+ S0Increase = 1 << 27,
+ SenderPaced = 1L << 31,
+ SnIncrease = 1 << 26,
+
+ /*! <i>Streaming</i> transfer mode.
+ The server transmits at a throughput sufficient for real-time playback of
+ audio or video. URIs with audio or video often support the
+ \ref DH_TransferMode_Interactive and \ref DH_TransferMode_Bulk transfer modes.
+ The most well-known exception to this general claim is for live streams.
+ */
+ StreamingTransferMode = 1 << 24,
+
+ TimeBasedSeek = 1 << 30
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/DlnaMaps.cs b/MediaBrowser.Model/Dlna/DlnaMaps.cs
new file mode 100644
index 000000000..8dadc32d6
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/DlnaMaps.cs
@@ -0,0 +1,56 @@
+namespace MediaBrowser.Model.Dlna
+{
+ public class DlnaMaps
+ {
+ private static readonly string DefaultStreaming =
+ FlagsToString(DlnaFlags.StreamingTransferMode |
+ DlnaFlags.BackgroundTransferMode |
+ DlnaFlags.ConnectionStall |
+ DlnaFlags.ByteBasedSeek |
+ DlnaFlags.DlnaV15);
+
+ private static readonly string DefaultInteractive =
+ FlagsToString(DlnaFlags.InteractiveTransferMode |
+ DlnaFlags.BackgroundTransferMode |
+ DlnaFlags.ConnectionStall |
+ DlnaFlags.ByteBasedSeek |
+ DlnaFlags.DlnaV15);
+
+ public static string FlagsToString(DlnaFlags flags)
+ {
+ return string.Format("{0:X8}{1:D24}", (ulong)flags, 0);
+ }
+
+ public static string GetOrgOpValue(bool hasKnownRuntime, bool isDirectStream, TranscodeSeekInfo profileTranscodeSeekInfo)
+ {
+ if (hasKnownRuntime)
+ {
+ string orgOp = string.Empty;
+
+ // Time-based seeking currently only possible when transcoding
+ orgOp += isDirectStream ? "0" : "1";
+
+ // Byte-based seeking only possible when not transcoding
+ orgOp += isDirectStream || profileTranscodeSeekInfo == TranscodeSeekInfo.Bytes ? "1" : "0";
+
+ return orgOp;
+ }
+
+ // No seeking is available if we don't know the content runtime
+ return "00";
+ }
+
+ public static string GetImageOrgOpValue()
+ {
+ string orgOp = string.Empty;
+
+ // Time-based seeking currently only possible when transcoding
+ orgOp += "0";
+
+ // Byte-based seeking only possible when not transcoding
+ orgOp += "0";
+
+ return orgOp;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/DlnaProfileType.cs b/MediaBrowser.Model/Dlna/DlnaProfileType.cs
new file mode 100644
index 000000000..1bad14081
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/DlnaProfileType.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Model.Dlna
+{
+ public enum DlnaProfileType
+ {
+ Audio = 0,
+ Video = 1,
+ Photo = 2
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/EncodingContext.cs b/MediaBrowser.Model/Dlna/EncodingContext.cs
new file mode 100644
index 000000000..f83d8ddc8
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/EncodingContext.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Model.Dlna
+{
+ public enum EncodingContext
+ {
+ Streaming = 0,
+ Static = 1
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/HeaderMatchType.cs b/MediaBrowser.Model/Dlna/HeaderMatchType.cs
new file mode 100644
index 000000000..7a0d5c24f
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/HeaderMatchType.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Model.Dlna
+{
+ public enum HeaderMatchType
+ {
+ Equals = 0,
+ Regex = 1,
+ Substring = 2
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/HttpHeaderInfo.cs b/MediaBrowser.Model/Dlna/HttpHeaderInfo.cs
new file mode 100644
index 000000000..b4fa4e0af
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/HttpHeaderInfo.cs
@@ -0,0 +1,17 @@
+using System.Xml.Serialization;
+using MediaBrowser.Model.Dlna;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class HttpHeaderInfo
+ {
+ [XmlAttribute("name")]
+ public string Name { get; set; }
+
+ [XmlAttribute("value")]
+ public string Value { get; set; }
+
+ [XmlAttribute("match")]
+ public HeaderMatchType Match { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs b/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs
new file mode 100644
index 000000000..70191ff23
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs
@@ -0,0 +1,11 @@
+using System;
+using MediaBrowser.Model.Events;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public interface IDeviceDiscovery
+ {
+ event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered;
+ event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft;
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/ITranscoderSupport.cs b/MediaBrowser.Model/Dlna/ITranscoderSupport.cs
new file mode 100644
index 000000000..14723bd27
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/ITranscoderSupport.cs
@@ -0,0 +1,25 @@
+namespace MediaBrowser.Model.Dlna
+{
+ public interface ITranscoderSupport
+ {
+ bool CanEncodeToAudioCodec(string codec);
+ bool CanEncodeToSubtitleCodec(string codec);
+ bool CanExtractSubtitles(string codec);
+ }
+
+ public class FullTranscoderSupport : ITranscoderSupport
+ {
+ public bool CanEncodeToAudioCodec(string codec)
+ {
+ return true;
+ }
+ public bool CanEncodeToSubtitleCodec(string codec)
+ {
+ return true;
+ }
+ public bool CanExtractSubtitles(string codec)
+ {
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfile.cs b/MediaBrowser.Model/Dlna/MediaFormatProfile.cs
new file mode 100644
index 000000000..f3d04335f
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/MediaFormatProfile.cs
@@ -0,0 +1,113 @@
+
+namespace MediaBrowser.Model.Dlna
+{
+ public enum MediaFormatProfile
+ {
+ MP3,
+ WMA_BASE,
+ WMA_FULL,
+ LPCM16_44_MONO,
+ LPCM16_44_STEREO,
+ LPCM16_48_MONO,
+ LPCM16_48_STEREO,
+ AAC_ISO,
+ AAC_ISO_320,
+ AAC_ADTS,
+ AAC_ADTS_320,
+ FLAC,
+ OGG,
+
+ JPEG_SM,
+ JPEG_MED,
+ JPEG_LRG,
+ JPEG_TN,
+ PNG_LRG,
+ PNG_TN,
+ GIF_LRG,
+ RAW,
+
+ MPEG1,
+ MPEG_PS_PAL,
+ MPEG_PS_NTSC,
+ MPEG_TS_SD_EU,
+ MPEG_TS_SD_EU_ISO,
+ MPEG_TS_SD_EU_T,
+ MPEG_TS_SD_NA,
+ MPEG_TS_SD_NA_ISO,
+ MPEG_TS_SD_NA_T,
+ MPEG_TS_SD_KO,
+ MPEG_TS_SD_KO_ISO,
+ MPEG_TS_SD_KO_T,
+ MPEG_TS_JP_T,
+ AVI,
+ MATROSKA,
+ FLV,
+ DVR_MS,
+ WTV,
+ OGV,
+ AVC_MP4_MP_SD_AAC_MULT5,
+ AVC_MP4_MP_SD_MPEG1_L3,
+ AVC_MP4_MP_SD_AC3,
+ AVC_MP4_MP_HD_720p_AAC,
+ AVC_MP4_MP_HD_1080i_AAC,
+ AVC_MP4_HP_HD_AAC,
+ AVC_TS_MP_HD_AAC_MULT5,
+ AVC_TS_MP_HD_AAC_MULT5_T,
+ AVC_TS_MP_HD_AAC_MULT5_ISO,
+ AVC_TS_MP_HD_MPEG1_L3,
+ AVC_TS_MP_HD_MPEG1_L3_T,
+ AVC_TS_MP_HD_MPEG1_L3_ISO,
+ AVC_TS_MP_HD_AC3,
+ AVC_TS_MP_HD_AC3_T,
+ AVC_TS_MP_HD_AC3_ISO,
+ AVC_TS_HP_HD_MPEG1_L2_T,
+ AVC_TS_HP_HD_MPEG1_L2_ISO,
+ AVC_TS_MP_SD_AAC_MULT5,
+ AVC_TS_MP_SD_AAC_MULT5_T,
+ AVC_TS_MP_SD_AAC_MULT5_ISO,
+ AVC_TS_MP_SD_MPEG1_L3,
+ AVC_TS_MP_SD_MPEG1_L3_T,
+ AVC_TS_MP_SD_MPEG1_L3_ISO,
+ AVC_TS_HP_SD_MPEG1_L2_T,
+ AVC_TS_HP_SD_MPEG1_L2_ISO,
+ AVC_TS_MP_SD_AC3,
+ AVC_TS_MP_SD_AC3_T,
+ AVC_TS_MP_SD_AC3_ISO,
+ AVC_TS_HD_DTS_T,
+ AVC_TS_HD_DTS_ISO,
+ WMVMED_BASE,
+ WMVMED_FULL,
+ WMVMED_PRO,
+ WMVHIGH_FULL,
+ WMVHIGH_PRO,
+ VC1_ASF_AP_L1_WMA,
+ VC1_ASF_AP_L2_WMA,
+ VC1_ASF_AP_L3_WMA,
+ VC1_TS_AP_L1_AC3_ISO,
+ VC1_TS_AP_L2_AC3_ISO,
+ VC1_TS_HD_DTS_ISO,
+ VC1_TS_HD_DTS_T,
+ MPEG4_P2_MP4_ASP_AAC,
+ MPEG4_P2_MP4_SP_L6_AAC,
+ MPEG4_P2_MP4_NDSD,
+ MPEG4_P2_TS_ASP_AAC,
+ MPEG4_P2_TS_ASP_AAC_T,
+ MPEG4_P2_TS_ASP_AAC_ISO,
+ MPEG4_P2_TS_ASP_MPEG1_L3,
+ MPEG4_P2_TS_ASP_MPEG1_L3_T,
+ MPEG4_P2_TS_ASP_MPEG1_L3_ISO,
+ MPEG4_P2_TS_ASP_MPEG2_L2,
+ MPEG4_P2_TS_ASP_MPEG2_L2_T,
+ MPEG4_P2_TS_ASP_MPEG2_L2_ISO,
+ MPEG4_P2_TS_ASP_AC3,
+ MPEG4_P2_TS_ASP_AC3_T,
+ MPEG4_P2_TS_ASP_AC3_ISO,
+ AVC_TS_HD_50_LPCM_T,
+ AVC_MP4_LPCM,
+ MPEG4_P2_3GPP_SP_L0B_AAC,
+ MPEG4_P2_3GPP_SP_L0B_AMR,
+ AVC_3GPP_BL_QCIF15_AAC,
+ MPEG4_H263_3GPP_P0_L10_AMR,
+ MPEG4_H263_MP4_P0_L10_AAC
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
new file mode 100644
index 000000000..b6f329387
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
@@ -0,0 +1,439 @@
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.MediaInfo;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class MediaFormatProfileResolver
+ {
+ public string[] ResolveVideoFormat(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType)
+ {
+ return ResolveVideoFormatInternal(container, videoCodec, audioCodec, width, height, timestampType)
+ .Select(i => i.ToString())
+ .ToArray();
+ }
+
+ private MediaFormatProfile[] ResolveVideoFormatInternal(string container, string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType)
+ {
+ if (StringHelper.EqualsIgnoreCase(container, "asf"))
+ {
+ MediaFormatProfile? val = ResolveVideoASFFormat(videoCodec, audioCodec, width, height);
+ return val.HasValue ? new MediaFormatProfile[] { val.Value } : new MediaFormatProfile[] { };
+ }
+
+ if (StringHelper.EqualsIgnoreCase(container, "mp4"))
+ {
+ MediaFormatProfile? val = ResolveVideoMP4Format(videoCodec, audioCodec, width, height);
+ return val.HasValue ? new MediaFormatProfile[] { val.Value } : new MediaFormatProfile[] { };
+ }
+
+ if (StringHelper.EqualsIgnoreCase(container, "avi"))
+ return new MediaFormatProfile[] { MediaFormatProfile.AVI };
+
+ if (StringHelper.EqualsIgnoreCase(container, "mkv"))
+ return new MediaFormatProfile[] { MediaFormatProfile.MATROSKA };
+
+ if (StringHelper.EqualsIgnoreCase(container, "mpeg2ps") ||
+ StringHelper.EqualsIgnoreCase(container, "ts"))
+
+ return new MediaFormatProfile[] { MediaFormatProfile.MPEG_PS_NTSC, MediaFormatProfile.MPEG_PS_PAL };
+
+ if (StringHelper.EqualsIgnoreCase(container, "mpeg1video"))
+ return new MediaFormatProfile[] { MediaFormatProfile.MPEG1 };
+
+ if (StringHelper.EqualsIgnoreCase(container, "mpeg2ts") ||
+ StringHelper.EqualsIgnoreCase(container, "mpegts") ||
+ StringHelper.EqualsIgnoreCase(container, "m2ts"))
+ {
+
+ return ResolveVideoMPEG2TSFormat(videoCodec, audioCodec, width, height, timestampType);
+ }
+
+ if (StringHelper.EqualsIgnoreCase(container, "flv"))
+ return new MediaFormatProfile[] { MediaFormatProfile.FLV };
+
+ if (StringHelper.EqualsIgnoreCase(container, "wtv"))
+ return new MediaFormatProfile[] { MediaFormatProfile.WTV };
+
+ if (StringHelper.EqualsIgnoreCase(container, "3gp"))
+ {
+ MediaFormatProfile? val = ResolveVideo3GPFormat(videoCodec, audioCodec);
+ return val.HasValue ? new MediaFormatProfile[] { val.Value } : new MediaFormatProfile[] { };
+ }
+
+ if (StringHelper.EqualsIgnoreCase(container, "ogv") || StringHelper.EqualsIgnoreCase(container, "ogg"))
+ return new MediaFormatProfile[] { MediaFormatProfile.OGV };
+
+ return new MediaFormatProfile[] { };
+ }
+
+ private MediaFormatProfile[] ResolveVideoMPEG2TSFormat(string videoCodec, string audioCodec, int? width, int? height, TransportStreamTimestamp timestampType)
+ {
+ string suffix = "";
+
+ switch (timestampType)
+ {
+ case TransportStreamTimestamp.None:
+ suffix = "_ISO";
+ break;
+ case TransportStreamTimestamp.Valid:
+ suffix = "_T";
+ break;
+ }
+
+ string resolution = "S";
+ if ((width.HasValue && width.Value > 720) || (height.HasValue && height.Value > 576))
+ {
+ resolution = "H";
+ }
+
+ if (StringHelper.EqualsIgnoreCase(videoCodec, "mpeg2video"))
+ {
+ List<MediaFormatProfile> list = new List<MediaFormatProfile>();
+
+ list.Add(ValueOf("MPEG_TS_SD_NA" + suffix));
+ list.Add(ValueOf("MPEG_TS_SD_EU" + suffix));
+ list.Add(ValueOf("MPEG_TS_SD_KO" + suffix));
+
+ if ((timestampType == TransportStreamTimestamp.Valid) && StringHelper.EqualsIgnoreCase(audioCodec, "aac"))
+ {
+ list.Add(MediaFormatProfile.MPEG_TS_JP_T);
+ }
+ return list.ToArray(list.Count);
+ }
+ if (StringHelper.EqualsIgnoreCase(videoCodec, "h264"))
+ {
+ if (StringHelper.EqualsIgnoreCase(audioCodec, "lpcm"))
+ return new MediaFormatProfile[] { MediaFormatProfile.AVC_TS_HD_50_LPCM_T };
+
+ if (StringHelper.EqualsIgnoreCase(audioCodec, "dts"))
+ {
+ if (timestampType == TransportStreamTimestamp.None)
+ {
+ return new MediaFormatProfile[] { MediaFormatProfile.AVC_TS_HD_DTS_ISO };
+ }
+ return new MediaFormatProfile[] { MediaFormatProfile.AVC_TS_HD_DTS_T };
+ }
+
+ if (StringHelper.EqualsIgnoreCase(audioCodec, "mp2"))
+ {
+ if (timestampType == TransportStreamTimestamp.None)
+ {
+ return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_HP_{0}D_MPEG1_L2_ISO", resolution)) };
+ }
+
+ return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_HP_{0}D_MPEG1_L2_T", resolution)) };
+ }
+
+ if (StringHelper.EqualsIgnoreCase(audioCodec, "aac"))
+ return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_MP_{0}D_AAC_MULT5{1}", resolution, suffix)) };
+
+ if (StringHelper.EqualsIgnoreCase(audioCodec, "mp3"))
+ return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_MP_{0}D_MPEG1_L3{1}", resolution, suffix)) };
+
+ if (string.IsNullOrEmpty(audioCodec) ||
+ StringHelper.EqualsIgnoreCase(audioCodec, "ac3"))
+ return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_MP_{0}D_AC3{1}", resolution, suffix)) };
+ }
+ else if (StringHelper.EqualsIgnoreCase(videoCodec, "vc1"))
+ {
+ if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "ac3"))
+ {
+ if ((width.HasValue && width.Value > 720) || (height.HasValue && height.Value > 576))
+ {
+ return new MediaFormatProfile[] { MediaFormatProfile.VC1_TS_AP_L2_AC3_ISO };
+ }
+ return new MediaFormatProfile[] { MediaFormatProfile.VC1_TS_AP_L1_AC3_ISO };
+ }
+ if (StringHelper.EqualsIgnoreCase(audioCodec, "dts"))
+ {
+ suffix = StringHelper.EqualsIgnoreCase(suffix, "_ISO") ? suffix : "_T";
+
+ return new MediaFormatProfile[] { ValueOf(string.Format("VC1_TS_HD_DTS{0}", suffix)) };
+ }
+
+ }
+ else if (StringHelper.EqualsIgnoreCase(videoCodec, "mpeg4") || StringHelper.EqualsIgnoreCase(videoCodec, "msmpeg4"))
+ {
+ if (StringHelper.EqualsIgnoreCase(audioCodec, "aac"))
+ return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_AAC{0}", suffix)) };
+ if (StringHelper.EqualsIgnoreCase(audioCodec, "mp3"))
+ return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_MPEG1_L3{0}", suffix)) };
+ if (StringHelper.EqualsIgnoreCase(audioCodec, "mp2"))
+ return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_MPEG2_L2{0}", suffix)) };
+ if (StringHelper.EqualsIgnoreCase(audioCodec, "ac3"))
+ return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_AC3{0}", suffix)) };
+ }
+
+ return new MediaFormatProfile[]{};
+ }
+
+ private MediaFormatProfile ValueOf(string value)
+ {
+ return (MediaFormatProfile)Enum.Parse(typeof(MediaFormatProfile), value, true);
+ }
+
+ private MediaFormatProfile? ResolveVideoMP4Format(string videoCodec, string audioCodec, int? width, int? height)
+ {
+ if (StringHelper.EqualsIgnoreCase(videoCodec, "h264"))
+ {
+ if (StringHelper.EqualsIgnoreCase(audioCodec, "lpcm"))
+ return MediaFormatProfile.AVC_MP4_LPCM;
+ if (string.IsNullOrEmpty(audioCodec) ||
+ StringHelper.EqualsIgnoreCase(audioCodec, "ac3"))
+ {
+ return MediaFormatProfile.AVC_MP4_MP_SD_AC3;
+ }
+ if (StringHelper.EqualsIgnoreCase(audioCodec, "mp3"))
+ {
+ return MediaFormatProfile.AVC_MP4_MP_SD_MPEG1_L3;
+ }
+ if (width.HasValue && height.HasValue)
+ {
+ if ((width.Value <= 720) && (height.Value <= 576))
+ {
+ if (StringHelper.EqualsIgnoreCase(audioCodec, "aac"))
+ return MediaFormatProfile.AVC_MP4_MP_SD_AAC_MULT5;
+ }
+ else if ((width.Value <= 1280) && (height.Value <= 720))
+ {
+ if (StringHelper.EqualsIgnoreCase(audioCodec, "aac"))
+ return MediaFormatProfile.AVC_MP4_MP_HD_720p_AAC;
+ }
+ else if ((width.Value <= 1920) && (height.Value <= 1080))
+ {
+ if (StringHelper.EqualsIgnoreCase(audioCodec, "aac"))
+ {
+ return MediaFormatProfile.AVC_MP4_MP_HD_1080i_AAC;
+ }
+ }
+ }
+ }
+ else if (StringHelper.EqualsIgnoreCase(videoCodec, "mpeg4") ||
+ StringHelper.EqualsIgnoreCase(videoCodec, "msmpeg4"))
+ {
+ if (width.HasValue && height.HasValue && width.Value <= 720 && height.Value <= 576)
+ {
+ if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "aac"))
+ return MediaFormatProfile.MPEG4_P2_MP4_ASP_AAC;
+ if (StringHelper.EqualsIgnoreCase(audioCodec, "ac3") || StringHelper.EqualsIgnoreCase(audioCodec, "mp3"))
+ {
+ return MediaFormatProfile.MPEG4_P2_MP4_NDSD;
+ }
+ }
+ else if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "aac"))
+ {
+ return MediaFormatProfile.MPEG4_P2_MP4_SP_L6_AAC;
+ }
+ }
+ else if (StringHelper.EqualsIgnoreCase(videoCodec, "h263") && StringHelper.EqualsIgnoreCase(audioCodec, "aac"))
+ {
+ return MediaFormatProfile.MPEG4_H263_MP4_P0_L10_AAC;
+ }
+
+ return null;
+ }
+
+ private MediaFormatProfile? ResolveVideo3GPFormat(string videoCodec, string audioCodec)
+ {
+ if (StringHelper.EqualsIgnoreCase(videoCodec, "h264"))
+ {
+ if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "aac"))
+ return MediaFormatProfile.AVC_3GPP_BL_QCIF15_AAC;
+ }
+ else if (StringHelper.EqualsIgnoreCase(videoCodec, "mpeg4") ||
+ StringHelper.EqualsIgnoreCase(videoCodec, "msmpeg4"))
+ {
+ if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "wma"))
+ return MediaFormatProfile.MPEG4_P2_3GPP_SP_L0B_AAC;
+ if (StringHelper.EqualsIgnoreCase(audioCodec, "amrnb"))
+ return MediaFormatProfile.MPEG4_P2_3GPP_SP_L0B_AMR;
+ }
+ else if (StringHelper.EqualsIgnoreCase(videoCodec, "h263") && StringHelper.EqualsIgnoreCase(audioCodec, "amrnb"))
+ {
+ return MediaFormatProfile.MPEG4_H263_3GPP_P0_L10_AMR;
+ }
+
+ return null;
+ }
+
+ private MediaFormatProfile? ResolveVideoASFFormat(string videoCodec, string audioCodec, int? width, int? height)
+ {
+ if (StringHelper.EqualsIgnoreCase(videoCodec, "wmv") &&
+ (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "wma") || StringHelper.EqualsIgnoreCase(videoCodec, "wmapro")))
+ {
+
+ if (width.HasValue && height.HasValue)
+ {
+ if ((width.Value <= 720) && (height.Value <= 576))
+ {
+ if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "wma"))
+ {
+ return MediaFormatProfile.WMVMED_FULL;
+ }
+ return MediaFormatProfile.WMVMED_PRO;
+ }
+ }
+
+ if (string.IsNullOrEmpty(audioCodec) || StringHelper.EqualsIgnoreCase(audioCodec, "wma"))
+ {
+ return MediaFormatProfile.WMVHIGH_FULL;
+ }
+ return MediaFormatProfile.WMVHIGH_PRO;
+ }
+
+ if (StringHelper.EqualsIgnoreCase(videoCodec, "vc1"))
+ {
+ if (width.HasValue && height.HasValue)
+ {
+ if ((width.Value <= 720) && (height.Value <= 576))
+ return MediaFormatProfile.VC1_ASF_AP_L1_WMA;
+ if ((width.Value <= 1280) && (height.Value <= 720))
+ return MediaFormatProfile.VC1_ASF_AP_L2_WMA;
+ if ((width.Value <= 1920) && (height.Value <= 1080))
+ return MediaFormatProfile.VC1_ASF_AP_L3_WMA;
+ }
+ }
+ else if (StringHelper.EqualsIgnoreCase(videoCodec, "mpeg2video"))
+ {
+ return MediaFormatProfile.DVR_MS;
+ }
+
+ return null;
+ }
+
+ public MediaFormatProfile? ResolveAudioFormat(string container, int? bitrate, int? frequency, int? channels)
+ {
+ if (StringHelper.EqualsIgnoreCase(container, "asf"))
+ return ResolveAudioASFFormat(bitrate);
+
+ if (StringHelper.EqualsIgnoreCase(container, "mp3"))
+ return MediaFormatProfile.MP3;
+
+ if (StringHelper.EqualsIgnoreCase(container, "lpcm"))
+ return ResolveAudioLPCMFormat(frequency, channels);
+
+ if (StringHelper.EqualsIgnoreCase(container, "mp4") ||
+ StringHelper.EqualsIgnoreCase(container, "aac"))
+ return ResolveAudioMP4Format(bitrate);
+
+ if (StringHelper.EqualsIgnoreCase(container, "adts"))
+ return ResolveAudioADTSFormat(bitrate);
+
+ if (StringHelper.EqualsIgnoreCase(container, "flac"))
+ return MediaFormatProfile.FLAC;
+
+ if (StringHelper.EqualsIgnoreCase(container, "oga") ||
+ StringHelper.EqualsIgnoreCase(container, "ogg"))
+ return MediaFormatProfile.OGG;
+
+ return null;
+ }
+
+ private MediaFormatProfile ResolveAudioASFFormat(int? bitrate)
+ {
+ if (bitrate.HasValue && bitrate.Value <= 193)
+ {
+ return MediaFormatProfile.WMA_BASE;
+ }
+ return MediaFormatProfile.WMA_FULL;
+ }
+
+ private MediaFormatProfile? ResolveAudioLPCMFormat(int? frequency, int? channels)
+ {
+ if (frequency.HasValue && channels.HasValue)
+ {
+ if (frequency.Value == 44100 && channels.Value == 1)
+ {
+ return MediaFormatProfile.LPCM16_44_MONO;
+ }
+ if (frequency.Value == 44100 && channels.Value == 2)
+ {
+ return MediaFormatProfile.LPCM16_44_STEREO;
+ }
+ if (frequency.Value == 48000 && channels.Value == 1)
+ {
+ return MediaFormatProfile.LPCM16_48_MONO;
+ }
+ if (frequency.Value == 48000 && channels.Value == 2)
+ {
+ return MediaFormatProfile.LPCM16_48_STEREO;
+ }
+
+ return null;
+ }
+
+ return MediaFormatProfile.LPCM16_48_STEREO;
+ }
+
+ private MediaFormatProfile ResolveAudioMP4Format(int? bitrate)
+ {
+ if (bitrate.HasValue && bitrate.Value <= 320)
+ {
+ return MediaFormatProfile.AAC_ISO_320;
+ }
+ return MediaFormatProfile.AAC_ISO;
+ }
+
+ private MediaFormatProfile ResolveAudioADTSFormat(int? bitrate)
+ {
+ if (bitrate.HasValue && bitrate.Value <= 320)
+ {
+ return MediaFormatProfile.AAC_ADTS_320;
+ }
+ return MediaFormatProfile.AAC_ADTS;
+ }
+
+ public MediaFormatProfile? ResolveImageFormat(string container, int? width, int? height)
+ {
+ if (StringHelper.EqualsIgnoreCase(container, "jpeg") ||
+ StringHelper.EqualsIgnoreCase(container, "jpg"))
+ return ResolveImageJPGFormat(width, height);
+
+ if (StringHelper.EqualsIgnoreCase(container, "png"))
+ return ResolveImagePNGFormat(width, height);
+
+ if (StringHelper.EqualsIgnoreCase(container, "gif"))
+ return MediaFormatProfile.GIF_LRG;
+
+ if (StringHelper.EqualsIgnoreCase(container, "raw"))
+ return MediaFormatProfile.RAW;
+
+ return null;
+ }
+
+ private MediaFormatProfile ResolveImageJPGFormat(int? width, int? height)
+ {
+ if (width.HasValue && height.HasValue)
+ {
+ if ((width.Value <= 160) && (height.Value <= 160))
+ return MediaFormatProfile.JPEG_TN;
+
+ if ((width.Value <= 640) && (height.Value <= 480))
+ return MediaFormatProfile.JPEG_SM;
+
+ if ((width.Value <= 1024) && (height.Value <= 768))
+ {
+ return MediaFormatProfile.JPEG_MED;
+ }
+
+ return MediaFormatProfile.JPEG_LRG;
+ }
+
+ return MediaFormatProfile.JPEG_SM;
+ }
+
+ private MediaFormatProfile ResolveImagePNGFormat(int? width, int? height)
+ {
+ if (width.HasValue && height.HasValue)
+ {
+ if ((width.Value <= 160) && (height.Value <= 160))
+ return MediaFormatProfile.PNG_TN;
+ }
+
+ return MediaFormatProfile.PNG_LRG;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs b/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs
new file mode 100644
index 000000000..4ed412985
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/PlaybackErrorCode.cs
@@ -0,0 +1,10 @@
+
+namespace MediaBrowser.Model.Dlna
+{
+ public enum PlaybackErrorCode
+ {
+ NotAllowed = 0,
+ NoCompatibleStream = 1,
+ RateLimitExceeded = 2
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/ProfileCondition.cs b/MediaBrowser.Model/Dlna/ProfileCondition.cs
new file mode 100644
index 000000000..9234a2713
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/ProfileCondition.cs
@@ -0,0 +1,38 @@
+using System.Xml.Serialization;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class ProfileCondition
+ {
+ [XmlAttribute("condition")]
+ public ProfileConditionType Condition { get; set; }
+
+ [XmlAttribute("property")]
+ public ProfileConditionValue Property { get; set; }
+
+ [XmlAttribute("value")]
+ public string Value { get; set; }
+
+ [XmlAttribute("isRequired")]
+ public bool IsRequired { get; set; }
+
+ public ProfileCondition()
+ {
+ IsRequired = true;
+ }
+
+ public ProfileCondition(ProfileConditionType condition, ProfileConditionValue property, string value)
+ : this(condition, property, value, false)
+ {
+
+ }
+
+ public ProfileCondition(ProfileConditionType condition, ProfileConditionValue property, string value, bool isRequired)
+ {
+ Condition = condition;
+ Property = property;
+ Value = value;
+ IsRequired = isRequired;
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/ProfileConditionType.cs b/MediaBrowser.Model/Dlna/ProfileConditionType.cs
new file mode 100644
index 000000000..b0a94c5b3
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/ProfileConditionType.cs
@@ -0,0 +1,11 @@
+namespace MediaBrowser.Model.Dlna
+{
+ public enum ProfileConditionType
+ {
+ Equals = 0,
+ NotEquals = 1,
+ LessThanEqual = 2,
+ GreaterThanEqual = 3,
+ EqualsAny = 4
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/ProfileConditionValue.cs b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs
new file mode 100644
index 000000000..a96e9ac36
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs
@@ -0,0 +1,29 @@
+namespace MediaBrowser.Model.Dlna
+{
+ public enum ProfileConditionValue
+ {
+ AudioChannels = 0,
+ AudioBitrate = 1,
+ AudioProfile = 2,
+ Width = 3,
+ Height = 4,
+ Has64BitOffsets = 5,
+ PacketLength = 6,
+ VideoBitDepth = 7,
+ VideoBitrate = 8,
+ VideoFramerate = 9,
+ VideoLevel = 10,
+ VideoProfile = 11,
+ VideoTimestamp = 12,
+ IsAnamorphic = 13,
+ RefFrames = 14,
+ NumAudioStreams = 16,
+ NumVideoStreams = 17,
+ IsSecondaryAudio = 18,
+ VideoCodecTag = 19,
+ IsAvc = 20,
+ IsInterlaced = 21,
+ AudioSampleRate = 22,
+ AudioBitDepth = 23
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs b/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs
new file mode 100644
index 000000000..8efdb0660
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/ResolutionConfiguration.cs
@@ -0,0 +1,14 @@
+namespace MediaBrowser.Model.Dlna
+{
+ public class ResolutionConfiguration
+ {
+ public int MaxWidth { get; set; }
+ public int MaxBitrate { get; set; }
+
+ public ResolutionConfiguration(int maxWidth, int maxBitrate)
+ {
+ MaxWidth = maxWidth;
+ MaxBitrate = maxBitrate;
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
new file mode 100644
index 000000000..4fdf4972f
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class ResolutionNormalizer
+ {
+ private static readonly ResolutionConfiguration[] Configurations =
+ new []
+ {
+ new ResolutionConfiguration(426, 320000),
+ new ResolutionConfiguration(640, 400000),
+ new ResolutionConfiguration(720, 950000),
+ new ResolutionConfiguration(1280, 2500000),
+ new ResolutionConfiguration(1920, 4000000),
+ new ResolutionConfiguration(3840, 35000000)
+ };
+
+ public static ResolutionOptions Normalize(int? inputBitrate,
+ int? unused1,
+ int? unused2,
+ int outputBitrate,
+ string inputCodec,
+ string outputCodec,
+ int? maxWidth,
+ int? maxHeight)
+ {
+ // If the bitrate isn't changing, then don't downlscale the resolution
+ if (inputBitrate.HasValue && outputBitrate >= inputBitrate.Value)
+ {
+ if (maxWidth.HasValue || maxHeight.HasValue)
+ {
+ return new ResolutionOptions
+ {
+ MaxWidth = maxWidth,
+ MaxHeight = maxHeight
+ };
+ }
+ }
+
+ var resolutionConfig = GetResolutionConfiguration(outputBitrate);
+ if (resolutionConfig != null)
+ {
+ var originvalValue = maxWidth;
+
+ maxWidth = Math.Min(resolutionConfig.MaxWidth, maxWidth ?? resolutionConfig.MaxWidth);
+ if (!originvalValue.HasValue || originvalValue.Value != maxWidth.Value)
+ {
+ maxHeight = null;
+ }
+ }
+
+ return new ResolutionOptions
+ {
+ MaxWidth = maxWidth,
+ MaxHeight = maxHeight
+ };
+ }
+
+ private static ResolutionConfiguration GetResolutionConfiguration(int outputBitrate)
+ {
+ ResolutionConfiguration previousOption = null;
+
+ foreach (var config in Configurations)
+ {
+ if (outputBitrate <= config.MaxBitrate)
+ {
+ return previousOption ?? config;
+ }
+
+ previousOption = config;
+ }
+
+ return null;
+ }
+
+ private static double GetVideoBitrateScaleFactor(string codec)
+ {
+ if (StringHelper.EqualsIgnoreCase(codec, "h265") ||
+ StringHelper.EqualsIgnoreCase(codec, "hevc") ||
+ StringHelper.EqualsIgnoreCase(codec, "vp9"))
+ {
+ return .5;
+ }
+ return 1;
+ }
+
+ public static int ScaleBitrate(int bitrate, string inputVideoCodec, string outputVideoCodec)
+ {
+ var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec);
+ var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec);
+ var scaleFactor = outputScaleFactor/inputScaleFactor;
+ var newBitrate = scaleFactor*bitrate;
+
+ return Convert.ToInt32(newBitrate);
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/ResolutionOptions.cs b/MediaBrowser.Model/Dlna/ResolutionOptions.cs
new file mode 100644
index 000000000..6b711cfa0
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/ResolutionOptions.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Model.Dlna
+{
+ public class ResolutionOptions
+ {
+ public int? MaxWidth { get; set; }
+ public int? MaxHeight { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/ResponseProfile.cs b/MediaBrowser.Model/Dlna/ResponseProfile.cs
new file mode 100644
index 000000000..742253fa3
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/ResponseProfile.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+using MediaBrowser.Model.Dlna;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class ResponseProfile
+ {
+ [XmlAttribute("container")]
+ public string Container { get; set; }
+
+ [XmlAttribute("audioCodec")]
+ public string AudioCodec { get; set; }
+
+ [XmlAttribute("videoCodec")]
+ public string VideoCodec { get; set; }
+
+ [XmlAttribute("type")]
+ public DlnaProfileType Type { get; set; }
+
+ [XmlAttribute("orgPn")]
+ public string OrgPn { get; set; }
+
+ [XmlAttribute("mimeType")]
+ public string MimeType { get; set; }
+
+ public ProfileCondition[] Conditions { get; set; }
+
+ public ResponseProfile()
+ {
+ Conditions = new ProfileCondition[] {};
+ }
+
+ public string[] GetContainers()
+ {
+ return ContainerProfile.SplitValue(Container);
+ }
+
+ public string[] GetAudioCodecs()
+ {
+ return ContainerProfile.SplitValue(AudioCodec);
+ }
+
+ public string[] GetVideoCodecs()
+ {
+ return ContainerProfile.SplitValue(VideoCodec);
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/SearchCriteria.cs b/MediaBrowser.Model/Dlna/SearchCriteria.cs
new file mode 100644
index 000000000..533605d89
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/SearchCriteria.cs
@@ -0,0 +1,75 @@
+using MediaBrowser.Model.Extensions;
+using System;
+using System.Text.RegularExpressions;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class SearchCriteria
+ {
+ public SearchType SearchType { get; set; }
+
+ /// <summary>
+ /// Splits the specified string.
+ /// </summary>
+ /// <param name="str">The string.</param>
+ /// <param name="term">The term.</param>
+ /// <param name="limit">The limit.</param>
+ /// <returns>System.String[].</returns>
+ private string[] RegexSplit(string str, string term, int limit)
+ {
+ return new Regex(term).Split(str, limit);
+ }
+
+ /// <summary>
+ /// Splits the specified string.
+ /// </summary>
+ /// <param name="str">The string.</param>
+ /// <param name="term">The term.</param>
+ /// <returns>System.String[].</returns>
+ private string[] RegexSplit(string str, string term)
+ {
+ return Regex.Split(str, term, RegexOptions.IgnoreCase);
+ }
+
+ public SearchCriteria(string search)
+ {
+ if (string.IsNullOrEmpty(search))
+ {
+ throw new ArgumentNullException("search");
+ }
+
+ SearchType = SearchType.Unknown;
+
+ String[] factors = RegexSplit(search, "(and|or)");
+ foreach (String factor in factors)
+ {
+ String[] subFactors = RegexSplit(factor.Trim().Trim('(').Trim(')').Trim(), "\\s", 3);
+
+ if (subFactors.Length == 3)
+ {
+
+ if (StringHelper.EqualsIgnoreCase("upnp:class", subFactors[0]) &&
+ (StringHelper.EqualsIgnoreCase("=", subFactors[1]) || StringHelper.EqualsIgnoreCase("derivedfrom", subFactors[1])))
+ {
+ if (StringHelper.EqualsIgnoreCase("\"object.item.imageItem\"", subFactors[2]) || StringHelper.EqualsIgnoreCase("\"object.item.imageItem.photo\"", subFactors[2]))
+ {
+ SearchType = SearchType.Image;
+ }
+ else if (StringHelper.EqualsIgnoreCase("\"object.item.videoItem\"", subFactors[2]))
+ {
+ SearchType = SearchType.Video;
+ }
+ else if (StringHelper.EqualsIgnoreCase("\"object.container.playlistContainer\"", subFactors[2]))
+ {
+ SearchType = SearchType.Playlist;
+ }
+ else if (StringHelper.EqualsIgnoreCase("\"object.container.album.musicAlbum\"", subFactors[2]))
+ {
+ SearchType = SearchType.MusicAlbum;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/SearchType.cs b/MediaBrowser.Model/Dlna/SearchType.cs
new file mode 100644
index 000000000..27b207879
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/SearchType.cs
@@ -0,0 +1,12 @@
+namespace MediaBrowser.Model.Dlna
+{
+ public enum SearchType
+ {
+ Unknown = 0,
+ Audio = 1,
+ Image = 2,
+ Video = 3,
+ Playlist = 4,
+ MusicAlbum = 5
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/SortCriteria.cs b/MediaBrowser.Model/Dlna/SortCriteria.cs
new file mode 100644
index 000000000..600a2f58e
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/SortCriteria.cs
@@ -0,0 +1,17 @@
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class SortCriteria
+ {
+ public SortOrder SortOrder
+ {
+ get { return SortOrder.Ascending; }
+ }
+
+ public SortCriteria(string value)
+ {
+
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
new file mode 100644
index 000000000..840abf618
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -0,0 +1,1900 @@
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Session;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class StreamBuilder
+ {
+ private readonly ILogger _logger;
+ private readonly ITranscoderSupport _transcoderSupport;
+
+ public StreamBuilder(ITranscoderSupport transcoderSupport, ILogger logger)
+ {
+ _transcoderSupport = transcoderSupport;
+ _logger = logger;
+ }
+
+ public StreamBuilder(ILogger logger)
+ : this(new FullTranscoderSupport(), logger)
+ {
+ }
+
+ public StreamInfo BuildAudioItem(AudioOptions options)
+ {
+ ValidateAudioInput(options);
+
+ var mediaSources = new List<MediaSourceInfo>();
+ foreach (MediaSourceInfo i in options.MediaSources)
+ {
+ if (string.IsNullOrEmpty(options.MediaSourceId) ||
+ StringHelper.EqualsIgnoreCase(i.Id, options.MediaSourceId))
+ {
+ mediaSources.Add(i);
+ }
+ }
+
+ var streams = new List<StreamInfo>();
+ foreach (MediaSourceInfo i in mediaSources)
+ {
+ StreamInfo streamInfo = BuildAudioItem(i, options);
+ if (streamInfo != null)
+ {
+ streams.Add(streamInfo);
+ }
+ }
+
+ foreach (StreamInfo stream in streams)
+ {
+ stream.DeviceId = options.DeviceId;
+ stream.DeviceProfileId = options.Profile.Id;
+ }
+
+ return GetOptimalStream(streams, options.GetMaxBitrate(true) ?? 0);
+ }
+
+ public StreamInfo BuildVideoItem(VideoOptions options)
+ {
+ ValidateInput(options);
+
+ var mediaSources = new List<MediaSourceInfo>();
+ foreach (MediaSourceInfo i in options.MediaSources)
+ {
+ if (string.IsNullOrEmpty(options.MediaSourceId) ||
+ StringHelper.EqualsIgnoreCase(i.Id, options.MediaSourceId))
+ {
+ mediaSources.Add(i);
+ }
+ }
+
+ var streams = new List<StreamInfo>();
+ foreach (MediaSourceInfo i in mediaSources)
+ {
+ StreamInfo streamInfo = BuildVideoItem(i, options);
+ if (streamInfo != null)
+ {
+ streams.Add(streamInfo);
+ }
+ }
+
+ foreach (StreamInfo stream in streams)
+ {
+ stream.DeviceId = options.DeviceId;
+ stream.DeviceProfileId = options.Profile.Id;
+ }
+
+ return GetOptimalStream(streams, options.GetMaxBitrate(false) ?? 0);
+ }
+
+ private StreamInfo GetOptimalStream(List<StreamInfo> streams, long maxBitrate)
+ {
+ var sorted = SortMediaSources(streams, maxBitrate);
+
+ foreach (StreamInfo stream in sorted)
+ {
+ return stream;
+ }
+
+ return null;
+ }
+
+ private StreamInfo[] SortMediaSources(List<StreamInfo> streams, long maxBitrate)
+ {
+ return streams.OrderBy(i =>
+ {
+ // Nothing beats direct playing a file
+ if (i.PlayMethod == PlayMethod.DirectPlay && i.MediaSource.Protocol == MediaProtocol.File)
+ {
+ return 0;
+ }
+
+ return 1;
+
+ }).ThenBy(i =>
+ {
+ switch (i.PlayMethod)
+ {
+ // Let's assume direct streaming a file is just as desirable as direct playing a remote url
+ case PlayMethod.DirectStream:
+ case PlayMethod.DirectPlay:
+ return 0;
+ default:
+ return 1;
+ }
+
+ }).ThenBy(i =>
+ {
+ switch (i.MediaSource.Protocol)
+ {
+ case MediaProtocol.File:
+ return 0;
+ default:
+ return 1;
+ }
+
+ }).ThenBy(i =>
+ {
+ if (maxBitrate > 0)
+ {
+ if (i.MediaSource.Bitrate.HasValue)
+ {
+ return Math.Abs(i.MediaSource.Bitrate.Value - maxBitrate);
+ }
+ }
+
+ return 0;
+
+ }).ThenBy(streams.IndexOf).ToArray();
+ }
+
+ private TranscodeReason? GetTranscodeReasonForFailedCondition(ProfileCondition condition)
+ {
+ switch (condition.Property)
+ {
+ case ProfileConditionValue.AudioBitrate:
+ if (condition.Condition == ProfileConditionType.LessThanEqual)
+ {
+ return TranscodeReason.AudioBitrateNotSupported;
+ }
+ return TranscodeReason.AudioBitrateNotSupported;
+
+ case ProfileConditionValue.AudioChannels:
+ if (condition.Condition == ProfileConditionType.LessThanEqual)
+ {
+ return TranscodeReason.AudioChannelsNotSupported;
+ }
+ return TranscodeReason.AudioChannelsNotSupported;
+
+ case ProfileConditionValue.AudioProfile:
+ return TranscodeReason.AudioProfileNotSupported;
+
+ case ProfileConditionValue.AudioSampleRate:
+ return TranscodeReason.AudioSampleRateNotSupported;
+
+ case ProfileConditionValue.Has64BitOffsets:
+ // TODO
+ return null;
+
+ case ProfileConditionValue.Height:
+ return TranscodeReason.VideoResolutionNotSupported;
+
+ case ProfileConditionValue.IsAnamorphic:
+ return TranscodeReason.AnamorphicVideoNotSupported;
+
+ case ProfileConditionValue.IsAvc:
+ // TODO
+ return null;
+
+ case ProfileConditionValue.IsInterlaced:
+ return TranscodeReason.InterlacedVideoNotSupported;
+
+ case ProfileConditionValue.IsSecondaryAudio:
+ return TranscodeReason.SecondaryAudioNotSupported;
+
+ case ProfileConditionValue.NumAudioStreams:
+ // TODO
+ return null;
+
+ case ProfileConditionValue.NumVideoStreams:
+ // TODO
+ return null;
+
+ case ProfileConditionValue.PacketLength:
+ // TODO
+ return null;
+
+ case ProfileConditionValue.RefFrames:
+ return TranscodeReason.RefFramesNotSupported;
+
+ case ProfileConditionValue.VideoBitDepth:
+ return TranscodeReason.VideoBitDepthNotSupported;
+
+ case ProfileConditionValue.AudioBitDepth:
+ return TranscodeReason.AudioBitDepthNotSupported;
+
+ case ProfileConditionValue.VideoBitrate:
+ return TranscodeReason.VideoBitrateNotSupported;
+
+ case ProfileConditionValue.VideoCodecTag:
+ return TranscodeReason.VideoCodecNotSupported;
+
+ case ProfileConditionValue.VideoFramerate:
+ return TranscodeReason.VideoFramerateNotSupported;
+
+ case ProfileConditionValue.VideoLevel:
+ return TranscodeReason.VideoLevelNotSupported;
+
+ case ProfileConditionValue.VideoProfile:
+ return TranscodeReason.VideoProfileNotSupported;
+
+ case ProfileConditionValue.VideoTimestamp:
+ // TODO
+ return null;
+
+ case ProfileConditionValue.Width:
+ return TranscodeReason.VideoResolutionNotSupported;
+
+ default:
+ return null;
+ }
+ }
+
+ public static string NormalizeMediaSourceFormatIntoSingleContainer(string inputContainer, string unused1, DeviceProfile profile, DlnaProfileType type)
+ {
+ if (string.IsNullOrEmpty(inputContainer))
+ {
+ return null;
+ }
+
+ var formats = ContainerProfile.SplitValue(inputContainer);
+
+ if (formats.Length == 1)
+ {
+ return formats[0];
+ }
+
+ if (profile != null)
+ {
+ foreach (var format in formats)
+ {
+ foreach (var directPlayProfile in profile.DirectPlayProfiles)
+ {
+ if (directPlayProfile.Type == type)
+ {
+ if (directPlayProfile.SupportsContainer(format))
+ {
+ return format;
+ }
+ }
+ }
+ }
+ }
+
+ return formats[0];
+ }
+
+ private StreamInfo BuildAudioItem(MediaSourceInfo item, AudioOptions options)
+ {
+ var transcodeReasons = new List<TranscodeReason>();
+
+ StreamInfo playlistItem = new StreamInfo
+ {
+ ItemId = options.ItemId,
+ MediaType = DlnaProfileType.Audio,
+ MediaSource = item,
+ RunTimeTicks = item.RunTimeTicks,
+ Context = options.Context,
+ DeviceProfile = options.Profile
+ };
+
+ if (options.ForceDirectPlay)
+ {
+ playlistItem.PlayMethod = PlayMethod.DirectPlay;
+ playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Audio);
+ return playlistItem;
+ }
+
+ if (options.ForceDirectStream)
+ {
+ playlistItem.PlayMethod = PlayMethod.DirectStream;
+ playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Audio);
+ return playlistItem;
+ }
+
+ MediaStream audioStream = item.GetDefaultAudioStream(null);
+
+ var directPlayInfo = GetAudioDirectPlayMethods(item, audioStream, options);
+
+ var directPlayMethods = directPlayInfo.Item1;
+ transcodeReasons.AddRange(directPlayInfo.Item2);
+
+ ConditionProcessor conditionProcessor = new ConditionProcessor();
+
+ int? inputAudioChannels = audioStream == null ? null : audioStream.Channels;
+ int? inputAudioBitrate = audioStream == null ? null : audioStream.BitDepth;
+ int? inputAudioSampleRate = audioStream == null ? null : audioStream.SampleRate;
+ int? inputAudioBitDepth = audioStream == null ? null : audioStream.BitDepth;
+
+ if (directPlayMethods.Count > 0)
+ {
+ string audioCodec = audioStream == null ? null : audioStream.Codec;
+
+ // Make sure audio codec profiles are satisfied
+ var conditions = new List<ProfileCondition>();
+ foreach (CodecProfile i in options.Profile.CodecProfiles)
+ {
+ if (i.Type == CodecType.Audio && i.ContainsAnyCodec(audioCodec, item.Container))
+ {
+ bool applyConditions = true;
+ foreach (ProfileCondition applyCondition in i.ApplyConditions)
+ {
+ if (!conditionProcessor.IsAudioConditionSatisfied(applyCondition, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth))
+ {
+ LogConditionFailure(options.Profile, "AudioCodecProfile", applyCondition, item);
+ applyConditions = false;
+ break;
+ }
+ }
+
+ if (applyConditions)
+ {
+ foreach (ProfileCondition c in i.Conditions)
+ {
+ conditions.Add(c);
+ }
+ }
+ }
+ }
+
+ bool all = true;
+ foreach (ProfileCondition c in conditions)
+ {
+ if (!conditionProcessor.IsAudioConditionSatisfied(c, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth))
+ {
+ LogConditionFailure(options.Profile, "AudioCodecProfile", c, item);
+ var transcodeReason = GetTranscodeReasonForFailedCondition(c);
+ if (transcodeReason.HasValue)
+ {
+ transcodeReasons.Add(transcodeReason.Value);
+ }
+ all = false;
+ break;
+ }
+ }
+
+ if (all)
+ {
+ if (directPlayMethods.Contains(PlayMethod.DirectStream))
+ {
+ playlistItem.PlayMethod = PlayMethod.DirectStream;
+ }
+
+ playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Audio);
+
+ return playlistItem;
+ }
+ }
+
+ TranscodingProfile transcodingProfile = null;
+ foreach (TranscodingProfile i in options.Profile.TranscodingProfiles)
+ {
+ if (i.Type == playlistItem.MediaType && i.Context == options.Context)
+ {
+ if (_transcoderSupport.CanEncodeToAudioCodec(i.AudioCodec ?? i.Container))
+ {
+ transcodingProfile = i;
+ break;
+ }
+ }
+ }
+
+ if (transcodingProfile != null)
+ {
+ if (!item.SupportsTranscoding)
+ {
+ return null;
+ }
+
+ SetStreamInfoOptionsFromTranscodingProfile(playlistItem, transcodingProfile);
+
+ var audioCodecProfiles = new List<CodecProfile>();
+ foreach (CodecProfile i in options.Profile.CodecProfiles)
+ {
+ if (i.Type == CodecType.Audio && i.ContainsAnyCodec(transcodingProfile.AudioCodec, transcodingProfile.Container))
+ {
+ audioCodecProfiles.Add(i);
+ }
+
+ if (audioCodecProfiles.Count >= 1) break;
+ }
+
+ var audioTranscodingConditions = new List<ProfileCondition>();
+ foreach (CodecProfile i in audioCodecProfiles)
+ {
+ bool applyConditions = true;
+ foreach (ProfileCondition applyCondition in i.ApplyConditions)
+ {
+ if (!conditionProcessor.IsAudioConditionSatisfied(applyCondition, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth))
+ {
+ LogConditionFailure(options.Profile, "AudioCodecProfile", applyCondition, item);
+ applyConditions = false;
+ break;
+ }
+ }
+
+ if (applyConditions)
+ {
+ foreach (ProfileCondition c in i.Conditions)
+ {
+ audioTranscodingConditions.Add(c);
+ }
+ }
+ }
+
+ ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true);
+
+ // Honor requested max channels
+ playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
+
+ var configuredBitrate = options.GetMaxBitrate(true);
+
+ long transcodingBitrate = options.AudioTranscodingBitrate ??
+ (options.Context == EncodingContext.Streaming ? options.Profile.MusicStreamingTranscodingBitrate : null) ??
+ configuredBitrate ??
+ 128000;
+
+ if (configuredBitrate.HasValue)
+ {
+ transcodingBitrate = Math.Min(configuredBitrate.Value, transcodingBitrate);
+ }
+
+ var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate);
+ playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate);
+ }
+
+ playlistItem.TranscodeReasons = transcodeReasons.ToArray();
+ return playlistItem;
+ }
+
+ private long? GetBitrateForDirectPlayCheck(MediaSourceInfo item, AudioOptions options, bool isAudio)
+ {
+ if (item.Protocol == MediaProtocol.File)
+ {
+ return options.Profile.MaxStaticBitrate;
+ }
+
+ return options.GetMaxBitrate(isAudio);
+ }
+
+ private Tuple<List<PlayMethod>, List<TranscodeReason>> GetAudioDirectPlayMethods(MediaSourceInfo item, MediaStream audioStream, AudioOptions options)
+ {
+ var transcodeReasons = new List<TranscodeReason>();
+
+ DirectPlayProfile directPlayProfile = null;
+ foreach (DirectPlayProfile i in options.Profile.DirectPlayProfiles)
+ {
+ if (i.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(i, item, audioStream))
+ {
+ directPlayProfile = i;
+ break;
+ }
+ }
+
+ var playMethods = new List<PlayMethod>();
+
+ if (directPlayProfile != null)
+ {
+ // While options takes the network and other factors into account. Only applies to direct stream
+ if (item.SupportsDirectStream)
+ {
+ if (IsAudioEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream))
+ {
+ if (options.EnableDirectStream)
+ {
+ playMethods.Add(PlayMethod.DirectStream);
+ }
+ }
+ else
+ {
+ transcodeReasons.Add(TranscodeReason.ContainerBitrateExceedsLimit);
+ }
+ }
+
+ // The profile describes what the device supports
+ // If device requirements are satisfied then allow both direct stream and direct play
+ if (item.SupportsDirectPlay)
+ {
+ if (IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, PlayMethod.DirectPlay))
+ {
+ if (options.EnableDirectPlay)
+ {
+ playMethods.Add(PlayMethod.DirectPlay);
+ }
+ }
+ else
+ {
+ transcodeReasons.Add(TranscodeReason.ContainerBitrateExceedsLimit);
+ }
+ }
+ }
+ else
+ {
+ transcodeReasons.InsertRange(0, GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles));
+
+ _logger.Info("Profile: {0}, No direct play profiles found for Path: {1}",
+ options.Profile.Name ?? "Unknown Profile",
+ item.Path ?? "Unknown path");
+ }
+
+ if (playMethods.Count > 0)
+ {
+ transcodeReasons.Clear();
+ }
+ else
+ {
+ transcodeReasons = transcodeReasons.Distinct().ToList();
+ }
+
+ return new Tuple<List<PlayMethod>, List<TranscodeReason>>(playMethods, transcodeReasons);
+ }
+
+ private List<TranscodeReason> GetTranscodeReasonsFromDirectPlayProfile(MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<DirectPlayProfile> directPlayProfiles)
+ {
+ var list = new List<TranscodeReason>();
+ var containerSupported = false;
+ var audioSupported = false;
+ var videoSupported = false;
+
+ foreach (var profile in directPlayProfiles)
+ {
+ audioSupported = false;
+ videoSupported = false;
+
+ // Check container type
+ if (profile.SupportsContainer(item.Container))
+ {
+ containerSupported = true;
+
+ if (videoStream != null)
+ {
+ if (profile.SupportsVideoCodec(videoStream.Codec))
+ {
+ videoSupported = true;
+ }
+ }
+
+ if (audioStream != null)
+ {
+ if (profile.SupportsAudioCodec(audioStream.Codec))
+ {
+ audioSupported = true;
+ }
+ }
+
+ if (videoSupported && audioSupported)
+ {
+ break;
+ }
+ }
+ }
+
+ if (!containerSupported)
+ {
+ list.Add(TranscodeReason.ContainerNotSupported);
+ }
+
+ if (videoStream != null && !videoSupported)
+ {
+ list.Add(TranscodeReason.VideoCodecNotSupported);
+ }
+
+ if (audioStream != null && !audioSupported)
+ {
+ list.Add(TranscodeReason.AudioCodecNotSupported);
+ }
+
+ return list;
+ }
+
+ private int? GetDefaultSubtitleStreamIndex(MediaSourceInfo item, SubtitleProfile[] subtitleProfiles)
+ {
+ int highestScore = -1;
+
+ foreach (MediaStream stream in item.MediaStreams)
+ {
+ if (stream.Type == MediaStreamType.Subtitle && stream.Score.HasValue)
+ {
+ if (stream.Score.Value > highestScore)
+ {
+ highestScore = stream.Score.Value;
+ }
+ }
+ }
+
+ var topStreams = new List<MediaStream>();
+ foreach (MediaStream stream in item.MediaStreams)
+ {
+ if (stream.Type == MediaStreamType.Subtitle && stream.Score.HasValue && stream.Score.Value == highestScore)
+ {
+ topStreams.Add(stream);
+ }
+ }
+
+ // If multiple streams have an equal score, try to pick the most efficient one
+ if (topStreams.Count > 1)
+ {
+ foreach (MediaStream stream in topStreams)
+ {
+ foreach (SubtitleProfile profile in subtitleProfiles)
+ {
+ if (profile.Method == SubtitleDeliveryMethod.External && StringHelper.EqualsIgnoreCase(profile.Format, stream.Codec))
+ {
+ return stream.Index;
+ }
+ }
+ }
+ }
+
+ // If no optimization panned out, just use the original default
+ return item.DefaultSubtitleStreamIndex;
+ }
+
+ private void SetStreamInfoOptionsFromTranscodingProfile(StreamInfo playlistItem, TranscodingProfile transcodingProfile)
+ {
+ if (string.IsNullOrEmpty(transcodingProfile.AudioCodec))
+ {
+ playlistItem.AudioCodecs = Array.Empty<string>();
+ }
+ else
+ {
+ playlistItem.AudioCodecs = transcodingProfile.AudioCodec.Split(',');
+ }
+
+ playlistItem.Container = transcodingProfile.Container;
+ playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
+ playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
+
+ if (string.IsNullOrEmpty(transcodingProfile.VideoCodec))
+ {
+ playlistItem.VideoCodecs = Array.Empty<string>();
+ }
+ else
+ {
+ playlistItem.VideoCodecs = transcodingProfile.VideoCodec.Split(',');
+ }
+
+ playlistItem.CopyTimestamps = transcodingProfile.CopyTimestamps;
+ playlistItem.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
+ playlistItem.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
+
+ playlistItem.BreakOnNonKeyFrames = transcodingProfile.BreakOnNonKeyFrames;
+
+ if (transcodingProfile.MinSegments > 0)
+ {
+ playlistItem.MinSegments = transcodingProfile.MinSegments;
+ }
+ if (transcodingProfile.SegmentLength > 0)
+ {
+ playlistItem.SegmentLength = transcodingProfile.SegmentLength;
+ }
+ playlistItem.SubProtocol = transcodingProfile.Protocol;
+
+ if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels))
+ {
+ int transcodingMaxAudioChannels;
+ if (int.TryParse(transcodingProfile.MaxAudioChannels, NumberStyles.Any, CultureInfo.InvariantCulture, out transcodingMaxAudioChannels))
+ {
+ playlistItem.TranscodingMaxAudioChannels = transcodingMaxAudioChannels;
+ }
+ }
+ }
+
+ private StreamInfo BuildVideoItem(MediaSourceInfo item, VideoOptions options)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ var transcodeReasons = new List<TranscodeReason>();
+
+ StreamInfo playlistItem = new StreamInfo
+ {
+ ItemId = options.ItemId,
+ MediaType = DlnaProfileType.Video,
+ MediaSource = item,
+ RunTimeTicks = item.RunTimeTicks,
+ Context = options.Context,
+ DeviceProfile = options.Profile
+ };
+
+ playlistItem.SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles);
+ MediaStream subtitleStream = playlistItem.SubtitleStreamIndex.HasValue ? item.GetMediaStream(MediaStreamType.Subtitle, playlistItem.SubtitleStreamIndex.Value) : null;
+
+ MediaStream audioStream = item.GetDefaultAudioStream(options.AudioStreamIndex ?? item.DefaultAudioStreamIndex);
+ if (audioStream != null)
+ {
+ playlistItem.AudioStreamIndex = audioStream.Index;
+ }
+
+ MediaStream videoStream = item.VideoStream;
+
+ // TODO: This doesn't accout for situation of device being able to handle media bitrate, but wifi connection not fast enough
+ var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, subtitleStream, options, PlayMethod.DirectPlay);
+ var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, subtitleStream, options, PlayMethod.DirectStream);
+ bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.Item1);
+ bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamEligibilityResult.Item1);
+
+ _logger.Info("Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
+ options.Profile.Name ?? "Unknown Profile",
+ item.Path ?? "Unknown path",
+ isEligibleForDirectPlay,
+ isEligibleForDirectStream);
+
+ if (isEligibleForDirectPlay || isEligibleForDirectStream)
+ {
+ // See if it can be direct played
+ var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, isEligibleForDirectPlay, isEligibleForDirectStream);
+ var directPlay = directPlayInfo.Item1;
+
+ if (directPlay != null)
+ {
+ playlistItem.PlayMethod = directPlay.Value;
+ playlistItem.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, item.Path, options.Profile, DlnaProfileType.Video);
+
+ if (subtitleStream != null)
+ {
+ SubtitleProfile subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, item.Container, null);
+
+ playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
+ playlistItem.SubtitleFormat = subtitleProfile.Format;
+ }
+
+ return playlistItem;
+ }
+
+ transcodeReasons.AddRange(directPlayInfo.Item2);
+ }
+
+ if (directPlayEligibilityResult.Item2.HasValue)
+ {
+ transcodeReasons.Add(directPlayEligibilityResult.Item2.Value);
+ }
+
+ if (directStreamEligibilityResult.Item2.HasValue)
+ {
+ transcodeReasons.Add(directStreamEligibilityResult.Item2.Value);
+ }
+
+ // Can't direct play, find the transcoding profile
+ TranscodingProfile transcodingProfile = null;
+ foreach (TranscodingProfile i in options.Profile.TranscodingProfiles)
+ {
+ if (i.Type == playlistItem.MediaType && i.Context == options.Context)
+ {
+ transcodingProfile = i;
+ break;
+ }
+ }
+
+ if (transcodingProfile != null)
+ {
+ if (!item.SupportsTranscoding)
+ {
+ return null;
+ }
+
+ if (subtitleStream != null)
+ {
+ SubtitleProfile subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Container, transcodingProfile.Protocol);
+
+ playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
+ playlistItem.SubtitleFormat = subtitleProfile.Format;
+ playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format };
+ }
+
+ playlistItem.PlayMethod = PlayMethod.Transcode;
+
+ SetStreamInfoOptionsFromTranscodingProfile(playlistItem, transcodingProfile);
+
+ ConditionProcessor conditionProcessor = new ConditionProcessor();
+
+ var isFirstAppliedCodecProfile = true;
+ foreach (CodecProfile i in options.Profile.CodecProfiles)
+ {
+ if (i.Type == CodecType.Video && i.ContainsAnyCodec(transcodingProfile.VideoCodec, transcodingProfile.Container))
+ {
+ bool applyConditions = true;
+ foreach (ProfileCondition applyCondition in i.ApplyConditions)
+ {
+ int? width = videoStream == null ? null : videoStream.Width;
+ int? height = videoStream == null ? null : videoStream.Height;
+ int? bitDepth = videoStream == null ? null : videoStream.BitDepth;
+ int? videoBitrate = videoStream == null ? null : videoStream.BitRate;
+ double? videoLevel = videoStream == null ? null : videoStream.Level;
+ string videoProfile = videoStream == null ? null : videoStream.Profile;
+ float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
+ bool? isAnamorphic = videoStream == null ? null : videoStream.IsAnamorphic;
+ bool? isInterlaced = videoStream == null ? (bool?)null : videoStream.IsInterlaced;
+ string videoCodecTag = videoStream == null ? null : videoStream.CodecTag;
+ bool? isAvc = videoStream == null ? null : videoStream.IsAVC;
+
+ TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp;
+ int? packetLength = videoStream == null ? null : videoStream.PacketLength;
+ int? refFrames = videoStream == null ? null : videoStream.RefFrames;
+
+ int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
+ int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
+
+ if (!conditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))
+ {
+ //LogConditionFailure(options.Profile, "VideoCodecProfile.ApplyConditions", applyCondition, item);
+ applyConditions = false;
+ break;
+ }
+ }
+
+ if (applyConditions)
+ {
+ var transcodingVideoCodecs = ContainerProfile.SplitValue(transcodingProfile.VideoCodec);
+ foreach (var transcodingVideoCodec in transcodingVideoCodecs)
+ {
+ if (i.ContainsAnyCodec(transcodingVideoCodec, transcodingProfile.Container))
+ {
+ ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingVideoCodec, true, isFirstAppliedCodecProfile);
+ isFirstAppliedCodecProfile = false;
+ }
+ }
+ }
+ }
+ }
+
+ // Honor requested max channels
+ playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
+
+ int audioBitrate = GetAudioBitrate(playlistItem.SubProtocol, options.GetMaxBitrate(false) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem);
+ playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
+
+ isFirstAppliedCodecProfile = true;
+ foreach (CodecProfile i in options.Profile.CodecProfiles)
+ {
+ if (i.Type == CodecType.VideoAudio && i.ContainsAnyCodec(transcodingProfile.AudioCodec, transcodingProfile.Container))
+ {
+ bool applyConditions = true;
+ foreach (ProfileCondition applyCondition in i.ApplyConditions)
+ {
+ bool? isSecondaryAudio = audioStream == null ? null : item.IsSecondaryAudio(audioStream);
+ int? inputAudioBitrate = audioStream == null ? null : audioStream.BitRate;
+ int? audioChannels = audioStream == null ? null : audioStream.Channels;
+ string audioProfile = audioStream == null ? null : audioStream.Profile;
+ int? inputAudioSampleRate = audioStream == null ? null : audioStream.SampleRate;
+ int? inputAudioBitDepth = audioStream == null ? null : audioStream.BitDepth;
+
+ if (!conditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio))
+ {
+ //LogConditionFailure(options.Profile, "VideoCodecProfile.ApplyConditions", applyCondition, item);
+ applyConditions = false;
+ break;
+ }
+ }
+
+ if (applyConditions)
+ {
+ var transcodingAudioCodecs = ContainerProfile.SplitValue(transcodingProfile.AudioCodec);
+ foreach (var transcodingAudioCodec in transcodingAudioCodecs)
+ {
+ if (i.ContainsAnyCodec(transcodingAudioCodec, transcodingProfile.Container))
+ {
+ ApplyTranscodingConditions(playlistItem, i.Conditions, transcodingAudioCodec, true, isFirstAppliedCodecProfile);
+ isFirstAppliedCodecProfile = false;
+ }
+ }
+ }
+ }
+ }
+
+ var maxBitrateSetting = options.GetMaxBitrate(false);
+ // Honor max rate
+ if (maxBitrateSetting.HasValue)
+ {
+ var availableBitrateForVideo = maxBitrateSetting.Value;
+
+ if (playlistItem.AudioBitrate.HasValue)
+ {
+ availableBitrateForVideo -= playlistItem.AudioBitrate.Value;
+ }
+
+ // Make sure the video bitrate is lower than bitrate settings but at least 64k
+ long currentValue = playlistItem.VideoBitrate ?? availableBitrateForVideo;
+ var longBitrate = Math.Max(Math.Min(availableBitrateForVideo, currentValue), 64000);
+ playlistItem.VideoBitrate = longBitrate >= int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate);
+ }
+ }
+
+ playlistItem.TranscodeReasons = transcodeReasons.ToArray();
+
+ return playlistItem;
+ }
+
+ private int GetDefaultAudioBitrateIfUnknown(MediaStream audioStream)
+ {
+ if ((audioStream.Channels ?? 0) >= 6)
+ {
+ return 384000;
+ }
+
+ return 192000;
+ }
+
+ private int GetAudioBitrate(string subProtocol, long maxTotalBitrate, string[] targetAudioCodecs, MediaStream audioStream, StreamInfo item)
+ {
+ var targetAudioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
+
+ var targetAudioChannels = item.GetTargetAudioChannels(targetAudioCodec);
+
+ int defaultBitrate = audioStream == null ? 192000 : audioStream.BitRate ?? GetDefaultAudioBitrateIfUnknown(audioStream);
+
+ // Reduce the bitrate if we're downmixing
+ if (targetAudioChannels.HasValue && audioStream != null && audioStream.Channels.HasValue && targetAudioChannels.Value < audioStream.Channels.Value)
+ {
+ defaultBitrate = targetAudioChannels.Value <= 2 ? 128000 : 192000;
+ }
+
+ int encoderAudioBitrateLimit = int.MaxValue;
+
+ if (audioStream != null)
+ {
+ // Seeing webm encoding failures when source has 1 audio channel and 22k bitrate.
+ // Any attempts to transcode over 64k will fail
+ if (audioStream.Channels.HasValue &&
+ audioStream.Channels.Value == 1)
+ {
+ if ((audioStream.BitRate ?? 0) < 64000)
+ {
+ encoderAudioBitrateLimit = 64000;
+ }
+ }
+ }
+
+ if (maxTotalBitrate > 0)
+ {
+ defaultBitrate = Math.Min(GetMaxAudioBitrateForTotalBitrate(maxTotalBitrate), defaultBitrate);
+ }
+
+ return Math.Min(defaultBitrate, encoderAudioBitrateLimit);
+ }
+
+ private int GetMaxAudioBitrateForTotalBitrate(long totalBitrate)
+ {
+ if (totalBitrate <= 640000)
+ {
+ return 128000;
+ }
+
+ if (totalBitrate <= 2000000)
+ {
+ return 384000;
+ }
+
+ if (totalBitrate <= 3000000)
+ {
+ return 448000;
+ }
+
+ return 640000;
+ }
+
+ private Tuple<PlayMethod?, List<TranscodeReason>> GetVideoDirectPlayProfile(VideoOptions options,
+ MediaSourceInfo mediaSource,
+ MediaStream videoStream,
+ MediaStream audioStream,
+ bool isEligibleForDirectPlay,
+ bool isEligibleForDirectStream)
+ {
+ DeviceProfile profile = options.Profile;
+
+ if (options.ForceDirectPlay)
+ {
+ return new Tuple<PlayMethod?, List<TranscodeReason>>(PlayMethod.DirectPlay, new List<TranscodeReason>());
+ }
+ if (options.ForceDirectStream)
+ {
+ return new Tuple<PlayMethod?, List<TranscodeReason>>(PlayMethod.DirectStream, new List<TranscodeReason>());
+ }
+
+ // See if it can be direct played
+ DirectPlayProfile directPlay = null;
+ foreach (DirectPlayProfile i in profile.DirectPlayProfiles)
+ {
+ if (i.Type == DlnaProfileType.Video && IsVideoDirectPlaySupported(i, mediaSource, videoStream, audioStream))
+ {
+ directPlay = i;
+ break;
+ }
+ }
+
+ if (directPlay == null)
+ {
+ _logger.Info("Profile: {0}, No direct play profiles found for Path: {1}",
+ profile.Name ?? "Unknown Profile",
+ mediaSource.Path ?? "Unknown path");
+
+ return new Tuple<PlayMethod?, List<TranscodeReason>>(null, GetTranscodeReasonsFromDirectPlayProfile(mediaSource, videoStream, audioStream, profile.DirectPlayProfiles));
+ }
+
+ string container = mediaSource.Container;
+
+ var conditions = new List<ProfileCondition>();
+ foreach (ContainerProfile i in profile.ContainerProfiles)
+ {
+ if (i.Type == DlnaProfileType.Video &&
+ i.ContainsContainer(container))
+ {
+ foreach (ProfileCondition c in i.Conditions)
+ {
+ conditions.Add(c);
+ }
+ }
+ }
+
+ ConditionProcessor conditionProcessor = new ConditionProcessor();
+
+ int? width = videoStream == null ? null : videoStream.Width;
+ int? height = videoStream == null ? null : videoStream.Height;
+ int? bitDepth = videoStream == null ? null : videoStream.BitDepth;
+ int? videoBitrate = videoStream == null ? null : videoStream.BitRate;
+ double? videoLevel = videoStream == null ? null : videoStream.Level;
+ string videoProfile = videoStream == null ? null : videoStream.Profile;
+ float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
+ bool? isAnamorphic = videoStream == null ? null : videoStream.IsAnamorphic;
+ bool? isInterlaced = videoStream == null ? (bool?)null : videoStream.IsInterlaced;
+ string videoCodecTag = videoStream == null ? null : videoStream.CodecTag;
+ bool? isAvc = videoStream == null ? null : videoStream.IsAVC;
+
+ int? audioBitrate = audioStream == null ? null : audioStream.BitRate;
+ int? audioChannels = audioStream == null ? null : audioStream.Channels;
+ string audioProfile = audioStream == null ? null : audioStream.Profile;
+ int? audioSampleRate = audioStream == null ? null : audioStream.SampleRate;
+ int? audioBitDepth = audioStream == null ? null : audioStream.BitDepth;
+
+ TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : mediaSource.Timestamp;
+ int? packetLength = videoStream == null ? null : videoStream.PacketLength;
+ int? refFrames = videoStream == null ? null : videoStream.RefFrames;
+
+ int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio);
+ int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video);
+
+ // Check container conditions
+ foreach (ProfileCondition i in conditions)
+ {
+ if (!conditionProcessor.IsVideoConditionSatisfied(i, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))
+ {
+ LogConditionFailure(profile, "VideoContainerProfile", i, mediaSource);
+
+ var transcodeReason = GetTranscodeReasonForFailedCondition(i);
+ var transcodeReasons = transcodeReason.HasValue
+ ? new List<TranscodeReason> { transcodeReason.Value }
+ : new List<TranscodeReason> { };
+
+ return new Tuple<PlayMethod?, List<TranscodeReason>>(null, transcodeReasons);
+ }
+ }
+
+ string videoCodec = videoStream == null ? null : videoStream.Codec;
+
+ conditions = new List<ProfileCondition>();
+ foreach (CodecProfile i in profile.CodecProfiles)
+ {
+ if (i.Type == CodecType.Video && i.ContainsAnyCodec(videoCodec, container))
+ {
+ bool applyConditions = true;
+ foreach (ProfileCondition applyCondition in i.ApplyConditions)
+ {
+ if (!conditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))
+ {
+ //LogConditionFailure(profile, "VideoCodecProfile.ApplyConditions", applyCondition, mediaSource);
+ applyConditions = false;
+ break;
+ }
+ }
+
+ if (applyConditions)
+ {
+ foreach (ProfileCondition c in i.Conditions)
+ {
+ conditions.Add(c);
+ }
+ }
+ }
+ }
+
+ foreach (ProfileCondition i in conditions)
+ {
+ if (!conditionProcessor.IsVideoConditionSatisfied(i, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))
+ {
+ LogConditionFailure(profile, "VideoCodecProfile", i, mediaSource);
+
+ var transcodeReason = GetTranscodeReasonForFailedCondition(i);
+ var transcodeReasons = transcodeReason.HasValue
+ ? new List<TranscodeReason> { transcodeReason.Value }
+ : new List<TranscodeReason> { };
+
+ return new Tuple<PlayMethod?, List<TranscodeReason>>(null, transcodeReasons);
+ }
+ }
+
+ if (audioStream != null)
+ {
+ string audioCodec = audioStream.Codec;
+
+ conditions = new List<ProfileCondition>();
+ bool? isSecondaryAudio = audioStream == null ? null : mediaSource.IsSecondaryAudio(audioStream);
+
+ foreach (CodecProfile i in profile.CodecProfiles)
+ {
+ if (i.Type == CodecType.VideoAudio && i.ContainsAnyCodec(audioCodec, container))
+ {
+ bool applyConditions = true;
+ foreach (ProfileCondition applyCondition in i.ApplyConditions)
+ {
+ if (!conditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio))
+ {
+ //LogConditionFailure(profile, "VideoAudioCodecProfile.ApplyConditions", applyCondition, mediaSource);
+ applyConditions = false;
+ break;
+ }
+ }
+
+ if (applyConditions)
+ {
+ foreach (ProfileCondition c in i.Conditions)
+ {
+ conditions.Add(c);
+ }
+ }
+ }
+ }
+
+ foreach (ProfileCondition i in conditions)
+ {
+ if (!conditionProcessor.IsVideoAudioConditionSatisfied(i, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio))
+ {
+ LogConditionFailure(profile, "VideoAudioCodecProfile", i, mediaSource);
+
+ var transcodeReason = GetTranscodeReasonForFailedCondition(i);
+ var transcodeReasons = transcodeReason.HasValue
+ ? new List<TranscodeReason> { transcodeReason.Value }
+ : new List<TranscodeReason> { };
+
+ return new Tuple<PlayMethod?, List<TranscodeReason>>(null, transcodeReasons);
+ }
+ }
+ }
+
+ if (isEligibleForDirectStream && mediaSource.SupportsDirectStream)
+ {
+ return new Tuple<PlayMethod?, List<TranscodeReason>>(PlayMethod.DirectStream, new List<TranscodeReason>());
+ }
+
+ return new Tuple<PlayMethod?, List<TranscodeReason>>(null, new List<TranscodeReason> { TranscodeReason.ContainerBitrateExceedsLimit });
+ }
+
+ private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo mediaSource)
+ {
+ _logger.Info("Profile: {0}, DirectPlay=false. Reason={1}.{2} Condition: {3}. ConditionValue: {4}. IsRequired: {5}. Path: {6}",
+ type,
+ profile.Name ?? "Unknown Profile",
+ condition.Property,
+ condition.Condition,
+ condition.Value ?? string.Empty,
+ condition.IsRequired,
+ mediaSource.Path ?? "Unknown path");
+ }
+
+ private ValueTuple<bool, TranscodeReason?> IsEligibleForDirectPlay(MediaSourceInfo item,
+ long maxBitrate,
+ MediaStream subtitleStream,
+ VideoOptions options,
+ PlayMethod playMethod)
+ {
+ if (subtitleStream != null)
+ {
+ SubtitleProfile subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, playMethod, _transcoderSupport, item.Container, null);
+
+ if (subtitleProfile.Method != SubtitleDeliveryMethod.External && subtitleProfile.Method != SubtitleDeliveryMethod.Embed)
+ {
+ _logger.Info("Not eligible for {0} due to unsupported subtitles", playMethod);
+ return new ValueTuple<bool, TranscodeReason?>(false, TranscodeReason.SubtitleCodecNotSupported);
+ }
+ }
+
+ var result = IsAudioEligibleForDirectPlay(item, maxBitrate, playMethod);
+
+ if (result)
+ {
+ return new ValueTuple<bool, TranscodeReason?>(result, null);
+ }
+
+ return new ValueTuple<bool, TranscodeReason?>(result, TranscodeReason.ContainerBitrateExceedsLimit);
+ }
+
+ public static SubtitleProfile GetSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, string outputContainer, string transcodingSubProtocol)
+ {
+ if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || !string.Equals(transcodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase)))
+ {
+ // Look for supported embedded subs of the same format
+ foreach (SubtitleProfile profile in subtitleProfiles)
+ {
+ if (!profile.SupportsLanguage(subtitleStream.Language))
+ {
+ continue;
+ }
+
+ if (profile.Method != SubtitleDeliveryMethod.Embed)
+ {
+ continue;
+ }
+
+ if (!ContainerProfile.ContainsContainer(profile.Container, outputContainer))
+ {
+ continue;
+ }
+
+ if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(subtitleStream, profile, transcodingSubProtocol, outputContainer))
+ {
+ continue;
+ }
+
+ if (subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format) && StringHelper.EqualsIgnoreCase(profile.Format, subtitleStream.Codec))
+ {
+ return profile;
+ }
+ }
+
+ // Look for supported embedded subs of a convertible format
+ foreach (SubtitleProfile profile in subtitleProfiles)
+ {
+ if (!profile.SupportsLanguage(subtitleStream.Language))
+ {
+ continue;
+ }
+
+ if (profile.Method != SubtitleDeliveryMethod.Embed)
+ {
+ continue;
+ }
+
+ if (!ContainerProfile.ContainsContainer(profile.Container, outputContainer))
+ {
+ continue;
+ }
+
+ if (playMethod == PlayMethod.Transcode && !IsSubtitleEmbedSupported(subtitleStream, profile, transcodingSubProtocol, outputContainer))
+ {
+ continue;
+ }
+
+ if (subtitleStream.IsTextSubtitleStream && subtitleStream.SupportsSubtitleConversionTo(profile.Format))
+ {
+ return profile;
+ }
+ }
+ }
+
+ // Look for an external or hls profile that matches the stream type (text/graphical) and doesn't require conversion
+ return GetExternalSubtitleProfile(mediaSource, subtitleStream, subtitleProfiles, playMethod, transcoderSupport, false) ??
+ GetExternalSubtitleProfile(mediaSource, subtitleStream, subtitleProfiles, playMethod, transcoderSupport, true) ??
+ new SubtitleProfile
+ {
+ Method = SubtitleDeliveryMethod.Encode,
+ Format = subtitleStream.Codec
+ };
+ }
+
+ private static bool IsSubtitleEmbedSupported(MediaStream subtitleStream, SubtitleProfile subtitleProfile, string transcodingSubProtocol, string transcodingContainer)
+ {
+ if (!string.IsNullOrEmpty(transcodingContainer))
+ {
+ var normalizedContainers = ContainerProfile.SplitValue(transcodingContainer);
+
+ if (ContainerProfile.ContainsContainer(normalizedContainers, "ts"))
+ {
+ return false;
+ }
+ if (ContainerProfile.ContainsContainer(normalizedContainers, "mpegts"))
+ {
+ return false;
+ }
+ if (ContainerProfile.ContainsContainer(normalizedContainers, "mp4"))
+ {
+ return false;
+ }
+ if (ContainerProfile.ContainsContainer(normalizedContainers, "mkv") ||
+ ContainerProfile.ContainsContainer(normalizedContainers, "matroska"))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static SubtitleProfile GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, bool allowConversion)
+ {
+ foreach (SubtitleProfile profile in subtitleProfiles)
+ {
+ if (profile.Method != SubtitleDeliveryMethod.External && profile.Method != SubtitleDeliveryMethod.Hls)
+ {
+ continue;
+ }
+
+ if (profile.Method == SubtitleDeliveryMethod.Hls && playMethod != PlayMethod.Transcode)
+ {
+ continue;
+ }
+
+ if (!profile.SupportsLanguage(subtitleStream.Language))
+ {
+ continue;
+ }
+
+ if (!subtitleStream.IsExternal && !transcoderSupport.CanExtractSubtitles(subtitleStream.Codec))
+ {
+ continue;
+ }
+
+ if ((profile.Method == SubtitleDeliveryMethod.External && subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format)) ||
+ (profile.Method == SubtitleDeliveryMethod.Hls && subtitleStream.IsTextSubtitleStream))
+ {
+ bool requiresConversion = !StringHelper.EqualsIgnoreCase(subtitleStream.Codec, profile.Format);
+
+ if (!requiresConversion)
+ {
+ return profile;
+ }
+
+ if (!allowConversion)
+ {
+ continue;
+ }
+
+ // TODO: Build this into subtitleStream.SupportsExternalStream
+ if (mediaSource.IsInfiniteStream)
+ {
+ continue;
+ }
+
+ if (subtitleStream.IsTextSubtitleStream && subtitleStream.SupportsExternalStream && subtitleStream.SupportsSubtitleConversionTo(profile.Format))
+ {
+ return profile;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod)
+ {
+ // Don't restrict by bitrate if coming from an external domain
+ if (item.IsRemote)
+ {
+ return true;
+ }
+
+ var requestedMaxBitrate = maxBitrate > 0 ? maxBitrate : 1000000;
+
+ // If we don't know the bitrate, then force a transcode if requested max bitrate is under 40 mbps
+ var itemBitrate = item.Bitrate ??
+ 40000000;
+
+ if (itemBitrate > requestedMaxBitrate)
+ {
+ _logger.Info("Bitrate exceeds " + playMethod + " limit: media bitrate: {0}, max bitrate: {1}", itemBitrate.ToString(CultureInfo.InvariantCulture), requestedMaxBitrate.ToString(CultureInfo.InvariantCulture));
+ return false;
+ }
+
+ return true;
+ }
+
+ private void ValidateInput(VideoOptions options)
+ {
+ ValidateAudioInput(options);
+
+ if (options.AudioStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
+ {
+ throw new ArgumentException("MediaSourceId is required when a specific audio stream is requested");
+ }
+
+ if (options.SubtitleStreamIndex.HasValue && string.IsNullOrEmpty(options.MediaSourceId))
+ {
+ throw new ArgumentException("MediaSourceId is required when a specific subtitle stream is requested");
+ }
+ }
+
+ private void ValidateAudioInput(AudioOptions options)
+ {
+ if (options.ItemId.Equals(Guid.Empty))
+ {
+ throw new ArgumentException("ItemId is required");
+ }
+ if (string.IsNullOrEmpty(options.DeviceId))
+ {
+ throw new ArgumentException("DeviceId is required");
+ }
+ if (options.Profile == null)
+ {
+ throw new ArgumentException("Profile is required");
+ }
+ if (options.MediaSources == null)
+ {
+ throw new ArgumentException("MediaSources is required");
+ }
+ }
+
+ private void ApplyTranscodingConditions(StreamInfo item, List<CodecProfile> codecProfiles)
+ {
+ foreach (var profile in codecProfiles)
+ {
+ ApplyTranscodingConditions(item, profile);
+ }
+ }
+
+ private void ApplyTranscodingConditions(StreamInfo item, CodecProfile codecProfile)
+ {
+ var codecs = ContainerProfile.SplitValue(codecProfile.Codec);
+ if (codecs.Length == 0)
+ {
+ ApplyTranscodingConditions(item, codecProfile.Conditions, null, true, true);
+ return;
+ }
+
+ var enableNonQualified = true;
+
+ foreach (var codec in codecs)
+ {
+ ApplyTranscodingConditions(item, codecProfile.Conditions, codec, true, enableNonQualified);
+ enableNonQualified = false;
+ }
+ }
+
+ private void ApplyTranscodingConditions(StreamInfo item, IEnumerable<ProfileCondition> conditions, string qualifier, bool enableQualifiedConditions, bool enableNonQualifiedConditions)
+ {
+ foreach (ProfileCondition condition in conditions)
+ {
+ string value = condition.Value;
+
+ if (string.IsNullOrEmpty(value))
+ {
+ continue;
+ }
+
+ // No way to express this
+ if (condition.Condition == ProfileConditionType.GreaterThanEqual)
+ {
+ continue;
+ }
+
+ switch (condition.Property)
+ {
+ case ProfileConditionValue.AudioBitrate:
+ {
+ if (!enableNonQualifiedConditions)
+ {
+ continue;
+ }
+
+ int num;
+ if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num))
+ {
+ if (condition.Condition == ProfileConditionType.Equals)
+ {
+ item.AudioBitrate = num;
+ }
+ else if (condition.Condition == ProfileConditionType.LessThanEqual)
+ {
+ item.AudioBitrate = Math.Min(num, item.AudioBitrate ?? num);
+ }
+ else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
+ {
+ item.AudioBitrate = Math.Max(num, item.AudioBitrate ?? num);
+ }
+ }
+ break;
+ }
+ case ProfileConditionValue.AudioChannels:
+ {
+ if (string.IsNullOrEmpty(qualifier))
+ {
+ if (!enableNonQualifiedConditions)
+ {
+ continue;
+ }
+ }
+ else
+ {
+ if (!enableQualifiedConditions)
+ {
+ continue;
+ }
+ }
+
+ int num;
+ if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num))
+ {
+ if (condition.Condition == ProfileConditionType.Equals)
+ {
+ item.SetOption(qualifier, "audiochannels", num.ToString(CultureInfo.InvariantCulture));
+ }
+ else if (condition.Condition == ProfileConditionType.LessThanEqual)
+ {
+ item.SetOption(qualifier, "audiochannels", Math.Min(num, item.GetTargetAudioChannels(qualifier) ?? num).ToString(CultureInfo.InvariantCulture));
+ }
+ else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
+ {
+ item.SetOption(qualifier, "audiochannels", Math.Max(num, item.GetTargetAudioChannels(qualifier) ?? num).ToString(CultureInfo.InvariantCulture));
+ }
+ }
+ break;
+ }
+ case ProfileConditionValue.IsAvc:
+ {
+ if (!enableNonQualifiedConditions)
+ {
+ continue;
+ }
+
+ bool isAvc;
+ if (bool.TryParse(value, out isAvc))
+ {
+ if (isAvc && condition.Condition == ProfileConditionType.Equals)
+ {
+ item.RequireAvc = true;
+ }
+ else if (!isAvc && condition.Condition == ProfileConditionType.NotEquals)
+ {
+ item.RequireAvc = true;
+ }
+ }
+ break;
+ }
+ case ProfileConditionValue.IsAnamorphic:
+ {
+ if (!enableNonQualifiedConditions)
+ {
+ continue;
+ }
+
+ bool isAnamorphic;
+ if (bool.TryParse(value, out isAnamorphic))
+ {
+ if (isAnamorphic && condition.Condition == ProfileConditionType.Equals)
+ {
+ item.RequireNonAnamorphic = true;
+ }
+ else if (!isAnamorphic && condition.Condition == ProfileConditionType.NotEquals)
+ {
+ item.RequireNonAnamorphic = true;
+ }
+ }
+ break;
+ }
+ case ProfileConditionValue.IsInterlaced:
+ {
+ if (string.IsNullOrEmpty(qualifier))
+ {
+ if (!enableNonQualifiedConditions)
+ {
+ continue;
+ }
+ }
+ else
+ {
+ if (!enableQualifiedConditions)
+ {
+ continue;
+ }
+ }
+
+ bool isInterlaced;
+ if (bool.TryParse(value, out isInterlaced))
+ {
+ if (!isInterlaced && condition.Condition == ProfileConditionType.Equals)
+ {
+ item.SetOption(qualifier, "deinterlace", "true");
+ }
+ else if (isInterlaced && condition.Condition == ProfileConditionType.NotEquals)
+ {
+ item.SetOption(qualifier, "deinterlace", "true");
+ }
+ }
+ break;
+ }
+ case ProfileConditionValue.AudioProfile:
+ case ProfileConditionValue.Has64BitOffsets:
+ case ProfileConditionValue.PacketLength:
+ case ProfileConditionValue.NumAudioStreams:
+ case ProfileConditionValue.NumVideoStreams:
+ case ProfileConditionValue.IsSecondaryAudio:
+ case ProfileConditionValue.VideoTimestamp:
+ {
+ // Not supported yet
+ break;
+ }
+ case ProfileConditionValue.RefFrames:
+ {
+ if (string.IsNullOrEmpty(qualifier))
+ {
+ if (!enableNonQualifiedConditions)
+ {
+ continue;
+ }
+ }
+ else
+ {
+ if (!enableQualifiedConditions)
+ {
+ continue;
+ }
+ }
+
+ int num;
+ if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num))
+ {
+ if (condition.Condition == ProfileConditionType.Equals)
+ {
+ item.SetOption(qualifier, "maxrefframes", num.ToString(CultureInfo.InvariantCulture));
+ }
+ else if (condition.Condition == ProfileConditionType.LessThanEqual)
+ {
+ item.SetOption(qualifier, "maxrefframes", Math.Min(num, item.GetTargetRefFrames(qualifier) ?? num).ToString(CultureInfo.InvariantCulture));
+ }
+ else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
+ {
+ item.SetOption(qualifier, "maxrefframes", Math.Max(num, item.GetTargetRefFrames(qualifier) ?? num).ToString(CultureInfo.InvariantCulture));
+ }
+ }
+ break;
+ }
+ case ProfileConditionValue.VideoBitDepth:
+ {
+ if (string.IsNullOrEmpty(qualifier))
+ {
+ if (!enableNonQualifiedConditions)
+ {
+ continue;
+ }
+ }
+ else
+ {
+ if (!enableQualifiedConditions)
+ {
+ continue;
+ }
+ }
+
+ int num;
+ if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num))
+ {
+ if (condition.Condition == ProfileConditionType.Equals)
+ {
+ item.SetOption(qualifier, "videobitdepth", num.ToString(CultureInfo.InvariantCulture));
+ }
+ else if (condition.Condition == ProfileConditionType.LessThanEqual)
+ {
+ item.SetOption(qualifier, "videobitdepth", Math.Min(num, item.GetTargetVideoBitDepth(qualifier) ?? num).ToString(CultureInfo.InvariantCulture));
+ }
+ else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
+ {
+ item.SetOption(qualifier, "videobitdepth", Math.Max(num, item.GetTargetVideoBitDepth(qualifier) ?? num).ToString(CultureInfo.InvariantCulture));
+ }
+ }
+ break;
+ }
+ case ProfileConditionValue.VideoProfile:
+ {
+ if (string.IsNullOrEmpty(qualifier))
+ {
+ continue;
+ }
+
+ if (!string.IsNullOrEmpty(value))
+ {
+ // change from split by | to comma
+
+ // strip spaces to avoid having to encode
+ var values = value
+ .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+
+ if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny)
+ {
+ item.SetOption(qualifier, "profile", string.Join(",", values));
+ }
+ }
+ break;
+ }
+ case ProfileConditionValue.Height:
+ {
+ if (!enableNonQualifiedConditions)
+ {
+ continue;
+ }
+
+ int num;
+ if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num))
+ {
+ if (condition.Condition == ProfileConditionType.Equals)
+ {
+ item.MaxHeight = num;
+ }
+ else if (condition.Condition == ProfileConditionType.LessThanEqual)
+ {
+ item.MaxHeight = Math.Min(num, item.MaxHeight ?? num);
+ }
+ else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
+ {
+ item.MaxHeight = Math.Max(num, item.MaxHeight ?? num);
+ }
+ }
+ break;
+ }
+ case ProfileConditionValue.VideoBitrate:
+ {
+ if (!enableNonQualifiedConditions)
+ {
+ continue;
+ }
+
+ int num;
+ if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num))
+ {
+ if (condition.Condition == ProfileConditionType.Equals)
+ {
+ item.VideoBitrate = num;
+ }
+ else if (condition.Condition == ProfileConditionType.LessThanEqual)
+ {
+ item.VideoBitrate = Math.Min(num, item.VideoBitrate ?? num);
+ }
+ else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
+ {
+ item.VideoBitrate = Math.Max(num, item.VideoBitrate ?? num);
+ }
+ }
+ break;
+ }
+ case ProfileConditionValue.VideoFramerate:
+ {
+ if (!enableNonQualifiedConditions)
+ {
+ continue;
+ }
+
+ float num;
+ if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num))
+ {
+ if (condition.Condition == ProfileConditionType.Equals)
+ {
+ item.MaxFramerate = num;
+ }
+ else if (condition.Condition == ProfileConditionType.LessThanEqual)
+ {
+ item.MaxFramerate = Math.Min(num, item.MaxFramerate ?? num);
+ }
+ else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
+ {
+ item.MaxFramerate = Math.Max(num, item.MaxFramerate ?? num);
+ }
+ }
+ break;
+ }
+ case ProfileConditionValue.VideoLevel:
+ {
+ if (string.IsNullOrEmpty(qualifier))
+ {
+ continue;
+ }
+
+ int num;
+ if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num))
+ {
+ if (condition.Condition == ProfileConditionType.Equals)
+ {
+ item.SetOption(qualifier, "level", num.ToString(CultureInfo.InvariantCulture));
+ }
+ else if (condition.Condition == ProfileConditionType.LessThanEqual)
+ {
+ item.SetOption(qualifier, "level", Math.Min(num, item.GetTargetVideoLevel(qualifier) ?? num).ToString(CultureInfo.InvariantCulture));
+ }
+ else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
+ {
+ item.SetOption(qualifier, "level", Math.Max(num, item.GetTargetVideoLevel(qualifier) ?? num).ToString(CultureInfo.InvariantCulture));
+ }
+ }
+ break;
+ }
+ case ProfileConditionValue.Width:
+ {
+ if (!enableNonQualifiedConditions)
+ {
+ continue;
+ }
+
+ int num;
+ if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out num))
+ {
+ if (condition.Condition == ProfileConditionType.Equals)
+ {
+ item.MaxWidth = num;
+ }
+ else if (condition.Condition == ProfileConditionType.LessThanEqual)
+ {
+ item.MaxWidth = Math.Min(num, item.MaxWidth ?? num);
+ }
+ else if (condition.Condition == ProfileConditionType.GreaterThanEqual)
+ {
+ item.MaxWidth = Math.Max(num, item.MaxWidth ?? num);
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+
+ private bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
+ {
+ // Check container type
+ if (!profile.SupportsContainer(item.Container))
+ {
+ return false;
+ }
+
+ // Check audio codec
+ string audioCodec = audioStream == null ? null : audioStream.Codec;
+ if (!profile.SupportsAudioCodec(audioCodec))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool IsVideoDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream)
+ {
+ // Check container type
+ if (!profile.SupportsContainer(item.Container))
+ {
+ return false;
+ }
+
+ // Check video codec
+ string videoCodec = videoStream == null ? null : videoStream.Codec;
+ if (!profile.SupportsVideoCodec(videoCodec))
+ {
+ return false;
+ }
+
+ // Check audio codec
+ if (audioStream != null)
+ {
+ string audioCodec = audioStream == null ? null : audioStream.Codec;
+ if (!profile.SupportsAudioCodec(audioCodec))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
new file mode 100644
index 000000000..46a1cd68b
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -0,0 +1,1092 @@
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Session;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+
+namespace MediaBrowser.Model.Dlna
+{
+ /// <summary>
+ /// Class StreamInfo.
+ /// </summary>
+ public class StreamInfo
+ {
+ public StreamInfo()
+ {
+ AudioCodecs = new string[] { };
+ VideoCodecs = new string[] { };
+ SubtitleCodecs = new string[] { };
+ TranscodeReasons = new TranscodeReason[] { };
+ StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ public void SetOption(string qualifier, string name, string value)
+ {
+ if (string.IsNullOrEmpty(qualifier))
+ {
+ SetOption(name, value);
+ }
+ else
+ {
+ SetOption(qualifier + "-" + name, value);
+ }
+ }
+
+ public void SetOption(string name, string value)
+ {
+ StreamOptions[name] = value;
+ }
+
+ public string GetOption(string qualifier, string name)
+ {
+ var value = GetOption(qualifier + "-" + name);
+
+ if (string.IsNullOrEmpty(value))
+ {
+ value = GetOption(name);
+ }
+
+ return value;
+ }
+
+ public string GetOption(string name)
+ {
+ string value;
+ if (StreamOptions.TryGetValue(name, out value))
+ {
+ return value;
+ }
+
+ return null;
+ }
+
+ public Guid ItemId { get; set; }
+
+ public PlayMethod PlayMethod { get; set; }
+ public EncodingContext Context { get; set; }
+
+ public DlnaProfileType MediaType { get; set; }
+
+ public string Container { get; set; }
+
+ public string SubProtocol { get; set; }
+
+ public long StartPositionTicks { get; set; }
+
+ public int? SegmentLength { get; set; }
+ public int? MinSegments { get; set; }
+ public bool BreakOnNonKeyFrames { get; set; }
+
+ public bool RequireAvc { get; set; }
+ public bool RequireNonAnamorphic { get; set; }
+ public bool CopyTimestamps { get; set; }
+ public bool EnableMpegtsM2TsMode { get; set; }
+ public bool EnableSubtitlesInManifest { get; set; }
+ public string[] AudioCodecs { get; set; }
+ public string[] VideoCodecs { get; set; }
+
+ public int? AudioStreamIndex { get; set; }
+
+ public int? SubtitleStreamIndex { get; set; }
+
+ public int? TranscodingMaxAudioChannels { get; set; }
+ public int? GlobalMaxAudioChannels { get; set; }
+
+ public int? AudioBitrate { get; set; }
+
+ public int? VideoBitrate { get; set; }
+
+ public int? MaxWidth { get; set; }
+ public int? MaxHeight { get; set; }
+
+ public float? MaxFramerate { get; set; }
+
+ public DeviceProfile DeviceProfile { get; set; }
+ public string DeviceProfileId { get; set; }
+ public string DeviceId { get; set; }
+
+ public long? RunTimeTicks { get; set; }
+
+ public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
+
+ public bool EstimateContentLength { get; set; }
+
+ public MediaSourceInfo MediaSource { get; set; }
+
+ public string[] SubtitleCodecs { get; set; }
+ public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
+ public string SubtitleFormat { get; set; }
+
+ public string PlaySessionId { get; set; }
+ public TranscodeReason[] TranscodeReasons { get; set; }
+
+ public Dictionary<string, string> StreamOptions { get; private set; }
+
+ public string MediaSourceId
+ {
+ get
+ {
+ return MediaSource == null ? null : MediaSource.Id;
+ }
+ }
+
+ public bool IsDirectStream
+ {
+ get
+ {
+ return PlayMethod == PlayMethod.DirectStream ||
+ PlayMethod == PlayMethod.DirectPlay;
+ }
+ }
+
+ public string ToUrl(string baseUrl, string accessToken)
+ {
+ if (PlayMethod == PlayMethod.DirectPlay)
+ {
+ return MediaSource.Path;
+ }
+
+ if (string.IsNullOrEmpty(baseUrl))
+ {
+ throw new ArgumentNullException(baseUrl);
+ }
+
+ List<string> list = new List<string>();
+ foreach (NameValuePair pair in BuildParams(this, accessToken))
+ {
+ if (string.IsNullOrEmpty(pair.Value))
+ {
+ continue;
+ }
+
+ // Try to keep the url clean by omitting defaults
+ if (StringHelper.EqualsIgnoreCase(pair.Name, "StartTimeTicks") &&
+ StringHelper.EqualsIgnoreCase(pair.Value, "0"))
+ {
+ continue;
+ }
+ if (StringHelper.EqualsIgnoreCase(pair.Name, "SubtitleStreamIndex") &&
+ StringHelper.EqualsIgnoreCase(pair.Value, "-1"))
+ {
+ continue;
+ }
+ if (StringHelper.EqualsIgnoreCase(pair.Name, "Static") &&
+ StringHelper.EqualsIgnoreCase(pair.Value, "false"))
+ {
+ continue;
+ }
+
+ var encodedValue = pair.Value.Replace(" ", "%20");
+
+ list.Add(string.Format("{0}={1}", pair.Name, encodedValue));
+ }
+
+ string queryString = string.Join("&", list.ToArray(list.Count));
+
+ return GetUrl(baseUrl, queryString);
+ }
+
+ private string GetUrl(string baseUrl, string queryString)
+ {
+ if (string.IsNullOrEmpty(baseUrl))
+ {
+ throw new ArgumentNullException(baseUrl);
+ }
+
+ string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container;
+
+ baseUrl = baseUrl.TrimEnd('/');
+
+ if (MediaType == DlnaProfileType.Audio)
+ {
+ if (StringHelper.EqualsIgnoreCase(SubProtocol, "hls"))
+ {
+ return string.Format("{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
+ }
+
+ return string.Format("{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
+ }
+
+ if (StringHelper.EqualsIgnoreCase(SubProtocol, "hls"))
+ {
+ return string.Format("{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
+ }
+
+ return string.Format("{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
+ }
+
+ private static List<NameValuePair> BuildParams(StreamInfo item, string accessToken)
+ {
+ List<NameValuePair> list = new List<NameValuePair>();
+
+ string audioCodecs = item.AudioCodecs.Length == 0 ?
+ string.Empty :
+ string.Join(",", item.AudioCodecs);
+
+ string videoCodecs = item.VideoCodecs.Length == 0 ?
+ string.Empty :
+ string.Join(",", item.VideoCodecs);
+
+ list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
+ list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
+ list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty));
+ list.Add(new NameValuePair("Static", item.IsDirectStream.ToString().ToLower()));
+ list.Add(new NameValuePair("VideoCodec", videoCodecs));
+ list.Add(new NameValuePair("AudioCodec", audioCodecs));
+ list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+
+ list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+ list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+
+ long startPositionTicks = item.StartPositionTicks;
+
+ var isHls = StringHelper.EqualsIgnoreCase(item.SubProtocol, "hls");
+
+ if (isHls)
+ {
+ list.Add(new NameValuePair("StartTimeTicks", string.Empty));
+ }
+ else
+ {
+ list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture)));
+ }
+
+ list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
+ list.Add(new NameValuePair("api_key", accessToken ?? string.Empty));
+
+ string liveStreamId = item.MediaSource == null ? null : item.MediaSource.LiveStreamId;
+ list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
+
+ list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
+
+
+ if (!item.IsDirectStream)
+ {
+ if (item.RequireNonAnamorphic)
+ {
+ list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString().ToLower()));
+ }
+
+ list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
+
+ if (item.EnableSubtitlesInManifest)
+ {
+ list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString().ToLower()));
+ }
+
+ if (item.EnableMpegtsM2TsMode)
+ {
+ list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString().ToLower()));
+ }
+
+ if (item.EstimateContentLength)
+ {
+ list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString().ToLower()));
+ }
+
+ if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto)
+ {
+ list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLower()));
+ }
+
+ if (item.CopyTimestamps)
+ {
+ list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString().ToLower()));
+ }
+
+ list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString().ToLower()));
+ }
+
+ list.Add(new NameValuePair("Tag", item.MediaSource.ETag ?? string.Empty));
+
+ string subtitleCodecs = item.SubtitleCodecs.Length == 0 ?
+ string.Empty :
+ string.Join(",", item.SubtitleCodecs);
+
+ list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
+
+ if (isHls)
+ {
+ list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
+
+ if (item.SegmentLength.HasValue)
+ {
+ list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture)));
+ }
+
+ if (item.MinSegments.HasValue)
+ {
+ list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
+ }
+
+ list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString()));
+ }
+
+ foreach (var pair in item.StreamOptions)
+ {
+ if (string.IsNullOrEmpty(pair.Value))
+ {
+ continue;
+ }
+
+ // strip spaces to avoid having to encode h264 profile names
+ list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", "")));
+ }
+
+ if (!item.IsDirectStream)
+ {
+ list.Add(new NameValuePair("TranscodeReasons", string.Join(",", item.TranscodeReasons.Distinct().Select(i => i.ToString()).ToArray())));
+ }
+
+ return list;
+ }
+
+ public List<SubtitleStreamInfo> GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken)
+ {
+ return GetExternalSubtitles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
+ }
+
+ public List<SubtitleStreamInfo> GetExternalSubtitles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken)
+ {
+ List<SubtitleStreamInfo> list = GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, enableAllProfiles, baseUrl, accessToken);
+ List<SubtitleStreamInfo> newList = new List<SubtitleStreamInfo>();
+
+ // First add the selected track
+ foreach (SubtitleStreamInfo stream in list)
+ {
+ if (stream.DeliveryMethod == SubtitleDeliveryMethod.External)
+ {
+ newList.Add(stream);
+ }
+ }
+
+ return newList;
+ }
+
+ public List<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken)
+ {
+ return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
+ }
+
+ public List<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken)
+ {
+ List<SubtitleStreamInfo> list = new List<SubtitleStreamInfo>();
+
+ // HLS will preserve timestamps so we can just grab the full subtitle stream
+ long startPositionTicks = StringHelper.EqualsIgnoreCase(SubProtocol, "hls")
+ ? 0
+ : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0);
+
+ // First add the selected track
+ if (SubtitleStreamIndex.HasValue)
+ {
+ foreach (MediaStream stream in MediaSource.MediaStreams)
+ {
+ if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value)
+ {
+ AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
+ }
+ }
+ }
+
+ if (!includeSelectedTrackOnly)
+ {
+ foreach (MediaStream stream in MediaSource.MediaStreams)
+ {
+ if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value))
+ {
+ AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
+ }
+ }
+ }
+
+ return list;
+ }
+
+ private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string accessToken, long startPositionTicks)
+ {
+ if (enableAllProfiles)
+ {
+ foreach (SubtitleProfile profile in DeviceProfile.SubtitleProfiles)
+ {
+ SubtitleStreamInfo info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport);
+
+ list.Add(info);
+ }
+ }
+ else
+ {
+ SubtitleStreamInfo info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport);
+
+ list.Add(info);
+ }
+ }
+
+ private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport)
+ {
+ SubtitleProfile subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol);
+ SubtitleStreamInfo info = new SubtitleStreamInfo
+ {
+ IsForced = stream.IsForced,
+ Language = stream.Language,
+ Name = stream.Language ?? "Unknown",
+ Format = subtitleProfile.Format,
+ Index = stream.Index,
+ DeliveryMethod = subtitleProfile.Method,
+ DisplayTitle = stream.DisplayTitle
+ };
+
+ if (info.DeliveryMethod == SubtitleDeliveryMethod.External)
+ {
+ if (MediaSource.Protocol == MediaProtocol.File || !StringHelper.EqualsIgnoreCase(stream.Codec, subtitleProfile.Format) || !stream.IsExternal)
+ {
+ info.Url = string.Format("{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
+ baseUrl,
+ ItemId,
+ MediaSourceId,
+ stream.Index.ToString(CultureInfo.InvariantCulture),
+ startPositionTicks.ToString(CultureInfo.InvariantCulture),
+ subtitleProfile.Format);
+
+ if (!string.IsNullOrEmpty(accessToken))
+ {
+ info.Url += "?api_key=" + accessToken;
+ }
+
+ info.IsExternalUrl = false;
+ }
+ else
+ {
+ info.Url = stream.Path;
+ info.IsExternalUrl = true;
+ }
+ }
+
+ return info;
+ }
+
+ /// <summary>
+ /// Returns the audio stream that will be used
+ /// </summary>
+ public MediaStream TargetAudioStream
+ {
+ get
+ {
+ if (MediaSource != null)
+ {
+ return MediaSource.GetDefaultAudioStream(AudioStreamIndex);
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Returns the video stream that will be used
+ /// </summary>
+ public MediaStream TargetVideoStream
+ {
+ get
+ {
+ if (MediaSource != null)
+ {
+ return MediaSource.VideoStream;
+ }
+
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Predicts the audio sample rate that will be in the output stream
+ /// </summary>
+ public int? TargetAudioSampleRate
+ {
+ get
+ {
+ MediaStream stream = TargetAudioStream;
+ return stream == null ? null : stream.SampleRate;
+ }
+ }
+
+ /// <summary>
+ /// Predicts the audio sample rate that will be in the output stream
+ /// </summary>
+ public int? TargetAudioBitDepth
+ {
+ get
+ {
+ if (IsDirectStream)
+ {
+ return TargetAudioStream == null ? (int?)null : TargetAudioStream.BitDepth;
+ }
+
+ var targetAudioCodecs = TargetAudioCodec;
+ var audioCodec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
+ if (!string.IsNullOrEmpty(audioCodec))
+ {
+ return GetTargetAudioBitDepth(audioCodec);
+ }
+
+ return TargetAudioStream == null ? (int?)null : TargetAudioStream.BitDepth;
+ }
+ }
+
+ /// <summary>
+ /// Predicts the audio sample rate that will be in the output stream
+ /// </summary>
+ public int? TargetVideoBitDepth
+ {
+ get
+ {
+ if (IsDirectStream)
+ {
+ return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth;
+ }
+
+ var targetVideoCodecs = TargetVideoCodec;
+ var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
+ if (!string.IsNullOrEmpty(videoCodec))
+ {
+ return GetTargetVideoBitDepth(videoCodec);
+ }
+
+ return TargetVideoStream == null ? (int?)null : TargetVideoStream.BitDepth;
+ }
+ }
+
+ /// <summary>
+ /// Gets the target reference frames.
+ /// </summary>
+ /// <value>The target reference frames.</value>
+ public int? TargetRefFrames
+ {
+ get
+ {
+ if (IsDirectStream)
+ {
+ return TargetVideoStream == null ? (int?)null : TargetVideoStream.RefFrames;
+ }
+
+ var targetVideoCodecs = TargetVideoCodec;
+ var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
+ if (!string.IsNullOrEmpty(videoCodec))
+ {
+ return GetTargetRefFrames(videoCodec);
+ }
+
+ return TargetVideoStream == null ? (int?)null : TargetVideoStream.RefFrames;
+ }
+ }
+
+ /// <summary>
+ /// Predicts the audio sample rate that will be in the output stream
+ /// </summary>
+ public float? TargetFramerate
+ {
+ get
+ {
+ MediaStream stream = TargetVideoStream;
+ return MaxFramerate.HasValue && !IsDirectStream
+ ? MaxFramerate
+ : stream == null ? null : stream.AverageFrameRate ?? stream.RealFrameRate;
+ }
+ }
+
+ /// <summary>
+ /// Predicts the audio sample rate that will be in the output stream
+ /// </summary>
+ public double? TargetVideoLevel
+ {
+ get
+ {
+ if (IsDirectStream)
+ {
+ return TargetVideoStream == null ? (double?)null : TargetVideoStream.Level;
+ }
+
+ var targetVideoCodecs = TargetVideoCodec;
+ var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
+ if (!string.IsNullOrEmpty(videoCodec))
+ {
+ return GetTargetVideoLevel(videoCodec);
+ }
+
+ return TargetVideoStream == null ? (double?)null : TargetVideoStream.Level;
+ }
+ }
+
+ public int? GetTargetVideoBitDepth(string codec)
+ {
+ var value = GetOption(codec, "videobitdepth");
+ if (string.IsNullOrEmpty(value))
+ {
+ return null;
+ }
+
+ int result;
+ if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out result))
+ {
+ return result;
+ }
+
+ return null;
+ }
+
+ public int? GetTargetAudioBitDepth(string codec)
+ {
+ var value = GetOption(codec, "audiobitdepth");
+ if (string.IsNullOrEmpty(value))
+ {
+ return null;
+ }
+
+ int result;
+ if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out result))
+ {
+ return result;
+ }
+
+ return null;
+ }
+
+ public double? GetTargetVideoLevel(string codec)
+ {
+ var value = GetOption(codec, "level");
+ if (string.IsNullOrEmpty(value))
+ {
+ return null;
+ }
+
+ double result;
+ if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result))
+ {
+ return result;
+ }
+
+ return null;
+ }
+
+ public int? GetTargetRefFrames(string codec)
+ {
+ var value = GetOption(codec, "maxrefframes");
+ if (string.IsNullOrEmpty(value))
+ {
+ return null;
+ }
+
+ int result;
+ if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result))
+ {
+ return result;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Predicts the audio sample rate that will be in the output stream
+ /// </summary>
+ public int? TargetPacketLength
+ {
+ get
+ {
+ MediaStream stream = TargetVideoStream;
+ return !IsDirectStream
+ ? null
+ : stream == null ? null : stream.PacketLength;
+ }
+ }
+
+ /// <summary>
+ /// Predicts the audio sample rate that will be in the output stream
+ /// </summary>
+ public string TargetVideoProfile
+ {
+ get
+ {
+ if (IsDirectStream)
+ {
+ return TargetVideoStream == null ? null : TargetVideoStream.Profile;
+ }
+
+ var targetVideoCodecs = TargetVideoCodec;
+ var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
+ if (!string.IsNullOrEmpty(videoCodec))
+ {
+ return GetOption(videoCodec, "profile");
+ }
+
+ return TargetVideoStream == null ? null : TargetVideoStream.Profile;
+ }
+ }
+
+ /// <summary>
+ /// Gets the target video codec tag.
+ /// </summary>
+ /// <value>The target video codec tag.</value>
+ public string TargetVideoCodecTag
+ {
+ get
+ {
+ MediaStream stream = TargetVideoStream;
+ return !IsDirectStream
+ ? null
+ : stream == null ? null : stream.CodecTag;
+ }
+ }
+
+ /// <summary>
+ /// Predicts the audio bitrate that will be in the output stream
+ /// </summary>
+ public int? TargetAudioBitrate
+ {
+ get
+ {
+ MediaStream stream = TargetAudioStream;
+ return AudioBitrate.HasValue && !IsDirectStream
+ ? AudioBitrate
+ : stream == null ? null : stream.BitRate;
+ }
+ }
+
+ /// <summary>
+ /// Predicts the audio channels that will be in the output stream
+ /// </summary>
+ public int? TargetAudioChannels
+ {
+ get
+ {
+ if (IsDirectStream)
+ {
+ return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels;
+ }
+
+ var targetAudioCodecs = TargetAudioCodec;
+ var codec = targetAudioCodecs.Length == 0 ? null : targetAudioCodecs[0];
+ if (!string.IsNullOrEmpty(codec))
+ {
+ return GetTargetRefFrames(codec);
+ }
+
+ return TargetAudioStream == null ? (int?)null : TargetAudioStream.Channels;
+ }
+ }
+
+ public int? GetTargetAudioChannels(string codec)
+ {
+ var defaultValue = GlobalMaxAudioChannels;
+
+ var value = GetOption(codec, "audiochannels");
+ if (string.IsNullOrEmpty(value))
+ {
+ return defaultValue;
+ }
+
+ int result;
+ if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out result))
+ {
+ return Math.Min(result, defaultValue ?? result);
+ }
+
+ return defaultValue;
+ }
+
+ /// <summary>
+ /// Predicts the audio codec that will be in the output stream
+ /// </summary>
+ public string[] TargetAudioCodec
+ {
+ get
+ {
+ MediaStream stream = TargetAudioStream;
+
+ string inputCodec = stream == null ? null : stream.Codec;
+
+ if (IsDirectStream)
+ {
+ return string.IsNullOrEmpty(inputCodec) ? new string[] { } : new[] { inputCodec };
+ }
+
+ foreach (string codec in AudioCodecs)
+ {
+ if (StringHelper.EqualsIgnoreCase(codec, inputCodec))
+ {
+ return string.IsNullOrEmpty(codec) ? new string[] { } : new[] { codec };
+ }
+ }
+
+ return AudioCodecs;
+ }
+ }
+
+ public string[] TargetVideoCodec
+ {
+ get
+ {
+ MediaStream stream = TargetVideoStream;
+
+ string inputCodec = stream == null ? null : stream.Codec;
+
+ if (IsDirectStream)
+ {
+ return string.IsNullOrEmpty(inputCodec) ? new string[] { } : new[] { inputCodec };
+ }
+
+ foreach (string codec in VideoCodecs)
+ {
+ if (StringHelper.EqualsIgnoreCase(codec, inputCodec))
+ {
+ return string.IsNullOrEmpty(codec) ? new string[] { } : new[] { codec };
+ }
+ }
+
+ return VideoCodecs;
+ }
+ }
+
+ /// <summary>
+ /// Predicts the audio channels that will be in the output stream
+ /// </summary>
+ public long? TargetSize
+ {
+ get
+ {
+ if (IsDirectStream)
+ {
+ return MediaSource.Size;
+ }
+
+ if (RunTimeTicks.HasValue)
+ {
+ int? totalBitrate = TargetTotalBitrate;
+
+ double totalSeconds = RunTimeTicks.Value;
+ // Convert to ms
+ totalSeconds /= 10000;
+ // Convert to seconds
+ totalSeconds /= 1000;
+
+ return totalBitrate.HasValue ?
+ Convert.ToInt64(totalBitrate.Value * totalSeconds) :
+ (long?)null;
+ }
+
+ return null;
+ }
+ }
+
+ public int? TargetVideoBitrate
+ {
+ get
+ {
+ MediaStream stream = TargetVideoStream;
+
+ return VideoBitrate.HasValue && !IsDirectStream
+ ? VideoBitrate
+ : stream == null ? null : stream.BitRate;
+ }
+ }
+
+ public TransportStreamTimestamp TargetTimestamp
+ {
+ get
+ {
+ TransportStreamTimestamp defaultValue = StringHelper.EqualsIgnoreCase(Container, "m2ts")
+ ? TransportStreamTimestamp.Valid
+ : TransportStreamTimestamp.None;
+
+ return !IsDirectStream
+ ? defaultValue
+ : MediaSource == null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None;
+ }
+ }
+
+ public int? TargetTotalBitrate
+ {
+ get
+ {
+ return (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0);
+ }
+ }
+
+ public bool? IsTargetAnamorphic
+ {
+ get
+ {
+ if (IsDirectStream)
+ {
+ return TargetVideoStream == null ? null : TargetVideoStream.IsAnamorphic;
+ }
+
+ return false;
+ }
+ }
+
+ public bool? IsTargetInterlaced
+ {
+ get
+ {
+ if (IsDirectStream)
+ {
+ return TargetVideoStream == null ? (bool?)null : TargetVideoStream.IsInterlaced;
+ }
+
+ var targetVideoCodecs = TargetVideoCodec;
+ var videoCodec = targetVideoCodecs.Length == 0 ? null : targetVideoCodecs[0];
+ if (!string.IsNullOrEmpty(videoCodec))
+ {
+ if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ }
+
+ return TargetVideoStream == null ? (bool?)null : TargetVideoStream.IsInterlaced;
+ }
+ }
+
+ public bool? IsTargetAVC
+ {
+ get
+ {
+ if (IsDirectStream)
+ {
+ return TargetVideoStream == null ? null : TargetVideoStream.IsAVC;
+ }
+
+ return true;
+ }
+ }
+
+ public int? TargetWidth
+ {
+ get
+ {
+ MediaStream videoStream = TargetVideoStream;
+
+ if (videoStream != null && videoStream.Width.HasValue && videoStream.Height.HasValue)
+ {
+ ImageSize size = new ImageSize
+ {
+ Width = videoStream.Width.Value,
+ Height = videoStream.Height.Value
+ };
+
+ double? maxWidth = MaxWidth.HasValue ? (double)MaxWidth.Value : (double?)null;
+ double? maxHeight = MaxHeight.HasValue ? (double)MaxHeight.Value : (double?)null;
+
+ ImageSize newSize = DrawingUtils.Resize(size,
+ 0,
+ 0,
+ maxWidth ?? 0,
+ maxHeight ?? 0);
+
+ return Convert.ToInt32(newSize.Width);
+ }
+
+ return MaxWidth;
+ }
+ }
+
+ public int? TargetHeight
+ {
+ get
+ {
+ MediaStream videoStream = TargetVideoStream;
+
+ if (videoStream != null && videoStream.Width.HasValue && videoStream.Height.HasValue)
+ {
+ ImageSize size = new ImageSize
+ {
+ Width = videoStream.Width.Value,
+ Height = videoStream.Height.Value
+ };
+
+ double? maxWidth = MaxWidth.HasValue ? (double)MaxWidth.Value : (double?)null;
+ double? maxHeight = MaxHeight.HasValue ? (double)MaxHeight.Value : (double?)null;
+
+ ImageSize newSize = DrawingUtils.Resize(size,
+ 0,
+ 0,
+ maxWidth ?? 0,
+ maxHeight ?? 0);
+
+ return Convert.ToInt32(newSize.Height);
+ }
+
+ return MaxHeight;
+ }
+ }
+
+ public int? TargetVideoStreamCount
+ {
+ get
+ {
+ if (IsDirectStream)
+ {
+ return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue);
+ }
+ return GetMediaStreamCount(MediaStreamType.Video, 1);
+ }
+ }
+
+ public int? TargetAudioStreamCount
+ {
+ get
+ {
+ if (IsDirectStream)
+ {
+ return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue);
+ }
+ return GetMediaStreamCount(MediaStreamType.Audio, 1);
+ }
+ }
+
+ private int? GetMediaStreamCount(MediaStreamType type, int limit)
+ {
+ var count = MediaSource.GetStreamCount(type);
+
+ if (count.HasValue)
+ {
+ count = Math.Min(count.Value, limit);
+ }
+
+ return count;
+ }
+
+ public List<MediaStream> GetSelectableAudioStreams()
+ {
+ return GetSelectableStreams(MediaStreamType.Audio);
+ }
+
+ public List<MediaStream> GetSelectableSubtitleStreams()
+ {
+ return GetSelectableStreams(MediaStreamType.Subtitle);
+ }
+
+ public List<MediaStream> GetSelectableStreams(MediaStreamType type)
+ {
+ List<MediaStream> list = new List<MediaStream>();
+
+ foreach (MediaStream stream in MediaSource.MediaStreams)
+ {
+ if (type == stream.Type)
+ {
+ list.Add(stream);
+ }
+ }
+
+ return list;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs b/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs
new file mode 100644
index 000000000..b4e13c5ba
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/SubtitleDeliveryMethod.cs
@@ -0,0 +1,22 @@
+namespace MediaBrowser.Model.Dlna
+{
+ public enum SubtitleDeliveryMethod
+ {
+ /// <summary>
+ /// The encode
+ /// </summary>
+ Encode = 0,
+ /// <summary>
+ /// The embed
+ /// </summary>
+ Embed = 1,
+ /// <summary>
+ /// The external
+ /// </summary>
+ External = 2,
+ /// <summary>
+ /// The HLS
+ /// </summary>
+ Hls = 3
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/SubtitleProfile.cs b/MediaBrowser.Model/Dlna/SubtitleProfile.cs
new file mode 100644
index 000000000..a7e61e69a
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/SubtitleProfile.cs
@@ -0,0 +1,46 @@
+using MediaBrowser.Model.Extensions;
+using System.Collections.Generic;
+using System.Xml.Serialization;
+using MediaBrowser.Model.Dlna;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class SubtitleProfile
+ {
+ [XmlAttribute("format")]
+ public string Format { get; set; }
+
+ [XmlAttribute("method")]
+ public SubtitleDeliveryMethod Method { get; set; }
+
+ [XmlAttribute("didlMode")]
+ public string DidlMode { get; set; }
+
+ [XmlAttribute("language")]
+ public string Language { get; set; }
+
+ [XmlAttribute("container")]
+ public string Container { get; set; }
+
+ public string[] GetLanguages()
+ {
+ return ContainerProfile.SplitValue(Language);
+ }
+
+ public bool SupportsLanguage(string subLanguage)
+ {
+ if (string.IsNullOrEmpty(Language))
+ {
+ return true;
+ }
+
+ if (string.IsNullOrEmpty(subLanguage))
+ {
+ subLanguage = "und";
+ }
+
+ var languages = GetLanguages();
+ return languages.Length == 0 || ListHelper.ContainsIgnoreCase(languages, subLanguage);
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/SubtitleStreamInfo.cs b/MediaBrowser.Model/Dlna/SubtitleStreamInfo.cs
new file mode 100644
index 000000000..7a89308dc
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/SubtitleStreamInfo.cs
@@ -0,0 +1,15 @@
+namespace MediaBrowser.Model.Dlna
+{
+ public class SubtitleStreamInfo
+ {
+ public string Url { get; set; }
+ public string Language { get; set; }
+ public string Name { get; set; }
+ public bool IsForced { get; set; }
+ public string Format { get; set; }
+ public string DisplayTitle { get; set; }
+ public int Index { get; set; }
+ public SubtitleDeliveryMethod DeliveryMethod { get; set; }
+ public bool IsExternalUrl { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/TranscodeSeekInfo.cs b/MediaBrowser.Model/Dlna/TranscodeSeekInfo.cs
new file mode 100644
index 000000000..564ce5c60
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/TranscodeSeekInfo.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Model.Dlna
+{
+ public enum TranscodeSeekInfo
+ {
+ Auto = 0,
+ Bytes = 1
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
new file mode 100644
index 000000000..8453fdf6d
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs
@@ -0,0 +1,59 @@
+using System.Collections.Generic;
+using System.Xml.Serialization;
+using MediaBrowser.Model.Dlna;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class TranscodingProfile
+ {
+ [XmlAttribute("container")]
+ public string Container { get; set; }
+
+ [XmlAttribute("type")]
+ public DlnaProfileType Type { get; set; }
+
+ [XmlAttribute("videoCodec")]
+ public string VideoCodec { get; set; }
+
+ [XmlAttribute("audioCodec")]
+ public string AudioCodec { get; set; }
+
+ [XmlAttribute("protocol")]
+ public string Protocol { get; set; }
+
+ [XmlAttribute("estimateContentLength")]
+ public bool EstimateContentLength { get; set; }
+
+ [XmlAttribute("enableMpegtsM2TsMode")]
+ public bool EnableMpegtsM2TsMode { get; set; }
+
+ [XmlAttribute("transcodeSeekInfo")]
+ public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
+
+ [XmlAttribute("copyTimestamps")]
+ public bool CopyTimestamps { get; set; }
+
+ [XmlAttribute("context")]
+ public EncodingContext Context { get; set; }
+
+ [XmlAttribute("enableSubtitlesInManifest")]
+ public bool EnableSubtitlesInManifest { get; set; }
+
+ [XmlAttribute("maxAudioChannels")]
+ public string MaxAudioChannels { get; set; }
+
+ [XmlAttribute("minSegments")]
+ public int MinSegments { get; set; }
+
+ [XmlAttribute("segmentLength")]
+ public int SegmentLength { get; set; }
+
+ [XmlAttribute("breakOnNonKeyFrames")]
+ public bool BreakOnNonKeyFrames { get; set; }
+
+ public string[] GetAudioCodecs()
+ {
+ return ContainerProfile.SplitValue(AudioCodec);
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs b/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs
new file mode 100644
index 000000000..f4b9d1e9b
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/UpnpDeviceInfo.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Net;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class UpnpDeviceInfo
+ {
+ public Uri Location { get; set; }
+ public Dictionary<string, string> Headers { get; set; }
+ public IpAddressInfo LocalIpAddress { get; set; }
+ public int LocalPort { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Dlna/VideoOptions.cs b/MediaBrowser.Model/Dlna/VideoOptions.cs
new file mode 100644
index 000000000..041d2cd5d
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/VideoOptions.cs
@@ -0,0 +1,11 @@
+namespace MediaBrowser.Model.Dlna
+{
+ /// <summary>
+ /// Class VideoOptions.
+ /// </summary>
+ public class VideoOptions : AudioOptions
+ {
+ public int? AudioStreamIndex { get; set; }
+ public int? SubtitleStreamIndex { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dlna/XmlAttribute.cs b/MediaBrowser.Model/Dlna/XmlAttribute.cs
new file mode 100644
index 000000000..e8e13ba0d
--- /dev/null
+++ b/MediaBrowser.Model/Dlna/XmlAttribute.cs
@@ -0,0 +1,13 @@
+using System.Xml.Serialization;
+
+namespace MediaBrowser.Model.Dlna
+{
+ public class XmlAttribute
+ {
+ [XmlAttribute("name")]
+ public string Name { get; set; }
+
+ [XmlAttribute("value")]
+ public string Value { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Drawing/DrawingUtils.cs b/MediaBrowser.Model/Drawing/DrawingUtils.cs
new file mode 100644
index 000000000..e6235cb06
--- /dev/null
+++ b/MediaBrowser.Model/Drawing/DrawingUtils.cs
@@ -0,0 +1,91 @@
+namespace MediaBrowser.Model.Drawing
+{
+ /// <summary>
+ /// Class DrawingUtils
+ /// </summary>
+ public static class DrawingUtils
+ {
+ /// <summary>
+ /// Resizes a set of dimensions
+ /// </summary>
+ /// <param name="size">The original size object</param>
+ /// <param name="width">A new fixed width, if desired</param>
+ /// <param name="height">A new fixed height, if desired</param>
+ /// <param name="maxWidth">A max fixed width, if desired</param>
+ /// <param name="maxHeight">A max fixed height, if desired</param>
+ /// <returns>A new size object</returns>
+ public static ImageSize Resize(ImageSize size,
+ double width,
+ double height,
+ double maxWidth,
+ double maxHeight)
+ {
+ double newWidth = size.Width;
+ double newHeight = size.Height;
+
+ if (width > 0 && height > 0)
+ {
+ newWidth = width;
+ newHeight = height;
+ }
+
+ else if (height > 0)
+ {
+ newWidth = GetNewWidth(newHeight, newWidth, height);
+ newHeight = height;
+ }
+
+ else if (width > 0)
+ {
+ newHeight = GetNewHeight(newHeight, newWidth, width);
+ newWidth = width;
+ }
+
+ if (maxHeight > 0 && maxHeight < newHeight)
+ {
+ newWidth = GetNewWidth(newHeight, newWidth, maxHeight);
+ newHeight = maxHeight;
+ }
+
+ if (maxWidth > 0 && maxWidth < newWidth)
+ {
+ newHeight = GetNewHeight(newHeight, newWidth, maxWidth);
+ newWidth = maxWidth;
+ }
+
+ return new ImageSize { Width = newWidth, Height = newHeight };
+ }
+
+ /// <summary>
+ /// Gets the new width.
+ /// </summary>
+ /// <param name="currentHeight">Height of the current.</param>
+ /// <param name="currentWidth">Width of the current.</param>
+ /// <param name="newHeight">The new height.</param>
+ /// <returns>System.Double.</returns>
+ private static double GetNewWidth(double currentHeight, double currentWidth, double newHeight)
+ {
+ double scaleFactor = newHeight;
+ scaleFactor /= currentHeight;
+ scaleFactor *= currentWidth;
+
+ return scaleFactor;
+ }
+
+ /// <summary>
+ /// Gets the new height.
+ /// </summary>
+ /// <param name="currentHeight">Height of the current.</param>
+ /// <param name="currentWidth">Width of the current.</param>
+ /// <param name="newWidth">The new width.</param>
+ /// <returns>System.Double.</returns>
+ private static double GetNewHeight(double currentHeight, double currentWidth, double newWidth)
+ {
+ double scaleFactor = newWidth;
+ scaleFactor /= currentWidth;
+ scaleFactor *= currentHeight;
+
+ return scaleFactor;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Drawing/ImageFormat.cs b/MediaBrowser.Model/Drawing/ImageFormat.cs
new file mode 100644
index 000000000..0172c9754
--- /dev/null
+++ b/MediaBrowser.Model/Drawing/ImageFormat.cs
@@ -0,0 +1,30 @@
+
+namespace MediaBrowser.Model.Drawing
+{
+ /// <summary>
+ /// Enum ImageOutputFormat
+ /// </summary>
+ public enum ImageFormat
+ {
+ /// <summary>
+ /// The BMP
+ /// </summary>
+ Bmp,
+ /// <summary>
+ /// The GIF
+ /// </summary>
+ Gif,
+ /// <summary>
+ /// The JPG
+ /// </summary>
+ Jpg,
+ /// <summary>
+ /// The PNG
+ /// </summary>
+ Png,
+ /// <summary>
+ /// The webp
+ /// </summary>
+ Webp
+ }
+}
diff --git a/MediaBrowser.Model/Drawing/ImageOrientation.cs b/MediaBrowser.Model/Drawing/ImageOrientation.cs
new file mode 100644
index 000000000..c320a8224
--- /dev/null
+++ b/MediaBrowser.Model/Drawing/ImageOrientation.cs
@@ -0,0 +1,15 @@
+
+namespace MediaBrowser.Model.Drawing
+{
+ public enum ImageOrientation
+ {
+ TopLeft = 1,
+ TopRight = 2,
+ BottomRight = 3,
+ BottomLeft = 4,
+ LeftTop = 5,
+ RightTop = 6,
+ RightBottom = 7,
+ LeftBottom = 8,
+ }
+}
diff --git a/MediaBrowser.Model/Drawing/ImageSize.cs b/MediaBrowser.Model/Drawing/ImageSize.cs
new file mode 100644
index 000000000..c2b0291bd
--- /dev/null
+++ b/MediaBrowser.Model/Drawing/ImageSize.cs
@@ -0,0 +1,93 @@
+using System.Globalization;
+
+namespace MediaBrowser.Model.Drawing
+{
+ /// <summary>
+ /// Struct ImageSize
+ /// </summary>
+ public struct ImageSize
+ {
+ private double _height;
+ private double _width;
+
+ /// <summary>
+ /// Gets or sets the height.
+ /// </summary>
+ /// <value>The height.</value>
+ public double Height
+ {
+ get
+ {
+ return _height;
+ }
+ set
+ {
+ _height = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the width.
+ /// </summary>
+ /// <value>The width.</value>
+ public double Width
+ {
+ get { return _width; }
+ set { _width = value; }
+ }
+
+ public bool Equals(ImageSize size)
+ {
+ return Width.Equals(size.Width) && Height.Equals(size.Height);
+ }
+
+ public override string ToString()
+ {
+ return string.Format("{0}-{1}", Width, Height);
+ }
+
+ public ImageSize(string value)
+ {
+ _width = 0;
+
+ _height = 0;
+
+ ParseValue(value);
+ }
+
+ public ImageSize(int width, int height)
+ {
+ _width = width;
+ _height = height;
+ }
+
+ public ImageSize(double width, double height)
+ {
+ _width = width;
+ _height = height;
+ }
+
+ private void ParseValue(string value)
+ {
+ if (!string.IsNullOrEmpty(value))
+ {
+ string[] parts = value.Split('-');
+
+ if (parts.Length == 2)
+ {
+ double val;
+
+ if (double.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out val))
+ {
+ _width = val;
+ }
+
+ if (double.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out val))
+ {
+ _height = val;
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs
new file mode 100644
index 000000000..3ddbc4150
--- /dev/null
+++ b/MediaBrowser.Model/Dto/BaseItemDto.cs
@@ -0,0 +1,791 @@
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.Library;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Model.Sync;
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Model.Dto
+{
+ /// <summary>
+ /// This is strictly used as a data transfer object from the api layer.
+ /// This holds information about a BaseItem in a format that is convenient for the client.
+ /// </summary>
+ public class BaseItemDto : IHasProviderIds, IItemDto, IHasServerId
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ public string OriginalTitle { get; set; }
+
+ /// <summary>
+ /// Gets or sets the server identifier.
+ /// </summary>
+ /// <value>The server identifier.</value>
+ public string ServerId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the etag.
+ /// </summary>
+ /// <value>The etag.</value>
+ public string Etag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the source.
+ /// </summary>
+ /// <value>The type of the source.</value>
+ public string SourceType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playlist item identifier.
+ /// </summary>
+ /// <value>The playlist item identifier.</value>
+ public string PlaylistItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date created.
+ /// </summary>
+ /// <value>The date created.</value>
+ public DateTime? DateCreated { get; set; }
+
+ public DateTime? DateLastMediaAdded { get; set; }
+ public string ExtraType { get; set; }
+
+ public int? AirsBeforeSeasonNumber { get; set; }
+ public int? AirsAfterSeasonNumber { get; set; }
+ public int? AirsBeforeEpisodeNumber { get; set; }
+ public bool? DisplaySpecialsWithSeasons { get; set; }
+ public bool? CanDelete { get; set; }
+ public bool? CanDownload { get; set; }
+
+ public bool? HasSubtitles { get; set; }
+
+ public string PreferredMetadataLanguage { get; set; }
+ public string PreferredMetadataCountryCode { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [supports synchronize].
+ /// </summary>
+ public bool? SupportsSync { get; set; }
+
+ public string Container { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the sort.
+ /// </summary>
+ /// <value>The name of the sort.</value>
+ public string SortName { get; set; }
+ public string ForcedSortName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the video3 D format.
+ /// </summary>
+ /// <value>The video3 D format.</value>
+ public Video3DFormat? Video3DFormat { get; set; }
+
+ /// <summary>
+ /// Gets or sets the premiere date.
+ /// </summary>
+ /// <value>The premiere date.</value>
+ public DateTime? PremiereDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the external urls.
+ /// </summary>
+ /// <value>The external urls.</value>
+ public ExternalUrl[] ExternalUrls { get; set; }
+
+ /// <summary>
+ /// Gets or sets the media versions.
+ /// </summary>
+ /// <value>The media versions.</value>
+ public MediaSourceInfo[] MediaSources { get; set; }
+
+ /// <summary>
+ /// Gets or sets the critic rating.
+ /// </summary>
+ /// <value>The critic rating.</value>
+ public float? CriticRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the game system.
+ /// </summary>
+ /// <value>The game system.</value>
+ public string GameSystem { get; set; }
+
+ public string[] ProductionLocations { get; set; }
+
+ public string[] MultiPartGameFiles { get; set; }
+
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ public string Path { get; set; }
+
+ public bool? EnableMediaSourceDisplay { get; set; }
+
+ /// <summary>
+ /// Gets or sets the official rating.
+ /// </summary>
+ /// <value>The official rating.</value>
+ public string OfficialRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the custom rating.
+ /// </summary>
+ /// <value>The custom rating.</value>
+ public string CustomRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the channel identifier.
+ /// </summary>
+ /// <value>The channel identifier.</value>
+ public Guid ChannelId { get; set; }
+ public string ChannelName { get; set; }
+ public string ServiceName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the overview.
+ /// </summary>
+ /// <value>The overview.</value>
+ public string Overview { get; set; }
+
+ /// <summary>
+ /// Gets or sets the taglines.
+ /// </summary>
+ /// <value>The taglines.</value>
+ public string[] Taglines { get; set; }
+
+ /// <summary>
+ /// Gets or sets the genres.
+ /// </summary>
+ /// <value>The genres.</value>
+ public string[] Genres { get; set; }
+
+ /// <summary>
+ /// Gets or sets the community rating.
+ /// </summary>
+ /// <value>The community rating.</value>
+ public float? CommunityRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the cumulative run time ticks.
+ /// </summary>
+ /// <value>The cumulative run time ticks.</value>
+ public long? CumulativeRunTimeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the run time ticks.
+ /// </summary>
+ /// <value>The run time ticks.</value>
+ public long? RunTimeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the play access.
+ /// </summary>
+ /// <value>The play access.</value>
+ public PlayAccess? PlayAccess { get; set; }
+
+ /// <summary>
+ /// Gets or sets the aspect ratio.
+ /// </summary>
+ /// <value>The aspect ratio.</value>
+ public string AspectRatio { get; set; }
+
+ /// <summary>
+ /// Gets or sets the production year.
+ /// </summary>
+ /// <value>The production year.</value>
+ public int? ProductionYear { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is place holder.
+ /// </summary>
+ /// <value><c>null</c> if [is place holder] contains no value, <c>true</c> if [is place holder]; otherwise, <c>false</c>.</value>
+ public bool? IsPlaceHolder { get; set; }
+
+ /// <summary>
+ /// Gets or sets the number.
+ /// </summary>
+ /// <value>The number.</value>
+ public string Number { get; set; }
+ public string ChannelNumber { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index number.
+ /// </summary>
+ /// <value>The index number.</value>
+ public int? IndexNumber { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index number end.
+ /// </summary>
+ /// <value>The index number end.</value>
+ public int? IndexNumberEnd { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent index number.
+ /// </summary>
+ /// <value>The parent index number.</value>
+ public int? ParentIndexNumber { get; set; }
+
+ /// <summary>
+ /// Gets or sets the trailer urls.
+ /// </summary>
+ /// <value>The trailer urls.</value>
+ public MediaUrl[] RemoteTrailers { get; set; }
+
+ /// <summary>
+ /// Gets or sets the provider ids.
+ /// </summary>
+ /// <value>The provider ids.</value>
+ public Dictionary<string, string> ProviderIds { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is HD.
+ /// </summary>
+ /// <value><c>null</c> if [is HD] contains no value, <c>true</c> if [is HD]; otherwise, <c>false</c>.</value>
+ public bool? IsHD { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is folder.
+ /// </summary>
+ /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
+ public bool? IsFolder { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent id.
+ /// </summary>
+ /// <value>The parent id.</value>
+ public Guid ParentId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ public string Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the people.
+ /// </summary>
+ /// <value>The people.</value>
+ public BaseItemPerson[] People { get; set; }
+
+ /// <summary>
+ /// Gets or sets the studios.
+ /// </summary>
+ /// <value>The studios.</value>
+ public NameGuidPair[] Studios { get; set; }
+
+ public NameGuidPair[] GenreItems { get; set; }
+
+ /// <summary>
+ /// If the item does not have a logo, this will hold the Id of the Parent that has one.
+ /// </summary>
+ /// <value>The parent logo item id.</value>
+ public string ParentLogoItemId { get; set; }
+
+ /// <summary>
+ /// If the item does not have any backdrops, this will hold the Id of the Parent that has one.
+ /// </summary>
+ /// <value>The parent backdrop item id.</value>
+ public string ParentBackdropItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent backdrop image tags.
+ /// </summary>
+ /// <value>The parent backdrop image tags.</value>
+ public string[] ParentBackdropImageTags { get; set; }
+
+ /// <summary>
+ /// Gets or sets the local trailer count.
+ /// </summary>
+ /// <value>The local trailer count.</value>
+ public int? LocalTrailerCount { get; set; }
+
+ /// <summary>
+ /// User data for this item based on the user it's being requested for
+ /// </summary>
+ /// <value>The user data.</value>
+ public UserItemDataDto UserData { get; set; }
+
+ /// <summary>
+ /// Gets or sets the recursive item count.
+ /// </summary>
+ /// <value>The recursive item count.</value>
+ public int? RecursiveItemCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets the child count.
+ /// </summary>
+ /// <value>The child count.</value>
+ public int? ChildCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the series.
+ /// </summary>
+ /// <value>The name of the series.</value>
+ public string SeriesName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the series id.
+ /// </summary>
+ /// <value>The series id.</value>
+ public Guid SeriesId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the season identifier.
+ /// </summary>
+ /// <value>The season identifier.</value>
+ public Guid SeasonId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the special feature count.
+ /// </summary>
+ /// <value>The special feature count.</value>
+ public int? SpecialFeatureCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets the display preferences id.
+ /// </summary>
+ /// <value>The display preferences id.</value>
+ public string DisplayPreferencesId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the status.
+ /// </summary>
+ /// <value>The status.</value>
+ public string Status { get; set; }
+
+ /// <summary>
+ /// Gets or sets the air time.
+ /// </summary>
+ /// <value>The air time.</value>
+ public string AirTime { get; set; }
+
+ /// <summary>
+ /// Gets or sets the air days.
+ /// </summary>
+ /// <value>The air days.</value>
+ public DayOfWeek[] AirDays { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tags.
+ /// </summary>
+ /// <value>The tags.</value>
+ public string[] Tags { get; set; }
+
+ /// <summary>
+ /// Gets or sets the primary image aspect ratio, after image enhancements.
+ /// </summary>
+ /// <value>The primary image aspect ratio.</value>
+ public double? PrimaryImageAspectRatio { get; set; }
+
+ /// <summary>
+ /// Gets or sets the artists.
+ /// </summary>
+ /// <value>The artists.</value>
+ public string[] Artists { get; set; }
+
+ /// <summary>
+ /// Gets or sets the artist items.
+ /// </summary>
+ /// <value>The artist items.</value>
+ public NameGuidPair[] ArtistItems { get; set; }
+
+ /// <summary>
+ /// Gets or sets the album.
+ /// </summary>
+ /// <value>The album.</value>
+ public string Album { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the collection.
+ /// </summary>
+ /// <value>The type of the collection.</value>
+ public string CollectionType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the display order.
+ /// </summary>
+ /// <value>The display order.</value>
+ public string DisplayOrder { get; set; }
+
+ /// <summary>
+ /// Gets or sets the album id.
+ /// </summary>
+ /// <value>The album id.</value>
+ public Guid AlbumId { get; set; }
+ /// <summary>
+ /// Gets or sets the album image tag.
+ /// </summary>
+ /// <value>The album image tag.</value>
+ public string AlbumPrimaryImageTag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the series primary image tag.
+ /// </summary>
+ /// <value>The series primary image tag.</value>
+ public string SeriesPrimaryImageTag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the album artist.
+ /// </summary>
+ /// <value>The album artist.</value>
+ public string AlbumArtist { get; set; }
+
+ /// <summary>
+ /// Gets or sets the album artists.
+ /// </summary>
+ /// <value>The album artists.</value>
+ public NameGuidPair[] AlbumArtists { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the season.
+ /// </summary>
+ /// <value>The name of the season.</value>
+ public string SeasonName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the media streams.
+ /// </summary>
+ /// <value>The media streams.</value>
+ public MediaStream[] MediaStreams { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the video.
+ /// </summary>
+ /// <value>The type of the video.</value>
+ public VideoType? VideoType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the part count.
+ /// </summary>
+ /// <value>The part count.</value>
+ public int? PartCount { get; set; }
+ public int? MediaSourceCount { get; set; }
+
+ /// <summary>
+ /// Determines whether the specified type is type.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <returns><c>true</c> if the specified type is type; otherwise, <c>false</c>.</returns>
+ public bool IsType(Type type)
+ {
+ return IsType(type.Name);
+ }
+
+ /// <summary>
+ /// Determines whether the specified type is type.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <returns><c>true</c> if the specified type is type; otherwise, <c>false</c>.</returns>
+ public bool IsType(string type)
+ {
+ return StringHelper.EqualsIgnoreCase(Type, type);
+ }
+
+ /// <summary>
+ /// Gets or sets the image tags.
+ /// </summary>
+ /// <value>The image tags.</value>
+ public Dictionary<ImageType, string> ImageTags { get; set; }
+
+ /// <summary>
+ /// Gets or sets the backdrop image tags.
+ /// </summary>
+ /// <value>The backdrop image tags.</value>
+ public string[] BackdropImageTags { get; set; }
+
+ /// <summary>
+ /// Gets or sets the screenshot image tags.
+ /// </summary>
+ /// <value>The screenshot image tags.</value>
+ public string[] ScreenshotImageTags { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent logo image tag.
+ /// </summary>
+ /// <value>The parent logo image tag.</value>
+ public string ParentLogoImageTag { get; set; }
+
+ /// <summary>
+ /// If the item does not have a art, this will hold the Id of the Parent that has one.
+ /// </summary>
+ /// <value>The parent art item id.</value>
+ public string ParentArtItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent art image tag.
+ /// </summary>
+ /// <value>The parent art image tag.</value>
+ public string ParentArtImageTag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the series thumb image tag.
+ /// </summary>
+ /// <value>The series thumb image tag.</value>
+ public string SeriesThumbImageTag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the series studio.
+ /// </summary>
+ /// <value>The series studio.</value>
+ public string SeriesStudio { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent thumb item id.
+ /// </summary>
+ /// <value>The parent thumb item id.</value>
+ public string ParentThumbItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent thumb image tag.
+ /// </summary>
+ /// <value>The parent thumb image tag.</value>
+ public string ParentThumbImageTag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent primary image item identifier.
+ /// </summary>
+ /// <value>The parent primary image item identifier.</value>
+ public string ParentPrimaryImageItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent primary image tag.
+ /// </summary>
+ /// <value>The parent primary image tag.</value>
+ public string ParentPrimaryImageTag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the chapters.
+ /// </summary>
+ /// <value>The chapters.</value>
+ public List<ChapterInfo> Chapters { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the location.
+ /// </summary>
+ /// <value>The type of the location.</value>
+ public LocationType? LocationType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the iso.
+ /// </summary>
+ /// <value>The type of the iso.</value>
+ public IsoType? IsoType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the media.
+ /// </summary>
+ /// <value>The type of the media.</value>
+ public string MediaType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the end date.
+ /// </summary>
+ /// <value>The end date.</value>
+ public DateTime? EndDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the home page URL.
+ /// </summary>
+ /// <value>The home page URL.</value>
+ public string HomePageUrl { get; set; }
+
+ /// <summary>
+ /// Gets or sets the locked fields.
+ /// </summary>
+ /// <value>The locked fields.</value>
+ public MetadataFields[] LockedFields { get; set; }
+
+ /// <summary>
+ /// Gets or sets the trailer count.
+ /// </summary>
+ /// <value>The trailer count.</value>
+ public int? TrailerCount { get; set; }
+ /// <summary>
+ /// Gets or sets the movie count.
+ /// </summary>
+ /// <value>The movie count.</value>
+ public int? MovieCount { get; set; }
+ /// <summary>
+ /// Gets or sets the series count.
+ /// </summary>
+ /// <value>The series count.</value>
+ public int? SeriesCount { get; set; }
+ public int? ProgramCount { get; set; }
+ /// <summary>
+ /// Gets or sets the episode count.
+ /// </summary>
+ /// <value>The episode count.</value>
+ public int? EpisodeCount { get; set; }
+ /// <summary>
+ /// Gets or sets the game count.
+ /// </summary>
+ /// <value>The game count.</value>
+ public int? GameCount { get; set; }
+ /// <summary>
+ /// Gets or sets the song count.
+ /// </summary>
+ /// <value>The song count.</value>
+ public int? SongCount { get; set; }
+ /// <summary>
+ /// Gets or sets the album count.
+ /// </summary>
+ /// <value>The album count.</value>
+ public int? AlbumCount { get; set; }
+ public int? ArtistCount { get; set; }
+ /// <summary>
+ /// Gets or sets the music video count.
+ /// </summary>
+ /// <value>The music video count.</value>
+ public int? MusicVideoCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [enable internet providers].
+ /// </summary>
+ /// <value><c>true</c> if [enable internet providers]; otherwise, <c>false</c>.</value>
+ public bool? LockData { get; set; }
+
+ public int? Width { get; set; }
+ public int? Height { get; set; }
+ public string CameraMake { get; set; }
+ public string CameraModel { get; set; }
+ public string Software { get; set; }
+ public double? ExposureTime { get; set; }
+ public double? FocalLength { get; set; }
+ public ImageOrientation? ImageOrientation { get; set; }
+ public double? Aperture { get; set; }
+ public double? ShutterSpeed { get; set; }
+ public double? Latitude { get; set; }
+ public double? Longitude { get; set; }
+ public double? Altitude { get; set; }
+ public int? IsoSpeedRating { get; set; }
+
+ /// <summary>
+ /// Used by RecordingGroup
+ /// </summary>
+ public int? RecordingCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets the series timer identifier.
+ /// </summary>
+ /// <value>The series timer identifier.</value>
+ public string SeriesTimerId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the program identifier.
+ /// </summary>
+ /// <value>The program identifier.</value>
+ public string ProgramId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the channel primary image tag.
+ /// </summary>
+ /// <value>The channel primary image tag.</value>
+ public string ChannelPrimaryImageTag { get; set; }
+
+ /// <summary>
+ /// The start date of the recording, in UTC.
+ /// </summary>
+ public DateTime? StartDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the completion percentage.
+ /// </summary>
+ /// <value>The completion percentage.</value>
+ public double? CompletionPercentage { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is repeat.
+ /// </summary>
+ /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value>
+ public bool? IsRepeat { get; set; }
+
+ /// <summary>
+ /// Gets or sets the episode title.
+ /// </summary>
+ /// <value>The episode title.</value>
+ public string EpisodeTitle { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the channel.
+ /// </summary>
+ /// <value>The type of the channel.</value>
+ public ChannelType? ChannelType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the audio.
+ /// </summary>
+ /// <value>The audio.</value>
+ public ProgramAudio? Audio { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is movie.
+ /// </summary>
+ /// <value><c>true</c> if this instance is movie; otherwise, <c>false</c>.</value>
+ public bool? IsMovie { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is sports.
+ /// </summary>
+ /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value>
+ public bool? IsSports { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is series.
+ /// </summary>
+ /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value>
+ public bool? IsSeries { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is live.
+ /// </summary>
+ /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value>
+ public bool? IsLive { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is news.
+ /// </summary>
+ /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value>
+ public bool? IsNews { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is kids.
+ /// </summary>
+ /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value>
+ public bool? IsKids { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is premiere.
+ /// </summary>
+ /// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value>
+ public bool? IsPremiere { get; set; }
+
+ /// <summary>
+ /// Gets or sets the timer identifier.
+ /// </summary>
+ /// <value>The timer identifier.</value>
+ public string TimerId { get; set; }
+ /// <summary>
+ /// Gets or sets the current program.
+ /// </summary>
+ /// <value>The current program.</value>
+ public BaseItemDto CurrentProgram { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/BaseItemPerson.cs b/MediaBrowser.Model/Dto/BaseItemPerson.cs
new file mode 100644
index 000000000..35b4e9249
--- /dev/null
+++ b/MediaBrowser.Model/Dto/BaseItemPerson.cs
@@ -0,0 +1,53 @@
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Model.Dto
+{
+ /// <summary>
+ /// This is used by the api to get information about a Person within a BaseItem
+ /// </summary>
+ public class BaseItemPerson
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the identifier.
+ /// </summary>
+ /// <value>The identifier.</value>
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the role.
+ /// </summary>
+ /// <value>The role.</value>
+ public string Role { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ public string Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the primary image tag.
+ /// </summary>
+ /// <value>The primary image tag.</value>
+ public string PrimaryImageTag { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance has primary image.
+ /// </summary>
+ /// <value><c>true</c> if this instance has primary image; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool HasPrimaryImage
+ {
+ get
+ {
+ return PrimaryImageTag != null;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/ChapterInfoDto.cs b/MediaBrowser.Model/Dto/ChapterInfoDto.cs
new file mode 100644
index 000000000..b68b55e70
--- /dev/null
+++ b/MediaBrowser.Model/Dto/ChapterInfoDto.cs
@@ -0,0 +1,38 @@
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Model.Dto
+{
+ /// <summary>
+ /// Class ChapterInfo
+ /// </summary>
+ public class ChapterInfoDto
+ {
+ /// <summary>
+ /// Gets or sets the start position ticks.
+ /// </summary>
+ /// <value>The start position ticks.</value>
+ public long StartPositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the image tag.
+ /// </summary>
+ /// <value>The image tag.</value>
+ public string ImageTag { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance has image.
+ /// </summary>
+ /// <value><c>true</c> if this instance has image; otherwise, <c>false</c>.</value>
+ [IgnoreDataMember]
+ public bool HasImage
+ {
+ get { return ImageTag != null; }
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/GameSystemSummary.cs b/MediaBrowser.Model/Dto/GameSystemSummary.cs
new file mode 100644
index 000000000..252868b66
--- /dev/null
+++ b/MediaBrowser.Model/Dto/GameSystemSummary.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using System;
+
+namespace MediaBrowser.Model.Dto
+{
+ /// <summary>
+ /// Class GameSystemSummary
+ /// </summary>
+ public class GameSystemSummary
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string DisplayName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the game count.
+ /// </summary>
+ /// <value>The game count.</value>
+ public int GameCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets the game extensions.
+ /// </summary>
+ /// <value>The game extensions.</value>
+ public string[] GameFileExtensions { get; set; }
+
+ /// <summary>
+ /// Gets or sets the client installed game count.
+ /// </summary>
+ /// <value>The client installed game count.</value>
+ public int ClientInstalledGameCount { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GameSystemSummary"/> class.
+ /// </summary>
+ public GameSystemSummary()
+ {
+ GameFileExtensions = new string[] {};
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/IHasServerId.cs b/MediaBrowser.Model/Dto/IHasServerId.cs
new file mode 100644
index 000000000..0515203da
--- /dev/null
+++ b/MediaBrowser.Model/Dto/IHasServerId.cs
@@ -0,0 +1,8 @@
+
+namespace MediaBrowser.Model.Dto
+{
+ public interface IHasServerId
+ {
+ string ServerId { get; }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/IItemDto.cs b/MediaBrowser.Model/Dto/IItemDto.cs
new file mode 100644
index 000000000..3e7d1c608
--- /dev/null
+++ b/MediaBrowser.Model/Dto/IItemDto.cs
@@ -0,0 +1,15 @@
+
+namespace MediaBrowser.Model.Dto
+{
+ /// <summary>
+ /// Interface IItemDto
+ /// </summary>
+ public interface IItemDto
+ {
+ /// <summary>
+ /// Gets or sets the primary image aspect ratio.
+ /// </summary>
+ /// <value>The primary image aspect ratio.</value>
+ double? PrimaryImageAspectRatio { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/ImageByNameInfo.cs b/MediaBrowser.Model/Dto/ImageByNameInfo.cs
new file mode 100644
index 000000000..b7921d993
--- /dev/null
+++ b/MediaBrowser.Model/Dto/ImageByNameInfo.cs
@@ -0,0 +1,32 @@
+
+namespace MediaBrowser.Model.Dto
+{
+ public class ImageByNameInfo
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the theme.
+ /// </summary>
+ /// <value>The theme.</value>
+ public string Theme { get; set; }
+ /// <summary>
+ /// Gets or sets the context.
+ /// </summary>
+ /// <value>The context.</value>
+ public string Context { get; set; }
+ /// <summary>
+ /// Gets or sets the length of the file.
+ /// </summary>
+ /// <value>The length of the file.</value>
+ public long FileLength { get; set; }
+ /// <summary>
+ /// Gets or sets the format.
+ /// </summary>
+ /// <value>The format.</value>
+ public string Format { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/ImageInfo.cs b/MediaBrowser.Model/Dto/ImageInfo.cs
new file mode 100644
index 000000000..5eabb16a5
--- /dev/null
+++ b/MediaBrowser.Model/Dto/ImageInfo.cs
@@ -0,0 +1,51 @@
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.Dto
+{
+ /// <summary>
+ /// Class ImageInfo
+ /// </summary>
+ public class ImageInfo
+ {
+ /// <summary>
+ /// Gets or sets the type of the image.
+ /// </summary>
+ /// <value>The type of the image.</value>
+ public ImageType ImageType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index of the image.
+ /// </summary>
+ /// <value>The index of the image.</value>
+ public int? ImageIndex { get; set; }
+
+ /// <summary>
+ /// The image tag
+ /// </summary>
+ public string ImageTag;
+
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ public string Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets the height.
+ /// </summary>
+ /// <value>The height.</value>
+ public int? Height { get; set; }
+
+ /// <summary>
+ /// Gets or sets the width.
+ /// </summary>
+ /// <value>The width.</value>
+ public int? Width { get; set; }
+
+ /// <summary>
+ /// Gets or sets the size.
+ /// </summary>
+ /// <value>The size.</value>
+ public long Size { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/ImageOptions.cs b/MediaBrowser.Model/Dto/ImageOptions.cs
new file mode 100644
index 000000000..98bd0279a
--- /dev/null
+++ b/MediaBrowser.Model/Dto/ImageOptions.cs
@@ -0,0 +1,110 @@
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.Dto
+{
+ /// <summary>
+ /// Class ImageOptions
+ /// </summary>
+ public class ImageOptions
+ {
+ /// <summary>
+ /// Gets or sets the type of the image.
+ /// </summary>
+ /// <value>The type of the image.</value>
+ public ImageType ImageType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index of the image.
+ /// </summary>
+ /// <value>The index of the image.</value>
+ public int? ImageIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the width.
+ /// </summary>
+ /// <value>The width.</value>
+ public int? Width { get; set; }
+
+ /// <summary>
+ /// Gets or sets the height.
+ /// </summary>
+ /// <value>The height.</value>
+ public int? Height { get; set; }
+
+ /// <summary>
+ /// Gets or sets the width of the max.
+ /// </summary>
+ /// <value>The width of the max.</value>
+ public int? MaxWidth { get; set; }
+
+ /// <summary>
+ /// Gets or sets the height of the max.
+ /// </summary>
+ /// <value>The height of the max.</value>
+ public int? MaxHeight { get; set; }
+
+ /// <summary>
+ /// Gets or sets the quality.
+ /// </summary>
+ /// <value>The quality.</value>
+ public int? Quality { get; set; }
+
+ /// <summary>
+ /// Gets or sets the image tag.
+ /// If set this will result in strong, unconditional response caching
+ /// </summary>
+ /// <value>The hash.</value>
+ public string Tag { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [crop whitespace].
+ /// </summary>
+ /// <value><c>null</c> if [crop whitespace] contains no value, <c>true</c> if [crop whitespace]; otherwise, <c>false</c>.</value>
+ public bool? CropWhitespace { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [enable image enhancers].
+ /// </summary>
+ /// <value><c>true</c> if [enable image enhancers]; otherwise, <c>false</c>.</value>
+ public bool EnableImageEnhancers { get; set; }
+
+ /// <summary>
+ /// Gets or sets the format.
+ /// </summary>
+ /// <value>The format.</value>
+ public ImageFormat? Format { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [add played indicator].
+ /// </summary>
+ /// <value><c>true</c> if [add played indicator]; otherwise, <c>false</c>.</value>
+ public bool AddPlayedIndicator { get; set; }
+
+ /// <summary>
+ /// Gets or sets the percent played.
+ /// </summary>
+ /// <value>The percent played.</value>
+ public int? PercentPlayed { get; set; }
+
+ /// <summary>
+ /// Gets or sets the un played count.
+ /// </summary>
+ /// <value>The un played count.</value>
+ public int? UnPlayedCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets the color of the background.
+ /// </summary>
+ /// <value>The color of the background.</value>
+ public string BackgroundColor { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ImageOptions" /> class.
+ /// </summary>
+ public ImageOptions()
+ {
+ EnableImageEnhancers = true;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/ItemCounts.cs b/MediaBrowser.Model/Dto/ItemCounts.cs
new file mode 100644
index 000000000..8ceb3a86b
--- /dev/null
+++ b/MediaBrowser.Model/Dto/ItemCounts.cs
@@ -0,0 +1,67 @@
+namespace MediaBrowser.Model.Dto
+{
+ /// <summary>
+ /// Class LibrarySummary
+ /// </summary>
+ public class ItemCounts
+ {
+ /// <summary>
+ /// Gets or sets the movie count.
+ /// </summary>
+ /// <value>The movie count.</value>
+ public int MovieCount { get; set; }
+ /// <summary>
+ /// Gets or sets the series count.
+ /// </summary>
+ /// <value>The series count.</value>
+ public int SeriesCount { get; set; }
+ /// <summary>
+ /// Gets or sets the episode count.
+ /// </summary>
+ /// <value>The episode count.</value>
+ public int EpisodeCount { get; set; }
+ /// <summary>
+ /// Gets or sets the game count.
+ /// </summary>
+ /// <value>The game count.</value>
+ public int GameCount { get; set; }
+ public int ArtistCount { get; set; }
+ public int ProgramCount { get; set; }
+ /// <summary>
+ /// Gets or sets the game system count.
+ /// </summary>
+ /// <value>The game system count.</value>
+ public int GameSystemCount { get; set; }
+ /// <summary>
+ /// Gets or sets the trailer count.
+ /// </summary>
+ /// <value>The trailer count.</value>
+ public int TrailerCount { get; set; }
+ /// <summary>
+ /// Gets or sets the song count.
+ /// </summary>
+ /// <value>The song count.</value>
+ public int SongCount { get; set; }
+ /// <summary>
+ /// Gets or sets the album count.
+ /// </summary>
+ /// <value>The album count.</value>
+ public int AlbumCount { get; set; }
+ /// <summary>
+ /// Gets or sets the music video count.
+ /// </summary>
+ /// <value>The music video count.</value>
+ public int MusicVideoCount { get; set; }
+ /// <summary>
+ /// Gets or sets the box set count.
+ /// </summary>
+ /// <value>The box set count.</value>
+ public int BoxSetCount { get; set; }
+ /// <summary>
+ /// Gets or sets the book count.
+ /// </summary>
+ /// <value>The book count.</value>
+ public int BookCount { get; set; }
+ public int ItemCount { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/ItemIndex.cs b/MediaBrowser.Model/Dto/ItemIndex.cs
new file mode 100644
index 000000000..96cef622b
--- /dev/null
+++ b/MediaBrowser.Model/Dto/ItemIndex.cs
@@ -0,0 +1,21 @@
+
+namespace MediaBrowser.Model.Dto
+{
+ /// <summary>
+ /// Class ItemIndex
+ /// </summary>
+ public class ItemIndex
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item count.
+ /// </summary>
+ /// <value>The item count.</value>
+ public int ItemCount { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
new file mode 100644
index 000000000..d1d068fb6
--- /dev/null
+++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs
@@ -0,0 +1,236 @@
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.MediaInfo;
+using System.Collections.Generic;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Session;
+using System;
+
+namespace MediaBrowser.Model.Dto
+{
+ public class MediaSourceInfo
+ {
+ public MediaProtocol Protocol { get; set; }
+ public string Id { get; set; }
+
+ public string Path { get; set; }
+
+ public string EncoderPath { get; set; }
+ public MediaProtocol? EncoderProtocol { get; set; }
+
+ public MediaSourceType Type { get; set; }
+
+ public string Container { get; set; }
+ public long? Size { get; set; }
+
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Differentiate internet url vs local network
+ /// </summary>
+ public bool IsRemote { get; set; }
+
+ public string ETag { get; set; }
+ public long? RunTimeTicks { get; set; }
+ public bool ReadAtNativeFramerate { get; set; }
+ public bool IgnoreDts { get; set; }
+ public bool IgnoreIndex { get; set; }
+ public bool GenPtsInput { get; set; }
+ public bool SupportsTranscoding { get; set; }
+ public bool SupportsDirectStream { get; set; }
+ public bool SupportsDirectPlay { get; set; }
+ public bool IsInfiniteStream { get; set; }
+ public bool RequiresOpening { get; set; }
+ public string OpenToken { get; set; }
+ public bool RequiresClosing { get; set; }
+ public string LiveStreamId { get; set; }
+ public int? BufferMs { get; set; }
+
+ public bool RequiresLooping { get; set; }
+
+ public bool SupportsProbing { get; set; }
+
+ public VideoType? VideoType { get; set; }
+
+ public IsoType? IsoType { get; set; }
+
+ public Video3DFormat? Video3DFormat { get; set; }
+
+ public List<MediaStream> MediaStreams { get; set; }
+
+ public string[] Formats { get; set; }
+
+ public int? Bitrate { get; set; }
+
+ public TransportStreamTimestamp? Timestamp { get; set; }
+ public Dictionary<string, string> RequiredHttpHeaders { get; set; }
+
+ public string TranscodingUrl { get; set; }
+ public string TranscodingSubProtocol { get; set; }
+ public string TranscodingContainer { get; set; }
+
+ public int? AnalyzeDurationMs { get; set; }
+
+ public MediaSourceInfo()
+ {
+ Formats = new string[] { };
+ MediaStreams = new List<MediaStream>();
+ RequiredHttpHeaders = new Dictionary<string, string>();
+ SupportsTranscoding = true;
+ SupportsDirectStream = true;
+ SupportsDirectPlay = true;
+ SupportsProbing = true;
+ }
+
+ public void InferTotalBitrate(bool force = false)
+ {
+ if (MediaStreams == null)
+ {
+ return;
+ }
+
+ if (!force && Bitrate.HasValue)
+ {
+ return;
+ }
+
+ var bitrate = 0;
+ foreach (var stream in MediaStreams)
+ {
+ if (!stream.IsExternal)
+ {
+ bitrate += stream.BitRate ?? 0;
+ }
+ }
+
+ if (bitrate > 0)
+ {
+ Bitrate = bitrate;
+ }
+ }
+
+ [IgnoreDataMember]
+ public TranscodeReason[] TranscodeReasons { get; set; }
+
+ public int? DefaultAudioStreamIndex { get; set; }
+ public int? DefaultSubtitleStreamIndex { get; set; }
+
+ [IgnoreDataMember]
+ public MediaStream DefaultAudioStream
+ {
+ get { return GetDefaultAudioStream(DefaultAudioStreamIndex); }
+ }
+
+ public MediaStream GetDefaultAudioStream(int? defaultIndex)
+ {
+ if (defaultIndex.HasValue)
+ {
+ var val = defaultIndex.Value;
+
+ foreach (MediaStream i in MediaStreams)
+ {
+ if (i.Type == MediaStreamType.Audio && i.Index == val)
+ {
+ return i;
+ }
+ }
+ }
+
+ foreach (MediaStream i in MediaStreams)
+ {
+ if (i.Type == MediaStreamType.Audio && i.IsDefault)
+ {
+ return i;
+ }
+ }
+
+ foreach (MediaStream i in MediaStreams)
+ {
+ if (i.Type == MediaStreamType.Audio)
+ {
+ return i;
+ }
+ }
+
+ return null;
+ }
+
+ [IgnoreDataMember]
+ public MediaStream VideoStream
+ {
+ get
+ {
+ foreach (MediaStream i in MediaStreams)
+ {
+ if (i.Type == MediaStreamType.Video)
+ {
+ return i;
+ }
+ }
+
+ return null;
+ }
+ }
+
+ public MediaStream GetMediaStream(MediaStreamType type, int index)
+ {
+ foreach (MediaStream i in MediaStreams)
+ {
+ if (i.Type == type && i.Index == index)
+ {
+ return i;
+ }
+ }
+
+ return null;
+ }
+
+ public int? GetStreamCount(MediaStreamType type)
+ {
+ int numMatches = 0;
+ int numStreams = 0;
+
+ foreach (MediaStream i in MediaStreams)
+ {
+ numStreams++;
+ if (i.Type == type)
+ {
+ numMatches++;
+ }
+ }
+
+ if (numStreams == 0)
+ {
+ return null;
+ }
+
+ return numMatches;
+ }
+
+ public bool? IsSecondaryAudio(MediaStream stream)
+ {
+ // Look for the first audio track marked as default
+ foreach (MediaStream currentStream in MediaStreams)
+ {
+ if (currentStream.Type == MediaStreamType.Audio && currentStream.IsDefault)
+ {
+ if (currentStream.Index != stream.Index)
+ {
+ return true;
+ }
+ }
+ }
+
+ // Look for the first audio track
+ foreach (MediaStream currentStream in MediaStreams)
+ {
+ if (currentStream.Type == MediaStreamType.Audio)
+ {
+ return currentStream.Index != stream.Index;
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/MediaSourceType.cs b/MediaBrowser.Model/Dto/MediaSourceType.cs
new file mode 100644
index 000000000..e04978502
--- /dev/null
+++ b/MediaBrowser.Model/Dto/MediaSourceType.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Model.Dto
+{
+ public enum MediaSourceType
+ {
+ Default = 0,
+ Grouping = 1,
+ Placeholder = 2
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs
new file mode 100644
index 000000000..aa8b33c81
--- /dev/null
+++ b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs
@@ -0,0 +1,27 @@
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Providers;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Dto
+{
+ public class MetadataEditorInfo
+ {
+ public ParentalRating[] ParentalRatingOptions { get; set; }
+ public CountryInfo[] Countries { get; set; }
+ public CultureDto[] Cultures { get; set; }
+ public ExternalIdInfo[] ExternalIdInfos { get; set; }
+
+ public string ContentType { get; set; }
+ public NameValuePair[] ContentTypeOptions { get; set; }
+
+ public MetadataEditorInfo()
+ {
+ ParentalRatingOptions = new ParentalRating[] { };
+ Countries = new CountryInfo[] { };
+ Cultures = new CultureDto[] { };
+ ExternalIdInfos = new ExternalIdInfo[] { };
+ ContentTypeOptions = new NameValuePair[] { };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/NameIdPair.cs b/MediaBrowser.Model/Dto/NameIdPair.cs
new file mode 100644
index 000000000..50318ac95
--- /dev/null
+++ b/MediaBrowser.Model/Dto/NameIdPair.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace MediaBrowser.Model.Dto
+{
+ public class NameIdPair
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the identifier.
+ /// </summary>
+ /// <value>The identifier.</value>
+ public string Id { get; set; }
+ }
+
+ public class NameGuidPair
+ {
+ public string Name { get; set; }
+ public Guid Id { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/NameValuePair.cs b/MediaBrowser.Model/Dto/NameValuePair.cs
new file mode 100644
index 000000000..a6e687949
--- /dev/null
+++ b/MediaBrowser.Model/Dto/NameValuePair.cs
@@ -0,0 +1,28 @@
+
+namespace MediaBrowser.Model.Dto
+{
+ public class NameValuePair
+ {
+ public NameValuePair()
+ {
+
+ }
+
+ public NameValuePair(string name, string value)
+ {
+ Name = name;
+ Value = value;
+ }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the value.
+ /// </summary>
+ /// <value>The value.</value>
+ public string Value { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/RatingType.cs b/MediaBrowser.Model/Dto/RatingType.cs
new file mode 100644
index 000000000..f151adce9
--- /dev/null
+++ b/MediaBrowser.Model/Dto/RatingType.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Model.Dto
+{
+ public enum RatingType
+ {
+ Score,
+ Likes
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dto/RecommendationDto.cs b/MediaBrowser.Model/Dto/RecommendationDto.cs
new file mode 100644
index 000000000..79d3d6c6f
--- /dev/null
+++ b/MediaBrowser.Model/Dto/RecommendationDto.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace MediaBrowser.Model.Dto
+{
+ public class RecommendationDto
+ {
+ public BaseItemDto[] Items { get; set; }
+
+ public RecommendationType RecommendationType { get; set; }
+
+ public string BaselineItemName { get; set; }
+
+ public Guid CategoryId { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/RecommendationType.cs b/MediaBrowser.Model/Dto/RecommendationType.cs
new file mode 100644
index 000000000..1adf9b082
--- /dev/null
+++ b/MediaBrowser.Model/Dto/RecommendationType.cs
@@ -0,0 +1,17 @@
+namespace MediaBrowser.Model.Dto
+{
+ public enum RecommendationType
+ {
+ SimilarToRecentlyPlayed = 0,
+
+ SimilarToLikedItem = 1,
+
+ HasDirectorFromRecentlyPlayed = 2,
+
+ HasActorFromRecentlyPlayed = 3,
+
+ HasLikedDirector = 4,
+
+ HasLikedActor = 5
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Dto/UserDto.cs b/MediaBrowser.Model/Dto/UserDto.cs
new file mode 100644
index 000000000..f42c495ad
--- /dev/null
+++ b/MediaBrowser.Model/Dto/UserDto.cs
@@ -0,0 +1,125 @@
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Connect;
+using MediaBrowser.Model.Users;
+using System;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Model.Dto
+{
+ /// <summary>
+ /// Class UserDto
+ /// </summary>
+ public class UserDto : IItemDto, IHasServerId
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the server identifier.
+ /// </summary>
+ /// <value>The server identifier.</value>
+ public string ServerId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the server.
+ /// This is not used by the server and is for client-side usage only.
+ /// </summary>
+ /// <value>The name of the server.</value>
+ public string ServerName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the connect user.
+ /// </summary>
+ /// <value>The name of the connect user.</value>
+ public string ConnectUserName { get; set; }
+ /// <summary>
+ /// Gets or sets the connect user identifier.
+ /// </summary>
+ /// <value>The connect user identifier.</value>
+ public string ConnectUserId { get; set; }
+ /// <summary>
+ /// Gets or sets the type of the connect link.
+ /// </summary>
+ /// <value>The type of the connect link.</value>
+ public UserLinkType? ConnectLinkType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the primary image tag.
+ /// </summary>
+ /// <value>The primary image tag.</value>
+ public string PrimaryImageTag { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has password.
+ /// </summary>
+ /// <value><c>true</c> if this instance has password; otherwise, <c>false</c>.</value>
+ public bool HasPassword { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has configured password.
+ /// </summary>
+ /// <value><c>true</c> if this instance has configured password; otherwise, <c>false</c>.</value>
+ public bool HasConfiguredPassword { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has configured easy password.
+ /// </summary>
+ /// <value><c>true</c> if this instance has configured easy password; otherwise, <c>false</c>.</value>
+ public bool HasConfiguredEasyPassword { get; set; }
+
+ public bool? EnableAutoLogin { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last login date.
+ /// </summary>
+ /// <value>The last login date.</value>
+ public DateTime? LastLoginDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last activity date.
+ /// </summary>
+ /// <value>The last activity date.</value>
+ public DateTime? LastActivityDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the configuration.
+ /// </summary>
+ /// <value>The configuration.</value>
+ public UserConfiguration Configuration { get; set; }
+
+ /// <summary>
+ /// Gets or sets the policy.
+ /// </summary>
+ /// <value>The policy.</value>
+ public UserPolicy Policy { get; set; }
+
+ /// <summary>
+ /// Gets or sets the primary image aspect ratio.
+ /// </summary>
+ /// <value>The primary image aspect ratio.</value>
+ public double? PrimaryImageAspectRatio { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserDto"/> class.
+ /// </summary>
+ public UserDto()
+ {
+ Configuration = new UserConfiguration();
+ Policy = new UserPolicy();
+ }
+
+ public override string ToString()
+ {
+ return Name ?? base.ToString();
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Dto/UserItemDataDto.cs b/MediaBrowser.Model/Dto/UserItemDataDto.cs
new file mode 100644
index 000000000..507dbb06d
--- /dev/null
+++ b/MediaBrowser.Model/Dto/UserItemDataDto.cs
@@ -0,0 +1,76 @@
+using System;
+
+namespace MediaBrowser.Model.Dto
+{
+ /// <summary>
+ /// Class UserItemDataDto
+ /// </summary>
+ public class UserItemDataDto
+ {
+ /// <summary>
+ /// Gets or sets the rating.
+ /// </summary>
+ /// <value>The rating.</value>
+ public double? Rating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the played percentage.
+ /// </summary>
+ /// <value>The played percentage.</value>
+ public double? PlayedPercentage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the unplayed item count.
+ /// </summary>
+ /// <value>The unplayed item count.</value>
+ public int? UnplayedItemCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playback position ticks.
+ /// </summary>
+ /// <value>The playback position ticks.</value>
+ public long PlaybackPositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the play count.
+ /// </summary>
+ /// <value>The play count.</value>
+ public int PlayCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is favorite.
+ /// </summary>
+ /// <value><c>true</c> if this instance is favorite; otherwise, <c>false</c>.</value>
+ public bool IsFavorite { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="UserItemDataDto" /> is likes.
+ /// </summary>
+ /// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value>
+ public bool? Likes { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last played date.
+ /// </summary>
+ /// <value>The last played date.</value>
+ public DateTime? LastPlayedDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="UserItemDataDto" /> is played.
+ /// </summary>
+ /// <value><c>true</c> if played; otherwise, <c>false</c>.</value>
+ public bool Played { get; set; }
+
+ /// <summary>
+ /// Gets or sets the key.
+ /// </summary>
+ /// <value>The key.</value>
+ public string Key { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item identifier.
+ /// </summary>
+ /// <value>The item identifier.</value>
+ public string ItemId { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Entities/ChapterInfo.cs b/MediaBrowser.Model/Entities/ChapterInfo.cs
new file mode 100644
index 000000000..c24ca553b
--- /dev/null
+++ b/MediaBrowser.Model/Entities/ChapterInfo.cs
@@ -0,0 +1,31 @@
+using System;
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Class ChapterInfo
+ /// </summary>
+ public class ChapterInfo
+ {
+ /// <summary>
+ /// Gets or sets the start position ticks.
+ /// </summary>
+ /// <value>The start position ticks.</value>
+ public long StartPositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the image path.
+ /// </summary>
+ /// <value>The image path.</value>
+ public string ImagePath { get; set; }
+ public DateTime ImageDateModified { get; set; }
+
+ public string ImageTag { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Entities/CollectionType.cs b/MediaBrowser.Model/Entities/CollectionType.cs
new file mode 100644
index 000000000..f49e73c16
--- /dev/null
+++ b/MediaBrowser.Model/Entities/CollectionType.cs
@@ -0,0 +1,58 @@
+namespace MediaBrowser.Model.Entities
+{
+ public static class CollectionType
+ {
+ public const string Movies = "movies";
+
+ public const string TvShows = "tvshows";
+
+ public const string Music = "music";
+
+ public const string MusicVideos = "musicvideos";
+
+ public const string Trailers = "trailers";
+
+ public const string HomeVideos = "homevideos";
+
+ public const string BoxSets = "boxsets";
+
+ public const string Books = "books";
+ public const string Photos = "photos";
+ public const string Games = "games";
+ public const string LiveTv = "livetv";
+ public const string Playlists = "playlists";
+ public const string Folders = "folders";
+ }
+
+ public static class SpecialFolder
+ {
+ public const string TvShowSeries = "TvShowSeries";
+ public const string TvGenres = "TvGenres";
+ public const string TvGenre = "TvGenre";
+ public const string TvLatest = "TvLatest";
+ public const string TvNextUp = "TvNextUp";
+ public const string TvResume = "TvResume";
+ public const string TvFavoriteSeries = "TvFavoriteSeries";
+ public const string TvFavoriteEpisodes = "TvFavoriteEpisodes";
+
+ public const string MovieLatest = "MovieLatest";
+ public const string MovieResume = "MovieResume";
+ public const string MovieMovies = "MovieMovies";
+ public const string MovieCollections = "MovieCollections";
+ public const string MovieFavorites = "MovieFavorites";
+ public const string MovieGenres = "MovieGenres";
+ public const string MovieGenre = "MovieGenre";
+
+ public const string MusicArtists = "MusicArtists";
+ public const string MusicAlbumArtists = "MusicAlbumArtists";
+ public const string MusicAlbums = "MusicAlbums";
+ public const string MusicGenres = "MusicGenres";
+ public const string MusicLatest = "MusicLatest";
+ public const string MusicPlaylists = "MusicPlaylists";
+ public const string MusicSongs = "MusicSongs";
+ public const string MusicFavorites = "MusicFavorites";
+ public const string MusicFavoriteArtists = "MusicFavoriteArtists";
+ public const string MusicFavoriteAlbums = "MusicFavoriteAlbums";
+ public const string MusicFavoriteSongs = "MusicFavoriteSongs";
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Entities/DisplayPreferences.cs b/MediaBrowser.Model/Entities/DisplayPreferences.cs
new file mode 100644
index 000000000..dc386f775
--- /dev/null
+++ b/MediaBrowser.Model/Entities/DisplayPreferences.cs
@@ -0,0 +1,99 @@
+using MediaBrowser.Model.Drawing;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Defines the display preferences for any item that supports them (usually Folders)
+ /// </summary>
+ public class DisplayPreferences
+ {
+ /// <summary>
+ /// The image scale
+ /// </summary>
+ private const double ImageScale = .9;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DisplayPreferences" /> class.
+ /// </summary>
+ public DisplayPreferences()
+ {
+ RememberIndexing = false;
+ PrimaryImageHeight = 250;
+ PrimaryImageWidth = 250;
+ ShowBackdrop = true;
+ CustomPrefs = new Dictionary<string, string>();
+ }
+
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ /// <value>The user id.</value>
+ public string Id { get; set; }
+ /// <summary>
+ /// Gets or sets the type of the view.
+ /// </summary>
+ /// <value>The type of the view.</value>
+ public string ViewType { get; set; }
+ /// <summary>
+ /// Gets or sets the sort by.
+ /// </summary>
+ /// <value>The sort by.</value>
+ public string SortBy { get; set; }
+ /// <summary>
+ /// Gets or sets the index by.
+ /// </summary>
+ /// <value>The index by.</value>
+ public string IndexBy { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether [remember indexing].
+ /// </summary>
+ /// <value><c>true</c> if [remember indexing]; otherwise, <c>false</c>.</value>
+ public bool RememberIndexing { get; set; }
+ /// <summary>
+ /// Gets or sets the height of the primary image.
+ /// </summary>
+ /// <value>The height of the primary image.</value>
+ public int PrimaryImageHeight { get; set; }
+ /// <summary>
+ /// Gets or sets the width of the primary image.
+ /// </summary>
+ /// <value>The width of the primary image.</value>
+ public int PrimaryImageWidth { get; set; }
+ /// <summary>
+ /// Gets or sets the custom prefs.
+ /// </summary>
+ /// <value>The custom prefs.</value>
+ public Dictionary<string, string> CustomPrefs { get; set; }
+ /// <summary>
+ /// Gets or sets the scroll direction.
+ /// </summary>
+ /// <value>The scroll direction.</value>
+ public ScrollDirection ScrollDirection { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether to show backdrops on this item.
+ /// </summary>
+ /// <value><c>true</c> if showing backdrops; otherwise, <c>false</c>.</value>
+ public bool ShowBackdrop { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether [remember sorting].
+ /// </summary>
+ /// <value><c>true</c> if [remember sorting]; otherwise, <c>false</c>.</value>
+ public bool RememberSorting { get; set; }
+ /// <summary>
+ /// Gets or sets the sort order.
+ /// </summary>
+ /// <value>The sort order.</value>
+ public SortOrder SortOrder { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether [show sidebar].
+ /// </summary>
+ /// <value><c>true</c> if [show sidebar]; otherwise, <c>false</c>.</value>
+ public bool ShowSidebar { get; set; }
+ /// <summary>
+ /// Gets or sets the client
+ /// </summary>
+ public string Client { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Entities/EmptyRequestResult.cs b/MediaBrowser.Model/Entities/EmptyRequestResult.cs
new file mode 100644
index 000000000..5c9a725fd
--- /dev/null
+++ b/MediaBrowser.Model/Entities/EmptyRequestResult.cs
@@ -0,0 +1,7 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ public class EmptyRequestResult
+ {
+ }
+}
diff --git a/MediaBrowser.Model/Entities/ExtraType.cs b/MediaBrowser.Model/Entities/ExtraType.cs
new file mode 100644
index 000000000..ab8da58c0
--- /dev/null
+++ b/MediaBrowser.Model/Entities/ExtraType.cs
@@ -0,0 +1,16 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ public enum ExtraType
+ {
+ Clip = 1,
+ Trailer = 2,
+ BehindTheScenes = 3,
+ DeletedScene = 4,
+ Interview = 5,
+ Scene = 6,
+ Sample = 7,
+ ThemeSong = 8,
+ ThemeVideo = 9
+ }
+}
diff --git a/MediaBrowser.Model/Entities/IHasProviderIds.cs b/MediaBrowser.Model/Entities/IHasProviderIds.cs
new file mode 100644
index 000000000..796850dbd
--- /dev/null
+++ b/MediaBrowser.Model/Entities/IHasProviderIds.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Since BaseItem and DTOBaseItem both have ProviderIds, this interface helps avoid code repition by using extension methods
+ /// </summary>
+ public interface IHasProviderIds
+ {
+ /// <summary>
+ /// Gets or sets the provider ids.
+ /// </summary>
+ /// <value>The provider ids.</value>
+ Dictionary<string, string> ProviderIds { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Entities/ImageType.cs b/MediaBrowser.Model/Entities/ImageType.cs
new file mode 100644
index 000000000..6e0ba717f
--- /dev/null
+++ b/MediaBrowser.Model/Entities/ImageType.cs
@@ -0,0 +1,58 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Enum ImageType
+ /// </summary>
+ public enum ImageType
+ {
+ /// <summary>
+ /// The primary
+ /// </summary>
+ Primary = 0,
+ /// <summary>
+ /// The art
+ /// </summary>
+ Art = 1,
+ /// <summary>
+ /// The backdrop
+ /// </summary>
+ Backdrop = 2,
+ /// <summary>
+ /// The banner
+ /// </summary>
+ Banner = 3,
+ /// <summary>
+ /// The logo
+ /// </summary>
+ Logo = 4,
+ /// <summary>
+ /// The thumb
+ /// </summary>
+ Thumb = 5,
+ /// <summary>
+ /// The disc
+ /// </summary>
+ Disc = 6,
+ /// <summary>
+ /// The box
+ /// </summary>
+ Box = 7,
+ /// <summary>
+ /// The screenshot
+ /// </summary>
+ Screenshot = 8,
+ /// <summary>
+ /// The menu
+ /// </summary>
+ Menu = 9,
+ /// <summary>
+ /// The chapter image
+ /// </summary>
+ Chapter = 10,
+ /// <summary>
+ /// The box rear
+ /// </summary>
+ BoxRear = 11
+ }
+}
diff --git a/MediaBrowser.Model/Entities/IsoType.cs b/MediaBrowser.Model/Entities/IsoType.cs
new file mode 100644
index 000000000..567b98ab9
--- /dev/null
+++ b/MediaBrowser.Model/Entities/IsoType.cs
@@ -0,0 +1,17 @@
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Enum IsoType
+ /// </summary>
+ public enum IsoType
+ {
+ /// <summary>
+ /// The DVD
+ /// </summary>
+ Dvd,
+ /// <summary>
+ /// The blu ray
+ /// </summary>
+ BluRay
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Entities/LibraryUpdateInfo.cs b/MediaBrowser.Model/Entities/LibraryUpdateInfo.cs
new file mode 100644
index 000000000..dfab9add2
--- /dev/null
+++ b/MediaBrowser.Model/Entities/LibraryUpdateInfo.cs
@@ -0,0 +1,62 @@
+using System;
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Class LibraryUpdateInfo
+ /// </summary>
+ public class LibraryUpdateInfo
+ {
+ /// <summary>
+ /// Gets or sets the folders added to.
+ /// </summary>
+ /// <value>The folders added to.</value>
+ public string[] FoldersAddedTo { get; set; }
+ /// <summary>
+ /// Gets or sets the folders removed from.
+ /// </summary>
+ /// <value>The folders removed from.</value>
+ public string[] FoldersRemovedFrom { get; set; }
+
+ /// <summary>
+ /// Gets or sets the items added.
+ /// </summary>
+ /// <value>The items added.</value>
+ public string[] ItemsAdded { get; set; }
+
+ /// <summary>
+ /// Gets or sets the items removed.
+ /// </summary>
+ /// <value>The items removed.</value>
+ public string[] ItemsRemoved { get; set; }
+
+ /// <summary>
+ /// Gets or sets the items updated.
+ /// </summary>
+ /// <value>The items updated.</value>
+ public string[] ItemsUpdated { get; set; }
+
+ public string[] CollectionFolders { get; set; }
+
+ public bool IsEmpty
+ {
+ get
+ {
+ return FoldersAddedTo.Length == 0 && FoldersRemovedFrom.Length == 0 && ItemsAdded.Length == 0 && ItemsRemoved.Length == 0 && ItemsUpdated.Length == 0 && CollectionFolders.Length == 0;
+ }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LibraryUpdateInfo"/> class.
+ /// </summary>
+ public LibraryUpdateInfo()
+ {
+ FoldersAddedTo = new string[] { };
+ FoldersRemovedFrom = new string[] { };
+ ItemsAdded = new string[] { };
+ ItemsRemoved = new string[] { };
+ ItemsUpdated = new string[] { };
+ CollectionFolders = new string[] { };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Entities/LocationType.cs b/MediaBrowser.Model/Entities/LocationType.cs
new file mode 100644
index 000000000..84de803aa
--- /dev/null
+++ b/MediaBrowser.Model/Entities/LocationType.cs
@@ -0,0 +1,26 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Enum LocationType
+ /// </summary>
+ public enum LocationType
+ {
+ /// <summary>
+ /// The file system
+ /// </summary>
+ FileSystem = 0,
+ /// <summary>
+ /// The remote
+ /// </summary>
+ Remote = 1,
+ /// <summary>
+ /// The virtual
+ /// </summary>
+ Virtual = 2,
+ /// <summary>
+ /// The offline
+ /// </summary>
+ Offline = 3
+ }
+}
diff --git a/MediaBrowser.Model/Entities/MBRegistrationRecord.cs b/MediaBrowser.Model/Entities/MBRegistrationRecord.cs
new file mode 100644
index 000000000..00176fb34
--- /dev/null
+++ b/MediaBrowser.Model/Entities/MBRegistrationRecord.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace MediaBrowser.Model.Entities
+{
+ public class MBRegistrationRecord
+ {
+ public DateTime ExpirationDate { get; set; }
+ public bool IsRegistered { get; set; }
+ public bool RegChecked { get; set; }
+ public bool RegError { get; set; }
+ public bool TrialVersion { get; set; }
+ public bool IsValid { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
new file mode 100644
index 000000000..fc208459d
--- /dev/null
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -0,0 +1,475 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.MediaInfo;
+using System.Globalization;
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Class MediaStream
+ /// </summary>
+ public class MediaStream
+ {
+ /// <summary>
+ /// Gets or sets the codec.
+ /// </summary>
+ /// <value>The codec.</value>
+ public string Codec { get; set; }
+
+ /// <summary>
+ /// Gets or sets the codec tag.
+ /// </summary>
+ /// <value>The codec tag.</value>
+ public string CodecTag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the language.
+ /// </summary>
+ /// <value>The language.</value>
+ public string Language { get; set; }
+
+ public string ColorTransfer { get; set; }
+ public string ColorPrimaries { get; set; }
+ public string ColorSpace { get; set; }
+
+ /// <summary>
+ /// Gets or sets the comment.
+ /// </summary>
+ /// <value>The comment.</value>
+ public string Comment { get; set; }
+
+ public string TimeBase { get; set; }
+ public string CodecTimeBase { get; set; }
+
+ public string Title { get; set; }
+
+ public string VideoRange
+ {
+ get
+ {
+ if (Type != MediaStreamType.Video)
+ {
+ return null;
+ }
+
+ var colorTransfer = ColorTransfer;
+
+ if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase))
+ {
+ return "HDR";
+ }
+
+ return "SDR";
+ }
+ }
+
+ public string DisplayTitle
+ {
+ get
+ {
+ if (Type == MediaStreamType.Audio)
+ {
+ //if (!string.IsNullOrEmpty(Title))
+ //{
+ // return AddLanguageIfNeeded(Title);
+ //}
+
+ List<string> attributes = new List<string>();
+
+ if (!string.IsNullOrEmpty(Language))
+ {
+ attributes.Add(StringHelper.FirstToUpper(Language));
+ }
+ if (!string.IsNullOrEmpty(Codec) && !StringHelper.EqualsIgnoreCase(Codec, "dca"))
+ {
+ attributes.Add(AudioCodec.GetFriendlyName(Codec));
+ }
+ else if (!string.IsNullOrEmpty(Profile) && !StringHelper.EqualsIgnoreCase(Profile, "lc"))
+ {
+ attributes.Add(Profile);
+ }
+
+ if (!string.IsNullOrEmpty(ChannelLayout))
+ {
+ attributes.Add(ChannelLayout);
+ }
+ else if (Channels.HasValue)
+ {
+ attributes.Add(Channels.Value.ToString(CultureInfo.InvariantCulture) + " ch");
+ }
+ if (IsDefault)
+ {
+ attributes.Add("Default");
+ }
+
+ return string.Join(" ", attributes.ToArray(attributes.Count));
+ }
+
+ if (Type == MediaStreamType.Video)
+ {
+ List<string> attributes = new List<string>();
+
+ var resolutionText = GetResolutionText();
+
+ if (!string.IsNullOrEmpty(resolutionText))
+ {
+ attributes.Add(resolutionText);
+ }
+
+ if (!string.IsNullOrEmpty(Codec))
+ {
+ attributes.Add(Codec.ToUpper());
+ }
+
+ return string.Join(" ", attributes.ToArray(attributes.Count));
+ }
+
+ if (Type == MediaStreamType.Subtitle)
+ {
+ //if (!string.IsNullOrEmpty(Title))
+ //{
+ // return AddLanguageIfNeeded(Title);
+ //}
+
+ List<string> attributes = new List<string>();
+
+ if (!string.IsNullOrEmpty(Language))
+ {
+ attributes.Add(StringHelper.FirstToUpper(Language));
+ }
+ else
+ {
+ attributes.Add("Und");
+ }
+
+ if (IsDefault)
+ {
+ attributes.Add("Default");
+ }
+
+ if (IsForced)
+ {
+ attributes.Add("Forced");
+ }
+
+ string name = string.Join(" ", attributes.ToArray(attributes.Count));
+
+ return name;
+ }
+
+ if (Type == MediaStreamType.Video)
+ {
+
+ }
+
+ return null;
+ }
+ }
+
+ private string GetResolutionText()
+ {
+ var i = this;
+
+ if (i.Width.HasValue && i.Height.HasValue)
+ {
+ var width = i.Width.Value;
+ var height = i.Height.Value;
+
+ if (width >= 3800 || height >= 2000)
+ {
+ return "4K";
+ }
+ if (width >= 2500)
+ {
+ if (i.IsInterlaced)
+ {
+ return "1440I";
+ }
+ return "1440P";
+ }
+ if (width >= 1900 || height >= 1000)
+ {
+ if (i.IsInterlaced)
+ {
+ return "1080I";
+ }
+ return "1080P";
+ }
+ if (width >= 1260 || height >= 700)
+ {
+ if (i.IsInterlaced)
+ {
+ return "720I";
+ }
+ return "720P";
+ }
+ if (width >= 700 || height >= 440)
+ {
+
+ if (i.IsInterlaced)
+ {
+ return "480I";
+ }
+ return "480P";
+ }
+
+ return "SD";
+ }
+ return null;
+ }
+
+ private string AddLanguageIfNeeded(string title)
+ {
+ if (!string.IsNullOrEmpty(Language) &&
+ !string.Equals(Language, "und", StringComparison.OrdinalIgnoreCase) &&
+ !IsLanguageInTitle(title, Language))
+ {
+ title = StringHelper.FirstToUpper(Language) + " " + title;
+ }
+
+ return title;
+ }
+
+ private bool IsLanguageInTitle(string title, string language)
+ {
+ if (title.IndexOf(Language, StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ public string NalLengthSize { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is interlaced.
+ /// </summary>
+ /// <value><c>true</c> if this instance is interlaced; otherwise, <c>false</c>.</value>
+ public bool IsInterlaced { get; set; }
+
+ public bool? IsAVC { get; set; }
+
+ /// <summary>
+ /// Gets or sets the channel layout.
+ /// </summary>
+ /// <value>The channel layout.</value>
+ public string ChannelLayout { get; set; }
+
+ /// <summary>
+ /// Gets or sets the bit rate.
+ /// </summary>
+ /// <value>The bit rate.</value>
+ public int? BitRate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the bit depth.
+ /// </summary>
+ /// <value>The bit depth.</value>
+ public int? BitDepth { get; set; }
+
+ /// <summary>
+ /// Gets or sets the reference frames.
+ /// </summary>
+ /// <value>The reference frames.</value>
+ public int? RefFrames { get; set; }
+
+ /// <summary>
+ /// Gets or sets the length of the packet.
+ /// </summary>
+ /// <value>The length of the packet.</value>
+ public int? PacketLength { get; set; }
+
+ /// <summary>
+ /// Gets or sets the channels.
+ /// </summary>
+ /// <value>The channels.</value>
+ public int? Channels { get; set; }
+
+ /// <summary>
+ /// Gets or sets the sample rate.
+ /// </summary>
+ /// <value>The sample rate.</value>
+ public int? SampleRate { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is default.
+ /// </summary>
+ /// <value><c>true</c> if this instance is default; otherwise, <c>false</c>.</value>
+ public bool IsDefault { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is forced.
+ /// </summary>
+ /// <value><c>true</c> if this instance is forced; otherwise, <c>false</c>.</value>
+ public bool IsForced { get; set; }
+
+ /// <summary>
+ /// Gets or sets the height.
+ /// </summary>
+ /// <value>The height.</value>
+ public int? Height { get; set; }
+
+ /// <summary>
+ /// Gets or sets the width.
+ /// </summary>
+ /// <value>The width.</value>
+ public int? Width { get; set; }
+
+ /// <summary>
+ /// Gets or sets the average frame rate.
+ /// </summary>
+ /// <value>The average frame rate.</value>
+ public float? AverageFrameRate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the real frame rate.
+ /// </summary>
+ /// <value>The real frame rate.</value>
+ public float? RealFrameRate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the profile.
+ /// </summary>
+ /// <value>The profile.</value>
+ public string Profile { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ public MediaStreamType Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the aspect ratio.
+ /// </summary>
+ /// <value>The aspect ratio.</value>
+ public string AspectRatio { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index.
+ /// </summary>
+ /// <value>The index.</value>
+ public int Index { get; set; }
+
+ /// <summary>
+ /// Gets or sets the score.
+ /// </summary>
+ /// <value>The score.</value>
+ public int? Score { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is external.
+ /// </summary>
+ /// <value><c>true</c> if this instance is external; otherwise, <c>false</c>.</value>
+ public bool IsExternal { get; set; }
+
+ /// <summary>
+ /// Gets or sets the method.
+ /// </summary>
+ /// <value>The method.</value>
+ public SubtitleDeliveryMethod? DeliveryMethod { get; set; }
+ /// <summary>
+ /// Gets or sets the delivery URL.
+ /// </summary>
+ /// <value>The delivery URL.</value>
+ public string DeliveryUrl { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is external URL.
+ /// </summary>
+ /// <value><c>null</c> if [is external URL] contains no value, <c>true</c> if [is external URL]; otherwise, <c>false</c>.</value>
+ public bool? IsExternalUrl { get; set; }
+
+ public bool IsTextSubtitleStream
+ {
+ get
+ {
+ if (Type != MediaStreamType.Subtitle) return false;
+
+ if (string.IsNullOrEmpty(Codec) && !IsExternal)
+ {
+ return false;
+ }
+
+ return IsTextFormat(Codec);
+ }
+ }
+
+ public static bool IsTextFormat(string format)
+ {
+ string codec = format ?? string.Empty;
+
+ // sub = external .sub file
+
+ return codec.IndexOf("pgs", StringComparison.OrdinalIgnoreCase) == -1 &&
+ codec.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) == -1 &&
+ codec.IndexOf("dvbsub", StringComparison.OrdinalIgnoreCase) == -1 &&
+ !StringHelper.EqualsIgnoreCase(codec, "sub") &&
+ !StringHelper.EqualsIgnoreCase(codec, "dvb_subtitle");
+ }
+
+ public bool SupportsSubtitleConversionTo(string toCodec)
+ {
+ if (!IsTextSubtitleStream)
+ {
+ return false;
+ }
+
+ var fromCodec = Codec;
+
+ // Can't convert from this
+ if (StringHelper.EqualsIgnoreCase(fromCodec, "ass"))
+ {
+ return false;
+ }
+ if (StringHelper.EqualsIgnoreCase(fromCodec, "ssa"))
+ {
+ return false;
+ }
+
+ // Can't convert to this
+ if (StringHelper.EqualsIgnoreCase(toCodec, "ass"))
+ {
+ return false;
+ }
+ if (StringHelper.EqualsIgnoreCase(toCodec, "ssa"))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [supports external stream].
+ /// </summary>
+ /// <value><c>true</c> if [supports external stream]; otherwise, <c>false</c>.</value>
+ public bool SupportsExternalStream { get; set; }
+
+ /// <summary>
+ /// Gets or sets the filename.
+ /// </summary>
+ /// <value>The filename.</value>
+ public string Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets the pixel format.
+ /// </summary>
+ /// <value>The pixel format.</value>
+ public string PixelFormat { get; set; }
+
+ /// <summary>
+ /// Gets or sets the level.
+ /// </summary>
+ /// <value>The level.</value>
+ public double? Level { get; set; }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is anamorphic.
+ /// </summary>
+ /// <value><c>true</c> if this instance is anamorphic; otherwise, <c>false</c>.</value>
+ public bool? IsAnamorphic { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Entities/MediaStreamType.cs b/MediaBrowser.Model/Entities/MediaStreamType.cs
new file mode 100644
index 000000000..084a411f9
--- /dev/null
+++ b/MediaBrowser.Model/Entities/MediaStreamType.cs
@@ -0,0 +1,25 @@
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Enum MediaStreamType
+ /// </summary>
+ public enum MediaStreamType
+ {
+ /// <summary>
+ /// The audio
+ /// </summary>
+ Audio,
+ /// <summary>
+ /// The video
+ /// </summary>
+ Video,
+ /// <summary>
+ /// The subtitle
+ /// </summary>
+ Subtitle,
+ /// <summary>
+ /// The embedded image
+ /// </summary>
+ EmbeddedImage
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Entities/MediaType.cs b/MediaBrowser.Model/Entities/MediaType.cs
new file mode 100644
index 000000000..0c9bde6fb
--- /dev/null
+++ b/MediaBrowser.Model/Entities/MediaType.cs
@@ -0,0 +1,30 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Class MediaType
+ /// </summary>
+ public class MediaType
+ {
+ /// <summary>
+ /// The video
+ /// </summary>
+ public const string Video = "Video";
+ /// <summary>
+ /// The audio
+ /// </summary>
+ public const string Audio = "Audio";
+ /// <summary>
+ /// The game
+ /// </summary>
+ public const string Game = "Game";
+ /// <summary>
+ /// The photo
+ /// </summary>
+ public const string Photo = "Photo";
+ /// <summary>
+ /// The book
+ /// </summary>
+ public const string Book = "Book";
+ }
+}
diff --git a/MediaBrowser.Model/Entities/MediaUrl.cs b/MediaBrowser.Model/Entities/MediaUrl.cs
new file mode 100644
index 000000000..2e17bba8a
--- /dev/null
+++ b/MediaBrowser.Model/Entities/MediaUrl.cs
@@ -0,0 +1,9 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ public class MediaUrl
+ {
+ public string Url { get; set; }
+ public string Name { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Entities/MetadataFields.cs b/MediaBrowser.Model/Entities/MetadataFields.cs
new file mode 100644
index 000000000..85f2da31e
--- /dev/null
+++ b/MediaBrowser.Model/Entities/MetadataFields.cs
@@ -0,0 +1,46 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Enum MetadataFields
+ /// </summary>
+ public enum MetadataFields
+ {
+ /// <summary>
+ /// The cast
+ /// </summary>
+ Cast,
+ /// <summary>
+ /// The genres
+ /// </summary>
+ Genres,
+ /// <summary>
+ /// The production locations
+ /// </summary>
+ ProductionLocations,
+ /// <summary>
+ /// The studios
+ /// </summary>
+ Studios,
+ /// <summary>
+ /// The tags
+ /// </summary>
+ Tags,
+ /// <summary>
+ /// The name
+ /// </summary>
+ Name,
+ /// <summary>
+ /// The overview
+ /// </summary>
+ Overview,
+ /// <summary>
+ /// The runtime
+ /// </summary>
+ Runtime,
+ /// <summary>
+ /// The official rating
+ /// </summary>
+ OfficialRating
+ }
+}
diff --git a/MediaBrowser.Model/Entities/MetadataProviders.cs b/MediaBrowser.Model/Entities/MetadataProviders.cs
new file mode 100644
index 000000000..efd4339d5
--- /dev/null
+++ b/MediaBrowser.Model/Entities/MetadataProviders.cs
@@ -0,0 +1,41 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Enum MetadataProviders
+ /// </summary>
+ public enum MetadataProviders
+ {
+ Gamesdb = 1,
+ /// <summary>
+ /// The imdb
+ /// </summary>
+ Imdb = 2,
+ /// <summary>
+ /// The TMDB
+ /// </summary>
+ Tmdb = 3,
+ /// <summary>
+ /// The TVDB
+ /// </summary>
+ Tvdb = 4,
+ /// <summary>
+ /// The tvcom
+ /// </summary>
+ Tvcom = 5,
+ /// <summary>
+ /// Tmdb Collection Id
+ /// </summary>
+ TmdbCollection = 7,
+ MusicBrainzAlbum = 8,
+ MusicBrainzAlbumArtist = 9,
+ MusicBrainzArtist = 10,
+ MusicBrainzReleaseGroup = 11,
+ Zap2It = 12,
+ TvRage = 15,
+ AudioDbArtist = 16,
+ AudioDbAlbum = 17,
+ MusicBrainzTrack = 18,
+ TvMaze = 19
+ }
+}
diff --git a/MediaBrowser.Model/Entities/PackageReviewInfo.cs b/MediaBrowser.Model/Entities/PackageReviewInfo.cs
new file mode 100644
index 000000000..52500a41e
--- /dev/null
+++ b/MediaBrowser.Model/Entities/PackageReviewInfo.cs
@@ -0,0 +1,38 @@
+using System;
+
+namespace MediaBrowser.Model.Entities
+{
+ public class PackageReviewInfo
+ {
+ /// <summary>
+ /// The package id (database key) for this review
+ /// </summary>
+ public int id { get; set; }
+
+ /// <summary>
+ /// The rating value
+ /// </summary>
+ public int rating { get; set; }
+
+ /// <summary>
+ /// Whether or not this review recommends this item
+ /// </summary>
+ public bool recommend { get; set; }
+
+ /// <summary>
+ /// A short description of the review
+ /// </summary>
+ public string title { get; set; }
+
+ /// <summary>
+ /// A full review
+ /// </summary>
+ public string review { get; set; }
+
+ /// <summary>
+ /// Time of review
+ /// </summary>
+ public DateTime timestamp { get; set; }
+
+ }
+}
diff --git a/MediaBrowser.Model/Entities/ParentalRating.cs b/MediaBrowser.Model/Entities/ParentalRating.cs
new file mode 100644
index 000000000..302c1e299
--- /dev/null
+++ b/MediaBrowser.Model/Entities/ParentalRating.cs
@@ -0,0 +1,32 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Class ParentalRating
+ /// </summary>
+ public class ParentalRating
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the value.
+ /// </summary>
+ /// <value>The value.</value>
+ public int Value { get; set; }
+
+ public ParentalRating()
+ {
+
+ }
+
+ public ParentalRating(string name, int value)
+ {
+ Name = name;
+ Value = value;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Entities/PersonType.cs b/MediaBrowser.Model/Entities/PersonType.cs
new file mode 100644
index 000000000..bc274972d
--- /dev/null
+++ b/MediaBrowser.Model/Entities/PersonType.cs
@@ -0,0 +1,42 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Struct PersonType
+ /// </summary>
+ public class PersonType
+ {
+ /// <summary>
+ /// The actor
+ /// </summary>
+ public const string Actor = "Actor";
+ /// <summary>
+ /// The director
+ /// </summary>
+ public const string Director = "Director";
+ /// <summary>
+ /// The composer
+ /// </summary>
+ public const string Composer = "Composer";
+ /// <summary>
+ /// The writer
+ /// </summary>
+ public const string Writer = "Writer";
+ /// <summary>
+ /// The guest star
+ /// </summary>
+ public const string GuestStar = "GuestStar";
+ /// <summary>
+ /// The producer
+ /// </summary>
+ public const string Producer = "Producer";
+ /// <summary>
+ /// The conductor
+ /// </summary>
+ public const string Conductor = "Conductor";
+ /// <summary>
+ /// The lyricist
+ /// </summary>
+ public const string Lyricist = "Lyricist";
+ }
+}
diff --git a/MediaBrowser.Model/Entities/PluginSecurityInfo.cs b/MediaBrowser.Model/Entities/PluginSecurityInfo.cs
new file mode 100644
index 000000000..5cab55013
--- /dev/null
+++ b/MediaBrowser.Model/Entities/PluginSecurityInfo.cs
@@ -0,0 +1,21 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Class PluginSecurityInfo
+ /// </summary>
+ public class PluginSecurityInfo
+ {
+ /// <summary>
+ /// Gets or sets the supporter key.
+ /// </summary>
+ /// <value>The supporter key.</value>
+ public string SupporterKey { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is MB supporter.
+ /// </summary>
+ /// <value><c>true</c> if this instance is MB supporter; otherwise, <c>false</c>.</value>
+ public bool IsMBSupporter { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
new file mode 100644
index 000000000..e10232baa
--- /dev/null
+++ b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Class ProviderIdsExtensions
+ /// </summary>
+ public static class ProviderIdsExtensions
+ {
+ /// <summary>
+ /// Determines whether [has provider identifier] [the specified instance].
+ /// </summary>
+ /// <param name="instance">The instance.</param>
+ /// <param name="provider">The provider.</param>
+ /// <returns><c>true</c> if [has provider identifier] [the specified instance]; otherwise, <c>false</c>.</returns>
+ public static bool HasProviderId(this IHasProviderIds instance, MetadataProviders provider)
+ {
+ return !string.IsNullOrEmpty(instance.GetProviderId(provider.ToString()));
+ }
+
+ /// <summary>
+ /// Gets a provider id
+ /// </summary>
+ /// <param name="instance">The instance.</param>
+ /// <param name="provider">The provider.</param>
+ /// <returns>System.String.</returns>
+ public static string GetProviderId(this IHasProviderIds instance, MetadataProviders provider)
+ {
+ return instance.GetProviderId(provider.ToString());
+ }
+
+ /// <summary>
+ /// Gets a provider id
+ /// </summary>
+ /// <param name="instance">The instance.</param>
+ /// <param name="name">The name.</param>
+ /// <returns>System.String.</returns>
+ public static string GetProviderId(this IHasProviderIds instance, string name)
+ {
+ if (instance == null)
+ {
+ throw new ArgumentNullException("instance");
+ }
+
+ if (instance.ProviderIds == null)
+ {
+ return null;
+ }
+
+ string id;
+ instance.ProviderIds.TryGetValue(name, out id);
+ return id;
+ }
+
+ /// <summary>
+ /// Sets a provider id
+ /// </summary>
+ /// <param name="instance">The instance.</param>
+ /// <param name="name">The name.</param>
+ /// <param name="value">The value.</param>
+ public static void SetProviderId(this IHasProviderIds instance, string name, string value)
+ {
+ if (instance == null)
+ {
+ throw new ArgumentNullException("instance");
+ }
+
+ // If it's null remove the key from the dictionary
+ if (string.IsNullOrEmpty(value))
+ {
+ if (instance.ProviderIds != null)
+ {
+ if (instance.ProviderIds.ContainsKey(name))
+ {
+ instance.ProviderIds.Remove(name);
+ }
+ }
+ }
+ else
+ {
+ // Ensure it exists
+ if (instance.ProviderIds == null)
+ {
+ instance.ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+
+ instance.ProviderIds[name] = value;
+ }
+ }
+
+ /// <summary>
+ /// Sets a provider id
+ /// </summary>
+ /// <param name="instance">The instance.</param>
+ /// <param name="provider">The provider.</param>
+ /// <param name="value">The value.</param>
+ public static void SetProviderId(this IHasProviderIds instance, MetadataProviders provider, string value)
+ {
+ instance.SetProviderId(provider.ToString(), value);
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Entities/ScrollDirection.cs b/MediaBrowser.Model/Entities/ScrollDirection.cs
new file mode 100644
index 000000000..ed2210300
--- /dev/null
+++ b/MediaBrowser.Model/Entities/ScrollDirection.cs
@@ -0,0 +1,17 @@
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Enum ScrollDirection
+ /// </summary>
+ public enum ScrollDirection
+ {
+ /// <summary>
+ /// The horizontal
+ /// </summary>
+ Horizontal,
+ /// <summary>
+ /// The vertical
+ /// </summary>
+ Vertical
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Entities/SeriesStatus.cs b/MediaBrowser.Model/Entities/SeriesStatus.cs
new file mode 100644
index 000000000..d04a2856c
--- /dev/null
+++ b/MediaBrowser.Model/Entities/SeriesStatus.cs
@@ -0,0 +1,18 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Enum SeriesStatus
+ /// </summary>
+ public enum SeriesStatus
+ {
+ /// <summary>
+ /// The continuing
+ /// </summary>
+ Continuing,
+ /// <summary>
+ /// The ended
+ /// </summary>
+ Ended
+ }
+}
diff --git a/MediaBrowser.Model/Entities/SortOrder.cs b/MediaBrowser.Model/Entities/SortOrder.cs
new file mode 100644
index 000000000..5130449ba
--- /dev/null
+++ b/MediaBrowser.Model/Entities/SortOrder.cs
@@ -0,0 +1,17 @@
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Enum SortOrder
+ /// </summary>
+ public enum SortOrder
+ {
+ /// <summary>
+ /// The ascending
+ /// </summary>
+ Ascending,
+ /// <summary>
+ /// The descending
+ /// </summary>
+ Descending
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Entities/TrailerType.cs b/MediaBrowser.Model/Entities/TrailerType.cs
new file mode 100644
index 000000000..085f461cf
--- /dev/null
+++ b/MediaBrowser.Model/Entities/TrailerType.cs
@@ -0,0 +1,11 @@
+namespace MediaBrowser.Model.Entities
+{
+ public enum TrailerType
+ {
+ ComingSoonToTheaters = 1,
+ ComingSoonToDvd = 2,
+ ComingSoonToStreaming = 3,
+ Archive = 4,
+ LocalTrailer = 5
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Entities/UserDataSaveReason.cs b/MediaBrowser.Model/Entities/UserDataSaveReason.cs
new file mode 100644
index 000000000..d9691f395
--- /dev/null
+++ b/MediaBrowser.Model/Entities/UserDataSaveReason.cs
@@ -0,0 +1,34 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Enum UserDataSaveReason
+ /// </summary>
+ public enum UserDataSaveReason
+ {
+ /// <summary>
+ /// The playback start
+ /// </summary>
+ PlaybackStart = 1,
+ /// <summary>
+ /// The playback progress
+ /// </summary>
+ PlaybackProgress = 2,
+ /// <summary>
+ /// The playback finished
+ /// </summary>
+ PlaybackFinished = 3,
+ /// <summary>
+ /// The toggle played
+ /// </summary>
+ TogglePlayed = 4,
+ /// <summary>
+ /// The update user rating
+ /// </summary>
+ UpdateUserRating = 5,
+ /// <summary>
+ /// The import
+ /// </summary>
+ Import = 6
+ }
+}
diff --git a/MediaBrowser.Model/Entities/Video3DFormat.cs b/MediaBrowser.Model/Entities/Video3DFormat.cs
new file mode 100644
index 000000000..722df4281
--- /dev/null
+++ b/MediaBrowser.Model/Entities/Video3DFormat.cs
@@ -0,0 +1,12 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ public enum Video3DFormat
+ {
+ HalfSideBySide,
+ FullSideBySide,
+ FullTopAndBottom,
+ HalfTopAndBottom,
+ MVC
+ }
+}
diff --git a/MediaBrowser.Model/Entities/VideoType.cs b/MediaBrowser.Model/Entities/VideoType.cs
new file mode 100644
index 000000000..05c2fa32c
--- /dev/null
+++ b/MediaBrowser.Model/Entities/VideoType.cs
@@ -0,0 +1,26 @@
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Enum VideoType
+ /// </summary>
+ public enum VideoType
+ {
+ /// <summary>
+ /// The video file
+ /// </summary>
+ VideoFile,
+ /// <summary>
+ /// The iso
+ /// </summary>
+ Iso,
+ /// <summary>
+ /// The DVD
+ /// </summary>
+ Dvd,
+ /// <summary>
+ /// The blu ray
+ /// </summary>
+ BluRay
+ }
+}
diff --git a/MediaBrowser.Model/Entities/VirtualFolderInfo.cs b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs
new file mode 100644
index 000000000..68f713295
--- /dev/null
+++ b/MediaBrowser.Model/Entities/VirtualFolderInfo.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using MediaBrowser.Model.Configuration;
+using System;
+
+namespace MediaBrowser.Model.Entities
+{
+ /// <summary>
+ /// Used to hold information about a user's list of configured virtual folders
+ /// </summary>
+ public class VirtualFolderInfo
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the locations.
+ /// </summary>
+ /// <value>The locations.</value>
+ public string[] Locations { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the collection.
+ /// </summary>
+ /// <value>The type of the collection.</value>
+ public string CollectionType { get; set; }
+
+ public LibraryOptions LibraryOptions { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="VirtualFolderInfo"/> class.
+ /// </summary>
+ public VirtualFolderInfo()
+ {
+ Locations = new string[] {};
+ }
+
+ /// <summary>
+ /// Gets or sets the item identifier.
+ /// </summary>
+ /// <value>The item identifier.</value>
+ public string ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the primary image item identifier.
+ /// </summary>
+ /// <value>The primary image item identifier.</value>
+ public string PrimaryImageItemId { get; set; }
+
+ public double? RefreshProgress { get; set; }
+ public string RefreshStatus { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Events/GenericEventArgs.cs b/MediaBrowser.Model/Events/GenericEventArgs.cs
new file mode 100644
index 000000000..3c558577a
--- /dev/null
+++ b/MediaBrowser.Model/Events/GenericEventArgs.cs
@@ -0,0 +1,33 @@
+using System;
+
+namespace MediaBrowser.Model.Events
+{
+ /// <summary>
+ /// Provides a generic EventArgs subclass that can hold any kind of object
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public class GenericEventArgs<T> : EventArgs
+ {
+ /// <summary>
+ /// Gets or sets the argument.
+ /// </summary>
+ /// <value>The argument.</value>
+ public T Argument { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GenericEventArgs{T}"/> class.
+ /// </summary>
+ /// <param name="arg">The argument.</param>
+ public GenericEventArgs(T arg)
+ {
+ Argument = arg;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GenericEventArgs{T}"/> class.
+ /// </summary>
+ public GenericEventArgs()
+ {
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Extensions/LinqExtensions.cs b/MediaBrowser.Model/Extensions/LinqExtensions.cs
new file mode 100644
index 000000000..09ace42e8
--- /dev/null
+++ b/MediaBrowser.Model/Extensions/LinqExtensions.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Extensions
+{
+ // MoreLINQ - Extensions to LINQ to Objects
+ // Copyright (c) 2008 Jonathan Skeet. All rights reserved.
+ //
+ // Licensed under the Apache License, Version 2.0 (the "License");
+ // you may not use this file except in compliance with the License.
+ // You may obtain a copy of the License at
+ //
+ // http://www.apache.org/licenses/LICENSE-2.0
+ //
+ // Unless required by applicable law or agreed to in writing, software
+ // distributed under the License is distributed on an "AS IS" BASIS,
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ // See the License for the specific language governing permissions and
+ // limitations under the License.
+
+ public static class LinqExtensions
+ {
+ /// <summary>
+ /// Returns all distinct elements of the given source, where "distinctness"
+ /// is determined via a projection and the default equality comparer for the projected type.
+ /// </summary>
+ /// <remarks>
+ /// This operator uses deferred execution and streams the results, although
+ /// a set of already-seen keys is retained. If a key is seen multiple times,
+ /// only the first element with that key is returned.
+ /// </remarks>
+ /// <typeparam name="TSource">Type of the source sequence</typeparam>
+ /// <typeparam name="TKey">Type of the projected element</typeparam>
+ /// <param name="source">Source sequence</param>
+ /// <param name="keySelector">Projection for determining "distinctness"</param>
+ /// <returns>A sequence consisting of distinct elements from the source sequence,
+ /// comparing them by the specified key projection.</returns>
+
+ public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source,
+ Func<TSource, TKey> keySelector)
+ {
+ return source.DistinctBy(keySelector, null);
+ }
+
+ public static TSource[] ToArray<TSource>(this IEnumerable<TSource> source, int count)
+ {
+ if (source == null) throw new ArgumentNullException("source");
+ if (count < 0) throw new ArgumentOutOfRangeException("count");
+ var array = new TSource[count];
+ int i = 0;
+ foreach (var item in source)
+ {
+ array[i++] = item;
+ }
+ return array;
+ }
+
+ /// <summary>
+ /// Returns all distinct elements of the given source, where "distinctness"
+ /// is determined via a projection and the specified comparer for the projected type.
+ /// </summary>
+ /// <remarks>
+ /// This operator uses deferred execution and streams the results, although
+ /// a set of already-seen keys is retained. If a key is seen multiple times,
+ /// only the first element with that key is returned.
+ /// </remarks>
+ /// <typeparam name="TSource">Type of the source sequence</typeparam>
+ /// <typeparam name="TKey">Type of the projected element</typeparam>
+ /// <param name="source">Source sequence</param>
+ /// <param name="keySelector">Projection for determining "distinctness"</param>
+ /// <param name="comparer">The equality comparer to use to determine whether or not keys are equal.
+ /// If null, the default equality comparer for <c>TSource</c> is used.</param>
+ /// <returns>A sequence consisting of distinct elements from the source sequence,
+ /// comparing them by the specified key projection.</returns>
+
+ public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source,
+ Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
+ {
+ if (source == null) throw new ArgumentNullException("source");
+ if (keySelector == null) throw new ArgumentNullException("keySelector");
+ return DistinctByImpl(source, keySelector, comparer);
+ }
+
+ private static IEnumerable<TSource> DistinctByImpl<TSource, TKey>(IEnumerable<TSource> source,
+ Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
+ {
+ var knownKeys = new HashSet<TKey>(comparer);
+ foreach (var element in source)
+ {
+ if (knownKeys.Add(keySelector(element)))
+ {
+ yield return element;
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Extensions/ListHelper.cs b/MediaBrowser.Model/Extensions/ListHelper.cs
new file mode 100644
index 000000000..243ae3105
--- /dev/null
+++ b/MediaBrowser.Model/Extensions/ListHelper.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace MediaBrowser.Model.Extensions
+{
+ public static class ListHelper
+ {
+ public static bool ContainsIgnoreCase(string[] list, string value)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ foreach (var item in list)
+ {
+ if (string.Equals(item, value, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Extensions/StringHelper.cs b/MediaBrowser.Model/Extensions/StringHelper.cs
new file mode 100644
index 000000000..fa79d09db
--- /dev/null
+++ b/MediaBrowser.Model/Extensions/StringHelper.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Text;
+
+namespace MediaBrowser.Model.Extensions
+{
+ /// <summary>
+ /// Isolating these helpers allow this entire project to be easily converted to Java
+ /// </summary>
+ public static class StringHelper
+ {
+ /// <summary>
+ /// Equalses the ignore case.
+ /// </summary>
+ /// <param name="str1">The STR1.</param>
+ /// <param name="str2">The STR2.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+ public static bool EqualsIgnoreCase(string str1, string str2)
+ {
+ return string.Equals(str1, str2, StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Replaces the specified STR.
+ /// </summary>
+ /// <param name="str">The STR.</param>
+ /// <param name="oldValue">The old value.</param>
+ /// <param name="newValue">The new value.</param>
+ /// <param name="comparison">The comparison.</param>
+ /// <returns>System.String.</returns>
+ public static string Replace(this string str, string oldValue, string newValue, StringComparison comparison)
+ {
+ var sb = new StringBuilder();
+
+ var previousIndex = 0;
+ var index = str.IndexOf(oldValue, comparison);
+
+ while (index != -1)
+ {
+ sb.Append(str.Substring(previousIndex, index - previousIndex));
+ sb.Append(newValue);
+ index += oldValue.Length;
+
+ previousIndex = index;
+ index = str.IndexOf(oldValue, index, comparison);
+ }
+
+ sb.Append(str.Substring(previousIndex));
+
+ return sb.ToString();
+ }
+
+ public static string FirstToUpper(this string str)
+ {
+ return string.IsNullOrEmpty(str) ? "" : str.Substring(0, 1).ToUpper() + str.Substring(1);
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Globalization/CountryInfo.cs b/MediaBrowser.Model/Globalization/CountryInfo.cs
new file mode 100644
index 000000000..16aea8436
--- /dev/null
+++ b/MediaBrowser.Model/Globalization/CountryInfo.cs
@@ -0,0 +1,33 @@
+
+namespace MediaBrowser.Model.Globalization
+{
+ /// <summary>
+ /// Class CountryInfo
+ /// </summary>
+ public class CountryInfo
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the display name.
+ /// </summary>
+ /// <value>The display name.</value>
+ public string DisplayName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the two letter ISO region.
+ /// </summary>
+ /// <value>The name of the two letter ISO region.</value>
+ public string TwoLetterISORegionName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the three letter ISO region.
+ /// </summary>
+ /// <value>The name of the three letter ISO region.</value>
+ public string ThreeLetterISORegionName { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Globalization/CultureDto.cs b/MediaBrowser.Model/Globalization/CultureDto.cs
new file mode 100644
index 000000000..6d79aaebb
--- /dev/null
+++ b/MediaBrowser.Model/Globalization/CultureDto.cs
@@ -0,0 +1,52 @@
+using global::System;
+
+namespace MediaBrowser.Model.Globalization
+{
+ /// <summary>
+ /// Class CultureDto
+ /// </summary>
+ public class CultureDto
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the display name.
+ /// </summary>
+ /// <value>The display name.</value>
+ public string DisplayName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the two letter ISO language.
+ /// </summary>
+ /// <value>The name of the two letter ISO language.</value>
+ public string TwoLetterISOLanguageName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the three letter ISO language.
+ /// </summary>
+ /// <value>The name of the three letter ISO language.</value>
+ public string ThreeLetterISOLanguageName
+ {
+ get
+ {
+ var vals = ThreeLetterISOLanguageNames;
+ if (vals.Length > 0)
+ {
+ return vals[0];
+ }
+ return null;
+ }
+ }
+
+ public string[] ThreeLetterISOLanguageNames { get; set; }
+
+ public CultureDto()
+ {
+ ThreeLetterISOLanguageNames = new string[] {};
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
new file mode 100644
index 000000000..9c7a937f3
--- /dev/null
+++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs
@@ -0,0 +1,63 @@
+using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
+using System.Globalization;
+
+namespace MediaBrowser.Model.Globalization
+{
+ /// <summary>
+ /// Interface ILocalizationManager
+ /// </summary>
+ public interface ILocalizationManager
+ {
+ /// <summary>
+ /// Gets the cultures.
+ /// </summary>
+ /// <returns>IEnumerable{CultureDto}.</returns>
+ CultureDto[] GetCultures();
+ /// <summary>
+ /// Gets the countries.
+ /// </summary>
+ /// <returns>IEnumerable{CountryInfo}.</returns>
+ CountryInfo[] GetCountries();
+ /// <summary>
+ /// Gets the parental ratings.
+ /// </summary>
+ /// <returns>IEnumerable{ParentalRating}.</returns>
+ ParentalRating[] GetParentalRatings();
+ /// <summary>
+ /// Gets the rating level.
+ /// </summary>
+ /// <param name="rating">The rating.</param>
+ /// <returns>System.Int32.</returns>
+ int? GetRatingLevel(string rating);
+
+ /// <summary>
+ /// Gets the localized string.
+ /// </summary>
+ /// <param name="phrase">The phrase.</param>
+ /// <param name="culture">The culture.</param>
+ /// <returns>System.String.</returns>
+ string GetLocalizedString(string phrase, string culture);
+
+ /// <summary>
+ /// Gets the localized string.
+ /// </summary>
+ /// <param name="phrase">The phrase.</param>
+ /// <returns>System.String.</returns>
+ string GetLocalizedString(string phrase);
+
+ /// <summary>
+ /// Gets the localization options.
+ /// </summary>
+ /// <returns>IEnumerable{LocalizatonOption}.</returns>
+ LocalizatonOption[] GetLocalizationOptions();
+
+ string RemoveDiacritics(string text);
+
+ string NormalizeFormKD(string text);
+
+ bool HasUnicodeCategory(string value, UnicodeCategory category);
+
+ CultureDto FindLanguageInfo(string language);
+ }
+}
diff --git a/MediaBrowser.Model/Globalization/LocalizatonOption.cs b/MediaBrowser.Model/Globalization/LocalizatonOption.cs
new file mode 100644
index 000000000..61749cbc3
--- /dev/null
+++ b/MediaBrowser.Model/Globalization/LocalizatonOption.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Model.Globalization
+{
+ public class LocalizatonOption
+ {
+ public string Name { get; set; }
+ public string Value { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/IO/FileSystemEntryInfo.cs b/MediaBrowser.Model/IO/FileSystemEntryInfo.cs
new file mode 100644
index 000000000..f17e2e5c3
--- /dev/null
+++ b/MediaBrowser.Model/IO/FileSystemEntryInfo.cs
@@ -0,0 +1,27 @@
+
+namespace MediaBrowser.Model.IO
+{
+ /// <summary>
+ /// Class FileSystemEntryInfo
+ /// </summary>
+ public class FileSystemEntryInfo
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <value>The path.</value>
+ public string Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ public FileSystemEntryType Type { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/IO/FileSystemEntryType.cs b/MediaBrowser.Model/IO/FileSystemEntryType.cs
new file mode 100644
index 000000000..e7c67c606
--- /dev/null
+++ b/MediaBrowser.Model/IO/FileSystemEntryType.cs
@@ -0,0 +1,25 @@
+namespace MediaBrowser.Model.IO
+{
+ /// <summary>
+ /// Enum FileSystemEntryType
+ /// </summary>
+ public enum FileSystemEntryType
+ {
+ /// <summary>
+ /// The file
+ /// </summary>
+ File,
+ /// <summary>
+ /// The directory
+ /// </summary>
+ Directory,
+ /// <summary>
+ /// The network computer
+ /// </summary>
+ NetworkComputer,
+ /// <summary>
+ /// The network share
+ /// </summary>
+ NetworkShare
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/IO/FileSystemMetadata.cs b/MediaBrowser.Model/IO/FileSystemMetadata.cs
new file mode 100644
index 000000000..665bc255c
--- /dev/null
+++ b/MediaBrowser.Model/IO/FileSystemMetadata.cs
@@ -0,0 +1,54 @@
+using System;
+
+namespace MediaBrowser.Model.IO
+{
+ public class FileSystemMetadata
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="FileSystemMetadata"/> is exists.
+ /// </summary>
+ /// <value><c>true</c> if exists; otherwise, <c>false</c>.</value>
+ public bool Exists { get; set; }
+ /// <summary>
+ /// Gets or sets the full name.
+ /// </summary>
+ /// <value>The full name.</value>
+ public string FullName { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the extension.
+ /// </summary>
+ /// <value>The extension.</value>
+ public string Extension { get; set; }
+ /// <summary>
+ /// Gets or sets the length.
+ /// </summary>
+ /// <value>The length.</value>
+ public long Length { get; set; }
+ /// <summary>
+ /// Gets or sets the name of the directory.
+ /// </summary>
+ /// <value>The name of the directory.</value>
+ public string DirectoryName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last write time UTC.
+ /// </summary>
+ /// <value>The last write time UTC.</value>
+ public DateTime LastWriteTimeUtc { get; set; }
+ /// <summary>
+ /// Gets or sets the creation time UTC.
+ /// </summary>
+ /// <value>The creation time UTC.</value>
+ public DateTime CreationTimeUtc { get; set; }
+ /// <summary>
+ /// Gets a value indicating whether this instance is directory.
+ /// </summary>
+ /// <value><c>true</c> if this instance is directory; otherwise, <c>false</c>.</value>
+ public bool IsDirectory { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs
new file mode 100644
index 000000000..3200affd6
--- /dev/null
+++ b/MediaBrowser.Model/IO/IFileSystem.cs
@@ -0,0 +1,454 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace MediaBrowser.Model.IO
+{
+ /// <summary>
+ /// Interface IFileSystem
+ /// </summary>
+ public interface IFileSystem
+ {
+ void AddShortcutHandler(IShortcutHandler handler);
+
+ /// <summary>
+ /// Determines whether the specified filename is shortcut.
+ /// </summary>
+ /// <param name="filename">The filename.</param>
+ /// <returns><c>true</c> if the specified filename is shortcut; otherwise, <c>false</c>.</returns>
+ bool IsShortcut(string filename);
+
+ /// <summary>
+ /// Resolves the shortcut.
+ /// </summary>
+ /// <param name="filename">The filename.</param>
+ /// <returns>System.String.</returns>
+ string ResolveShortcut(string filename);
+
+ /// <summary>
+ /// Creates the shortcut.
+ /// </summary>
+ /// <param name="shortcutPath">The shortcut path.</param>
+ /// <param name="target">The target.</param>
+ void CreateShortcut(string shortcutPath, string target);
+
+ string MakeAbsolutePath(string folderPath, string filePath);
+
+ /// <summary>
+ /// Returns a <see cref="FileSystemMetadata"/> object for the specified file or directory path.
+ /// </summary>
+ /// <param name="path">A path to a file or directory.</param>
+ /// <returns>A <see cref="FileSystemMetadata"/> object.</returns>
+ /// <remarks>If the specified path points to a directory, the returned <see cref="FileSystemMetadata"/> object's
+ /// <see cref="FileSystemMetadata.IsDirectory"/> property will be set to true and all other properties will reflect the properties of the directory.</remarks>
+ FileSystemMetadata GetFileSystemInfo(string path);
+
+ /// <summary>
+ /// Returns a <see cref="FileSystemMetadata"/> object for the specified file path.
+ /// </summary>
+ /// <param name="path">A path to a file.</param>
+ /// <returns>A <see cref="FileSystemMetadata"/> object.</returns>
+ /// <remarks><para>If the specified path points to a directory, the returned <see cref="FileSystemMetadata"/> object's
+ /// <see cref="FileSystemMetadata.IsDirectory"/> property and the <see cref="FileSystemMetadata.Exists"/> property will both be set to false.</para>
+ /// <para>For automatic handling of files <b>and</b> directories, use <see cref="GetFileSystemInfo"/>.</para></remarks>
+ FileSystemMetadata GetFileInfo(string path);
+
+ /// <summary>
+ /// Returns a <see cref="FileSystemMetadata"/> object for the specified directory path.
+ /// </summary>
+ /// <param name="path">A path to a directory.</param>
+ /// <returns>A <see cref="FileSystemMetadata"/> object.</returns>
+ /// <remarks><para>If the specified path points to a file, the returned <see cref="FileSystemMetadata"/> object's
+ /// <see cref="FileSystemMetadata.IsDirectory"/> property will be set to true and the <see cref="FileSystemMetadata.Exists"/> property will be set to false.</para>
+ /// <para>For automatic handling of files <b>and</b> directories, use <see cref="GetFileSystemInfo"/>.</para></remarks>
+ FileSystemMetadata GetDirectoryInfo(string path);
+
+ /// <summary>
+ /// Gets the valid filename.
+ /// </summary>
+ /// <param name="filename">The filename.</param>
+ /// <returns>System.String.</returns>
+ string GetValidFilename(string filename);
+
+ /// <summary>
+ /// Gets the creation time UTC.
+ /// </summary>
+ /// <param name="info">The information.</param>
+ /// <returns>DateTime.</returns>
+ DateTime GetCreationTimeUtc(FileSystemMetadata info);
+
+ /// <summary>
+ /// Gets the creation time UTC.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>DateTime.</returns>
+ DateTime GetCreationTimeUtc(string path);
+
+ /// <summary>
+ /// Gets the last write time UTC.
+ /// </summary>
+ /// <param name="info">The information.</param>
+ /// <returns>DateTime.</returns>
+ DateTime GetLastWriteTimeUtc(FileSystemMetadata info);
+
+ /// <summary>
+ /// Gets the last write time UTC.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>DateTime.</returns>
+ DateTime GetLastWriteTimeUtc(string path);
+
+ /// <summary>
+ /// Gets the file stream.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="mode">The mode.</param>
+ /// <param name="access">The access.</param>
+ /// <param name="share">The share.</param>
+ /// <param name="isAsync">if set to <c>true</c> [is asynchronous].</param>
+ /// <returns>FileStream.</returns>
+ Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false);
+
+ Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, FileOpenOptions fileOpenOptions);
+
+ /// <summary>
+ /// Opens the read.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>Stream.</returns>
+ Stream OpenRead(String path);
+
+ string DefaultDirectory { get; }
+
+ /// <summary>
+ /// Swaps the files.
+ /// </summary>
+ /// <param name="file1">The file1.</param>
+ /// <param name="file2">The file2.</param>
+ void SwapFiles(string file1, string file2);
+
+ bool AreEqual(string path1, string path2);
+
+ /// <summary>
+ /// Determines whether [contains sub path] [the specified parent path].
+ /// </summary>
+ /// <param name="parentPath">The parent path.</param>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if [contains sub path] [the specified parent path]; otherwise, <c>false</c>.</returns>
+ bool ContainsSubPath(string parentPath, string path);
+
+ /// <summary>
+ /// Determines whether [is root path] [the specified path].
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if [is root path] [the specified path]; otherwise, <c>false</c>.</returns>
+ bool IsRootPath(string path);
+
+ /// <summary>
+ /// Normalizes the path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>System.String.</returns>
+ string NormalizePath(string path);
+
+ string GetDirectoryName(string path);
+
+ /// <summary>
+ /// Gets the file name without extension.
+ /// </summary>
+ /// <param name="info">The information.</param>
+ /// <returns>System.String.</returns>
+ string GetFileNameWithoutExtension(FileSystemMetadata info);
+
+ /// <summary>
+ /// Gets the file name without extension.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>System.String.</returns>
+ string GetFileNameWithoutExtension(string path);
+
+ /// <summary>
+ /// Determines whether [is path file] [the specified path].
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if [is path file] [the specified path]; otherwise, <c>false</c>.</returns>
+ bool IsPathFile(string path);
+
+ /// <summary>
+ /// Deletes the file.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ void DeleteFile(string path);
+
+ /// <summary>
+ /// Deletes the directory.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+ void DeleteDirectory(string path, bool recursive);
+
+ /// <summary>
+ /// Gets the directories.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+ /// <returns>IEnumerable&lt;DirectoryInfo&gt;.</returns>
+ IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false);
+
+ /// <summary>
+ /// Gets the files.
+ /// </summary>
+ IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false);
+
+ IEnumerable<FileSystemMetadata> GetFiles(string path, string [] extensions, bool enableCaseSensitiveExtensions, bool recursive);
+
+ /// <summary>
+ /// Gets the file system entries.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+ /// <returns>IEnumerable&lt;FileSystemMetadata&gt;.</returns>
+ IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false);
+
+ /// <summary>
+ /// Creates the directory.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ void CreateDirectory(string path);
+
+ /// <summary>
+ /// Copies the file.
+ /// </summary>
+ /// <param name="source">The source.</param>
+ /// <param name="target">The target.</param>
+ /// <param name="overwrite">if set to <c>true</c> [overwrite].</param>
+ void CopyFile(string source, string target, bool overwrite);
+
+ /// <summary>
+ /// Moves the file.
+ /// </summary>
+ /// <param name="source">The source.</param>
+ /// <param name="target">The target.</param>
+ void MoveFile(string source, string target);
+
+ /// <summary>
+ /// Moves the directory.
+ /// </summary>
+ /// <param name="source">The source.</param>
+ /// <param name="target">The target.</param>
+ void MoveDirectory(string source, string target);
+
+ /// <summary>
+ /// Directories the exists.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+ bool DirectoryExists(string path);
+
+ /// <summary>
+ /// Files the exists.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
+ bool FileExists(string path);
+
+ /// <summary>
+ /// Reads all text.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>System.String.</returns>
+ string ReadAllText(string path);
+
+ byte[] ReadAllBytes(string path);
+
+ void WriteAllBytes(string path, byte[] bytes);
+
+ /// <summary>
+ /// Writes all text.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="text">The text.</param>
+ void WriteAllText(string path, string text);
+
+ /// <summary>
+ /// Writes all text.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="text">The text.</param>
+ /// <param name="encoding">The encoding.</param>
+ void WriteAllText(string path, string text, Encoding encoding);
+
+ /// <summary>
+ /// Reads all text.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="encoding">The encoding.</param>
+ /// <returns>System.String.</returns>
+ string ReadAllText(string path, Encoding encoding);
+
+ string[] ReadAllLines(string path);
+
+ void WriteAllLines(string path, IEnumerable<string> lines);
+
+ /// <summary>
+ /// Gets the directory paths.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+ /// <returns>IEnumerable&lt;System.String&gt;.</returns>
+ IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false);
+
+ /// <summary>
+ /// Gets the file paths.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+ /// <returns>IEnumerable&lt;System.String&gt;.</returns>
+ IEnumerable<string> GetFilePaths(string path, bool recursive = false);
+ IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive);
+
+ /// <summary>
+ /// Gets the file system entry paths.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="recursive">if set to <c>true</c> [recursive].</param>
+ /// <returns>IEnumerable&lt;System.String&gt;.</returns>
+ IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false);
+
+ void SetHidden(string path, bool isHidden);
+ void SetReadOnly(string path, bool readOnly);
+ void SetAttributes(string path, bool isHidden, bool readOnly);
+
+ char DirectorySeparatorChar { get; }
+
+ string GetFullPath(string path);
+
+ List<FileSystemMetadata> GetDrives();
+
+ void SetExecutable(string path);
+ }
+
+ public enum FileOpenMode
+ {
+ //
+ // Summary:
+ // Specifies that the operating system should create a new file. This requires System.Security.Permissions.FileIOPermissionAccess.Write
+ // permission. If the file already exists, an System.IO.IOException exception is
+ // thrown.
+ CreateNew = 1,
+ //
+ // Summary:
+ // Specifies that the operating system should create a new file. If the file already
+ // exists, it will be overwritten. This requires System.Security.Permissions.FileIOPermissionAccess.Write
+ // permission. FileMode.Create is equivalent to requesting that if the file does
+ // not exist, use System.IO.FileMode.CreateNew; otherwise, use System.IO.FileMode.Truncate.
+ // If the file already exists but is a hidden file, an System.UnauthorizedAccessException
+ // exception is thrown.
+ Create = 2,
+ //
+ // Summary:
+ // Specifies that the operating system should open an existing file. The ability
+ // to open the file is dependent on the value specified by the System.IO.FileAccess
+ // enumeration. A System.IO.FileNotFoundException exception is thrown if the file
+ // does not exist.
+ Open = 3,
+ //
+ // Summary:
+ // Specifies that the operating system should open a file if it exists; otherwise,
+ // a new file should be created. If the file is opened with FileAccess.Read, System.Security.Permissions.FileIOPermissionAccess.Read
+ // permission is required. If the file access is FileAccess.Write, System.Security.Permissions.FileIOPermissionAccess.Write
+ // permission is required. If the file is opened with FileAccess.ReadWrite, both
+ // System.Security.Permissions.FileIOPermissionAccess.Read and System.Security.Permissions.FileIOPermissionAccess.Write
+ // permissions are required.
+ OpenOrCreate = 4
+ }
+
+ public enum FileAccessMode
+ {
+ //
+ // Summary:
+ // Read access to the file. Data can be read from the file. Combine with Write for
+ // read/write access.
+ Read = 1,
+ //
+ // Summary:
+ // Write access to the file. Data can be written to the file. Combine with Read
+ // for read/write access.
+ Write = 2
+ }
+
+ public enum FileShareMode
+ {
+ //
+ // Summary:
+ // Declines sharing of the current file. Any request to open the file (by this process
+ // or another process) will fail until the file is closed.
+ None = 0,
+ //
+ // Summary:
+ // Allows subsequent opening of the file for reading. If this flag is not specified,
+ // any request to open the file for reading (by this process or another process)
+ // will fail until the file is closed. However, even if this flag is specified,
+ // additional permissions might still be needed to access the file.
+ Read = 1,
+ //
+ // Summary:
+ // Allows subsequent opening of the file for writing. If this flag is not specified,
+ // any request to open the file for writing (by this process or another process)
+ // will fail until the file is closed. However, even if this flag is specified,
+ // additional permissions might still be needed to access the file.
+ Write = 2,
+ //
+ // Summary:
+ // Allows subsequent opening of the file for reading or writing. If this flag is
+ // not specified, any request to open the file for reading or writing (by this process
+ // or another process) will fail until the file is closed. However, even if this
+ // flag is specified, additional permissions might still be needed to access the
+ // file.
+ ReadWrite = 3
+ }
+
+ //
+ // Summary:
+ // Represents advanced options for creating a System.IO.FileStream object.
+ [Flags]
+ public enum FileOpenOptions
+ {
+ //
+ // Summary:
+ // Indicates that the system should write through any intermediate cache and go
+ // directly to disk.
+ WriteThrough = int.MinValue,
+ //
+ // Summary:
+ // Indicates that no additional options should be used when creating a System.IO.FileStream
+ // object.
+ None = 0,
+ //
+ // Summary:
+ // Indicates that a file is encrypted and can be decrypted only by using the same
+ // user account used for encryption.
+ Encrypted = 16384,
+ //
+ // Summary:
+ // Indicates that a file is automatically deleted when it is no longer in use.
+ DeleteOnClose = 67108864,
+ //
+ // Summary:
+ // Indicates that the file is to be accessed sequentially from beginning to end.
+ // The system can use this as a hint to optimize file caching. If an application
+ // moves the file pointer for random access, optimum caching may not occur; however,
+ // correct operation is still guaranteed.
+ SequentialScan = 134217728,
+ //
+ // Summary:
+ // Indicates that the file is accessed randomly. The system can use this as a hint
+ // to optimize file caching.
+ RandomAccess = 268435456,
+ //
+ // Summary:
+ // Indicates that a file can be used for asynchronous reading and writing.
+ Asynchronous = 1073741824
+ }
+}
diff --git a/MediaBrowser.Model/IO/IIsoManager.cs b/MediaBrowser.Model/IO/IIsoManager.cs
new file mode 100644
index 000000000..92c4d5aee
--- /dev/null
+++ b/MediaBrowser.Model/IO/IIsoManager.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Model.IO
+{
+ public interface IIsoManager : IDisposable
+ {
+ /// <summary>
+ /// Mounts the specified iso path.
+ /// </summary>
+ /// <param name="isoPath">The iso path.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>IsoMount.</returns>
+ /// <exception cref="ArgumentNullException">isoPath</exception>
+ /// <exception cref="IOException">Unable to create mount.</exception>
+ Task<IIsoMount> Mount(string isoPath, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Determines whether this instance can mount the specified path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if this instance can mount the specified path; otherwise, <c>false</c>.</returns>
+ bool CanMount(string path);
+
+ /// <summary>
+ /// Adds the parts.
+ /// </summary>
+ /// <param name="mounters">The mounters.</param>
+ void AddParts(IEnumerable<IIsoMounter> mounters);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/IO/IIsoMount.cs b/MediaBrowser.Model/IO/IIsoMount.cs
new file mode 100644
index 000000000..4f8f8b5d2
--- /dev/null
+++ b/MediaBrowser.Model/IO/IIsoMount.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace MediaBrowser.Model.IO
+{
+ /// <summary>
+ /// Interface IIsoMount
+ /// </summary>
+ public interface IIsoMount : IDisposable
+ {
+ /// <summary>
+ /// Gets or sets the iso path.
+ /// </summary>
+ /// <value>The iso path.</value>
+ string IsoPath { get; }
+
+ /// <summary>
+ /// Gets the mounted path.
+ /// </summary>
+ /// <value>The mounted path.</value>
+ string MountedPath { get; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/IO/IIsoMounter.cs b/MediaBrowser.Model/IO/IIsoMounter.cs
new file mode 100644
index 000000000..7efbc2024
--- /dev/null
+++ b/MediaBrowser.Model/IO/IIsoMounter.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Model.IO
+{
+ public interface IIsoMounter : IDisposable
+ {
+ /// <summary>
+ /// Mounts the specified iso path.
+ /// </summary>
+ /// <param name="isoPath">The iso path.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>IsoMount.</returns>
+ /// <exception cref="ArgumentNullException">isoPath</exception>
+ /// <exception cref="IOException">Unable to create mount.</exception>
+ Task<IIsoMount> Mount(string isoPath, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Determines whether this instance can mount the specified path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if this instance can mount the specified path; otherwise, <c>false</c>.</returns>
+ bool CanMount(string path);
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+ }
+}
diff --git a/MediaBrowser.Model/IO/IShortcutHandler.cs b/MediaBrowser.Model/IO/IShortcutHandler.cs
new file mode 100644
index 000000000..16255e51f
--- /dev/null
+++ b/MediaBrowser.Model/IO/IShortcutHandler.cs
@@ -0,0 +1,25 @@
+
+namespace MediaBrowser.Model.IO
+{
+ public interface IShortcutHandler
+ {
+ /// <summary>
+ /// Gets the extension.
+ /// </summary>
+ /// <value>The extension.</value>
+ string Extension { get; }
+ /// <summary>
+ /// Resolves the specified shortcut path.
+ /// </summary>
+ /// <param name="shortcutPath">The shortcut path.</param>
+ /// <returns>System.String.</returns>
+ string Resolve(string shortcutPath);
+ /// <summary>
+ /// Creates the specified shortcut path.
+ /// </summary>
+ /// <param name="shortcutPath">The shortcut path.</param>
+ /// <param name="targetPath">The target path.</param>
+ /// <returns>System.String.</returns>
+ void Create(string shortcutPath, string targetPath);
+ }
+}
diff --git a/MediaBrowser.Model/IO/IStreamHelper.cs b/MediaBrowser.Model/IO/IStreamHelper.cs
new file mode 100644
index 000000000..7ed6015c0
--- /dev/null
+++ b/MediaBrowser.Model/IO/IStreamHelper.cs
@@ -0,0 +1,19 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Model.IO
+{
+ public interface IStreamHelper
+ {
+ Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken);
+
+ Task CopyToAsync(Stream source, Stream destination, int bufferSize, int emptyReadLimit, CancellationToken cancellationToken);
+
+ Task<int> CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken);
+ Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken);
+
+ Task CopyUntilCancelled(Stream source, Stream target, int bufferSize, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Model/IO/IZipClient.cs b/MediaBrowser.Model/IO/IZipClient.cs
new file mode 100644
index 000000000..c1dfc6cd6
--- /dev/null
+++ b/MediaBrowser.Model/IO/IZipClient.cs
@@ -0,0 +1,69 @@
+using System.IO;
+
+namespace MediaBrowser.Model.IO
+{
+ /// <summary>
+ /// Interface IZipClient
+ /// </summary>
+ public interface IZipClient
+ {
+ /// <summary>
+ /// Extracts all.
+ /// </summary>
+ /// <param name="sourceFile">The source file.</param>
+ /// <param name="targetPath">The target path.</param>
+ /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
+ void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles);
+
+ /// <summary>
+ /// Extracts all.
+ /// </summary>
+ /// <param name="source">The source.</param>
+ /// <param name="targetPath">The target path.</param>
+ /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
+ void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles);
+
+ void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles);
+ void ExtractFirstFileFromGz(Stream source, string targetPath, string defaultFileName);
+
+ /// <summary>
+ /// Extracts all from zip.
+ /// </summary>
+ /// <param name="source">The source.</param>
+ /// <param name="targetPath">The target path.</param>
+ /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
+ void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles);
+
+ /// <summary>
+ /// Extracts all from7z.
+ /// </summary>
+ /// <param name="sourceFile">The source file.</param>
+ /// <param name="targetPath">The target path.</param>
+ /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
+ void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles);
+
+ /// <summary>
+ /// Extracts all from7z.
+ /// </summary>
+ /// <param name="source">The source.</param>
+ /// <param name="targetPath">The target path.</param>
+ /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
+ void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles);
+
+ /// <summary>
+ /// Extracts all from tar.
+ /// </summary>
+ /// <param name="sourceFile">The source file.</param>
+ /// <param name="targetPath">The target path.</param>
+ /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
+ void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles);
+
+ /// <summary>
+ /// Extracts all from tar.
+ /// </summary>
+ /// <param name="source">The source.</param>
+ /// <param name="targetPath">The target path.</param>
+ /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
+ void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles);
+ }
+}
diff --git a/MediaBrowser.Model/IO/StreamDefaults.cs b/MediaBrowser.Model/IO/StreamDefaults.cs
new file mode 100644
index 000000000..1e99ff4b5
--- /dev/null
+++ b/MediaBrowser.Model/IO/StreamDefaults.cs
@@ -0,0 +1,19 @@
+
+namespace MediaBrowser.Model.IO
+{
+ /// <summary>
+ /// Class StreamDefaults
+ /// </summary>
+ public static class StreamDefaults
+ {
+ /// <summary>
+ /// The default copy to buffer size
+ /// </summary>
+ public const int DefaultCopyToBufferSize = 81920;
+
+ /// <summary>
+ /// The default file stream buffer size
+ /// </summary>
+ public const int DefaultFileStreamBufferSize = 81920;
+ }
+}
diff --git a/MediaBrowser.Model/Library/PlayAccess.cs b/MediaBrowser.Model/Library/PlayAccess.cs
new file mode 100644
index 000000000..6ec845fc7
--- /dev/null
+++ b/MediaBrowser.Model/Library/PlayAccess.cs
@@ -0,0 +1,9 @@
+
+namespace MediaBrowser.Model.Library
+{
+ public enum PlayAccess
+ {
+ Full = 0,
+ None = 1
+ }
+}
diff --git a/MediaBrowser.Model/Library/UserViewQuery.cs b/MediaBrowser.Model/Library/UserViewQuery.cs
new file mode 100644
index 000000000..8b60cfd98
--- /dev/null
+++ b/MediaBrowser.Model/Library/UserViewQuery.cs
@@ -0,0 +1,33 @@
+using System;
+
+namespace MediaBrowser.Model.Library
+{
+ public class UserViewQuery
+ {
+ /// <summary>
+ /// Gets or sets the user identifier.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [include external content].
+ /// </summary>
+ /// <value><c>true</c> if [include external content]; otherwise, <c>false</c>.</value>
+ public bool IncludeExternalContent { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [include hidden].
+ /// </summary>
+ /// <value><c>true</c> if [include hidden]; otherwise, <c>false</c>.</value>
+ public bool IncludeHidden { get; set; }
+
+ public string[] PresetViews { get; set; }
+
+ public UserViewQuery()
+ {
+ IncludeExternalContent = true;
+ PresetViews = new string[] {};
+ }
+ }
+}
diff --git a/MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs b/MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs
new file mode 100644
index 000000000..b5bd6ced0
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/BaseTimerInfoDto.cs
@@ -0,0 +1,127 @@
+using MediaBrowser.Model.Dto;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.LiveTv
+{
+ public class BaseTimerInfoDto : IHasServerId
+ {
+ /// <summary>
+ /// Id of the recording.
+ /// </summary>
+ public string Id { get; set; }
+
+ public string Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the server identifier.
+ /// </summary>
+ /// <value>The server identifier.</value>
+ public string ServerId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the external identifier.
+ /// </summary>
+ /// <value>The external identifier.</value>
+ public string ExternalId { get; set; }
+
+ /// <summary>
+ /// ChannelId of the recording.
+ /// </summary>
+ public Guid ChannelId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the external channel identifier.
+ /// </summary>
+ /// <value>The external channel identifier.</value>
+ public string ExternalChannelId { get; set; }
+
+ /// <summary>
+ /// ChannelName of the recording.
+ /// </summary>
+ public string ChannelName { get; set; }
+
+ public string ChannelPrimaryImageTag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the program identifier.
+ /// </summary>
+ /// <value>The program identifier.</value>
+ public string ProgramId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the external program identifier.
+ /// </summary>
+ /// <value>The external program identifier.</value>
+ public string ExternalProgramId { get; set; }
+
+ /// <summary>
+ /// Name of the recording.
+ /// </summary>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Description of the recording.
+ /// </summary>
+ public string Overview { get; set; }
+
+ /// <summary>
+ /// The start date of the recording, in UTC.
+ /// </summary>
+ public DateTime StartDate { get; set; }
+
+ /// <summary>
+ /// The end date of the recording, in UTC.
+ /// </summary>
+ public DateTime EndDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the service.
+ /// </summary>
+ /// <value>The name of the service.</value>
+ public string ServiceName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public int Priority { get; set; }
+
+ /// <summary>
+ /// Gets or sets the pre padding seconds.
+ /// </summary>
+ /// <value>The pre padding seconds.</value>
+ public int PrePaddingSeconds { get; set; }
+
+ /// <summary>
+ /// Gets or sets the post padding seconds.
+ /// </summary>
+ /// <value>The post padding seconds.</value>
+ public int PostPaddingSeconds { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is pre padding required.
+ /// </summary>
+ /// <value><c>true</c> if this instance is pre padding required; otherwise, <c>false</c>.</value>
+ public bool IsPrePaddingRequired { get; set; }
+
+ /// <summary>
+ /// If the item does not have any backdrops, this will hold the Id of the Parent that has one.
+ /// </summary>
+ /// <value>The parent backdrop item id.</value>
+ public string ParentBackdropItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent backdrop image tags.
+ /// </summary>
+ /// <value>The parent backdrop image tags.</value>
+ public string[] ParentBackdropImageTags { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is post padding required.
+ /// </summary>
+ /// <value><c>true</c> if this instance is post padding required; otherwise, <c>false</c>.</value>
+ public bool IsPostPaddingRequired { get; set; }
+ public KeepUntil KeepUntil { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/LiveTv/ChannelType.cs b/MediaBrowser.Model/LiveTv/ChannelType.cs
new file mode 100644
index 000000000..bca16f839
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/ChannelType.cs
@@ -0,0 +1,19 @@
+
+namespace MediaBrowser.Model.LiveTv
+{
+ /// <summary>
+ /// Enum ChannelType
+ /// </summary>
+ public enum ChannelType
+ {
+ /// <summary>
+ /// The TV
+ /// </summary>
+ TV,
+
+ /// <summary>
+ /// The radio
+ /// </summary>
+ Radio
+ }
+}
diff --git a/MediaBrowser.Model/LiveTv/DayPattern.cs b/MediaBrowser.Model/LiveTv/DayPattern.cs
new file mode 100644
index 000000000..8251795dc
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/DayPattern.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Model.LiveTv
+{
+ public enum DayPattern
+ {
+ Daily,
+ Weekdays,
+ Weekends
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/LiveTv/GuideInfo.cs b/MediaBrowser.Model/LiveTv/GuideInfo.cs
new file mode 100644
index 000000000..c21f6d871
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/GuideInfo.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace MediaBrowser.Model.LiveTv
+{
+ public class GuideInfo
+ {
+ /// <summary>
+ /// Gets or sets the start date.
+ /// </summary>
+ /// <value>The start date.</value>
+ public DateTime StartDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the end date.
+ /// </summary>
+ /// <value>The end date.</value>
+ public DateTime EndDate { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
new file mode 100644
index 000000000..e3abd5974
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs
@@ -0,0 +1,104 @@
+using MediaBrowser.Model.Entities;
+using System;
+
+namespace MediaBrowser.Model.LiveTv
+{
+ /// <summary>
+ /// Class ChannelQuery.
+ /// </summary>
+ public class LiveTvChannelQuery
+ {
+ /// <summary>
+ /// Gets or sets the type of the channel.
+ /// </summary>
+ /// <value>The type of the channel.</value>
+ public ChannelType? ChannelType { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is favorite.
+ /// </summary>
+ /// <value><c>null</c> if [is favorite] contains no value, <c>true</c> if [is favorite]; otherwise, <c>false</c>.</value>
+ public bool? IsFavorite { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is liked.
+ /// </summary>
+ /// <value><c>null</c> if [is liked] contains no value, <c>true</c> if [is liked]; otherwise, <c>false</c>.</value>
+ public bool? IsLiked { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is disliked.
+ /// </summary>
+ /// <value><c>null</c> if [is disliked] contains no value, <c>true</c> if [is disliked]; otherwise, <c>false</c>.</value>
+ public bool? IsDisliked { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [enable favorite sorting].
+ /// </summary>
+ /// <value><c>true</c> if [enable favorite sorting]; otherwise, <c>false</c>.</value>
+ public bool EnableFavoriteSorting { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user identifier.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Skips over a given number of items within the results. Use for paging.
+ /// </summary>
+ /// <value>The start index.</value>
+ public int? StartIndex { get; set; }
+
+ /// <summary>
+ /// The maximum number of items to return
+ /// </summary>
+ /// <value>The limit.</value>
+ public int? Limit { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [add current program].
+ /// </summary>
+ /// <value><c>true</c> if [add current program]; otherwise, <c>false</c>.</value>
+ public bool AddCurrentProgram { get; set; }
+ public bool EnableUserData { get; set; }
+
+ /// <summary>
+ /// Used to specific whether to return news or not
+ /// </summary>
+ /// <remarks>If set to null, all programs will be returned</remarks>
+ public bool? IsNews { get; set; }
+
+ /// <summary>
+ /// Used to specific whether to return movies or not
+ /// </summary>
+ /// <remarks>If set to null, all programs will be returned</remarks>
+ public bool? IsMovie { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is kids.
+ /// </summary>
+ /// <value><c>null</c> if [is kids] contains no value, <c>true</c> if [is kids]; otherwise, <c>false</c>.</value>
+ public bool? IsKids { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is sports.
+ /// </summary>
+ /// <value><c>null</c> if [is sports] contains no value, <c>true</c> if [is sports]; otherwise, <c>false</c>.</value>
+ public bool? IsSports { get; set; }
+ public bool? IsSeries { get; set; }
+
+ public string[] SortBy { get; set; }
+
+ /// <summary>
+ /// The sort order to return results with
+ /// </summary>
+ /// <value>The sort order.</value>
+ public SortOrder? SortOrder { get; set; }
+
+ public LiveTvChannelQuery()
+ {
+ EnableUserData = true;
+ SortBy = new string[] {};
+ }
+ }
+}
diff --git a/MediaBrowser.Model/LiveTv/LiveTvInfo.cs b/MediaBrowser.Model/LiveTv/LiveTvInfo.cs
new file mode 100644
index 000000000..331b1101b
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/LiveTvInfo.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using System;
+
+namespace MediaBrowser.Model.LiveTv
+{
+ public class LiveTvInfo
+ {
+ /// <summary>
+ /// Gets or sets the services.
+ /// </summary>
+ /// <value>The services.</value>
+ public LiveTvServiceInfo[] Services { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is enabled.
+ /// </summary>
+ /// <value><c>true</c> if this instance is enabled; otherwise, <c>false</c>.</value>
+ public bool IsEnabled { get; set; }
+
+ /// <summary>
+ /// Gets or sets the enabled users.
+ /// </summary>
+ /// <value>The enabled users.</value>
+ public string[] EnabledUsers { get; set; }
+
+ public LiveTvInfo()
+ {
+ Services = new LiveTvServiceInfo[] { };
+ EnabledUsers = new string[] {};
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs
new file mode 100644
index 000000000..eb4f20f9e
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs
@@ -0,0 +1,88 @@
+using MediaBrowser.Model.Dto;
+using System;
+
+namespace MediaBrowser.Model.LiveTv
+{
+ public class LiveTvOptions
+ {
+ public int? GuideDays { get; set; }
+ public string RecordingPath { get; set; }
+ public string MovieRecordingPath { get; set; }
+ public string SeriesRecordingPath { get; set; }
+ public bool EnableRecordingSubfolders { get; set; }
+ public bool EnableOriginalAudioWithEncodedRecordings { get; set; }
+
+ public TunerHostInfo[] TunerHosts { get; set; }
+ public ListingsProviderInfo[] ListingProviders { get; set; }
+
+ public int PrePaddingSeconds { get; set; }
+ public int PostPaddingSeconds { get; set; }
+
+ public string[] MediaLocationsCreated { get; set; }
+
+ public string RecordingPostProcessor { get; set; }
+ public string RecordingPostProcessorArguments { get; set; }
+
+ public LiveTvOptions()
+ {
+ TunerHosts = new TunerHostInfo[] { };
+ ListingProviders = new ListingsProviderInfo[] { };
+ MediaLocationsCreated = new string[] { };
+ RecordingPostProcessorArguments = "\"{path}\"";
+ }
+ }
+
+ public class TunerHostInfo
+ {
+ public string Id { get; set; }
+ public string Url { get; set; }
+ public string Type { get; set; }
+ public string DeviceId { get; set; }
+ public string FriendlyName { get; set; }
+ public bool ImportFavoritesOnly { get; set; }
+ public bool AllowHWTranscoding { get; set; }
+ public bool EnableStreamLooping { get; set; }
+ public string Source { get; set; }
+ public int TunerCount { get; set; }
+ public string UserAgent { get; set; }
+
+ public TunerHostInfo()
+ {
+ AllowHWTranscoding = true;
+ }
+ }
+
+ public class ListingsProviderInfo
+ {
+ public string Id { get; set; }
+ public string Type { get; set; }
+ public string Username { get; set; }
+ public string Password { get; set; }
+ public string ListingsId { get; set; }
+ public string ZipCode { get; set; }
+ public string Country { get; set; }
+ public string Path { get; set; }
+
+ public string[] EnabledTuners { get; set; }
+ public bool EnableAllTuners { get; set; }
+ public string[] NewsCategories { get; set; }
+ public string[] SportsCategories { get; set; }
+ public string[] KidsCategories { get; set; }
+ public string[] MovieCategories { get; set; }
+ public NameValuePair[] ChannelMappings { get; set; }
+ public string MoviePrefix { get; set; }
+ public string PreferredLanguage { get; set; }
+ public string UserAgent { get; set; }
+
+ public ListingsProviderInfo()
+ {
+ NewsCategories = new string[] { "news", "journalism", "documentary", "current affairs" };
+ SportsCategories = new string[] { "sports", "basketball", "baseball", "football" };
+ KidsCategories = new string[] { "kids", "family", "children", "childrens", "disney" };
+ MovieCategories = new string[] { "movie" };
+ EnabledTuners = new string[] { };
+ EnableAllTuners = true;
+ ChannelMappings = new NameValuePair[] {};
+ }
+ }
+}
diff --git a/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs b/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs
new file mode 100644
index 000000000..23eedfc88
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/LiveTvServiceInfo.cs
@@ -0,0 +1,58 @@
+using System;
+
+namespace MediaBrowser.Model.LiveTv
+{
+ /// <summary>
+ /// Class ServiceInfo
+ /// </summary>
+ public class LiveTvServiceInfo
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the home page URL.
+ /// </summary>
+ /// <value>The home page URL.</value>
+ public string HomePageUrl { get; set; }
+
+ /// <summary>
+ /// Gets or sets the status.
+ /// </summary>
+ /// <value>The status.</value>
+ public LiveTvServiceStatus Status { get; set; }
+
+ /// <summary>
+ /// Gets or sets the status message.
+ /// </summary>
+ /// <value>The status message.</value>
+ public string StatusMessage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the version.
+ /// </summary>
+ /// <value>The version.</value>
+ public string Version { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has update available.
+ /// </summary>
+ /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
+ public bool HasUpdateAvailable { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is visible.
+ /// </summary>
+ /// <value><c>true</c> if this instance is visible; otherwise, <c>false</c>.</value>
+ public bool IsVisible { get; set; }
+
+ public string[] Tuners { get; set; }
+
+ public LiveTvServiceInfo()
+ {
+ Tuners = new string[] { };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/LiveTv/LiveTvServiceStatus.cs b/MediaBrowser.Model/LiveTv/LiveTvServiceStatus.cs
new file mode 100644
index 000000000..20fe84500
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/LiveTvServiceStatus.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Model.LiveTv
+{
+ public enum LiveTvServiceStatus
+ {
+ Ok = 0,
+ Unavailable = 1
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/LiveTv/LiveTvTunerStatus.cs b/MediaBrowser.Model/LiveTv/LiveTvTunerStatus.cs
new file mode 100644
index 000000000..055199fca
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/LiveTvTunerStatus.cs
@@ -0,0 +1,10 @@
+namespace MediaBrowser.Model.LiveTv
+{
+ public enum LiveTvTunerStatus
+ {
+ Available = 0,
+ Disabled = 1,
+ RecordingTv = 2,
+ LiveTv = 3
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/LiveTv/ProgramAudio.cs b/MediaBrowser.Model/LiveTv/ProgramAudio.cs
new file mode 100644
index 000000000..9a272492c
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/ProgramAudio.cs
@@ -0,0 +1,12 @@
+namespace MediaBrowser.Model.LiveTv
+{
+ public enum ProgramAudio
+ {
+ Mono,
+ Stereo,
+ Dolby,
+ DolbyDigital,
+ Thx,
+ Atmos
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/LiveTv/ProgramQuery.cs b/MediaBrowser.Model/LiveTv/ProgramQuery.cs
new file mode 100644
index 000000000..89160948c
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/ProgramQuery.cs
@@ -0,0 +1,117 @@
+using MediaBrowser.Model.Entities;
+using System;
+using MediaBrowser.Model.Querying;
+
+namespace MediaBrowser.Model.LiveTv
+{
+ /// <summary>
+ /// Class ProgramQuery.
+ /// </summary>
+ public class ProgramQuery
+ {
+ public ProgramQuery()
+ {
+ ChannelIds = new Guid[] { };
+ OrderBy = new Tuple<string, SortOrder>[] { };
+ Genres = new string[] {};
+ GenreIds = new Guid[] { };
+ EnableTotalRecordCount = true;
+ EnableUserData = true;
+ }
+
+ public bool EnableTotalRecordCount { get; set; }
+ public bool EnableUserData { get; set; }
+
+ /// <summary>
+ /// Fields to return within the items, in addition to basic information
+ /// </summary>
+ /// <value>The fields.</value>
+ public ItemFields[] Fields { get; set; }
+ public bool? EnableImages { get; set; }
+ public int? ImageTypeLimit { get; set; }
+ public ImageType[] EnableImageTypes { get; set; }
+
+ /// <summary>
+ /// Gets or sets the channel ids.
+ /// </summary>
+ /// <value>The channel ids.</value>
+ public Guid[] ChannelIds { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user identifier.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public string UserId { get; set; }
+ public string SeriesTimerId { get; set; }
+ public string Name { get; set; }
+
+ /// <summary>
+ /// The earliest date for which a program starts to return
+ /// </summary>
+ public DateTime? MinStartDate { get; set; }
+
+ /// <summary>
+ /// The latest date for which a program starts to return
+ /// </summary>
+ public DateTime? MaxStartDate { get; set; }
+
+ /// <summary>
+ /// The earliest date for which a program ends to return
+ /// </summary>
+ public DateTime? MinEndDate { get; set; }
+
+ /// <summary>
+ /// The latest date for which a program ends to return
+ /// </summary>
+ public DateTime? MaxEndDate { get; set; }
+
+ /// <summary>
+ /// Used to specific whether to return news or not
+ /// </summary>
+ /// <remarks>If set to null, all programs will be returned</remarks>
+ public bool? IsNews { get; set; }
+
+ /// <summary>
+ /// Used to specific whether to return movies or not
+ /// </summary>
+ /// <remarks>If set to null, all programs will be returned</remarks>
+ public bool? IsMovie { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is kids.
+ /// </summary>
+ /// <value><c>null</c> if [is kids] contains no value, <c>true</c> if [is kids]; otherwise, <c>false</c>.</value>
+ public bool? IsKids { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is sports.
+ /// </summary>
+ /// <value><c>null</c> if [is sports] contains no value, <c>true</c> if [is sports]; otherwise, <c>false</c>.</value>
+ public bool? IsSports { get; set; }
+
+ /// <summary>
+ /// Skips over a given number of items within the results. Use for paging.
+ /// </summary>
+ public int? StartIndex { get; set; }
+ public bool? IsSeries { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has aired.
+ /// </summary>
+ /// <value><c>null</c> if [has aired] contains no value, <c>true</c> if [has aired]; otherwise, <c>false</c>.</value>
+ public bool? HasAired { get; set; }
+
+ /// <summary>
+ /// The maximum number of items to return
+ /// </summary>
+ public int? Limit { get; set; }
+
+ public Tuple<string, SortOrder>[] OrderBy { get; set; }
+
+ /// <summary>
+ /// Limit results to items containing specific genres
+ /// </summary>
+ /// <value>The genres.</value>
+ public Guid[] GenreIds { get; set; }
+ public string[] Genres { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/LiveTv/RecommendedProgramQuery.cs b/MediaBrowser.Model/LiveTv/RecommendedProgramQuery.cs
new file mode 100644
index 000000000..9972c4c3f
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/RecommendedProgramQuery.cs
@@ -0,0 +1,73 @@
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace MediaBrowser.Model.LiveTv
+{
+ public class RecommendedProgramQuery
+ {
+ /// <summary>
+ /// Fields to return within the items, in addition to basic information
+ /// </summary>
+ /// <value>The fields.</value>
+ public ItemFields[] Fields { get; set; }
+ public bool? EnableImages { get; set; }
+ public int? ImageTypeLimit { get; set; }
+ public ImageType[] EnableImageTypes { get; set; }
+ public Guid[] GenreIds { get; set; }
+
+ public bool EnableTotalRecordCount { get; set; }
+
+ public RecommendedProgramQuery()
+ {
+ EnableTotalRecordCount = true;
+ GenreIds = new Guid[] { };
+ }
+
+ /// <summary>
+ /// Gets or sets the user identifier.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public string UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is airing.
+ /// </summary>
+ /// <value><c>true</c> if this instance is airing; otherwise, <c>false</c>.</value>
+ public bool? IsAiring { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has aired.
+ /// </summary>
+ /// <value><c>null</c> if [has aired] contains no value, <c>true</c> if [has aired]; otherwise, <c>false</c>.</value>
+ public bool? HasAired { get; set; }
+
+ /// <summary>
+ /// The maximum number of items to return
+ /// </summary>
+ /// <value>The limit.</value>
+ public int? Limit { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is movie.
+ /// </summary>
+ /// <value><c>null</c> if [is movie] contains no value, <c>true</c> if [is movie]; otherwise, <c>false</c>.</value>
+ public bool? IsNews { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is movie.
+ /// </summary>
+ /// <value><c>null</c> if [is movie] contains no value, <c>true</c> if [is movie]; otherwise, <c>false</c>.</value>
+ public bool? IsMovie { get; set; }
+ public bool? IsSeries { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is kids.
+ /// </summary>
+ /// <value><c>null</c> if [is kids] contains no value, <c>true</c> if [is kids]; otherwise, <c>false</c>.</value>
+ public bool? IsKids { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is sports.
+ /// </summary>
+ /// <value><c>null</c> if [is sports] contains no value, <c>true</c> if [is sports]; otherwise, <c>false</c>.</value>
+ public bool? IsSports { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/LiveTv/RecordingQuery.cs b/MediaBrowser.Model/LiveTv/RecordingQuery.cs
new file mode 100644
index 000000000..7d20441a5
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/RecordingQuery.cs
@@ -0,0 +1,82 @@
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Querying;
+using System;
+
+namespace MediaBrowser.Model.LiveTv
+{
+ /// <summary>
+ /// Class RecordingQuery.
+ /// </summary>
+ public class RecordingQuery
+ {
+ /// <summary>
+ /// Gets or sets the channel identifier.
+ /// </summary>
+ /// <value>The channel identifier.</value>
+ public string ChannelId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user identifier.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the identifier.
+ /// </summary>
+ /// <value>The identifier.</value>
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Skips over a given number of items within the results. Use for paging.
+ /// </summary>
+ /// <value>The start index.</value>
+ public int? StartIndex { get; set; }
+
+ /// <summary>
+ /// The maximum number of items to return
+ /// </summary>
+ /// <value>The limit.</value>
+ public int? Limit { get; set; }
+
+ /// <summary>
+ /// Gets or sets the status.
+ /// </summary>
+ /// <value>The status.</value>
+ public RecordingStatus? Status { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is in progress.
+ /// </summary>
+ /// <value><c>null</c> if [is in progress] contains no value, <c>true</c> if [is in progress]; otherwise, <c>false</c>.</value>
+ public bool? IsInProgress { get; set; }
+
+ /// <summary>
+ /// Gets or sets the series timer identifier.
+ /// </summary>
+ /// <value>The series timer identifier.</value>
+ public string SeriesTimerId { get; set; }
+
+ /// <summary>
+ /// Fields to return within the items, in addition to basic information
+ /// </summary>
+ /// <value>The fields.</value>
+ public ItemFields[] Fields { get; set; }
+ public bool? EnableImages { get; set; }
+ public bool? IsLibraryItem { get; set; }
+ public bool? IsNews { get; set; }
+ public bool? IsMovie { get; set; }
+ public bool? IsSeries { get; set; }
+ public bool? IsKids { get; set; }
+ public bool? IsSports { get; set; }
+ public int? ImageTypeLimit { get; set; }
+ public ImageType[] EnableImageTypes { get; set; }
+
+ public bool EnableTotalRecordCount { get; set; }
+
+ public RecordingQuery()
+ {
+ EnableTotalRecordCount = true;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/LiveTv/RecordingStatus.cs b/MediaBrowser.Model/LiveTv/RecordingStatus.cs
new file mode 100644
index 000000000..496e6f421
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/RecordingStatus.cs
@@ -0,0 +1,14 @@
+
+namespace MediaBrowser.Model.LiveTv
+{
+ public enum RecordingStatus
+ {
+ New,
+ InProgress,
+ Completed,
+ Cancelled,
+ ConflictedOk,
+ ConflictedNotOk,
+ Error
+ }
+}
diff --git a/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs
new file mode 100644
index 000000000..593996352
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs
@@ -0,0 +1,92 @@
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Model.LiveTv
+{
+ /// <summary>
+ /// Class SeriesTimerInfoDto.
+ /// </summary>
+ public class SeriesTimerInfoDto : BaseTimerInfoDto
+ {
+ public SeriesTimerInfoDto()
+ {
+ ImageTags = new Dictionary<ImageType, string>();
+ Days = new DayOfWeek[] { };
+ Type = "SeriesTimer";
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [record any time].
+ /// </summary>
+ /// <value><c>true</c> if [record any time]; otherwise, <c>false</c>.</value>
+ public bool RecordAnyTime { get; set; }
+
+ public bool SkipEpisodesInLibrary { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [record any channel].
+ /// </summary>
+ /// <value><c>true</c> if [record any channel]; otherwise, <c>false</c>.</value>
+ public bool RecordAnyChannel { get; set; }
+
+ public int KeepUpTo { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [record new only].
+ /// </summary>
+ /// <value><c>true</c> if [record new only]; otherwise, <c>false</c>.</value>
+ public bool RecordNewOnly { get; set; }
+
+ /// <summary>
+ /// Gets or sets the days.
+ /// </summary>
+ /// <value>The days.</value>
+ public DayOfWeek[] Days { get; set; }
+
+ /// <summary>
+ /// Gets or sets the day pattern.
+ /// </summary>
+ /// <value>The day pattern.</value>
+ public DayPattern? DayPattern { get; set; }
+
+ /// <summary>
+ /// Gets or sets the image tags.
+ /// </summary>
+ /// <value>The image tags.</value>
+ public Dictionary<ImageType, string> ImageTags { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent thumb item id.
+ /// </summary>
+ /// <value>The parent thumb item id.</value>
+ public string ParentThumbItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent thumb image tag.
+ /// </summary>
+ /// <value>The parent thumb image tag.</value>
+ public string ParentThumbImageTag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent primary image item identifier.
+ /// </summary>
+ /// <value>The parent primary image item identifier.</value>
+ public string ParentPrimaryImageItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent primary image tag.
+ /// </summary>
+ /// <value>The parent primary image tag.</value>
+ public string ParentPrimaryImageTag { get; set; }
+ }
+
+ public enum KeepUntil
+ {
+ UntilDeleted,
+ UntilSpaceNeeded,
+ UntilWatched,
+ UntilDate
+ }
+}
diff --git a/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs b/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs
new file mode 100644
index 000000000..95260cc0e
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs
@@ -0,0 +1,19 @@
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.LiveTv
+{
+ public class SeriesTimerQuery
+ {
+ /// <summary>
+ /// Gets or sets the sort by - SortName, Priority
+ /// </summary>
+ /// <value>The sort by.</value>
+ public string SortBy { get; set; }
+
+ /// <summary>
+ /// Gets or sets the sort order.
+ /// </summary>
+ /// <value>The sort order.</value>
+ public SortOrder SortOrder { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/LiveTv/TimerInfoDto.cs b/MediaBrowser.Model/LiveTv/TimerInfoDto.cs
new file mode 100644
index 000000000..d1aa3118f
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/TimerInfoDto.cs
@@ -0,0 +1,43 @@
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Model.LiveTv
+{
+ public class TimerInfoDto : BaseTimerInfoDto
+ {
+ public TimerInfoDto()
+ {
+ Type = "Timer";
+ }
+
+ /// <summary>
+ /// Gets or sets the status.
+ /// </summary>
+ /// <value>The status.</value>
+ public RecordingStatus Status { get; set; }
+
+ /// <summary>
+ /// Gets or sets the series timer identifier.
+ /// </summary>
+ /// <value>The series timer identifier.</value>
+ public string SeriesTimerId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the external series timer identifier.
+ /// </summary>
+ /// <value>The external series timer identifier.</value>
+ public string ExternalSeriesTimerId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the run time ticks.
+ /// </summary>
+ /// <value>The run time ticks.</value>
+ public long? RunTimeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the program information.
+ /// </summary>
+ /// <value>The program information.</value>
+ public BaseItemDto ProgramInfo { get; set; }
+
+ }
+}
diff --git a/MediaBrowser.Model/LiveTv/TimerQuery.cs b/MediaBrowser.Model/LiveTv/TimerQuery.cs
new file mode 100644
index 000000000..c6202680c
--- /dev/null
+++ b/MediaBrowser.Model/LiveTv/TimerQuery.cs
@@ -0,0 +1,23 @@
+namespace MediaBrowser.Model.LiveTv
+{
+ public class TimerQuery
+ {
+ /// <summary>
+ /// Gets or sets the channel identifier.
+ /// </summary>
+ /// <value>The channel identifier.</value>
+ public string ChannelId { get; set; }
+
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the series timer identifier.
+ /// </summary>
+ /// <value>The series timer identifier.</value>
+ public string SeriesTimerId { get; set; }
+
+ public bool? IsActive { get; set; }
+
+ public bool? IsScheduled { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Logging/IConsoleLogger.cs b/MediaBrowser.Model/Logging/IConsoleLogger.cs
new file mode 100644
index 000000000..a8c282d65
--- /dev/null
+++ b/MediaBrowser.Model/Logging/IConsoleLogger.cs
@@ -0,0 +1,7 @@
+namespace MediaBrowser.Model.Logging
+{
+ public interface IConsoleLogger
+ {
+ void WriteLine(string message);
+ }
+}
diff --git a/MediaBrowser.Model/Logging/ILogManager.cs b/MediaBrowser.Model/Logging/ILogManager.cs
new file mode 100644
index 000000000..e6a10cf18
--- /dev/null
+++ b/MediaBrowser.Model/Logging/ILogManager.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Model.Logging
+{
+ /// <summary>
+ /// Interface ILogManager
+ /// </summary>
+ public interface ILogManager
+ {
+ /// <summary>
+ /// Gets or sets the log level.
+ /// </summary>
+ /// <value>The log level.</value>
+ LogSeverity LogSeverity { get; set; }
+
+ /// <summary>
+ /// Gets or sets the exception message prefix.
+ /// </summary>
+ /// <value>The exception message prefix.</value>
+ string ExceptionMessagePrefix { get; set; }
+
+ /// <summary>
+ /// Gets the logger.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>ILogger.</returns>
+ ILogger GetLogger(string name);
+
+ /// <summary>
+ /// Reloads the logger.
+ /// </summary>
+ Task ReloadLogger(LogSeverity severity, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Occurs when [logger loaded].
+ /// </summary>
+ event EventHandler LoggerLoaded;
+
+ /// <summary>
+ /// Flushes this instance.
+ /// </summary>
+ void Flush();
+
+ /// <summary>
+ /// Adds the console output.
+ /// </summary>
+ void AddConsoleOutput();
+
+ /// <summary>
+ /// Removes the console output.
+ /// </summary>
+ void RemoveConsoleOutput();
+ }
+}
diff --git a/MediaBrowser.Model/Logging/ILogger.cs b/MediaBrowser.Model/Logging/ILogger.cs
new file mode 100644
index 000000000..be9d6fc50
--- /dev/null
+++ b/MediaBrowser.Model/Logging/ILogger.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Text;
+
+namespace MediaBrowser.Model.Logging
+{
+ /// <summary>
+ /// Interface ILogger
+ /// </summary>
+ public interface ILogger
+ {
+ /// <summary>
+ /// Infoes the specified message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="paramList">The param list.</param>
+ void Info(string message, params object[] paramList);
+
+ /// <summary>
+ /// Errors the specified message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="paramList">The param list.</param>
+ void Error(string message, params object[] paramList);
+
+ /// <summary>
+ /// Warns the specified message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="paramList">The param list.</param>
+ void Warn(string message, params object[] paramList);
+
+ /// <summary>
+ /// Debugs the specified message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="paramList">The param list.</param>
+ void Debug(string message, params object[] paramList);
+
+ /// <summary>
+ /// Fatals the specified message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="paramList">The param list.</param>
+ void Fatal(string message, params object[] paramList);
+
+ /// <summary>
+ /// Fatals the exception.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="exception">The exception.</param>
+ /// <param name="paramList">The param list.</param>
+ void FatalException(string message, Exception exception, params object[] paramList);
+
+ /// <summary>
+ /// Logs the exception.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="exception">The exception.</param>
+ /// <param name="paramList">The param list.</param>
+ void ErrorException(string message, Exception exception, params object[] paramList);
+
+ /// <summary>
+ /// Logs the multiline.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="severity">The severity.</param>
+ /// <param name="additionalContent">Content of the additional.</param>
+ void LogMultiline(string message, LogSeverity severity, StringBuilder additionalContent);
+
+ /// <summary>
+ /// Logs the specified severity.
+ /// </summary>
+ /// <param name="severity">The severity.</param>
+ /// <param name="message">The message.</param>
+ /// <param name="paramList">The parameter list.</param>
+ void Log(LogSeverity severity, string message, params object[] paramList);
+ }
+}
diff --git a/MediaBrowser.Model/Logging/LogHelper.cs b/MediaBrowser.Model/Logging/LogHelper.cs
new file mode 100644
index 000000000..cf1c02186
--- /dev/null
+++ b/MediaBrowser.Model/Logging/LogHelper.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Text;
+
+namespace MediaBrowser.Model.Logging
+{
+ /// <summary>
+ /// Class LogHelper
+ /// </summary>
+ public static class LogHelper
+ {
+ /// <summary>
+ /// Gets the log message.
+ /// </summary>
+ /// <param name="exception">The exception.</param>
+ /// <returns>StringBuilder.</returns>
+ public static StringBuilder GetLogMessage(Exception exception)
+ {
+ if (exception == null)
+ {
+ throw new ArgumentNullException("exception");
+ }
+
+ var messageText = new StringBuilder();
+
+ messageText.AppendLine(exception.ToString());
+
+ messageText.AppendLine(exception.GetType().FullName);
+
+ LogExceptionData(messageText, exception);
+
+ messageText.AppendLine(exception.StackTrace ?? "No Stack Trace Available");
+
+ // Log the InnerExceptions, if any
+ AppendInnerExceptions(messageText, exception);
+
+ messageText.AppendLine(string.Empty);
+
+ return messageText;
+ }
+
+ /// <summary>
+ /// Appends the inner exceptions.
+ /// </summary>
+ /// <param name="messageText">The message text.</param>
+ /// <param name="e">The e.</param>
+ private static void AppendInnerExceptions(StringBuilder messageText, Exception e)
+ {
+ var aggregate = e as AggregateException;
+
+ if (aggregate != null && aggregate.InnerExceptions != null)
+ {
+ foreach (var ex in aggregate.InnerExceptions)
+ {
+ AppendInnerException(messageText, ex);
+ AppendInnerExceptions(messageText, ex);
+ }
+ }
+
+ else if (e.InnerException != null)
+ {
+ AppendInnerException(messageText, e.InnerException);
+ AppendInnerExceptions(messageText, e.InnerException);
+ }
+ }
+
+ /// <summary>
+ /// Appends the inner exception.
+ /// </summary>
+ /// <param name="messageText">The message text.</param>
+ /// <param name="e">The e.</param>
+ private static void AppendInnerException(StringBuilder messageText, Exception e)
+ {
+ messageText.AppendLine("InnerException: " + e.GetType().FullName);
+ messageText.AppendLine(e.ToString());
+
+ LogExceptionData(messageText, e);
+
+ if (e.StackTrace != null)
+ {
+ messageText.AppendLine(e.StackTrace);
+ }
+ }
+
+ /// <summary>
+ /// Logs the exception data.
+ /// </summary>
+ /// <param name="messageText">The message text.</param>
+ /// <param name="e">The e.</param>
+ private static void LogExceptionData(StringBuilder messageText, Exception e)
+ {
+ foreach (var key in e.Data.Keys)
+ {
+ messageText.AppendLine(key + ": " + e.Data[key]);
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Logging/LogSeverity.cs b/MediaBrowser.Model/Logging/LogSeverity.cs
new file mode 100644
index 000000000..ae0487289
--- /dev/null
+++ b/MediaBrowser.Model/Logging/LogSeverity.cs
@@ -0,0 +1,30 @@
+
+namespace MediaBrowser.Model.Logging
+{
+ /// <summary>
+ /// Enum LogSeverity
+ /// </summary>
+ public enum LogSeverity
+ {
+ /// <summary>
+ /// The info
+ /// </summary>
+ Info,
+ /// <summary>
+ /// The debug
+ /// </summary>
+ Debug,
+ /// <summary>
+ /// The warn
+ /// </summary>
+ Warn,
+ /// <summary>
+ /// The error
+ /// </summary>
+ Error,
+ /// <summary>
+ /// The fatal
+ /// </summary>
+ Fatal
+ }
+}
diff --git a/MediaBrowser.Model/Logging/NullLogger.cs b/MediaBrowser.Model/Logging/NullLogger.cs
new file mode 100644
index 000000000..d211d2567
--- /dev/null
+++ b/MediaBrowser.Model/Logging/NullLogger.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Text;
+
+namespace MediaBrowser.Model.Logging
+{
+ public class NullLogger : ILogger
+ {
+ public void Info(string message, params object[] paramList)
+ {
+ }
+
+ public void Error(string message, params object[] paramList)
+ {
+ }
+
+ public void Warn(string message, params object[] paramList)
+ {
+ }
+
+ public void Debug(string message, params object[] paramList)
+ {
+ }
+
+ public void Fatal(string message, params object[] paramList)
+ {
+ }
+
+ public void FatalException(string message, Exception exception, params object[] paramList)
+ {
+ }
+
+ public void Log(LogSeverity severity, string message, params object[] paramList)
+ {
+ }
+
+ public void ErrorException(string message, Exception exception, params object[] paramList)
+ {
+ }
+
+ public void LogMultiline(string message, LogSeverity severity, StringBuilder additionalContent)
+ {
+ }
+ }
+}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
new file mode 100644
index 000000000..1b1a24b68
--- /dev/null
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\SharedVersion.cs"/>
+ </ItemGroup>
+
+</Project>
diff --git a/MediaBrowser.Model/MediaInfo/AudioCodec.cs b/MediaBrowser.Model/MediaInfo/AudioCodec.cs
new file mode 100644
index 000000000..93aba2f43
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/AudioCodec.cs
@@ -0,0 +1,26 @@
+namespace MediaBrowser.Model.MediaInfo
+{
+ public class AudioCodec
+ {
+ public const string AAC = "aac";
+ public const string MP3 = "mp3";
+ public const string AC3 = "ac3";
+
+ public static string GetFriendlyName(string codec)
+ {
+ if (string.IsNullOrEmpty(codec)) return "";
+
+ switch (codec.ToLower())
+ {
+ case "ac3":
+ return "Dolby Digital";
+ case "eac3":
+ return "Dolby Digital+";
+ case "dca":
+ return "DTS";
+ default:
+ return codec.ToUpper();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs b/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs
new file mode 100644
index 000000000..1b573fba7
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs
@@ -0,0 +1,37 @@
+using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.MediaInfo
+{
+ /// <summary>
+ /// Represents the result of BDInfo output
+ /// </summary>
+ public class BlurayDiscInfo
+ {
+ /// <summary>
+ /// Gets or sets the media streams.
+ /// </summary>
+ /// <value>The media streams.</value>
+ public MediaStream[] MediaStreams { get; set; }
+
+ /// <summary>
+ /// Gets or sets the run time ticks.
+ /// </summary>
+ /// <value>The run time ticks.</value>
+ public long? RunTimeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the files.
+ /// </summary>
+ /// <value>The files.</value>
+ public string[] Files { get; set; }
+
+ public string PlaylistName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the chapters.
+ /// </summary>
+ /// <value>The chapters.</value>
+ public double[] Chapters { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/MediaInfo/Container.cs b/MediaBrowser.Model/MediaInfo/Container.cs
new file mode 100644
index 000000000..3762edf9f
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/Container.cs
@@ -0,0 +1,9 @@
+
+namespace MediaBrowser.Model.MediaInfo
+{
+ public class Container
+ {
+ public const string MP4 = "mp4";
+ public const string MKV = "mkv";
+ }
+}
diff --git a/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs b/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs
new file mode 100644
index 000000000..78d5b197f
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs
@@ -0,0 +1,16 @@
+
+namespace MediaBrowser.Model.MediaInfo
+{
+ /// <summary>
+ /// Interface IBlurayExaminer
+ /// </summary>
+ public interface IBlurayExaminer
+ {
+ /// <summary>
+ /// Gets the disc info.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>BlurayDiscInfo.</returns>
+ BlurayDiscInfo GetDiscInfo(string path);
+ }
+}
diff --git a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs
new file mode 100644
index 000000000..d36aa9944
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs
@@ -0,0 +1,47 @@
+using MediaBrowser.Model.Dlna;
+using System;
+
+namespace MediaBrowser.Model.MediaInfo
+{
+ public class LiveStreamRequest
+ {
+ public string OpenToken { get; set; }
+ public Guid UserId { get; set; }
+ public string PlaySessionId { get; set; }
+ public long? MaxStreamingBitrate { get; set; }
+ public long? StartTimeTicks { get; set; }
+ public int? AudioStreamIndex { get; set; }
+ public int? SubtitleStreamIndex { get; set; }
+ public int? MaxAudioChannels { get; set; }
+ public Guid ItemId { get; set; }
+ public DeviceProfile DeviceProfile { get; set; }
+
+ public bool EnableDirectPlay { get; set; }
+ public bool EnableDirectStream { get; set; }
+ public MediaProtocol[] DirectPlayProtocols { get; set; }
+
+ public LiveStreamRequest()
+ {
+ EnableDirectPlay = true;
+ EnableDirectStream = true;
+ DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http };
+ }
+
+ public LiveStreamRequest(AudioOptions options)
+ {
+ MaxStreamingBitrate = options.MaxBitrate;
+ ItemId = options.ItemId;
+ DeviceProfile = options.Profile;
+ MaxAudioChannels = options.MaxAudioChannels;
+
+ DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http };
+
+ VideoOptions videoOptions = options as VideoOptions;
+ if (videoOptions != null)
+ {
+ AudioStreamIndex = videoOptions.AudioStreamIndex;
+ SubtitleStreamIndex = videoOptions.SubtitleStreamIndex;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Model/MediaInfo/LiveStreamResponse.cs b/MediaBrowser.Model/MediaInfo/LiveStreamResponse.cs
new file mode 100644
index 000000000..e79e37a71
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/LiveStreamResponse.cs
@@ -0,0 +1,9 @@
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Model.MediaInfo
+{
+ public class LiveStreamResponse
+ {
+ public MediaSourceInfo MediaSource { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/MediaInfo/MediaInfo.cs b/MediaBrowser.Model/MediaInfo/MediaInfo.cs
new file mode 100644
index 000000000..55545e23a
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/MediaInfo.cs
@@ -0,0 +1,68 @@
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.MediaInfo
+{
+ public class MediaInfo : MediaSourceInfo, IHasProviderIds
+ {
+ private static readonly string[] EmptyStringArray = new string[] {};
+
+ public ChapterInfo[] Chapters { get; set; }
+
+ /// <summary>
+ /// Gets or sets the album.
+ /// </summary>
+ /// <value>The album.</value>
+ public string Album { get; set; }
+ /// <summary>
+ /// Gets or sets the artists.
+ /// </summary>
+ /// <value>The artists.</value>
+ public string[] Artists { get; set; }
+ /// <summary>
+ /// Gets or sets the album artists.
+ /// </summary>
+ /// <value>The album artists.</value>
+ public string[] AlbumArtists { get; set; }
+ /// <summary>
+ /// Gets or sets the studios.
+ /// </summary>
+ /// <value>The studios.</value>
+ public string[] Studios { get; set; }
+ public string[] Genres { get; set; }
+ public int? IndexNumber { get; set; }
+ public int? ParentIndexNumber { get; set; }
+ public int? ProductionYear { get; set; }
+ public DateTime? PremiereDate { get; set; }
+ public BaseItemPerson[] People { get; set; }
+ public Dictionary<string, string> ProviderIds { get; set; }
+ /// <summary>
+ /// Gets or sets the official rating.
+ /// </summary>
+ /// <value>The official rating.</value>
+ public string OfficialRating { get; set; }
+ /// <summary>
+ /// Gets or sets the official rating description.
+ /// </summary>
+ /// <value>The official rating description.</value>
+ public string OfficialRatingDescription { get; set; }
+ /// <summary>
+ /// Gets or sets the overview.
+ /// </summary>
+ /// <value>The overview.</value>
+ public string Overview { get; set; }
+
+ public MediaInfo()
+ {
+ Chapters = new ChapterInfo[] { };
+ Artists = new string[] {};
+ AlbumArtists = EmptyStringArray;
+ Studios = new string[] {};
+ Genres = new string[] {};
+ People = new BaseItemPerson[] { };
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/MediaInfo/MediaProtocol.cs b/MediaBrowser.Model/MediaInfo/MediaProtocol.cs
new file mode 100644
index 000000000..5882ecde0
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/MediaProtocol.cs
@@ -0,0 +1,13 @@
+namespace MediaBrowser.Model.MediaInfo
+{
+ public enum MediaProtocol
+ {
+ File = 0,
+ Http = 1,
+ Rtmp = 2,
+ Rtsp = 3,
+ Udp = 4,
+ Rtp = 5,
+ Ftp = 6
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs b/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs
new file mode 100644
index 000000000..c68c047f6
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs
@@ -0,0 +1,50 @@
+using MediaBrowser.Model.Dlna;
+using System.Collections.Generic;
+using System;
+
+namespace MediaBrowser.Model.MediaInfo
+{
+ public class PlaybackInfoRequest
+ {
+ public Guid Id { get; set; }
+
+ public Guid UserId { get; set; }
+
+ public long? MaxStreamingBitrate { get; set; }
+
+ public long? StartTimeTicks { get; set; }
+
+ public int? AudioStreamIndex { get; set; }
+
+ public int? SubtitleStreamIndex { get; set; }
+
+ public int? MaxAudioChannels { get; set; }
+
+ public string MediaSourceId { get; set; }
+
+ public string LiveStreamId { get; set; }
+
+ public DeviceProfile DeviceProfile { get; set; }
+
+ public bool EnableDirectPlay { get; set; }
+ public bool EnableDirectStream { get; set; }
+ public bool EnableTranscoding { get; set; }
+ public bool AllowVideoStreamCopy { get; set; }
+ public bool AllowAudioStreamCopy { get; set; }
+ public bool IsPlayback { get; set; }
+ public bool AutoOpenLiveStream { get; set; }
+
+ public MediaProtocol[] DirectPlayProtocols { get; set; }
+
+ public PlaybackInfoRequest()
+ {
+ EnableDirectPlay = true;
+ EnableDirectStream = true;
+ EnableTranscoding = true;
+ AllowVideoStreamCopy = true;
+ AllowAudioStreamCopy = true;
+ IsPlayback = true;
+ DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs b/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs
new file mode 100644
index 000000000..b38fec7d4
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/PlaybackInfoResponse.cs
@@ -0,0 +1,32 @@
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.MediaInfo
+{
+ public class PlaybackInfoResponse
+ {
+ /// <summary>
+ /// Gets or sets the media sources.
+ /// </summary>
+ /// <value>The media sources.</value>
+ public MediaSourceInfo[] MediaSources { get; set; }
+
+ /// <summary>
+ /// Gets or sets the play session identifier.
+ /// </summary>
+ /// <value>The play session identifier.</value>
+ public string PlaySessionId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the error code.
+ /// </summary>
+ /// <value>The error code.</value>
+ public PlaybackErrorCode? ErrorCode { get; set; }
+
+ public PlaybackInfoResponse()
+ {
+ MediaSources = new MediaSourceInfo[] { };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs b/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs
new file mode 100644
index 000000000..60b0bb54d
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs
@@ -0,0 +1,13 @@
+namespace MediaBrowser.Model.MediaInfo
+{
+ public class SubtitleFormat
+ {
+ public const string SRT = "srt";
+ public const string SSA = "ssa";
+ public const string ASS = "ass";
+ public const string VTT = "vtt";
+ public const string SUB = "sub";
+ public const string SMI = "smi";
+ public const string TTML = "ttml";
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/MediaInfo/SubtitleTrackEvent.cs b/MediaBrowser.Model/MediaInfo/SubtitleTrackEvent.cs
new file mode 100644
index 000000000..b4ab6ed97
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/SubtitleTrackEvent.cs
@@ -0,0 +1,11 @@
+
+namespace MediaBrowser.Model.MediaInfo
+{
+ public class SubtitleTrackEvent
+ {
+ public string Id { get; set; }
+ public string Text { get; set; }
+ public long StartPositionTicks { get; set; }
+ public long EndPositionTicks { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs
new file mode 100644
index 000000000..d3a3bb1d0
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/SubtitleTrackInfo.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.MediaInfo
+{
+ public class SubtitleTrackInfo
+ {
+ public SubtitleTrackEvent[] TrackEvents { get; set; }
+
+ public SubtitleTrackInfo()
+ {
+ TrackEvents = new SubtitleTrackEvent[] { };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/MediaInfo/TransportStreamTimestamp.cs b/MediaBrowser.Model/MediaInfo/TransportStreamTimestamp.cs
new file mode 100644
index 000000000..4c808a8dc
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/TransportStreamTimestamp.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Model.MediaInfo
+{
+ public enum TransportStreamTimestamp
+ {
+ None,
+ Zero,
+ Valid
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/MediaInfo/VideoCodec.cs b/MediaBrowser.Model/MediaInfo/VideoCodec.cs
new file mode 100644
index 000000000..81755dac9
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/VideoCodec.cs
@@ -0,0 +1,14 @@
+namespace MediaBrowser.Model.MediaInfo
+{
+ public class VideoCodec
+ {
+ public const string H263 = "h263";
+ public const string H264 = "h264";
+ public const string H265 = "h265";
+ public const string MPEG4 = "mpeg4";
+ public const string MPEG1 = "mpeg1video";
+ public const string MPEG2 = "mpeg2video";
+ public const string MSMPEG4 = "msmpeg4";
+ public const string VC1 = "vc1";
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Net/EndPointInfo.cs b/MediaBrowser.Model/Net/EndPointInfo.cs
new file mode 100644
index 000000000..5a158e785
--- /dev/null
+++ b/MediaBrowser.Model/Net/EndPointInfo.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Model.Net
+{
+ public class EndPointInfo
+ {
+ public bool IsLocal { get; set; }
+ public bool IsInNetwork { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Net/HttpException.cs b/MediaBrowser.Model/Net/HttpException.cs
new file mode 100644
index 000000000..698b1bc7e
--- /dev/null
+++ b/MediaBrowser.Model/Net/HttpException.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Net;
+
+namespace MediaBrowser.Model.Net
+{
+ /// <summary>
+ /// Class HttpException
+ /// </summary>
+ public class HttpException : Exception
+ {
+ /// <summary>
+ /// Gets or sets the status code.
+ /// </summary>
+ /// <value>The status code.</value>
+ public HttpStatusCode? StatusCode { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is timed out.
+ /// </summary>
+ /// <value><c>true</c> if this instance is timed out; otherwise, <c>false</c>.</value>
+ public bool IsTimedOut { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpException" /> class.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="innerException">The inner exception.</param>
+ public HttpException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpException" /> class.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ public HttpException(string message)
+ : base(message)
+ {
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Net/HttpResponse.cs b/MediaBrowser.Model/Net/HttpResponse.cs
new file mode 100644
index 000000000..f4bd8e681
--- /dev/null
+++ b/MediaBrowser.Model/Net/HttpResponse.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+
+namespace MediaBrowser.Model.Net
+{
+ public class HttpResponse : IDisposable
+ {
+ /// <summary>
+ /// Gets or sets the type of the content.
+ /// </summary>
+ /// <value>The type of the content.</value>
+ public string ContentType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the response URL.
+ /// </summary>
+ /// <value>The response URL.</value>
+ public string ResponseUrl { get; set; }
+
+ /// <summary>
+ /// Gets or sets the content.
+ /// </summary>
+ /// <value>The content.</value>
+ public Stream Content { get; set; }
+
+ /// <summary>
+ /// Gets or sets the status code.
+ /// </summary>
+ /// <value>The status code.</value>
+ public HttpStatusCode StatusCode { get; set; }
+
+ /// <summary>
+ /// Gets or sets the length of the content.
+ /// </summary>
+ /// <value>The length of the content.</value>
+ public long? ContentLength { get; set; }
+
+ /// <summary>
+ /// Gets or sets the headers.
+ /// </summary>
+ /// <value>The headers.</value>
+ public Dictionary<string, string> Headers { get; set; }
+
+ private readonly IDisposable _disposable;
+
+ public HttpResponse(IDisposable disposable)
+ {
+ _disposable = disposable;
+ }
+ public HttpResponse()
+ {
+ }
+
+ public void Dispose()
+ {
+ if (_disposable != null)
+ {
+ _disposable.Dispose();
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Net/IAcceptSocket.cs b/MediaBrowser.Model/Net/IAcceptSocket.cs
new file mode 100644
index 000000000..af5a1fcfb
--- /dev/null
+++ b/MediaBrowser.Model/Net/IAcceptSocket.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace MediaBrowser.Model.Net
+{
+ public class SocketCreateException : Exception
+ {
+ public SocketCreateException(string errorCode, Exception originalException)
+ : base(errorCode, originalException)
+ {
+ ErrorCode = errorCode;
+ }
+
+ public string ErrorCode { get; private set; }
+ }
+}
diff --git a/MediaBrowser.Model/Net/ISocket.cs b/MediaBrowser.Model/Net/ISocket.cs
new file mode 100644
index 000000000..6a6781026
--- /dev/null
+++ b/MediaBrowser.Model/Net/ISocket.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Model.Net
+{
+ /// <summary>
+ /// Provides a common interface across platforms for UDP sockets used by this SSDP implementation.
+ /// </summary>
+ public interface ISocket : IDisposable
+ {
+ IpAddressInfo LocalIPAddress { get; }
+
+ Task<SocketReceiveResult> ReceiveAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken);
+
+ int Receive(byte[] buffer, int offset, int count);
+
+ IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback);
+ SocketReceiveResult EndReceive(IAsyncResult result);
+
+ /// <summary>
+ /// Sends a UDP message to a particular end point (uni or multicast).
+ /// </summary>
+ Task SendToAsync(byte[] buffer, int offset, int bytes, IpEndPointInfo endPoint, CancellationToken cancellationToken);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs
new file mode 100644
index 000000000..6a4b99600
--- /dev/null
+++ b/MediaBrowser.Model/Net/ISocketFactory.cs
@@ -0,0 +1,49 @@
+
+using System.IO;
+
+namespace MediaBrowser.Model.Net
+{
+ /// <summary>
+ /// Implemented by components that can create a platform specific UDP socket implementation, and wrap it in the cross platform <see cref="ISocket"/> interface.
+ /// </summary>
+ public interface ISocketFactory
+ {
+
+ /// <summary>
+ /// Createa a new unicast socket using the specified local port number.
+ /// </summary>
+ /// <param name="localPort">The local port to bind to.</param>
+ /// <returns>A <see cref="ISocket"/> implementation.</returns>
+ ISocket CreateUdpSocket(int localPort);
+
+ ISocket CreateUdpBroadcastSocket(int localPort);
+
+ ISocket CreateTcpSocket(IpAddressInfo remoteAddress, int remotePort);
+
+ /// <summary>
+ /// Createa a new unicast socket using the specified local port number.
+ /// </summary>
+ ISocket CreateSsdpUdpSocket(IpAddressInfo localIp, int localPort);
+
+ /// <summary>
+ /// Createa a new multicast socket using the specified multicast IP address, multicast time to live and local port.
+ /// </summary>
+ /// <param name="ipAddress">The multicast IP address to bind to.</param>
+ /// <param name="multicastTimeToLive">The multicast time to live value. Actually a maximum number of network hops for UDP packets.</param>
+ /// <param name="localPort">The local port to bind to.</param>
+ /// <returns>A <see cref="ISocket"/> implementation.</returns>
+ ISocket CreateUdpMulticastSocket(string ipAddress, int multicastTimeToLive, int localPort);
+
+ Stream CreateNetworkStream(ISocket socket, bool ownsSocket);
+ }
+
+ public enum SocketType
+ {
+ Stream
+ }
+
+ public enum ProtocolType
+ {
+ Tcp
+ }
+}
diff --git a/MediaBrowser.Model/Net/IpAddressInfo.cs b/MediaBrowser.Model/Net/IpAddressInfo.cs
new file mode 100644
index 000000000..520f8fb54
--- /dev/null
+++ b/MediaBrowser.Model/Net/IpAddressInfo.cs
@@ -0,0 +1,37 @@
+using System;
+
+namespace MediaBrowser.Model.Net
+{
+ public class IpAddressInfo
+ {
+ public static IpAddressInfo Any = new IpAddressInfo("0.0.0.0", IpAddressFamily.InterNetwork);
+ public static IpAddressInfo IPv6Any = new IpAddressInfo("00000000000000000000", IpAddressFamily.InterNetworkV6);
+ public static IpAddressInfo Loopback = new IpAddressInfo("127.0.0.1", IpAddressFamily.InterNetwork);
+ public static IpAddressInfo IPv6Loopback = new IpAddressInfo("::1", IpAddressFamily.InterNetworkV6);
+
+ public string Address { get; set; }
+ public IpAddressFamily AddressFamily { get; set; }
+
+ public IpAddressInfo(string address, IpAddressFamily addressFamily)
+ {
+ Address = address;
+ AddressFamily = addressFamily;
+ }
+
+ public bool Equals(IpAddressInfo address)
+ {
+ return string.Equals(address.Address, Address, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public override String ToString()
+ {
+ return Address;
+ }
+ }
+
+ public enum IpAddressFamily
+ {
+ InterNetwork,
+ InterNetworkV6
+ }
+}
diff --git a/MediaBrowser.Model/Net/IpEndPointInfo.cs b/MediaBrowser.Model/Net/IpEndPointInfo.cs
new file mode 100644
index 000000000..b5cadc429
--- /dev/null
+++ b/MediaBrowser.Model/Net/IpEndPointInfo.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Globalization;
+
+namespace MediaBrowser.Model.Net
+{
+ public class IpEndPointInfo
+ {
+ public IpAddressInfo IpAddress { get; set; }
+
+ public int Port { get; set; }
+
+ public IpEndPointInfo()
+ {
+
+ }
+
+ public IpEndPointInfo(IpAddressInfo address, int port)
+ {
+ IpAddress = address;
+ Port = port;
+ }
+
+ public override string ToString()
+ {
+ var ipAddresString = IpAddress == null ? string.Empty : IpAddress.ToString();
+
+ return ipAddresString + ":" + Port.ToString(CultureInfo.InvariantCulture);
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs
new file mode 100644
index 000000000..d66d62fea
--- /dev/null
+++ b/MediaBrowser.Model/Net/MimeTypes.cs
@@ -0,0 +1,346 @@
+using MediaBrowser.Model.Extensions;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Model.Net
+{
+ /// <summary>
+ /// Class MimeTypes
+ /// </summary>
+ public static class MimeTypes
+ {
+ /// <summary>
+ /// Any extension in this list is considered a video file - can be added to at runtime for extensibility
+ /// </summary>
+ private static readonly string[] VideoFileExtensions = new string[]
+ {
+ ".mkv",
+ ".m2t",
+ ".m2ts",
+ ".img",
+ ".iso",
+ ".mk3d",
+ ".ts",
+ ".rmvb",
+ ".mov",
+ ".avi",
+ ".mpg",
+ ".mpeg",
+ ".wmv",
+ ".mp4",
+ ".divx",
+ ".dvr-ms",
+ ".wtv",
+ ".ogm",
+ ".ogv",
+ ".asf",
+ ".m4v",
+ ".flv",
+ ".f4v",
+ ".3gp",
+ ".webm",
+ ".mts",
+ ".m2v",
+ ".rec"
+ };
+
+ private static Dictionary<string, string> GetVideoFileExtensionsDictionary()
+ {
+ Dictionary<string, string> dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (string ext in VideoFileExtensions)
+ {
+ dict[ext] = ext;
+ }
+
+ return dict;
+ }
+
+ private static readonly Dictionary<string, string> VideoFileExtensionsDictionary = GetVideoFileExtensionsDictionary();
+
+ // http://en.wikipedia.org/wiki/Internet_media_type
+ // Add more as needed
+
+ private static Dictionary<string, string> GetMimeTypeLookup()
+ {
+ Dictionary<string, string> dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+ dict.Add(".jpg", "image/jpeg");
+ dict.Add(".jpeg", "image/jpeg");
+ dict.Add(".tbn", "image/jpeg");
+ dict.Add(".png", "image/png");
+ dict.Add(".gif", "image/gif");
+ dict.Add(".tiff", "image/tiff");
+ dict.Add(".webp", "image/webp");
+ dict.Add(".ico", "image/vnd.microsoft.icon");
+ dict.Add(".mpg", "video/mpeg");
+ dict.Add(".mpeg", "video/mpeg");
+ dict.Add(".ogv", "video/ogg");
+ dict.Add(".mov", "video/quicktime");
+ dict.Add(".webm", "video/webm");
+ dict.Add(".mkv", "video/x-matroska");
+ dict.Add(".wmv", "video/x-ms-wmv");
+ dict.Add(".flv", "video/x-flv");
+ dict.Add(".avi", "video/x-msvideo");
+ dict.Add(".asf", "video/x-ms-asf");
+ dict.Add(".m4v", "video/x-m4v");
+ dict.Add(".m4s", "video/mp4");
+ dict.Add(".cbz", "application/x-cbz");
+ dict.Add(".cbr", "application/epub+zip");
+ dict.Add(".epub", "application/epub+zip");
+ dict.Add(".pdf", "application/pdf");
+ dict.Add(".mobi", "application/x-mobipocket-ebook");
+
+ dict.Add(".ass", "text/x-ssa");
+ dict.Add(".ssa", "text/x-ssa");
+
+ return dict;
+ }
+
+ private static readonly Dictionary<string, string> MimeTypeLookup = GetMimeTypeLookup();
+
+ private static readonly Dictionary<string, string> ExtensionLookup = CreateExtensionLookup();
+
+ private static Dictionary<string, string> CreateExtensionLookup()
+ {
+ var dict = MimeTypeLookup
+ .GroupBy(i => i.Value)
+ .ToDictionary(x => x.Key, x => x.First().Key, StringComparer.OrdinalIgnoreCase);
+
+ dict["image/jpg"] = ".jpg";
+ dict["image/x-png"] = ".png";
+
+ return dict;
+ }
+
+ public static string GetMimeType(string path)
+ {
+ return GetMimeType(path, true);
+ }
+
+ /// <summary>
+ /// Gets the type of the MIME.
+ /// </summary>
+ public static string GetMimeType(string path, bool enableStreamDefault)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ var ext = Path.GetExtension(path) ?? string.Empty;
+
+ string result;
+ if (MimeTypeLookup.TryGetValue(ext, out result))
+ {
+ return result;
+ }
+
+ // Type video
+ if (StringHelper.EqualsIgnoreCase(ext, ".3gp"))
+ {
+ return "video/3gpp";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".3g2"))
+ {
+ return "video/3gpp2";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".ts"))
+ {
+ return "video/mp2t";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".mpd"))
+ {
+ return "video/vnd.mpeg.dash.mpd";
+ }
+
+ // Catch-all for all video types that don't require specific mime types
+ if (VideoFileExtensionsDictionary.ContainsKey(ext))
+ {
+ return "video/" + ext.TrimStart('.').ToLower();
+ }
+
+ // Type text
+ if (StringHelper.EqualsIgnoreCase(ext, ".css"))
+ {
+ return "text/css";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".csv"))
+ {
+ return "text/csv";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".html"))
+ {
+ return "text/html; charset=UTF-8";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".htm"))
+ {
+ return "text/html; charset=UTF-8";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".txt"))
+ {
+ return "text/plain";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".xml"))
+ {
+ return "application/xml";
+ }
+
+ // Type audio
+ if (StringHelper.EqualsIgnoreCase(ext, ".mp3"))
+ {
+ return "audio/mpeg";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".m4a"))
+ {
+ return "audio/mp4";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".aac"))
+ {
+ return "audio/mp4";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".webma"))
+ {
+ return "audio/webm";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".wav"))
+ {
+ return "audio/wav";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".wma"))
+ {
+ return "audio/x-ms-wma";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".flac"))
+ {
+ return "audio/flac";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".aac"))
+ {
+ return "audio/x-aac";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".ogg"))
+ {
+ return "audio/ogg";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".oga"))
+ {
+ return "audio/ogg";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".opus"))
+ {
+ return "audio/ogg";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".ac3"))
+ {
+ return "audio/ac3";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".dsf"))
+ {
+ return "audio/dsf";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".m4b"))
+ {
+ return "audio/m4b";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".xsp"))
+ {
+ return "audio/xsp";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".dsp"))
+ {
+ return "audio/dsp";
+ }
+
+ // Playlists
+ if (StringHelper.EqualsIgnoreCase(ext, ".m3u8"))
+ {
+ return "application/x-mpegURL";
+ }
+
+ // Misc
+ if (StringHelper.EqualsIgnoreCase(ext, ".dll"))
+ {
+ return "application/octet-stream";
+ }
+
+ // Web
+ if (StringHelper.EqualsIgnoreCase(ext, ".js"))
+ {
+ return "application/x-javascript";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".json"))
+ {
+ return "application/json";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".map"))
+ {
+ return "application/x-javascript";
+ }
+
+ if (StringHelper.EqualsIgnoreCase(ext, ".woff"))
+ {
+ return "font/woff";
+ }
+
+ if (StringHelper.EqualsIgnoreCase(ext, ".ttf"))
+ {
+ return "font/ttf";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".eot"))
+ {
+ return "application/vnd.ms-fontobject";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".svg"))
+ {
+ return "image/svg+xml";
+ }
+ if (StringHelper.EqualsIgnoreCase(ext, ".svgz"))
+ {
+ return "image/svg+xml";
+ }
+
+ if (StringHelper.EqualsIgnoreCase(ext, ".srt"))
+ {
+ return "text/plain";
+ }
+
+ if (StringHelper.EqualsIgnoreCase(ext, ".vtt"))
+ {
+ return "text/vtt";
+ }
+
+ if (StringHelper.EqualsIgnoreCase(ext, ".ttml"))
+ {
+ return "application/ttml+xml";
+ }
+
+ if (enableStreamDefault)
+ {
+ return "application/octet-stream";
+ }
+
+ return null;
+ }
+
+ public static string ToExtension(string mimeType)
+ {
+ if (string.IsNullOrEmpty(mimeType))
+ {
+ throw new ArgumentNullException("mimeType");
+ }
+
+ // handle text/html; charset=UTF-8
+ mimeType = mimeType.Split(';')[0];
+
+ string result;
+ if (ExtensionLookup.TryGetValue(mimeType, out result))
+ {
+ return result;
+ }
+ return null;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Net/NetworkShare.cs b/MediaBrowser.Model/Net/NetworkShare.cs
new file mode 100644
index 000000000..5ce84eeed
--- /dev/null
+++ b/MediaBrowser.Model/Net/NetworkShare.cs
@@ -0,0 +1,31 @@
+
+namespace MediaBrowser.Model.Net
+{
+ public class NetworkShare
+ {
+ /// <summary>
+ /// The name of the computer that this share belongs to
+ /// </summary>
+ public string Server { get; set; }
+
+ /// <summary>
+ /// Share name
+ /// </summary>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Local path
+ /// </summary>
+ public string Path { get; set; }
+
+ /// <summary>
+ /// Share type
+ /// </summary>
+ public NetworkShareType ShareType { get; set; }
+
+ /// <summary>
+ /// Comment
+ /// </summary>
+ public string Remark { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Net/NetworkShareType.cs b/MediaBrowser.Model/Net/NetworkShareType.cs
new file mode 100644
index 000000000..41dc9003e
--- /dev/null
+++ b/MediaBrowser.Model/Net/NetworkShareType.cs
@@ -0,0 +1,30 @@
+
+namespace MediaBrowser.Model.Net
+{
+ /// <summary>
+ /// Enum NetworkShareType
+ /// </summary>
+ public enum NetworkShareType
+ {
+ /// <summary>
+ /// Disk share
+ /// </summary>
+ Disk,
+ /// <summary>
+ /// Printer share
+ /// </summary>
+ Printer,
+ /// <summary>
+ /// Device share
+ /// </summary>
+ Device,
+ /// <summary>
+ /// IPC share
+ /// </summary>
+ Ipc,
+ /// <summary>
+ /// Special share
+ /// </summary>
+ Special
+ }
+}
diff --git a/MediaBrowser.Model/Net/SocketReceiveResult.cs b/MediaBrowser.Model/Net/SocketReceiveResult.cs
new file mode 100644
index 000000000..483e2297b
--- /dev/null
+++ b/MediaBrowser.Model/Net/SocketReceiveResult.cs
@@ -0,0 +1,25 @@
+
+namespace MediaBrowser.Model.Net
+{
+ /// <summary>
+ /// Used by the sockets wrapper to hold raw data received from a UDP socket.
+ /// </summary>
+ public sealed class SocketReceiveResult
+ {
+ /// <summary>
+ /// The buffer to place received data into.
+ /// </summary>
+ public byte[] Buffer { get; set; }
+
+ /// <summary>
+ /// The number of bytes received.
+ /// </summary>
+ public int ReceivedBytes { get; set; }
+
+ /// <summary>
+ /// The <see cref="IpEndPointInfo"/> the data was received from.
+ /// </summary>
+ public IpEndPointInfo RemoteEndPoint { get; set; }
+ public IpAddressInfo LocalIPAddress { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Net/WebSocketMessage.cs b/MediaBrowser.Model/Net/WebSocketMessage.cs
new file mode 100644
index 000000000..c049a96ef
--- /dev/null
+++ b/MediaBrowser.Model/Net/WebSocketMessage.cs
@@ -0,0 +1,24 @@
+
+namespace MediaBrowser.Model.Net
+{
+ /// <summary>
+ /// Class WebSocketMessage
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public class WebSocketMessage<T>
+ {
+ /// <summary>
+ /// Gets or sets the type of the message.
+ /// </summary>
+ /// <value>The type of the message.</value>
+ public string MessageType { get; set; }
+ public string MessageId { get; set; }
+ public string ServerId { get; set; }
+ /// <summary>
+ /// Gets or sets the data.
+ /// </summary>
+ /// <value>The data.</value>
+ public T Data { get; set; }
+ }
+
+}
diff --git a/MediaBrowser.Model/News/INewsService.cs b/MediaBrowser.Model/News/INewsService.cs
new file mode 100644
index 000000000..4c92664d9
--- /dev/null
+++ b/MediaBrowser.Model/News/INewsService.cs
@@ -0,0 +1,17 @@
+using MediaBrowser.Model.Querying;
+
+namespace MediaBrowser.Model.News
+{
+ /// <summary>
+ /// Interface INewsFeed
+ /// </summary>
+ public interface INewsService
+ {
+ /// <summary>
+ /// Gets the product news.
+ /// </summary>
+ /// <param name="query">The query.</param>
+ /// <returns>QueryResult{NewsItem}.</returns>
+ QueryResult<NewsItem> GetProductNews(NewsQuery query);
+ }
+}
diff --git a/MediaBrowser.Model/News/NewsItem.cs b/MediaBrowser.Model/News/NewsItem.cs
new file mode 100644
index 000000000..2a05c420a
--- /dev/null
+++ b/MediaBrowser.Model/News/NewsItem.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace MediaBrowser.Model.News
+{
+ public class NewsItem
+ {
+ public string Title { get; set; }
+ public string Link { get; set; }
+ public string Description { get; set; }
+ public string DescriptionHtml { get; set; }
+ public string Guid { get; set; }
+ public DateTime Date { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/News/NewsQuery.cs b/MediaBrowser.Model/News/NewsQuery.cs
new file mode 100644
index 000000000..567888921
--- /dev/null
+++ b/MediaBrowser.Model/News/NewsQuery.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Model.News
+{
+ public class NewsQuery
+ {
+ public int? StartIndex { get; set; }
+
+ public int? Limit { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Notifications/NotificationLevel.cs b/MediaBrowser.Model/Notifications/NotificationLevel.cs
new file mode 100644
index 000000000..a49ee2fe6
--- /dev/null
+++ b/MediaBrowser.Model/Notifications/NotificationLevel.cs
@@ -0,0 +1,10 @@
+
+namespace MediaBrowser.Model.Notifications
+{
+ public enum NotificationLevel
+ {
+ Normal = 0,
+ Warning = 1,
+ Error = 2
+ }
+}
diff --git a/MediaBrowser.Model/Notifications/NotificationOption.cs b/MediaBrowser.Model/Notifications/NotificationOption.cs
new file mode 100644
index 000000000..cda9e311d
--- /dev/null
+++ b/MediaBrowser.Model/Notifications/NotificationOption.cs
@@ -0,0 +1,56 @@
+using System;
+
+namespace MediaBrowser.Model.Notifications
+{
+ public class NotificationOption
+ {
+ public string Type { get; set; }
+
+ /// <summary>
+ /// User Ids to not monitor (it's opt out)
+ /// </summary>
+ public string[] DisabledMonitorUsers { get; set; }
+
+ /// <summary>
+ /// User Ids to send to (if SendToUserMode == Custom)
+ /// </summary>
+ public string[] SendToUsers { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="NotificationOption"/> is enabled.
+ /// </summary>
+ /// <value><c>true</c> if enabled; otherwise, <c>false</c>.</value>
+ public bool Enabled { get; set; }
+
+ /// <summary>
+ /// Gets or sets the title format string.
+ /// </summary>
+ /// <value>The title format string.</value>
+ public string Title { get; set; }
+
+ /// <summary>
+ /// Gets or sets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public string Description { get; set; }
+
+ /// <summary>
+ /// Gets or sets the disabled services.
+ /// </summary>
+ /// <value>The disabled services.</value>
+ public string[] DisabledServices { get; set; }
+
+ /// <summary>
+ /// Gets or sets the send to user mode.
+ /// </summary>
+ /// <value>The send to user mode.</value>
+ public SendToUserType SendToUserMode { get; set; }
+
+ public NotificationOption()
+ {
+ DisabledServices = new string[] {};
+ DisabledMonitorUsers = new string[] {};
+ SendToUsers = new string[] {};
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Notifications/NotificationOptions.cs b/MediaBrowser.Model/Notifications/NotificationOptions.cs
new file mode 100644
index 000000000..158e00b39
--- /dev/null
+++ b/MediaBrowser.Model/Notifications/NotificationOptions.cs
@@ -0,0 +1,132 @@
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.Users;
+using System;
+
+namespace MediaBrowser.Model.Notifications
+{
+ public class NotificationOptions
+ {
+ public NotificationOption[] Options { get; set; }
+
+ public NotificationOptions()
+ {
+ Options = new[]
+ {
+ new NotificationOption
+ {
+ Type = NotificationType.TaskFailed.ToString(),
+ Enabled = true,
+ SendToUserMode = SendToUserType.Admins
+ },
+ new NotificationOption
+ {
+ Type = NotificationType.ServerRestartRequired.ToString(),
+ Enabled = true,
+ SendToUserMode = SendToUserType.Admins
+ },
+ new NotificationOption
+ {
+ Type = NotificationType.ApplicationUpdateAvailable.ToString(),
+ Enabled = true,
+ SendToUserMode = SendToUserType.Admins
+ },
+ new NotificationOption
+ {
+ Type = NotificationType.ApplicationUpdateInstalled.ToString(),
+ Enabled = true,
+ SendToUserMode = SendToUserType.Admins
+ },
+ new NotificationOption
+ {
+ Type = NotificationType.PluginUpdateInstalled.ToString(),
+ Enabled = true,
+ SendToUserMode = SendToUserType.Admins
+ },
+ new NotificationOption
+ {
+ Type = NotificationType.PluginUninstalled.ToString(),
+ Enabled = true,
+ SendToUserMode = SendToUserType.Admins
+ },
+ new NotificationOption
+ {
+ Type = NotificationType.InstallationFailed.ToString(),
+ Enabled = true,
+ SendToUserMode = SendToUserType.Admins
+ },
+ new NotificationOption
+ {
+ Type = NotificationType.PluginInstalled.ToString(),
+ Enabled = true,
+ SendToUserMode = SendToUserType.Admins
+ },
+ new NotificationOption
+ {
+ Type = NotificationType.PluginError.ToString(),
+ Enabled = true,
+ SendToUserMode = SendToUserType.Admins
+ },
+ new NotificationOption
+ {
+ Type = NotificationType.UserLockedOut.ToString(),
+ Enabled = true,
+ SendToUserMode = SendToUserType.Admins
+ }
+ };
+ }
+
+ public NotificationOption GetOptions(string type)
+ {
+ foreach (NotificationOption i in Options)
+ {
+ if (StringHelper.EqualsIgnoreCase(type, i.Type)) return i;
+ }
+ return null;
+ }
+
+ public bool IsEnabled(string type)
+ {
+ NotificationOption opt = GetOptions(type);
+
+ return opt != null && opt.Enabled;
+ }
+
+ public bool IsServiceEnabled(string service, string notificationType)
+ {
+ NotificationOption opt = GetOptions(notificationType);
+
+ return opt == null ||
+ !ListHelper.ContainsIgnoreCase(opt.DisabledServices, service);
+ }
+
+ public bool IsEnabledToMonitorUser(string type, Guid userId)
+ {
+ NotificationOption opt = GetOptions(type);
+
+ return opt != null && opt.Enabled &&
+ !ListHelper.ContainsIgnoreCase(opt.DisabledMonitorUsers, userId.ToString(""));
+ }
+
+ public bool IsEnabledToSendToUser(string type, string userId, UserPolicy userPolicy)
+ {
+ NotificationOption opt = GetOptions(type);
+
+ if (opt != null && opt.Enabled)
+ {
+ if (opt.SendToUserMode == SendToUserType.All)
+ {
+ return true;
+ }
+
+ if (opt.SendToUserMode == SendToUserType.Admins && userPolicy.IsAdministrator)
+ {
+ return true;
+ }
+
+ return ListHelper.ContainsIgnoreCase(opt.SendToUsers, userId);
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Notifications/NotificationRequest.cs b/MediaBrowser.Model/Notifications/NotificationRequest.cs
new file mode 100644
index 000000000..9c89e40fb
--- /dev/null
+++ b/MediaBrowser.Model/Notifications/NotificationRequest.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Notifications
+{
+ public class NotificationRequest
+ {
+ public string Name { get; set; }
+
+ public string Description { get; set; }
+
+ public string Url { get; set; }
+
+ public NotificationLevel Level { get; set; }
+
+ public Guid[] UserIds { get; set; }
+
+ public DateTime Date { get; set; }
+
+ /// <summary>
+ /// The corresponding type name used in configuration. Not for display.
+ /// </summary>
+ public string NotificationType { get; set; }
+
+ public Dictionary<string, string> Variables { get; set; }
+
+ public SendToUserType? SendToUserMode { get; set; }
+
+ public NotificationRequest()
+ {
+ UserIds = Array.Empty<Guid>();
+ Date = DateTime.UtcNow;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Notifications/NotificationServiceInfo.cs b/MediaBrowser.Model/Notifications/NotificationServiceInfo.cs
new file mode 100644
index 000000000..0ffe7d4ae
--- /dev/null
+++ b/MediaBrowser.Model/Notifications/NotificationServiceInfo.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Model.Notifications
+{
+ public class NotificationServiceInfo
+ {
+ public string Name { get; set; }
+ public string Id { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Notifications/NotificationType.cs b/MediaBrowser.Model/Notifications/NotificationType.cs
new file mode 100644
index 000000000..eefd15808
--- /dev/null
+++ b/MediaBrowser.Model/Notifications/NotificationType.cs
@@ -0,0 +1,24 @@
+namespace MediaBrowser.Model.Notifications
+{
+ public enum NotificationType
+ {
+ ApplicationUpdateAvailable,
+ ApplicationUpdateInstalled,
+ AudioPlayback,
+ GamePlayback,
+ VideoPlayback,
+ AudioPlaybackStopped,
+ GamePlaybackStopped,
+ VideoPlaybackStopped,
+ InstallationFailed,
+ PluginError,
+ PluginInstalled,
+ PluginUpdateInstalled,
+ PluginUninstalled,
+ NewLibraryContent,
+ ServerRestartRequired,
+ TaskFailed,
+ CameraImageUploaded,
+ UserLockedOut
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Notifications/NotificationTypeInfo.cs b/MediaBrowser.Model/Notifications/NotificationTypeInfo.cs
new file mode 100644
index 000000000..4c3283d8e
--- /dev/null
+++ b/MediaBrowser.Model/Notifications/NotificationTypeInfo.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using System;
+
+namespace MediaBrowser.Model.Notifications
+{
+ public class NotificationTypeInfo
+ {
+ public string Type { get; set; }
+
+ public string Name { get; set; }
+
+ public bool Enabled { get; set; }
+
+ public string Category { get; set; }
+
+ public bool IsBasedOnUserEvent { get; set; }
+
+ public string DefaultTitle { get; set; }
+
+ public string DefaultDescription { get; set; }
+
+ public string[] Variables { get; set; }
+
+ public NotificationTypeInfo()
+ {
+ Variables = new string[] {};
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Notifications/SendToUserType.cs b/MediaBrowser.Model/Notifications/SendToUserType.cs
new file mode 100644
index 000000000..1998d3102
--- /dev/null
+++ b/MediaBrowser.Model/Notifications/SendToUserType.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Model.Notifications
+{
+ public enum SendToUserType
+ {
+ All = 0,
+ Admins = 1,
+ Custom = 2
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
new file mode 100644
index 000000000..4c5c8bf51
--- /dev/null
+++ b/MediaBrowser.Model/Playlists/PlaylistCreationRequest.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System;
+
+namespace MediaBrowser.Model.Playlists
+{
+ public class PlaylistCreationRequest
+ {
+ public string Name { get; set; }
+
+ public Guid[] ItemIdList { get; set; }
+
+ public string MediaType { get; set; }
+
+ public Guid UserId { get; set; }
+
+ public PlaylistCreationRequest()
+ {
+ ItemIdList = Array.Empty<Guid>();
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Playlists/PlaylistCreationResult.cs b/MediaBrowser.Model/Playlists/PlaylistCreationResult.cs
new file mode 100644
index 000000000..bbab8a18d
--- /dev/null
+++ b/MediaBrowser.Model/Playlists/PlaylistCreationResult.cs
@@ -0,0 +1,8 @@
+
+namespace MediaBrowser.Model.Playlists
+{
+ public class PlaylistCreationResult
+ {
+ public string Id { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Playlists/PlaylistItemQuery.cs b/MediaBrowser.Model/Playlists/PlaylistItemQuery.cs
new file mode 100644
index 000000000..0f6a0c8c5
--- /dev/null
+++ b/MediaBrowser.Model/Playlists/PlaylistItemQuery.cs
@@ -0,0 +1,37 @@
+using MediaBrowser.Model.Querying;
+
+namespace MediaBrowser.Model.Playlists
+{
+ public class PlaylistItemQuery
+ {
+ /// <summary>
+ /// Gets or sets the identifier.
+ /// </summary>
+ /// <value>The identifier.</value>
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user identifier.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public string UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start index.
+ /// </summary>
+ /// <value>The start index.</value>
+ public int? StartIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the limit.
+ /// </summary>
+ /// <value>The limit.</value>
+ public int? Limit { get; set; }
+
+ /// <summary>
+ /// Gets or sets the fields.
+ /// </summary>
+ /// <value>The fields.</value>
+ public ItemFields[] Fields { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Plugins/BasePluginConfiguration.cs b/MediaBrowser.Model/Plugins/BasePluginConfiguration.cs
new file mode 100644
index 000000000..9a8bfadd1
--- /dev/null
+++ b/MediaBrowser.Model/Plugins/BasePluginConfiguration.cs
@@ -0,0 +1,10 @@
+
+namespace MediaBrowser.Model.Plugins
+{
+ /// <summary>
+ /// Class BasePluginConfiguration
+ /// </summary>
+ public class BasePluginConfiguration
+ {
+ }
+}
diff --git a/MediaBrowser.Model/Plugins/IHasWebPages.cs b/MediaBrowser.Model/Plugins/IHasWebPages.cs
new file mode 100644
index 000000000..0745c3c60
--- /dev/null
+++ b/MediaBrowser.Model/Plugins/IHasWebPages.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Plugins
+{
+ public interface IHasWebPages
+ {
+ IEnumerable<PluginPageInfo> GetPages();
+ }
+}
diff --git a/MediaBrowser.Model/Plugins/PluginInfo.cs b/MediaBrowser.Model/Plugins/PluginInfo.cs
new file mode 100644
index 000000000..e7b16b0ec
--- /dev/null
+++ b/MediaBrowser.Model/Plugins/PluginInfo.cs
@@ -0,0 +1,45 @@
+using System;
+
+namespace MediaBrowser.Model.Plugins
+{
+ /// <summary>
+ /// This is a serializable stub class that is used by the api to provide information about installed plugins.
+ /// </summary>
+ public class PluginInfo
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the version.
+ /// </summary>
+ /// <value>The version.</value>
+ public string Version { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the configuration file.
+ /// </summary>
+ /// <value>The name of the configuration file.</value>
+ public string ConfigurationFileName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public string Description { get; set; }
+
+ /// <summary>
+ /// Gets or sets the unique id.
+ /// </summary>
+ /// <value>The unique id.</value>
+ public string Id { get; set; }
+ /// <summary>
+ /// Gets or sets the image URL.
+ /// </summary>
+ /// <value>The image URL.</value>
+ public string ImageUrl { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Plugins/PluginPageInfo.cs b/MediaBrowser.Model/Plugins/PluginPageInfo.cs
new file mode 100644
index 000000000..045a0072c
--- /dev/null
+++ b/MediaBrowser.Model/Plugins/PluginPageInfo.cs
@@ -0,0 +1,17 @@
+namespace MediaBrowser.Model.Plugins
+{
+ public class PluginPageInfo
+ {
+ public string Name { get; set; }
+
+ public string DisplayName { get; set; }
+
+ public string EmbeddedResourcePath { get; set; }
+
+ public bool EnableInMainMenu { get; set; }
+
+ public string MenuSection { get; set; }
+
+ public string MenuIcon { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Properties/AssemblyInfo.cs b/MediaBrowser.Model/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..fabfd908b
--- /dev/null
+++ b/MediaBrowser.Model/Properties/AssemblyInfo.cs
@@ -0,0 +1,23 @@
+using System.Reflection;
+using System.Resources;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("MediaBrowser.Model")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.Model")]
+[assembly: AssemblyCopyright("Copyright © 2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+[assembly: NeutralResourcesLanguage("en")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+// \ No newline at end of file
diff --git a/MediaBrowser.Model/Providers/ExternalIdInfo.cs b/MediaBrowser.Model/Providers/ExternalIdInfo.cs
new file mode 100644
index 000000000..2c5cfe91b
--- /dev/null
+++ b/MediaBrowser.Model/Providers/ExternalIdInfo.cs
@@ -0,0 +1,24 @@
+
+namespace MediaBrowser.Model.Providers
+{
+ public class ExternalIdInfo
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the key.
+ /// </summary>
+ /// <value>The key.</value>
+ public string Key { get; set; }
+
+ /// <summary>
+ /// Gets or sets the URL format string.
+ /// </summary>
+ /// <value>The URL format string.</value>
+ public string UrlFormatString { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Providers/ExternalUrl.cs b/MediaBrowser.Model/Providers/ExternalUrl.cs
new file mode 100644
index 000000000..fb744f446
--- /dev/null
+++ b/MediaBrowser.Model/Providers/ExternalUrl.cs
@@ -0,0 +1,17 @@
+namespace MediaBrowser.Model.Providers
+{
+ public class ExternalUrl
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the item.
+ /// </summary>
+ /// <value>The type of the item.</value>
+ public string Url { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Providers/ImageProviderInfo.cs b/MediaBrowser.Model/Providers/ImageProviderInfo.cs
new file mode 100644
index 000000000..199552640
--- /dev/null
+++ b/MediaBrowser.Model/Providers/ImageProviderInfo.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.Providers
+{
+ /// <summary>
+ /// Class ImageProviderInfo.
+ /// </summary>
+ public class ImageProviderInfo
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ public ImageType[] SupportedImages { get; set; }
+
+ public ImageProviderInfo()
+ {
+ SupportedImages = new ImageType[] { };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Providers/RemoteImageInfo.cs b/MediaBrowser.Model/Providers/RemoteImageInfo.cs
new file mode 100644
index 000000000..6db7f77bd
--- /dev/null
+++ b/MediaBrowser.Model/Providers/RemoteImageInfo.cs
@@ -0,0 +1,71 @@
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.Providers
+{
+ /// <summary>
+ /// Class RemoteImageInfo
+ /// </summary>
+ public class RemoteImageInfo
+ {
+ /// <summary>
+ /// Gets or sets the name of the provider.
+ /// </summary>
+ /// <value>The name of the provider.</value>
+ public string ProviderName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the URL.
+ /// </summary>
+ /// <value>The URL.</value>
+ public string Url { get; set; }
+
+ /// <summary>
+ /// Gets a url used for previewing a smaller version
+ /// </summary>
+ public string ThumbnailUrl { get; set; }
+
+ /// <summary>
+ /// Gets or sets the height.
+ /// </summary>
+ /// <value>The height.</value>
+ public int? Height { get; set; }
+
+ /// <summary>
+ /// Gets or sets the width.
+ /// </summary>
+ /// <value>The width.</value>
+ public int? Width { get; set; }
+
+ /// <summary>
+ /// Gets or sets the community rating.
+ /// </summary>
+ /// <value>The community rating.</value>
+ public double? CommunityRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the vote count.
+ /// </summary>
+ /// <value>The vote count.</value>
+ public int? VoteCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets the language.
+ /// </summary>
+ /// <value>The language.</value>
+ public string Language { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ public ImageType Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the rating.
+ /// </summary>
+ /// <value>The type of the rating.</value>
+ public RatingType RatingType { get; set; }
+ }
+
+}
diff --git a/MediaBrowser.Model/Providers/RemoteImageQuery.cs b/MediaBrowser.Model/Providers/RemoteImageQuery.cs
new file mode 100644
index 000000000..8d5231a25
--- /dev/null
+++ b/MediaBrowser.Model/Providers/RemoteImageQuery.cs
@@ -0,0 +1,15 @@
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.Providers
+{
+ public class RemoteImageQuery
+ {
+ public string ProviderName { get; set; }
+
+ public ImageType? ImageType { get; set; }
+
+ public bool IncludeDisabledProviders { get; set; }
+
+ public bool IncludeAllLanguages { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Providers/RemoteImageResult.cs b/MediaBrowser.Model/Providers/RemoteImageResult.cs
new file mode 100644
index 000000000..7e38badfc
--- /dev/null
+++ b/MediaBrowser.Model/Providers/RemoteImageResult.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Providers
+{
+ /// <summary>
+ /// Class RemoteImageResult.
+ /// </summary>
+ public class RemoteImageResult
+ {
+ /// <summary>
+ /// Gets or sets the images.
+ /// </summary>
+ /// <value>The images.</value>
+ public RemoteImageInfo[] Images { get; set; }
+
+ /// <summary>
+ /// Gets or sets the total record count.
+ /// </summary>
+ /// <value>The total record count.</value>
+ public int TotalRecordCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets the providers.
+ /// </summary>
+ /// <value>The providers.</value>
+ public string[] Providers { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Providers/RemoteSearchResult.cs b/MediaBrowser.Model/Providers/RemoteSearchResult.cs
new file mode 100644
index 000000000..b63cf2a9f
--- /dev/null
+++ b/MediaBrowser.Model/Providers/RemoteSearchResult.cs
@@ -0,0 +1,46 @@
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Providers
+{
+ public class RemoteSearchResult : IHasProviderIds
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the provider ids.
+ /// </summary>
+ /// <value>The provider ids.</value>
+ public Dictionary<string, string> ProviderIds { get; set; }
+ /// <summary>
+ /// Gets or sets the year.
+ /// </summary>
+ /// <value>The year.</value>
+ public int? ProductionYear { get; set; }
+ public int? IndexNumber { get; set; }
+ public int? IndexNumberEnd { get; set; }
+ public int? ParentIndexNumber { get; set; }
+
+ public DateTime? PremiereDate { get; set; }
+
+ public string ImageUrl { get; set; }
+
+ public string SearchProviderName { get; set; }
+
+ public string GameSystem { get; set; }
+ public string Overview { get; set; }
+
+ public RemoteSearchResult AlbumArtist { get; set; }
+ public RemoteSearchResult[] Artists { get; set; }
+
+ public RemoteSearchResult()
+ {
+ ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ Artists = new RemoteSearchResult[] { };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs b/MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs
new file mode 100644
index 000000000..0a4a52cd5
--- /dev/null
+++ b/MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace MediaBrowser.Model.Providers
+{
+ public class RemoteSubtitleInfo
+ {
+ public string ThreeLetterISOLanguageName { get; set; }
+ public string Id { get; set; }
+ public string ProviderName { get; set; }
+ public string Name { get; set; }
+ public string Format { get; set; }
+ public string Author { get; set; }
+ public string Comment { get; set; }
+ public DateTime? DateCreated { get; set; }
+ public float? CommunityRating { get; set; }
+ public int? DownloadCount { get; set; }
+ public bool? IsHashMatch { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Providers/SubtitleOptions.cs b/MediaBrowser.Model/Providers/SubtitleOptions.cs
new file mode 100644
index 000000000..f67b8870d
--- /dev/null
+++ b/MediaBrowser.Model/Providers/SubtitleOptions.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace MediaBrowser.Model.Providers
+{
+ public class SubtitleOptions
+ {
+ public bool SkipIfEmbeddedSubtitlesPresent { get; set; }
+ public bool SkipIfAudioTrackMatches { get; set; }
+ public string[] DownloadLanguages { get; set; }
+ public bool DownloadMovieSubtitles { get; set; }
+ public bool DownloadEpisodeSubtitles { get; set; }
+
+ public string OpenSubtitlesUsername { get; set; }
+ public string OpenSubtitlesPasswordHash { get; set; }
+ public bool IsOpenSubtitleVipAccount { get; set; }
+
+ public bool RequirePerfectMatch { get; set; }
+
+ public SubtitleOptions()
+ {
+ DownloadLanguages = new string[] {};
+
+ SkipIfAudioTrackMatches = true;
+ RequirePerfectMatch = true;
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Providers/SubtitleProviderInfo.cs b/MediaBrowser.Model/Providers/SubtitleProviderInfo.cs
new file mode 100644
index 000000000..ecce18bd5
--- /dev/null
+++ b/MediaBrowser.Model/Providers/SubtitleProviderInfo.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Model.Providers
+{
+ public class SubtitleProviderInfo
+ {
+ public string Name { get; set; }
+ public string Id { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Querying/AllThemeMediaResult.cs b/MediaBrowser.Model/Querying/AllThemeMediaResult.cs
new file mode 100644
index 000000000..89640eb65
--- /dev/null
+++ b/MediaBrowser.Model/Querying/AllThemeMediaResult.cs
@@ -0,0 +1,20 @@
+namespace MediaBrowser.Model.Querying
+{
+ public class AllThemeMediaResult
+ {
+ public ThemeMediaResult ThemeVideosResult { get; set; }
+
+ public ThemeMediaResult ThemeSongsResult { get; set; }
+
+ public ThemeMediaResult SoundtrackSongsResult { get; set; }
+
+ public AllThemeMediaResult()
+ {
+ ThemeVideosResult = new ThemeMediaResult();
+
+ ThemeSongsResult = new ThemeMediaResult();
+
+ SoundtrackSongsResult = new ThemeMediaResult();
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Querying/EpisodeQuery.cs b/MediaBrowser.Model/Querying/EpisodeQuery.cs
new file mode 100644
index 000000000..78fe943e3
--- /dev/null
+++ b/MediaBrowser.Model/Querying/EpisodeQuery.cs
@@ -0,0 +1,62 @@
+
+namespace MediaBrowser.Model.Querying
+{
+ public class EpisodeQuery
+ {
+ /// <summary>
+ /// Gets or sets the user identifier.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public string UserId { get; set; }
+ /// <summary>
+ /// Gets or sets the season identifier.
+ /// </summary>
+ /// <value>The season identifier.</value>
+ public string SeasonId { get; set; }
+ /// <summary>
+ /// Gets or sets the series identifier.
+ /// </summary>
+ /// <value>The series identifier.</value>
+ public string SeriesId { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is missing.
+ /// </summary>
+ /// <value><c>null</c> if [is missing] contains no value, <c>true</c> if [is missing]; otherwise, <c>false</c>.</value>
+ public bool? IsMissing { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is virtual unaired.
+ /// </summary>
+ /// <value><c>null</c> if [is virtual unaired] contains no value, <c>true</c> if [is virtual unaired]; otherwise, <c>false</c>.</value>
+ public bool? IsVirtualUnaired { get; set; }
+ /// <summary>
+ /// Gets or sets the season number.
+ /// </summary>
+ /// <value>The season number.</value>
+ public int? SeasonNumber { get; set; }
+ /// <summary>
+ /// Gets or sets the fields.
+ /// </summary>
+ /// <value>The fields.</value>
+ public ItemFields[] Fields { get; set; }
+ /// <summary>
+ /// Gets or sets the start index.
+ /// </summary>
+ /// <value>The start index.</value>
+ public int? StartIndex { get; set; }
+ /// <summary>
+ /// Gets or sets the limit.
+ /// </summary>
+ /// <value>The limit.</value>
+ public int? Limit { get; set; }
+ /// <summary>
+ /// Gets or sets the start item identifier.
+ /// </summary>
+ /// <value>The start item identifier.</value>
+ public string StartItemId { get; set; }
+
+ public EpisodeQuery()
+ {
+ Fields = new ItemFields[] { };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Querying/ItemCountsQuery.cs b/MediaBrowser.Model/Querying/ItemCountsQuery.cs
new file mode 100644
index 000000000..0bf681537
--- /dev/null
+++ b/MediaBrowser.Model/Querying/ItemCountsQuery.cs
@@ -0,0 +1,21 @@
+
+namespace MediaBrowser.Model.Querying
+{
+ /// <summary>
+ /// Class ItemCountsQuery
+ /// </summary>
+ public class ItemCountsQuery
+ {
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ /// <value>The user id.</value>
+ public string UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is favorite.
+ /// </summary>
+ /// <value><c>null</c> if [is favorite] contains no value, <c>true</c> if [is favorite]; otherwise, <c>false</c>.</value>
+ public bool? IsFavorite { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs
new file mode 100644
index 000000000..92fa3822b
--- /dev/null
+++ b/MediaBrowser.Model/Querying/ItemFields.cs
@@ -0,0 +1,222 @@
+namespace MediaBrowser.Model.Querying
+{
+ /// <summary>
+ /// Used to control the data that gets attached to DtoBaseItems
+ /// </summary>
+ public enum ItemFields
+ {
+ /// <summary>
+ /// The air time
+ /// </summary>
+ AirTime,
+
+ /// <summary>
+ /// The can delete
+ /// </summary>
+ CanDelete,
+
+ /// <summary>
+ /// The can download
+ /// </summary>
+ CanDownload,
+
+ /// <summary>
+ /// The channel information
+ /// </summary>
+ ChannelInfo,
+
+ /// <summary>
+ /// The chapters
+ /// </summary>
+ Chapters,
+
+ ChildCount,
+
+ /// <summary>
+ /// The cumulative run time ticks
+ /// </summary>
+ CumulativeRunTimeTicks,
+
+ /// <summary>
+ /// The custom rating
+ /// </summary>
+ CustomRating,
+
+ /// <summary>
+ /// The date created of the item
+ /// </summary>
+ DateCreated,
+
+ /// <summary>
+ /// The date last media added
+ /// </summary>
+ DateLastMediaAdded,
+
+ /// <summary>
+ /// Item display preferences
+ /// </summary>
+ DisplayPreferencesId,
+
+ /// <summary>
+ /// The etag
+ /// </summary>
+ Etag,
+
+ /// <summary>
+ /// The external urls
+ /// </summary>
+ ExternalUrls,
+
+ /// <summary>
+ /// Genres
+ /// </summary>
+ Genres,
+
+ /// <summary>
+ /// The home page URL
+ /// </summary>
+ HomePageUrl,
+
+ /// <summary>
+ /// The item counts
+ /// </summary>
+ ItemCounts,
+
+ /// <summary>
+ /// The media source count
+ /// </summary>
+ MediaSourceCount,
+
+ /// <summary>
+ /// The media versions
+ /// </summary>
+ MediaSources,
+
+ OriginalTitle,
+
+ /// <summary>
+ /// The item overview
+ /// </summary>
+ Overview,
+
+ /// <summary>
+ /// The id of the item's parent
+ /// </summary>
+ ParentId,
+
+ /// <summary>
+ /// The physical path of the item
+ /// </summary>
+ Path,
+
+ /// <summary>
+ /// The list of people for the item
+ /// </summary>
+ People,
+
+ PlayAccess,
+
+ /// <summary>
+ /// The production locations
+ /// </summary>
+ ProductionLocations,
+
+ /// <summary>
+ /// Imdb, tmdb, etc
+ /// </summary>
+ ProviderIds,
+
+ /// <summary>
+ /// The aspect ratio of the primary image
+ /// </summary>
+ PrimaryImageAspectRatio,
+
+ RecursiveItemCount,
+
+ /// <summary>
+ /// The settings
+ /// </summary>
+ Settings,
+
+ /// <summary>
+ /// The screenshot image tags
+ /// </summary>
+ ScreenshotImageTags,
+
+ SeriesPrimaryImage,
+
+ /// <summary>
+ /// The series studio
+ /// </summary>
+ SeriesStudio,
+
+ /// <summary>
+ /// The sort name of the item
+ /// </summary>
+ SortName,
+
+ /// <summary>
+ /// The special episode numbers
+ /// </summary>
+ SpecialEpisodeNumbers,
+
+ /// <summary>
+ /// The studios of the item
+ /// </summary>
+ Studios,
+
+ BasicSyncInfo,
+ /// <summary>
+ /// The synchronize information
+ /// </summary>
+ SyncInfo,
+
+ /// <summary>
+ /// The taglines of the item
+ /// </summary>
+ Taglines,
+
+ /// <summary>
+ /// The tags
+ /// </summary>
+ Tags,
+
+ /// <summary>
+ /// The trailer url of the item
+ /// </summary>
+ RemoteTrailers,
+
+ /// <summary>
+ /// The media streams
+ /// </summary>
+ MediaStreams,
+
+ /// <summary>
+ /// The season user data
+ /// </summary>
+ SeasonUserData,
+
+ /// <summary>
+ /// The service name
+ /// </summary>
+ ServiceName,
+ ThemeSongIds,
+ ThemeVideoIds,
+ ExternalEtag,
+ PresentationUniqueKey,
+ InheritedParentalRatingValue,
+ ExternalSeriesId,
+ SeriesPresentationUniqueKey,
+ DateLastRefreshed,
+ DateLastSaved,
+ RefreshState,
+ ChannelImage,
+ EnableMediaSourceDisplay,
+ Width,
+ Height,
+ ExtraIds,
+ LocalTrailerCount,
+ IsHD,
+ SpecialFeatureCount
+ }
+}
diff --git a/MediaBrowser.Model/Querying/ItemFilter.cs b/MediaBrowser.Model/Querying/ItemFilter.cs
new file mode 100644
index 000000000..ff28bd08c
--- /dev/null
+++ b/MediaBrowser.Model/Querying/ItemFilter.cs
@@ -0,0 +1,46 @@
+
+namespace MediaBrowser.Model.Querying
+{
+ /// <summary>
+ /// Enum ItemFilter
+ /// </summary>
+ public enum ItemFilter
+ {
+ /// <summary>
+ /// The item is a folder
+ /// </summary>
+ IsFolder = 1,
+ /// <summary>
+ /// The item is not folder
+ /// </summary>
+ IsNotFolder = 2,
+ /// <summary>
+ /// The item is unplayed
+ /// </summary>
+ IsUnplayed = 3,
+ /// <summary>
+ /// The item is played
+ /// </summary>
+ IsPlayed = 4,
+ /// <summary>
+ /// The item is a favorite
+ /// </summary>
+ IsFavorite = 5,
+ /// <summary>
+ /// The item is resumable
+ /// </summary>
+ IsResumable = 7,
+ /// <summary>
+ /// The likes
+ /// </summary>
+ Likes = 8,
+ /// <summary>
+ /// The dislikes
+ /// </summary>
+ Dislikes = 9,
+ /// <summary>
+ /// The is favorite or likes
+ /// </summary>
+ IsFavoriteOrLikes = 10
+ }
+}
diff --git a/MediaBrowser.Model/Querying/ItemSortBy.cs b/MediaBrowser.Model/Querying/ItemSortBy.cs
new file mode 100644
index 000000000..66bdc8aa5
--- /dev/null
+++ b/MediaBrowser.Model/Querying/ItemSortBy.cs
@@ -0,0 +1,81 @@
+
+namespace MediaBrowser.Model.Querying
+{
+ /// <summary>
+ /// These represent sort orders that are known by the core
+ /// </summary>
+ public static class ItemSortBy
+ {
+ public const string AiredEpisodeOrder = "AiredEpisodeOrder";
+ /// <summary>
+ /// The album
+ /// </summary>
+ public const string Album = "Album";
+ /// <summary>
+ /// The album artist
+ /// </summary>
+ public const string AlbumArtist = "AlbumArtist";
+ /// <summary>
+ /// The artist
+ /// </summary>
+ public const string Artist = "Artist";
+ /// <summary>
+ /// The date created
+ /// </summary>
+ public const string DateCreated = "DateCreated";
+ /// <summary>
+ /// The official rating
+ /// </summary>
+ public const string OfficialRating = "OfficialRating";
+ /// <summary>
+ /// The date played
+ /// </summary>
+ public const string DatePlayed = "DatePlayed";
+ /// <summary>
+ /// The premiere date
+ /// </summary>
+ public const string PremiereDate = "PremiereDate";
+ public const string StartDate = "StartDate";
+ /// <summary>
+ /// The sort name
+ /// </summary>
+ public const string SortName = "SortName";
+ public const string Name = "Name";
+ /// <summary>
+ /// The random
+ /// </summary>
+ public const string Random = "Random";
+ /// <summary>
+ /// The runtime
+ /// </summary>
+ public const string Runtime = "Runtime";
+ /// <summary>
+ /// The community rating
+ /// </summary>
+ public const string CommunityRating = "CommunityRating";
+ /// <summary>
+ /// The production year
+ /// </summary>
+ public const string ProductionYear = "ProductionYear";
+ /// <summary>
+ /// The play count
+ /// </summary>
+ public const string PlayCount = "PlayCount";
+ /// <summary>
+ /// The critic rating
+ /// </summary>
+ public const string CriticRating = "CriticRating";
+ public const string IsFolder = "IsFolder";
+ public const string IsUnplayed = "IsUnplayed";
+ public const string IsPlayed = "IsPlayed";
+ public const string SeriesSortName = "SeriesSortName";
+ public const string VideoBitRate = "VideoBitRate";
+ public const string AirTime = "AirTime";
+ public const string Studio = "Studio";
+ public const string Players = "Players";
+ public const string GameSystem = "GameSystem";
+ public const string IsFavoriteOrLiked = "IsFavoriteOrLiked";
+ public const string DateLastContentAdded = "DateLastContentAdded";
+ public const string SeriesDatePlayed = "SeriesDatePlayed";
+ }
+}
diff --git a/MediaBrowser.Model/Querying/LatestItemsQuery.cs b/MediaBrowser.Model/Querying/LatestItemsQuery.cs
new file mode 100644
index 000000000..88b079595
--- /dev/null
+++ b/MediaBrowser.Model/Querying/LatestItemsQuery.cs
@@ -0,0 +1,76 @@
+using System;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.Querying
+{
+ public class LatestItemsQuery
+ {
+ /// <summary>
+ /// The user to localize search results for
+ /// </summary>
+ /// <value>The user id.</value>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Specify this to localize the search to a specific item or folder. Omit to use the root.
+ /// </summary>
+ /// <value>The parent id.</value>
+ public Guid ParentId { get; set; }
+
+ /// <summary>
+ /// Skips over a given number of items within the results. Use for paging.
+ /// </summary>
+ /// <value>The start index.</value>
+ public int? StartIndex { get; set; }
+
+ /// <summary>
+ /// The maximum number of items to return
+ /// </summary>
+ /// <value>The limit.</value>
+ public int? Limit { get; set; }
+
+ /// <summary>
+ /// Fields to return within the items, in addition to basic information
+ /// </summary>
+ /// <value>The fields.</value>
+ public ItemFields[] Fields { get; set; }
+
+ /// <summary>
+ /// Gets or sets the include item types.
+ /// </summary>
+ /// <value>The include item types.</value>
+ public string[] IncludeItemTypes { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is played.
+ /// </summary>
+ /// <value><c>null</c> if [is played] contains no value, <c>true</c> if [is played]; otherwise, <c>false</c>.</value>
+ public bool? IsPlayed { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [group items].
+ /// </summary>
+ /// <value><c>true</c> if [group items]; otherwise, <c>false</c>.</value>
+ public bool GroupItems { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether [enable images].
+ /// </summary>
+ /// <value><c>null</c> if [enable images] contains no value, <c>true</c> if [enable images]; otherwise, <c>false</c>.</value>
+ public bool? EnableImages { get; set; }
+ /// <summary>
+ /// Gets or sets the image type limit.
+ /// </summary>
+ /// <value>The image type limit.</value>
+ public int? ImageTypeLimit { get; set; }
+ /// <summary>
+ /// Gets or sets the enable image types.
+ /// </summary>
+ /// <value>The enable image types.</value>
+ public ImageType[] EnableImageTypes { get; set; }
+
+ public LatestItemsQuery()
+ {
+ EnableImageTypes = new ImageType[] {};
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs b/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs
new file mode 100644
index 000000000..91417a4a7
--- /dev/null
+++ b/MediaBrowser.Model/Querying/MovieRecommendationQuery.cs
@@ -0,0 +1,39 @@
+
+namespace MediaBrowser.Model.Querying
+{
+ public class MovieRecommendationQuery
+ {
+ /// <summary>
+ /// Gets or sets the user identifier.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public string UserId { get; set; }
+ /// <summary>
+ /// Gets or sets the parent identifier.
+ /// </summary>
+ /// <value>The parent identifier.</value>
+ public string ParentId { get; set; }
+ /// <summary>
+ /// Gets or sets the item limit.
+ /// </summary>
+ /// <value>The item limit.</value>
+ public int ItemLimit { get; set; }
+ /// <summary>
+ /// Gets or sets the category limit.
+ /// </summary>
+ /// <value>The category limit.</value>
+ public int CategoryLimit { get; set; }
+ /// <summary>
+ /// Gets or sets the fields.
+ /// </summary>
+ /// <value>The fields.</value>
+ public ItemFields[] Fields { get; set; }
+
+ public MovieRecommendationQuery()
+ {
+ ItemLimit = 10;
+ CategoryLimit = 6;
+ Fields = new ItemFields[] { };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs
new file mode 100644
index 000000000..d20ff99c2
--- /dev/null
+++ b/MediaBrowser.Model/Querying/NextUpQuery.cs
@@ -0,0 +1,67 @@
+using MediaBrowser.Model.Entities;
+using System;
+
+namespace MediaBrowser.Model.Querying
+{
+ public class NextUpQuery
+ {
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ /// <value>The user id.</value>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent identifier.
+ /// </summary>
+ /// <value>The parent identifier.</value>
+ public string ParentId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the series id.
+ /// </summary>
+ /// <value>The series id.</value>
+ public string SeriesId { get; set; }
+
+ /// <summary>
+ /// Skips over a given number of items within the results. Use for paging.
+ /// </summary>
+ /// <value>The start index.</value>
+ public int? StartIndex { get; set; }
+
+ /// <summary>
+ /// The maximum number of items to return
+ /// </summary>
+ /// <value>The limit.</value>
+ public int? Limit { get; set; }
+
+ /// <summary>
+ /// Fields to return within the items, in addition to basic information
+ /// </summary>
+ /// <value>The fields.</value>
+ public ItemFields[] Fields { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether [enable images].
+ /// </summary>
+ /// <value><c>null</c> if [enable images] contains no value, <c>true</c> if [enable images]; otherwise, <c>false</c>.</value>
+ public bool? EnableImages { get; set; }
+ /// <summary>
+ /// Gets or sets the image type limit.
+ /// </summary>
+ /// <value>The image type limit.</value>
+ public int? ImageTypeLimit { get; set; }
+ /// <summary>
+ /// Gets or sets the enable image types.
+ /// </summary>
+ /// <value>The enable image types.</value>
+ public ImageType[] EnableImageTypes { get; set; }
+
+ public bool EnableTotalRecordCount { get; set; }
+
+ public NextUpQuery()
+ {
+ EnableImageTypes = new ImageType[] {};
+ EnableTotalRecordCount = true;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Querying/QueryFilters.cs b/MediaBrowser.Model/Querying/QueryFilters.cs
new file mode 100644
index 000000000..992bba303
--- /dev/null
+++ b/MediaBrowser.Model/Querying/QueryFilters.cs
@@ -0,0 +1,32 @@
+using MediaBrowser.Model.Dto;
+using System;
+
+namespace MediaBrowser.Model.Querying
+{
+ public class QueryFiltersLegacy
+ {
+ public string[] Genres { get; set; }
+ public string[] Tags { get; set; }
+ public string[] OfficialRatings { get; set; }
+ public int[] Years { get; set; }
+
+ public QueryFiltersLegacy()
+ {
+ Genres = new string[] {};
+ Tags = new string[] {};
+ OfficialRatings = new string[] {};
+ Years = new int[] { };
+ }
+ }
+ public class QueryFilters
+ {
+ public NameGuidPair[] Genres { get; set; }
+ public string[] Tags { get; set; }
+
+ public QueryFilters()
+ {
+ Tags = new string[] {};
+ Genres = new NameGuidPair[] { };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Querying/QueryResult.cs b/MediaBrowser.Model/Querying/QueryResult.cs
new file mode 100644
index 000000000..6f9923d08
--- /dev/null
+++ b/MediaBrowser.Model/Querying/QueryResult.cs
@@ -0,0 +1,23 @@
+
+namespace MediaBrowser.Model.Querying
+{
+ public class QueryResult<T>
+ {
+ /// <summary>
+ /// Gets or sets the items.
+ /// </summary>
+ /// <value>The items.</value>
+ public T[] Items { get; set; }
+
+ /// <summary>
+ /// The total number of records available
+ /// </summary>
+ /// <value>The total record count.</value>
+ public int TotalRecordCount { get; set; }
+
+ public QueryResult()
+ {
+ Items = new T[] { };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Querying/SessionQuery.cs b/MediaBrowser.Model/Querying/SessionQuery.cs
new file mode 100644
index 000000000..fa7df315c
--- /dev/null
+++ b/MediaBrowser.Model/Querying/SessionQuery.cs
@@ -0,0 +1,14 @@
+
+namespace MediaBrowser.Model.Querying
+{
+ /// <summary>
+ /// Class SessionQuery
+ /// </summary>
+ public class SessionQuery
+ {
+ /// <summary>
+ /// Filter by sessions that are allowed to be controlled by a given user
+ /// </summary>
+ public string ControllableByUserId { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Querying/SimilarItemsQuery.cs b/MediaBrowser.Model/Querying/SimilarItemsQuery.cs
new file mode 100644
index 000000000..0dd491550
--- /dev/null
+++ b/MediaBrowser.Model/Querying/SimilarItemsQuery.cs
@@ -0,0 +1,29 @@
+namespace MediaBrowser.Model.Querying
+{
+ public class SimilarItemsQuery
+ {
+ /// <summary>
+ /// The user to localize search results for
+ /// </summary>
+ /// <value>The user id.</value>
+ public string UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public string Id { get; set; }
+
+ /// <summary>
+ /// The maximum number of items to return
+ /// </summary>
+ /// <value>The limit.</value>
+ public int? Limit { get; set; }
+
+ /// <summary>
+ /// Fields to return within the items, in addition to basic information
+ /// </summary>
+ /// <value>The fields.</value>
+ public ItemFields[] Fields { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Querying/ThemeMediaResult.cs b/MediaBrowser.Model/Querying/ThemeMediaResult.cs
new file mode 100644
index 000000000..eae102bae
--- /dev/null
+++ b/MediaBrowser.Model/Querying/ThemeMediaResult.cs
@@ -0,0 +1,17 @@
+using MediaBrowser.Model.Dto;
+using System;
+
+namespace MediaBrowser.Model.Querying
+{
+ /// <summary>
+ /// Class ThemeMediaResult
+ /// </summary>
+ public class ThemeMediaResult : QueryResult<BaseItemDto>
+ {
+ /// <summary>
+ /// Gets or sets the owner id.
+ /// </summary>
+ /// <value>The owner id.</value>
+ public Guid OwnerId { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs
new file mode 100644
index 000000000..665b980eb
--- /dev/null
+++ b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs
@@ -0,0 +1,57 @@
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.Querying
+{
+ public class UpcomingEpisodesQuery
+ {
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ /// <value>The user id.</value>
+ public string UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent identifier.
+ /// </summary>
+ /// <value>The parent identifier.</value>
+ public string ParentId { get; set; }
+
+ /// <summary>
+ /// Skips over a given number of items within the results. Use for paging.
+ /// </summary>
+ /// <value>The start index.</value>
+ public int? StartIndex { get; set; }
+
+ /// <summary>
+ /// The maximum number of items to return
+ /// </summary>
+ /// <value>The limit.</value>
+ public int? Limit { get; set; }
+
+ /// <summary>
+ /// Fields to return within the items, in addition to basic information
+ /// </summary>
+ /// <value>The fields.</value>
+ public ItemFields[] Fields { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether [enable images].
+ /// </summary>
+ /// <value><c>null</c> if [enable images] contains no value, <c>true</c> if [enable images]; otherwise, <c>false</c>.</value>
+ public bool? EnableImages { get; set; }
+ /// <summary>
+ /// Gets or sets the image type limit.
+ /// </summary>
+ /// <value>The image type limit.</value>
+ public int? ImageTypeLimit { get; set; }
+ /// <summary>
+ /// Gets or sets the enable image types.
+ /// </summary>
+ /// <value>The enable image types.</value>
+ public ImageType[] EnableImageTypes { get; set; }
+
+ public UpcomingEpisodesQuery()
+ {
+ EnableImageTypes = new ImageType[] {};
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Querying/UserQuery.cs b/MediaBrowser.Model/Querying/UserQuery.cs
new file mode 100644
index 000000000..48dbd30aa
--- /dev/null
+++ b/MediaBrowser.Model/Querying/UserQuery.cs
@@ -0,0 +1,9 @@
+
+namespace MediaBrowser.Model.Querying
+{
+ public class UserQuery
+ {
+ public bool? IsHidden { get; set; }
+ public bool? IsDisabled { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Reflection/IAssemblyInfo.cs b/MediaBrowser.Model/Reflection/IAssemblyInfo.cs
new file mode 100644
index 000000000..e8e9c414c
--- /dev/null
+++ b/MediaBrowser.Model/Reflection/IAssemblyInfo.cs
@@ -0,0 +1,14 @@
+using System;
+using System.IO;
+using System.Reflection;
+
+namespace MediaBrowser.Model.Reflection
+{
+ public interface IAssemblyInfo
+ {
+ Stream GetManifestResourceStream(Type type, string resource);
+ string[] GetManifestResourceNames(Type type);
+
+ Assembly[] GetCurrentAssemblies();
+ }
+}
diff --git a/MediaBrowser.Model/Search/SearchHint.cs b/MediaBrowser.Model/Search/SearchHint.cs
new file mode 100644
index 000000000..daa3566cf
--- /dev/null
+++ b/MediaBrowser.Model/Search/SearchHint.cs
@@ -0,0 +1,158 @@
+using System;
+
+namespace MediaBrowser.Model.Search
+{
+ /// <summary>
+ /// Class SearchHintResult
+ /// </summary>
+ public class SearchHint
+ {
+ /// <summary>
+ /// Gets or sets the item id.
+ /// </summary>
+ /// <value>The item id.</value>
+ public Guid ItemId { get; set; }
+
+ public Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the matched term.
+ /// </summary>
+ /// <value>The matched term.</value>
+ public string MatchedTerm { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index number.
+ /// </summary>
+ /// <value>The index number.</value>
+ public int? IndexNumber { get; set; }
+
+ /// <summary>
+ /// Gets or sets the production year.
+ /// </summary>
+ /// <value>The production year.</value>
+ public int? ProductionYear { get; set; }
+
+ /// <summary>
+ /// Gets or sets the parent index number.
+ /// </summary>
+ /// <value>The parent index number.</value>
+ public int? ParentIndexNumber { get; set; }
+
+ /// <summary>
+ /// Gets or sets the image tag.
+ /// </summary>
+ /// <value>The image tag.</value>
+ public string PrimaryImageTag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the thumb image tag.
+ /// </summary>
+ /// <value>The thumb image tag.</value>
+ public string ThumbImageTag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the thumb image item identifier.
+ /// </summary>
+ /// <value>The thumb image item identifier.</value>
+ public string ThumbImageItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the backdrop image tag.
+ /// </summary>
+ /// <value>The backdrop image tag.</value>
+ public string BackdropImageTag { get; set; }
+
+ /// <summary>
+ /// Gets or sets the backdrop image item identifier.
+ /// </summary>
+ /// <value>The backdrop image item identifier.</value>
+ public string BackdropImageItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ public string Type { get; set; }
+
+ public bool? IsFolder { get; set; }
+
+ /// <summary>
+ /// Gets or sets the run time ticks.
+ /// </summary>
+ /// <value>The run time ticks.</value>
+ public long? RunTimeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the media.
+ /// </summary>
+ /// <value>The type of the media.</value>
+ public string MediaType { get; set; }
+
+ public DateTime? StartDate { get; set; }
+ public DateTime? EndDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the series.
+ /// </summary>
+ /// <value>The series.</value>
+ public string Series { get; set; }
+
+ public string Status { get; set; }
+
+ /// <summary>
+ /// Gets or sets the album.
+ /// </summary>
+ /// <value>The album.</value>
+ public string Album { get; set; }
+ public Guid AlbumId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the album artist.
+ /// </summary>
+ /// <value>The album artist.</value>
+ public string AlbumArtist { get; set; }
+
+ /// <summary>
+ /// Gets or sets the artists.
+ /// </summary>
+ /// <value>The artists.</value>
+ public string[] Artists { get; set; }
+
+ /// <summary>
+ /// Gets or sets the song count.
+ /// </summary>
+ /// <value>The song count.</value>
+ public int? SongCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets the episode count.
+ /// </summary>
+ /// <value>The episode count.</value>
+ public int? EpisodeCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets the channel identifier.
+ /// </summary>
+ /// <value>The channel identifier.</value>
+ public Guid ChannelId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the channel.
+ /// </summary>
+ /// <value>The name of the channel.</value>
+ public string ChannelName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the primary image aspect ratio.
+ /// </summary>
+ /// <value>The primary image aspect ratio.</value>
+ public double? PrimaryImageAspectRatio { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Search/SearchHintResult.cs b/MediaBrowser.Model/Search/SearchHintResult.cs
new file mode 100644
index 000000000..372528f82
--- /dev/null
+++ b/MediaBrowser.Model/Search/SearchHintResult.cs
@@ -0,0 +1,21 @@
+
+namespace MediaBrowser.Model.Search
+{
+ /// <summary>
+ /// Class SearchHintResult
+ /// </summary>
+ public class SearchHintResult
+ {
+ /// <summary>
+ /// Gets or sets the search hints.
+ /// </summary>
+ /// <value>The search hints.</value>
+ public SearchHint[] SearchHints { get; set; }
+
+ /// <summary>
+ /// Gets or sets the total record count.
+ /// </summary>
+ /// <value>The total record count.</value>
+ public int TotalRecordCount { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Search/SearchQuery.cs b/MediaBrowser.Model/Search/SearchQuery.cs
new file mode 100644
index 000000000..6a1861c8e
--- /dev/null
+++ b/MediaBrowser.Model/Search/SearchQuery.cs
@@ -0,0 +1,65 @@
+using System;
+
+namespace MediaBrowser.Model.Search
+{
+ public class SearchQuery
+ {
+ /// <summary>
+ /// The user to localize search results for
+ /// </summary>
+ /// <value>The user id.</value>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the search term.
+ /// </summary>
+ /// <value>The search term.</value>
+ public string SearchTerm { get; set; }
+
+ /// <summary>
+ /// Skips over a given number of items within the results. Use for paging.
+ /// </summary>
+ /// <value>The start index.</value>
+ public int? StartIndex { get; set; }
+
+ /// <summary>
+ /// The maximum number of items to return
+ /// </summary>
+ /// <value>The limit.</value>
+ public int? Limit { get; set; }
+
+ public bool IncludePeople { get; set; }
+ public bool IncludeMedia { get; set; }
+ public bool IncludeGenres { get; set; }
+ public bool IncludeStudios { get; set; }
+ public bool IncludeArtists { get; set; }
+
+ public string[] MediaTypes { get; set; }
+ public string[] IncludeItemTypes { get; set; }
+ public string[] ExcludeItemTypes { get; set; }
+ public string ParentId { get; set; }
+
+ public bool? IsMovie { get; set; }
+
+ public bool? IsSeries { get; set; }
+
+ public bool? IsNews { get; set; }
+
+ public bool? IsKids { get; set; }
+
+ public bool? IsSports { get; set; }
+
+ public SearchQuery()
+ {
+ IncludeArtists = true;
+ IncludeGenres = true;
+ IncludeMedia = true;
+ IncludePeople = true;
+ IncludeStudios = true;
+
+ MediaTypes = new string[] {};
+ IncludeItemTypes = new string[] {};
+ ExcludeItemTypes = new string[] {};
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Serialization/IJsonSerializer.cs b/MediaBrowser.Model/Serialization/IJsonSerializer.cs
new file mode 100644
index 000000000..a582beb7f
--- /dev/null
+++ b/MediaBrowser.Model/Serialization/IJsonSerializer.cs
@@ -0,0 +1,91 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Model.Serialization
+{
+ public interface IJsonSerializer
+ {
+ /// <summary>
+ /// Serializes to stream.
+ /// </summary>
+ /// <param name="obj">The obj.</param>
+ /// <param name="stream">The stream.</param>
+ /// <exception cref="System.ArgumentNullException">obj</exception>
+ void SerializeToStream(object obj, Stream stream);
+
+ /// <summary>
+ /// Serializes to file.
+ /// </summary>
+ /// <param name="obj">The obj.</param>
+ /// <param name="file">The file.</param>
+ /// <exception cref="System.ArgumentNullException">obj</exception>
+ void SerializeToFile(object obj, string file);
+
+ /// <summary>
+ /// Deserializes from file.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <param name="file">The file.</param>
+ /// <returns>System.Object.</returns>
+ /// <exception cref="System.ArgumentNullException">type</exception>
+ object DeserializeFromFile(Type type, string file);
+
+ /// <summary>
+ /// Deserializes from file.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="file">The file.</param>
+ /// <returns>``0.</returns>
+ /// <exception cref="System.ArgumentNullException">file</exception>
+ T DeserializeFromFile<T>(string file)
+ where T : class;
+
+ /// <summary>
+ /// Deserializes from stream.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="stream">The stream.</param>
+ /// <returns>``0.</returns>
+ /// <exception cref="System.ArgumentNullException">stream</exception>
+ T DeserializeFromStream<T>(Stream stream);
+
+ /// <summary>
+ /// Deserializes from string.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="text">The text.</param>
+ /// <returns>``0.</returns>
+ /// <exception cref="System.ArgumentNullException">text</exception>
+ T DeserializeFromString<T>(string text);
+
+ /// <summary>
+ /// Deserializes from stream.
+ /// </summary>
+ /// <param name="stream">The stream.</param>
+ /// <param name="type">The type.</param>
+ /// <returns>System.Object.</returns>
+ /// <exception cref="System.ArgumentNullException">stream</exception>
+ object DeserializeFromStream(Stream stream, Type type);
+
+ /// <summary>
+ /// Deserializes from string.
+ /// </summary>
+ /// <param name="json">The json.</param>
+ /// <param name="type">The type.</param>
+ /// <returns>System.Object.</returns>
+ /// <exception cref="System.ArgumentNullException">json</exception>
+ object DeserializeFromString(string json, Type type);
+
+ /// <summary>
+ /// Serializes to string.
+ /// </summary>
+ /// <param name="obj">The obj.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="System.ArgumentNullException">obj</exception>
+ string SerializeToString(object obj);
+
+ Task<object> DeserializeFromStreamAsync(Stream stream, Type type);
+ Task<T> DeserializeFromStreamAsync<T>(Stream stream);
+ }
+}
diff --git a/MediaBrowser.Model/Serialization/IXmlSerializer.cs b/MediaBrowser.Model/Serialization/IXmlSerializer.cs
new file mode 100644
index 000000000..b26b673f3
--- /dev/null
+++ b/MediaBrowser.Model/Serialization/IXmlSerializer.cs
@@ -0,0 +1,46 @@
+using System;
+using System.IO;
+
+namespace MediaBrowser.Model.Serialization
+{
+ public interface IXmlSerializer
+ {
+ /// <summary>
+ /// Deserializes from stream.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <param name="stream">The stream.</param>
+ /// <returns>System.Object.</returns>
+ object DeserializeFromStream(Type type, Stream stream);
+
+ /// <summary>
+ /// Serializes to stream.
+ /// </summary>
+ /// <param name="obj">The obj.</param>
+ /// <param name="stream">The stream.</param>
+ void SerializeToStream(object obj, Stream stream);
+
+ /// <summary>
+ /// Serializes to file.
+ /// </summary>
+ /// <param name="obj">The obj.</param>
+ /// <param name="file">The file.</param>
+ void SerializeToFile(object obj, string file);
+
+ /// <summary>
+ /// Deserializes from file.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <param name="file">The file.</param>
+ /// <returns>System.Object.</returns>
+ object DeserializeFromFile(Type type, string file);
+
+ /// <summary>
+ /// Deserializes from bytes.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <param name="buffer">The buffer.</param>
+ /// <returns>System.Object.</returns>
+ object DeserializeFromBytes(Type type, byte[] buffer);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Serialization/IgnoreDataMemberAttribute.cs b/MediaBrowser.Model/Serialization/IgnoreDataMemberAttribute.cs
new file mode 100644
index 000000000..8e23edc24
--- /dev/null
+++ b/MediaBrowser.Model/Serialization/IgnoreDataMemberAttribute.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace MediaBrowser.Model.Serialization
+{
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
+ public sealed class IgnoreDataMemberAttribute : Attribute
+ {
+ public IgnoreDataMemberAttribute()
+ {
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Services/ApiMemberAttribute.cs b/MediaBrowser.Model/Services/ApiMemberAttribute.cs
new file mode 100644
index 000000000..4a2831775
--- /dev/null
+++ b/MediaBrowser.Model/Services/ApiMemberAttribute.cs
@@ -0,0 +1,61 @@
+using System;
+
+namespace MediaBrowser.Model.Services
+{
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
+ public class ApiMemberAttribute : Attribute
+ {
+ /// <summary>
+ /// Gets or sets verb to which applies attribute. By default applies to all verbs.
+ /// </summary>
+ public string Verb { get; set; }
+
+ /// <summary>
+ /// Gets or sets parameter type: It can be only one of the following: path, query, body, form, or header.
+ /// </summary>
+ public string ParameterType { get; set; }
+
+ /// <summary>
+ /// Gets or sets unique name for the parameter. Each name must be unique, even if they are associated with different paramType values.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// Other notes on the name field:
+ /// If paramType is body, the name is used only for UI and codegeneration.
+ /// If paramType is path, the name field must correspond to the associated path segment from the path field in the api object.
+ /// If paramType is query, the name field corresponds to the query param name.
+ /// </para>
+ /// </remarks>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the human-readable description for the parameter.
+ /// </summary>
+ public string Description { get; set; }
+
+ /// <summary>
+ /// For path, query, and header paramTypes, this field must be a primitive. For body, this can be a complex or container datatype.
+ /// </summary>
+ public string DataType { get; set; }
+
+ /// <summary>
+ /// For path, this is always true. Otherwise, this field tells the client whether or not the field must be supplied.
+ /// </summary>
+ public bool IsRequired { get; set; }
+
+ /// <summary>
+ /// For query params, this specifies that a comma-separated list of values can be passed to the API. For path and body types, this field cannot be true.
+ /// </summary>
+ public bool AllowMultiple { get; set; }
+
+ /// <summary>
+ /// Gets or sets route to which applies attribute, matches using StartsWith. By default applies to all routes.
+ /// </summary>
+ public string Route { get; set; }
+
+ /// <summary>
+ /// Whether to exclude this property from being included in the ModelSchema
+ /// </summary>
+ public bool ExcludeInSchema { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Services/HttpUtility.cs b/MediaBrowser.Model/Services/HttpUtility.cs
new file mode 100644
index 000000000..5cc0cc37d
--- /dev/null
+++ b/MediaBrowser.Model/Services/HttpUtility.cs
@@ -0,0 +1,923 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+using MediaBrowser.Model.Services;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Model.Services
+{
+ public static class MyHttpUtility
+ {
+ // Must be sorted
+ static readonly long[] entities = new long[] {
+ (long)'A' << 56 | (long)'E' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24,
+ (long)'A' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
+ (long)'A' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
+ (long)'A' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
+ (long)'A' << 56 | (long)'l' << 48 | (long)'p' << 40 | (long)'h' << 32 | (long)'a' << 24,
+ (long)'A' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'g' << 24,
+ (long)'A' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16,
+ (long)'A' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
+ (long)'B' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32,
+ (long)'C' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'d' << 32 | (long)'i' << 24 | (long)'l' << 16,
+ (long)'C' << 56 | (long)'h' << 48 | (long)'i' << 40,
+ (long)'D' << 56 | (long)'a' << 48 | (long)'g' << 40 | (long)'g' << 32 | (long)'e' << 24 | (long)'r' << 16,
+ (long)'D' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'t' << 32 | (long)'a' << 24,
+ (long)'E' << 56 | (long)'T' << 48 | (long)'H' << 40,
+ (long)'E' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
+ (long)'E' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
+ (long)'E' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
+ (long)'E' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8,
+ (long)'E' << 56 | (long)'t' << 48 | (long)'a' << 40,
+ (long)'E' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
+ (long)'G' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'m' << 32 | (long)'a' << 24,
+ (long)'I' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
+ (long)'I' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
+ (long)'I' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
+ (long)'I' << 56 | (long)'o' << 48 | (long)'t' << 40 | (long)'a' << 32,
+ (long)'I' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
+ (long)'K' << 56 | (long)'a' << 48 | (long)'p' << 40 | (long)'p' << 32 | (long)'a' << 24,
+ (long)'L' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'b' << 32 | (long)'d' << 24 | (long)'a' << 16,
+ (long)'M' << 56 | (long)'u' << 48,
+ (long)'N' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16,
+ (long)'N' << 56 | (long)'u' << 48,
+ (long)'O' << 56 | (long)'E' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24,
+ (long)'O' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
+ (long)'O' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
+ (long)'O' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
+ (long)'O' << 56 | (long)'m' << 48 | (long)'e' << 40 | (long)'g' << 32 | (long)'a' << 24,
+ (long)'O' << 56 | (long)'m' << 48 | (long)'i' << 40 | (long)'c' << 32 | (long)'r' << 24 | (long)'o' << 16 | (long)'n' << 8,
+ (long)'O' << 56 | (long)'s' << 48 | (long)'l' << 40 | (long)'a' << 32 | (long)'s' << 24 | (long)'h' << 16,
+ (long)'O' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16,
+ (long)'O' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
+ (long)'P' << 56 | (long)'h' << 48 | (long)'i' << 40,
+ (long)'P' << 56 | (long)'i' << 48,
+ (long)'P' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'m' << 32 | (long)'e' << 24,
+ (long)'P' << 56 | (long)'s' << 48 | (long)'i' << 40,
+ (long)'R' << 56 | (long)'h' << 48 | (long)'o' << 40,
+ (long)'S' << 56 | (long)'c' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'o' << 24 | (long)'n' << 16,
+ (long)'S' << 56 | (long)'i' << 48 | (long)'g' << 40 | (long)'m' << 32 | (long)'a' << 24,
+ (long)'T' << 56 | (long)'H' << 48 | (long)'O' << 40 | (long)'R' << 32 | (long)'N' << 24,
+ (long)'T' << 56 | (long)'a' << 48 | (long)'u' << 40,
+ (long)'T' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'t' << 32 | (long)'a' << 24,
+ (long)'U' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
+ (long)'U' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
+ (long)'U' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
+ (long)'U' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8,
+ (long)'U' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
+ (long)'X' << 56 | (long)'i' << 48,
+ (long)'Y' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
+ (long)'Y' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
+ (long)'Z' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32,
+ (long)'a' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
+ (long)'a' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
+ (long)'a' << 56 | (long)'c' << 48 | (long)'u' << 40 | (long)'t' << 32 | (long)'e' << 24,
+ (long)'a' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24,
+ (long)'a' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
+ (long)'a' << 56 | (long)'l' << 48 | (long)'e' << 40 | (long)'f' << 32 | (long)'s' << 24 | (long)'y' << 16 | (long)'m' << 8,
+ (long)'a' << 56 | (long)'l' << 48 | (long)'p' << 40 | (long)'h' << 32 | (long)'a' << 24,
+ (long)'a' << 56 | (long)'m' << 48 | (long)'p' << 40,
+ (long)'a' << 56 | (long)'n' << 48 | (long)'d' << 40,
+ (long)'a' << 56 | (long)'n' << 48 | (long)'g' << 40,
+ (long)'a' << 56 | (long)'p' << 48 | (long)'o' << 40 | (long)'s' << 32,
+ (long)'a' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'g' << 24,
+ (long)'a' << 56 | (long)'s' << 48 | (long)'y' << 40 | (long)'m' << 32 | (long)'p' << 24,
+ (long)'a' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16,
+ (long)'a' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
+ (long)'b' << 56 | (long)'d' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24,
+ (long)'b' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32,
+ (long)'b' << 56 | (long)'r' << 48 | (long)'v' << 40 | (long)'b' << 32 | (long)'a' << 24 | (long)'r' << 16,
+ (long)'b' << 56 | (long)'u' << 48 | (long)'l' << 40 | (long)'l' << 32,
+ (long)'c' << 56 | (long)'a' << 48 | (long)'p' << 40,
+ (long)'c' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'d' << 32 | (long)'i' << 24 | (long)'l' << 16,
+ (long)'c' << 56 | (long)'e' << 48 | (long)'d' << 40 | (long)'i' << 32 | (long)'l' << 24,
+ (long)'c' << 56 | (long)'e' << 48 | (long)'n' << 40 | (long)'t' << 32,
+ (long)'c' << 56 | (long)'h' << 48 | (long)'i' << 40,
+ (long)'c' << 56 | (long)'i' << 48 | (long)'r' << 40 | (long)'c' << 32,
+ (long)'c' << 56 | (long)'l' << 48 | (long)'u' << 40 | (long)'b' << 32 | (long)'s' << 24,
+ (long)'c' << 56 | (long)'o' << 48 | (long)'n' << 40 | (long)'g' << 32,
+ (long)'c' << 56 | (long)'o' << 48 | (long)'p' << 40 | (long)'y' << 32,
+ (long)'c' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'r' << 24,
+ (long)'c' << 56 | (long)'u' << 48 | (long)'p' << 40,
+ (long)'c' << 56 | (long)'u' << 48 | (long)'r' << 40 | (long)'r' << 32 | (long)'e' << 24 | (long)'n' << 16,
+ (long)'d' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32,
+ (long)'d' << 56 | (long)'a' << 48 | (long)'g' << 40 | (long)'g' << 32 | (long)'e' << 24 | (long)'r' << 16,
+ (long)'d' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32,
+ (long)'d' << 56 | (long)'e' << 48 | (long)'g' << 40,
+ (long)'d' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'t' << 32 | (long)'a' << 24,
+ (long)'d' << 56 | (long)'i' << 48 | (long)'a' << 40 | (long)'m' << 32 | (long)'s' << 24,
+ (long)'d' << 56 | (long)'i' << 48 | (long)'v' << 40 | (long)'i' << 32 | (long)'d' << 24 | (long)'e' << 16,
+ (long)'e' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
+ (long)'e' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
+ (long)'e' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
+ (long)'e' << 56 | (long)'m' << 48 | (long)'p' << 40 | (long)'t' << 32 | (long)'y' << 24,
+ (long)'e' << 56 | (long)'m' << 48 | (long)'s' << 40 | (long)'p' << 32,
+ (long)'e' << 56 | (long)'n' << 48 | (long)'s' << 40 | (long)'p' << 32,
+ (long)'e' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8,
+ (long)'e' << 56 | (long)'q' << 48 | (long)'u' << 40 | (long)'i' << 32 | (long)'v' << 24,
+ (long)'e' << 56 | (long)'t' << 48 | (long)'a' << 40,
+ (long)'e' << 56 | (long)'t' << 48 | (long)'h' << 40,
+ (long)'e' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
+ (long)'e' << 56 | (long)'u' << 48 | (long)'r' << 40 | (long)'o' << 32,
+ (long)'e' << 56 | (long)'x' << 48 | (long)'i' << 40 | (long)'s' << 32 | (long)'t' << 24,
+ (long)'f' << 56 | (long)'n' << 48 | (long)'o' << 40 | (long)'f' << 32,
+ (long)'f' << 56 | (long)'o' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'l' << 24 | (long)'l' << 16,
+ (long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'c' << 32 | (long)'1' << 24 | (long)'2' << 16,
+ (long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'c' << 32 | (long)'1' << 24 | (long)'4' << 16,
+ (long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'c' << 32 | (long)'3' << 24 | (long)'4' << 16,
+ (long)'f' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'s' << 32 | (long)'l' << 24,
+ (long)'g' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'m' << 32 | (long)'a' << 24,
+ (long)'g' << 56 | (long)'e' << 48,
+ (long)'g' << 56 | (long)'t' << 48,
+ (long)'h' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32,
+ (long)'h' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32,
+ (long)'h' << 56 | (long)'e' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'t' << 24 | (long)'s' << 16,
+ (long)'h' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'l' << 32 | (long)'i' << 24 | (long)'p' << 16,
+ (long)'i' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
+ (long)'i' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
+ (long)'i' << 56 | (long)'e' << 48 | (long)'x' << 40 | (long)'c' << 32 | (long)'l' << 24,
+ (long)'i' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
+ (long)'i' << 56 | (long)'m' << 48 | (long)'a' << 40 | (long)'g' << 32 | (long)'e' << 24,
+ (long)'i' << 56 | (long)'n' << 48 | (long)'f' << 40 | (long)'i' << 32 | (long)'n' << 24,
+ (long)'i' << 56 | (long)'n' << 48 | (long)'t' << 40,
+ (long)'i' << 56 | (long)'o' << 48 | (long)'t' << 40 | (long)'a' << 32,
+ (long)'i' << 56 | (long)'q' << 48 | (long)'u' << 40 | (long)'e' << 32 | (long)'s' << 24 | (long)'t' << 16,
+ (long)'i' << 56 | (long)'s' << 48 | (long)'i' << 40 | (long)'n' << 32,
+ (long)'i' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
+ (long)'k' << 56 | (long)'a' << 48 | (long)'p' << 40 | (long)'p' << 32 | (long)'a' << 24,
+ (long)'l' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32,
+ (long)'l' << 56 | (long)'a' << 48 | (long)'m' << 40 | (long)'b' << 32 | (long)'d' << 24 | (long)'a' << 16,
+ (long)'l' << 56 | (long)'a' << 48 | (long)'n' << 40 | (long)'g' << 32,
+ (long)'l' << 56 | (long)'a' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24,
+ (long)'l' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32,
+ (long)'l' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'i' << 32 | (long)'l' << 24,
+ (long)'l' << 56 | (long)'d' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24,
+ (long)'l' << 56 | (long)'e' << 48,
+ (long)'l' << 56 | (long)'f' << 48 | (long)'l' << 40 | (long)'o' << 32 | (long)'o' << 24 | (long)'r' << 16,
+ (long)'l' << 56 | (long)'o' << 48 | (long)'w' << 40 | (long)'a' << 32 | (long)'s' << 24 | (long)'t' << 16,
+ (long)'l' << 56 | (long)'o' << 48 | (long)'z' << 40,
+ (long)'l' << 56 | (long)'r' << 48 | (long)'m' << 40,
+ (long)'l' << 56 | (long)'s' << 48 | (long)'a' << 40 | (long)'q' << 32 | (long)'u' << 24 | (long)'o' << 16,
+ (long)'l' << 56 | (long)'s' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24,
+ (long)'l' << 56 | (long)'t' << 48,
+ (long)'m' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'r' << 32,
+ (long)'m' << 56 | (long)'d' << 48 | (long)'a' << 40 | (long)'s' << 32 | (long)'h' << 24,
+ (long)'m' << 56 | (long)'i' << 48 | (long)'c' << 40 | (long)'r' << 32 | (long)'o' << 24,
+ (long)'m' << 56 | (long)'i' << 48 | (long)'d' << 40 | (long)'d' << 32 | (long)'o' << 24 | (long)'t' << 16,
+ (long)'m' << 56 | (long)'i' << 48 | (long)'n' << 40 | (long)'u' << 32 | (long)'s' << 24,
+ (long)'m' << 56 | (long)'u' << 48,
+ (long)'n' << 56 | (long)'a' << 48 | (long)'b' << 40 | (long)'l' << 32 | (long)'a' << 24,
+ (long)'n' << 56 | (long)'b' << 48 | (long)'s' << 40 | (long)'p' << 32,
+ (long)'n' << 56 | (long)'d' << 48 | (long)'a' << 40 | (long)'s' << 32 | (long)'h' << 24,
+ (long)'n' << 56 | (long)'e' << 48,
+ (long)'n' << 56 | (long)'i' << 48,
+ (long)'n' << 56 | (long)'o' << 48 | (long)'t' << 40,
+ (long)'n' << 56 | (long)'o' << 48 | (long)'t' << 40 | (long)'i' << 32 | (long)'n' << 24,
+ (long)'n' << 56 | (long)'s' << 48 | (long)'u' << 40 | (long)'b' << 32,
+ (long)'n' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16,
+ (long)'n' << 56 | (long)'u' << 48,
+ (long)'o' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
+ (long)'o' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
+ (long)'o' << 56 | (long)'e' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24,
+ (long)'o' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
+ (long)'o' << 56 | (long)'l' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'e' << 24,
+ (long)'o' << 56 | (long)'m' << 48 | (long)'e' << 40 | (long)'g' << 32 | (long)'a' << 24,
+ (long)'o' << 56 | (long)'m' << 48 | (long)'i' << 40 | (long)'c' << 32 | (long)'r' << 24 | (long)'o' << 16 | (long)'n' << 8,
+ (long)'o' << 56 | (long)'p' << 48 | (long)'l' << 40 | (long)'u' << 32 | (long)'s' << 24,
+ (long)'o' << 56 | (long)'r' << 48,
+ (long)'o' << 56 | (long)'r' << 48 | (long)'d' << 40 | (long)'f' << 32,
+ (long)'o' << 56 | (long)'r' << 48 | (long)'d' << 40 | (long)'m' << 32,
+ (long)'o' << 56 | (long)'s' << 48 | (long)'l' << 40 | (long)'a' << 32 | (long)'s' << 24 | (long)'h' << 16,
+ (long)'o' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'l' << 32 | (long)'d' << 24 | (long)'e' << 16,
+ (long)'o' << 56 | (long)'t' << 48 | (long)'i' << 40 | (long)'m' << 32 | (long)'e' << 24 | (long)'s' << 16,
+ (long)'o' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
+ (long)'p' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'a' << 32,
+ (long)'p' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'t' << 32,
+ (long)'p' << 56 | (long)'e' << 48 | (long)'r' << 40 | (long)'m' << 32 | (long)'i' << 24 | (long)'l' << 16,
+ (long)'p' << 56 | (long)'e' << 48 | (long)'r' << 40 | (long)'p' << 32,
+ (long)'p' << 56 | (long)'h' << 48 | (long)'i' << 40,
+ (long)'p' << 56 | (long)'i' << 48,
+ (long)'p' << 56 | (long)'i' << 48 | (long)'v' << 40,
+ (long)'p' << 56 | (long)'l' << 48 | (long)'u' << 40 | (long)'s' << 32 | (long)'m' << 24 | (long)'n' << 16,
+ (long)'p' << 56 | (long)'o' << 48 | (long)'u' << 40 | (long)'n' << 32 | (long)'d' << 24,
+ (long)'p' << 56 | (long)'r' << 48 | (long)'i' << 40 | (long)'m' << 32 | (long)'e' << 24,
+ (long)'p' << 56 | (long)'r' << 48 | (long)'o' << 40 | (long)'d' << 32,
+ (long)'p' << 56 | (long)'r' << 48 | (long)'o' << 40 | (long)'p' << 32,
+ (long)'p' << 56 | (long)'s' << 48 | (long)'i' << 40,
+ (long)'q' << 56 | (long)'u' << 48 | (long)'o' << 40 | (long)'t' << 32,
+ (long)'r' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32,
+ (long)'r' << 56 | (long)'a' << 48 | (long)'d' << 40 | (long)'i' << 32 | (long)'c' << 24,
+ (long)'r' << 56 | (long)'a' << 48 | (long)'n' << 40 | (long)'g' << 32,
+ (long)'r' << 56 | (long)'a' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24,
+ (long)'r' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32,
+ (long)'r' << 56 | (long)'c' << 48 | (long)'e' << 40 | (long)'i' << 32 | (long)'l' << 24,
+ (long)'r' << 56 | (long)'d' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24,
+ (long)'r' << 56 | (long)'e' << 48 | (long)'a' << 40 | (long)'l' << 32,
+ (long)'r' << 56 | (long)'e' << 48 | (long)'g' << 40,
+ (long)'r' << 56 | (long)'f' << 48 | (long)'l' << 40 | (long)'o' << 32 | (long)'o' << 24 | (long)'r' << 16,
+ (long)'r' << 56 | (long)'h' << 48 | (long)'o' << 40,
+ (long)'r' << 56 | (long)'l' << 48 | (long)'m' << 40,
+ (long)'r' << 56 | (long)'s' << 48 | (long)'a' << 40 | (long)'q' << 32 | (long)'u' << 24 | (long)'o' << 16,
+ (long)'r' << 56 | (long)'s' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24,
+ (long)'s' << 56 | (long)'b' << 48 | (long)'q' << 40 | (long)'u' << 32 | (long)'o' << 24,
+ (long)'s' << 56 | (long)'c' << 48 | (long)'a' << 40 | (long)'r' << 32 | (long)'o' << 24 | (long)'n' << 16,
+ (long)'s' << 56 | (long)'d' << 48 | (long)'o' << 40 | (long)'t' << 32,
+ (long)'s' << 56 | (long)'e' << 48 | (long)'c' << 40 | (long)'t' << 32,
+ (long)'s' << 56 | (long)'h' << 48 | (long)'y' << 40,
+ (long)'s' << 56 | (long)'i' << 48 | (long)'g' << 40 | (long)'m' << 32 | (long)'a' << 24,
+ (long)'s' << 56 | (long)'i' << 48 | (long)'g' << 40 | (long)'m' << 32 | (long)'a' << 24 | (long)'f' << 16,
+ (long)'s' << 56 | (long)'i' << 48 | (long)'m' << 40,
+ (long)'s' << 56 | (long)'p' << 48 | (long)'a' << 40 | (long)'d' << 32 | (long)'e' << 24 | (long)'s' << 16,
+ (long)'s' << 56 | (long)'u' << 48 | (long)'b' << 40,
+ (long)'s' << 56 | (long)'u' << 48 | (long)'b' << 40 | (long)'e' << 32,
+ (long)'s' << 56 | (long)'u' << 48 | (long)'m' << 40,
+ (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40,
+ (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'1' << 32,
+ (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'2' << 32,
+ (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'3' << 32,
+ (long)'s' << 56 | (long)'u' << 48 | (long)'p' << 40 | (long)'e' << 32,
+ (long)'s' << 56 | (long)'z' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24,
+ (long)'t' << 56 | (long)'a' << 48 | (long)'u' << 40,
+ (long)'t' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'r' << 32 | (long)'e' << 24 | (long)'4' << 16,
+ (long)'t' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'t' << 32 | (long)'a' << 24,
+ (long)'t' << 56 | (long)'h' << 48 | (long)'e' << 40 | (long)'t' << 32 | (long)'a' << 24 | (long)'s' << 16 | (long)'y' << 8 | (long)'m' << 0,
+ (long)'t' << 56 | (long)'h' << 48 | (long)'i' << 40 | (long)'n' << 32 | (long)'s' << 24 | (long)'p' << 16,
+ (long)'t' << 56 | (long)'h' << 48 | (long)'o' << 40 | (long)'r' << 32 | (long)'n' << 24,
+ (long)'t' << 56 | (long)'i' << 48 | (long)'l' << 40 | (long)'d' << 32 | (long)'e' << 24,
+ (long)'t' << 56 | (long)'i' << 48 | (long)'m' << 40 | (long)'e' << 32 | (long)'s' << 24,
+ (long)'t' << 56 | (long)'r' << 48 | (long)'a' << 40 | (long)'d' << 32 | (long)'e' << 24,
+ (long)'u' << 56 | (long)'A' << 48 | (long)'r' << 40 | (long)'r' << 32,
+ (long)'u' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
+ (long)'u' << 56 | (long)'a' << 48 | (long)'r' << 40 | (long)'r' << 32,
+ (long)'u' << 56 | (long)'c' << 48 | (long)'i' << 40 | (long)'r' << 32 | (long)'c' << 24,
+ (long)'u' << 56 | (long)'g' << 48 | (long)'r' << 40 | (long)'a' << 32 | (long)'v' << 24 | (long)'e' << 16,
+ (long)'u' << 56 | (long)'m' << 48 | (long)'l' << 40,
+ (long)'u' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'h' << 24,
+ (long)'u' << 56 | (long)'p' << 48 | (long)'s' << 40 | (long)'i' << 32 | (long)'l' << 24 | (long)'o' << 16 | (long)'n' << 8,
+ (long)'u' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
+ (long)'w' << 56 | (long)'e' << 48 | (long)'i' << 40 | (long)'e' << 32 | (long)'r' << 24 | (long)'p' << 16,
+ (long)'x' << 56 | (long)'i' << 48,
+ (long)'y' << 56 | (long)'a' << 48 | (long)'c' << 40 | (long)'u' << 32 | (long)'t' << 24 | (long)'e' << 16,
+ (long)'y' << 56 | (long)'e' << 48 | (long)'n' << 40,
+ (long)'y' << 56 | (long)'u' << 48 | (long)'m' << 40 | (long)'l' << 32,
+ (long)'z' << 56 | (long)'e' << 48 | (long)'t' << 40 | (long)'a' << 32,
+ (long)'z' << 56 | (long)'w' << 48 | (long)'j' << 40,
+ (long)'z' << 56 | (long)'w' << 48 | (long)'n' << 40 | (long)'j' << 32
+ };
+
+ static readonly char[] entities_values = new char[] {
+ '\u00C6',
+ '\u00C1',
+ '\u00C2',
+ '\u00C0',
+ '\u0391',
+ '\u00C5',
+ '\u00C3',
+ '\u00C4',
+ '\u0392',
+ '\u00C7',
+ '\u03A7',
+ '\u2021',
+ '\u0394',
+ '\u00D0',
+ '\u00C9',
+ '\u00CA',
+ '\u00C8',
+ '\u0395',
+ '\u0397',
+ '\u00CB',
+ '\u0393',
+ '\u00CD',
+ '\u00CE',
+ '\u00CC',
+ '\u0399',
+ '\u00CF',
+ '\u039A',
+ '\u039B',
+ '\u039C',
+ '\u00D1',
+ '\u039D',
+ '\u0152',
+ '\u00D3',
+ '\u00D4',
+ '\u00D2',
+ '\u03A9',
+ '\u039F',
+ '\u00D8',
+ '\u00D5',
+ '\u00D6',
+ '\u03A6',
+ '\u03A0',
+ '\u2033',
+ '\u03A8',
+ '\u03A1',
+ '\u0160',
+ '\u03A3',
+ '\u00DE',
+ '\u03A4',
+ '\u0398',
+ '\u00DA',
+ '\u00DB',
+ '\u00D9',
+ '\u03A5',
+ '\u00DC',
+ '\u039E',
+ '\u00DD',
+ '\u0178',
+ '\u0396',
+ '\u00E1',
+ '\u00E2',
+ '\u00B4',
+ '\u00E6',
+ '\u00E0',
+ '\u2135',
+ '\u03B1',
+ '\u0026',
+ '\u2227',
+ '\u2220',
+ '\u0027',
+ '\u00E5',
+ '\u2248',
+ '\u00E3',
+ '\u00E4',
+ '\u201E',
+ '\u03B2',
+ '\u00A6',
+ '\u2022',
+ '\u2229',
+ '\u00E7',
+ '\u00B8',
+ '\u00A2',
+ '\u03C7',
+ '\u02C6',
+ '\u2663',
+ '\u2245',
+ '\u00A9',
+ '\u21B5',
+ '\u222A',
+ '\u00A4',
+ '\u21D3',
+ '\u2020',
+ '\u2193',
+ '\u00B0',
+ '\u03B4',
+ '\u2666',
+ '\u00F7',
+ '\u00E9',
+ '\u00EA',
+ '\u00E8',
+ '\u2205',
+ '\u2003',
+ '\u2002',
+ '\u03B5',
+ '\u2261',
+ '\u03B7',
+ '\u00F0',
+ '\u00EB',
+ '\u20AC',
+ '\u2203',
+ '\u0192',
+ '\u2200',
+ '\u00BD',
+ '\u00BC',
+ '\u00BE',
+ '\u2044',
+ '\u03B3',
+ '\u2265',
+ '\u003E',
+ '\u21D4',
+ '\u2194',
+ '\u2665',
+ '\u2026',
+ '\u00ED',
+ '\u00EE',
+ '\u00A1',
+ '\u00EC',
+ '\u2111',
+ '\u221E',
+ '\u222B',
+ '\u03B9',
+ '\u00BF',
+ '\u2208',
+ '\u00EF',
+ '\u03BA',
+ '\u21D0',
+ '\u03BB',
+ '\u2329',
+ '\u00AB',
+ '\u2190',
+ '\u2308',
+ '\u201C',
+ '\u2264',
+ '\u230A',
+ '\u2217',
+ '\u25CA',
+ '\u200E',
+ '\u2039',
+ '\u2018',
+ '\u003C',
+ '\u00AF',
+ '\u2014',
+ '\u00B5',
+ '\u00B7',
+ '\u2212',
+ '\u03BC',
+ '\u2207',
+ '\u00A0',
+ '\u2013',
+ '\u2260',
+ '\u220B',
+ '\u00AC',
+ '\u2209',
+ '\u2284',
+ '\u00F1',
+ '\u03BD',
+ '\u00F3',
+ '\u00F4',
+ '\u0153',
+ '\u00F2',
+ '\u203E',
+ '\u03C9',
+ '\u03BF',
+ '\u2295',
+ '\u2228',
+ '\u00AA',
+ '\u00BA',
+ '\u00F8',
+ '\u00F5',
+ '\u2297',
+ '\u00F6',
+ '\u00B6',
+ '\u2202',
+ '\u2030',
+ '\u22A5',
+ '\u03C6',
+ '\u03C0',
+ '\u03D6',
+ '\u00B1',
+ '\u00A3',
+ '\u2032',
+ '\u220F',
+ '\u221D',
+ '\u03C8',
+ '\u0022',
+ '\u21D2',
+ '\u221A',
+ '\u232A',
+ '\u00BB',
+ '\u2192',
+ '\u2309',
+ '\u201D',
+ '\u211C',
+ '\u00AE',
+ '\u230B',
+ '\u03C1',
+ '\u200F',
+ '\u203A',
+ '\u2019',
+ '\u201A',
+ '\u0161',
+ '\u22C5',
+ '\u00A7',
+ '\u00AD',
+ '\u03C3',
+ '\u03C2',
+ '\u223C',
+ '\u2660',
+ '\u2282',
+ '\u2286',
+ '\u2211',
+ '\u2283',
+ '\u00B9',
+ '\u00B2',
+ '\u00B3',
+ '\u2287',
+ '\u00DF',
+ '\u03C4',
+ '\u2234',
+ '\u03B8',
+ '\u03D1',
+ '\u2009',
+ '\u00FE',
+ '\u02DC',
+ '\u00D7',
+ '\u2122',
+ '\u21D1',
+ '\u00FA',
+ '\u2191',
+ '\u00FB',
+ '\u00F9',
+ '\u00A8',
+ '\u03D2',
+ '\u03C5',
+ '\u00FC',
+ '\u2118',
+ '\u03BE',
+ '\u00FD',
+ '\u00A5',
+ '\u00FF',
+ '\u03B6',
+ '\u200D',
+ '\u200C'
+ };
+
+ #region Methods
+
+ static void WriteCharBytes(IList buf, char ch, Encoding e)
+ {
+ if (ch > 255)
+ {
+ foreach (byte b in e.GetBytes(new char[] { ch }))
+ buf.Add(b);
+ }
+ else
+ buf.Add((byte)ch);
+ }
+
+ public static string UrlDecode(string s, Encoding e)
+ {
+ if (null == s)
+ return null;
+
+ if (s.IndexOf('%') == -1 && s.IndexOf('+') == -1)
+ return s;
+
+ if (e == null)
+ e = Encoding.UTF8;
+
+ long len = s.Length;
+ var bytes = new List<byte>();
+ int xchar;
+ char ch;
+
+ for (int i = 0; i < len; i++)
+ {
+ ch = s[i];
+ if (ch == '%' && i + 2 < len && s[i + 1] != '%')
+ {
+ if (s[i + 1] == 'u' && i + 5 < len)
+ {
+ // unicode hex sequence
+ xchar = GetChar(s, i + 2, 4);
+ if (xchar != -1)
+ {
+ WriteCharBytes(bytes, (char)xchar, e);
+ i += 5;
+ }
+ else
+ WriteCharBytes(bytes, '%', e);
+ }
+ else if ((xchar = GetChar(s, i + 1, 2)) != -1)
+ {
+ WriteCharBytes(bytes, (char)xchar, e);
+ i += 2;
+ }
+ else
+ {
+ WriteCharBytes(bytes, '%', e);
+ }
+ continue;
+ }
+
+ if (ch == '+')
+ WriteCharBytes(bytes, ' ', e);
+ else
+ WriteCharBytes(bytes, ch, e);
+ }
+
+ byte[] buf = bytes.ToArray(bytes.Count);
+ bytes = null;
+ return e.GetString(buf, 0, buf.Length);
+
+ }
+
+ static int GetInt(byte b)
+ {
+ char c = (char)b;
+ if (c >= '0' && c <= '9')
+ return c - '0';
+
+ if (c >= 'a' && c <= 'f')
+ return c - 'a' + 10;
+
+ if (c >= 'A' && c <= 'F')
+ return c - 'A' + 10;
+
+ return -1;
+ }
+
+ static int GetChar(string str, int offset, int length)
+ {
+ int val = 0;
+ int end = length + offset;
+ for (int i = offset; i < end; i++)
+ {
+ char c = str[i];
+ if (c > 127)
+ return -1;
+
+ int current = GetInt((byte)c);
+ if (current == -1)
+ return -1;
+ val = (val << 4) + current;
+ }
+
+ return val;
+ }
+
+ static bool TryConvertKeyToEntity(string key, out char value)
+ {
+ var token = CalculateKeyValue(key);
+ if (token == 0)
+ {
+ value = '\0';
+ return false;
+ }
+
+ var idx = Array.BinarySearch(entities, token);
+ if (idx < 0)
+ {
+ value = '\0';
+ return false;
+ }
+
+ value = entities_values[idx];
+ return true;
+ }
+
+ static long CalculateKeyValue(string s)
+ {
+ if (s.Length > 8)
+ return 0;
+
+ long key = 0;
+ for (int i = 0; i < s.Length; ++i)
+ {
+ long ch = s[i];
+ if (ch > 'z' || ch < '0')
+ return 0;
+
+ key |= ch << ((7 - i) * 8);
+ }
+
+ return key;
+ }
+
+ /// <summary>
+ /// Decodes an HTML-encoded string and returns the decoded string.
+ /// </summary>
+ /// <param name="s">The HTML string to decode. </param>
+ /// <returns>The decoded text.</returns>
+ public static string HtmlDecode(string s)
+ {
+ if (s == null)
+ throw new ArgumentNullException("s");
+
+ if (s.IndexOf('&') == -1)
+ return s;
+
+ StringBuilder entity = new StringBuilder();
+ StringBuilder output = new StringBuilder();
+ int len = s.Length;
+ // 0 -> nothing,
+ // 1 -> right after '&'
+ // 2 -> between '&' and ';' but no '#'
+ // 3 -> '#' found after '&' and getting numbers
+ int state = 0;
+ int number = 0;
+ int digit_start = 0;
+ bool hex_number = false;
+
+ for (int i = 0; i < len; i++)
+ {
+ char c = s[i];
+ if (state == 0)
+ {
+ if (c == '&')
+ {
+ entity.Append(c);
+ state = 1;
+ }
+ else
+ {
+ output.Append(c);
+ }
+ continue;
+ }
+
+ if (c == '&')
+ {
+ state = 1;
+ if (digit_start > 0)
+ {
+ entity.Append(s, digit_start, i - digit_start);
+ digit_start = 0;
+ }
+
+ output.Append(entity.ToString());
+ entity.Length = 0;
+ entity.Append('&');
+ continue;
+ }
+
+ switch (state)
+ {
+ case 1:
+ if (c == ';')
+ {
+ state = 0;
+ output.Append(entity.ToString());
+ output.Append(c);
+ entity.Length = 0;
+ break;
+ }
+
+ number = 0;
+ hex_number = false;
+ if (c != '#')
+ {
+ state = 2;
+ }
+ else
+ {
+ state = 3;
+ }
+ entity.Append(c);
+
+ break;
+ case 2:
+ entity.Append(c);
+ if (c == ';')
+ {
+ string key = entity.ToString();
+ state = 0;
+ entity.Length = 0;
+
+ if (key.Length > 1)
+ {
+ var skey = key.Substring(1, key.Length - 2);
+ if (TryConvertKeyToEntity(skey, out c))
+ {
+ output.Append(c);
+ break;
+ }
+ }
+
+ output.Append(key);
+ }
+
+ break;
+ case 3:
+ if (c == ';')
+ {
+ if (number < 0x10000)
+ {
+ output.Append((char)number);
+ }
+ else
+ {
+ output.Append((char)(0xd800 + ((number - 0x10000) >> 10)));
+ output.Append((char)(0xdc00 + ((number - 0x10000) & 0x3ff)));
+ }
+ state = 0;
+ entity.Length = 0;
+ digit_start = 0;
+ break;
+ }
+
+ if (c == 'x' || c == 'X' && !hex_number)
+ {
+ digit_start = i;
+ hex_number = true;
+ break;
+ }
+
+ if (Char.IsDigit(c))
+ {
+ if (digit_start == 0)
+ digit_start = i;
+
+ number = number * (hex_number ? 16 : 10) + ((int)c - '0');
+ break;
+ }
+
+ if (hex_number)
+ {
+ if (c >= 'a' && c <= 'f')
+ {
+ number = number * 16 + 10 + ((int)c - 'a');
+ break;
+ }
+ if (c >= 'A' && c <= 'F')
+ {
+ number = number * 16 + 10 + ((int)c - 'A');
+ break;
+ }
+ }
+
+ state = 2;
+ if (digit_start > 0)
+ {
+ entity.Append(s, digit_start, i - digit_start);
+ digit_start = 0;
+ }
+
+ entity.Append(c);
+ break;
+ }
+ }
+
+ if (entity.Length > 0)
+ {
+ output.Append(entity);
+ }
+ else if (digit_start > 0)
+ {
+ output.Append(s, digit_start, s.Length - digit_start);
+ }
+ return output.ToString();
+ }
+
+ public static QueryParamCollection ParseQueryString(string query)
+ {
+ return ParseQueryString(query, Encoding.UTF8);
+ }
+
+ public static QueryParamCollection ParseQueryString(string query, Encoding encoding)
+ {
+ if (query == null)
+ throw new ArgumentNullException("query");
+ if (encoding == null)
+ throw new ArgumentNullException("encoding");
+ if (query.Length == 0 || (query.Length == 1 && query[0] == '?'))
+ return new QueryParamCollection();
+ if (query[0] == '?')
+ query = query.Substring(1);
+
+ QueryParamCollection result = new QueryParamCollection();
+ ParseQueryString(query, encoding, result);
+ return result;
+ }
+
+ internal static void ParseQueryString(string query, Encoding encoding, QueryParamCollection result)
+ {
+ if (query.Length == 0)
+ return;
+
+ string decoded = HtmlDecode(query);
+ int decodedLength = decoded.Length;
+ int namePos = 0;
+ bool first = true;
+ while (namePos <= decodedLength)
+ {
+ int valuePos = -1, valueEnd = -1;
+ for (int q = namePos; q < decodedLength; q++)
+ {
+ if (valuePos == -1 && decoded[q] == '=')
+ {
+ valuePos = q + 1;
+ }
+ else if (decoded[q] == '&')
+ {
+ valueEnd = q;
+ break;
+ }
+ }
+
+ if (first)
+ {
+ first = false;
+ if (decoded[namePos] == '?')
+ namePos++;
+ }
+
+ string name, value;
+ if (valuePos == -1)
+ {
+ name = null;
+ valuePos = namePos;
+ }
+ else
+ {
+ name = UrlDecode(decoded.Substring(namePos, valuePos - namePos - 1), encoding);
+ }
+ if (valueEnd < 0)
+ {
+ namePos = -1;
+ valueEnd = decoded.Length;
+ }
+ else
+ {
+ namePos = valueEnd + 1;
+ }
+ value = UrlDecode(decoded.Substring(valuePos, valueEnd - valuePos), encoding);
+
+ result.Add(name, value);
+ if (namePos == -1)
+ break;
+ }
+ }
+ #endregion // Methods
+ }
+}
diff --git a/MediaBrowser.Model/Services/IAsyncStreamWriter.cs b/MediaBrowser.Model/Services/IAsyncStreamWriter.cs
new file mode 100644
index 000000000..b10e12813
--- /dev/null
+++ b/MediaBrowser.Model/Services/IAsyncStreamWriter.cs
@@ -0,0 +1,11 @@
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Model.Services
+{
+ public interface IAsyncStreamWriter
+ {
+ Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Model/Services/IHasHeaders.cs b/MediaBrowser.Model/Services/IHasHeaders.cs
new file mode 100644
index 000000000..35e652b0f
--- /dev/null
+++ b/MediaBrowser.Model/Services/IHasHeaders.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Services
+{
+ public interface IHasHeaders
+ {
+ IDictionary<string, string> Headers { get; }
+ }
+}
diff --git a/MediaBrowser.Model/Services/IHasRequestFilter.cs b/MediaBrowser.Model/Services/IHasRequestFilter.cs
new file mode 100644
index 000000000..2164179d5
--- /dev/null
+++ b/MediaBrowser.Model/Services/IHasRequestFilter.cs
@@ -0,0 +1,21 @@
+
+namespace MediaBrowser.Model.Services
+{
+ public interface IHasRequestFilter
+ {
+ /// <summary>
+ /// Order in which Request Filters are executed.
+ /// &lt;0 Executed before global request filters
+ /// &gt;0 Executed after global request filters
+ /// </summary>
+ int Priority { get; }
+
+ /// <summary>
+ /// The request filter is executed before the service.
+ /// </summary>
+ /// <param name="req">The http request wrapper</param>
+ /// <param name="res">The http response wrapper</param>
+ /// <param name="requestDto">The request DTO</param>
+ void RequestFilter(IRequest req, IResponse res, object requestDto);
+ }
+}
diff --git a/MediaBrowser.Model/Services/IHttpRequest.cs b/MediaBrowser.Model/Services/IHttpRequest.cs
new file mode 100644
index 000000000..e1480f30a
--- /dev/null
+++ b/MediaBrowser.Model/Services/IHttpRequest.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Model.Services
+{
+ public interface IHttpRequest : IRequest
+ {
+ /// <summary>
+ /// The HttpResponse
+ /// </summary>
+ IHttpResponse HttpResponse { get; }
+
+ /// <summary>
+ /// The HTTP Verb
+ /// </summary>
+ string HttpMethod { get; }
+
+ /// <summary>
+ /// The IP Address of the X-Forwarded-For header, null if null or empty
+ /// </summary>
+ string XForwardedFor { get; }
+
+ /// <summary>
+ /// The Port number of the X-Forwarded-Port header, null if null or empty
+ /// </summary>
+ int? XForwardedPort { get; }
+
+ /// <summary>
+ /// The http or https scheme of the X-Forwarded-Proto header, null if null or empty
+ /// </summary>
+ string XForwardedProtocol { get; }
+
+ /// <summary>
+ /// The value of the X-Real-IP header, null if null or empty
+ /// </summary>
+ string XRealIp { get; }
+
+ /// <summary>
+ /// The value of the Accept HTTP Request Header
+ /// </summary>
+ string Accept { get; }
+ }
+}
diff --git a/MediaBrowser.Model/Services/IHttpResponse.cs b/MediaBrowser.Model/Services/IHttpResponse.cs
new file mode 100644
index 000000000..cd9c07d46
--- /dev/null
+++ b/MediaBrowser.Model/Services/IHttpResponse.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Model.Services
+{
+ public interface IHttpResponse : IResponse
+ {
+ //ICookies Cookies { get; }
+
+ /// <summary>
+ /// Adds a new Set-Cookie instruction to Response
+ /// </summary>
+ /// <param name="cookie"></param>
+ void SetCookie(Cookie cookie);
+
+ /// <summary>
+ /// Removes all pending Set-Cookie instructions
+ /// </summary>
+ void ClearCookies();
+ }
+}
diff --git a/MediaBrowser.Model/Services/IHttpResult.cs b/MediaBrowser.Model/Services/IHttpResult.cs
new file mode 100644
index 000000000..b912ef023
--- /dev/null
+++ b/MediaBrowser.Model/Services/IHttpResult.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Model.Services
+{
+ public interface IHttpResult : IHasHeaders
+ {
+ /// <summary>
+ /// The HTTP Response Status
+ /// </summary>
+ int Status { get; set; }
+
+ /// <summary>
+ /// The HTTP Response Status Code
+ /// </summary>
+ HttpStatusCode StatusCode { get; set; }
+
+ /// <summary>
+ /// The HTTP Response ContentType
+ /// </summary>
+ string ContentType { get; set; }
+
+ /// <summary>
+ /// Response DTO
+ /// </summary>
+ object Response { get; set; }
+
+ /// <summary>
+ /// Holds the request call context
+ /// </summary>
+ IRequest RequestContext { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs
new file mode 100644
index 000000000..681bab294
--- /dev/null
+++ b/MediaBrowser.Model/Services/IRequest.cs
@@ -0,0 +1,159 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.IO;
+
+namespace MediaBrowser.Model.Services
+{
+ public interface IRequest
+ {
+ /// <summary>
+ /// The underlying ASP.NET or HttpListener HttpRequest
+ /// </summary>
+ object OriginalRequest { get; }
+
+ IResponse Response { get; }
+
+ /// <summary>
+ /// The name of the service being called (e.g. Request DTO Name)
+ /// </summary>
+ string OperationName { get; set; }
+
+ /// <summary>
+ /// The Verb / HttpMethod or Action for this request
+ /// </summary>
+ string Verb { get; }
+
+ /// <summary>
+ /// The Request DTO, after it has been deserialized.
+ /// </summary>
+ object Dto { get; set; }
+
+ /// <summary>
+ /// The request ContentType
+ /// </summary>
+ string ContentType { get; }
+
+ bool IsLocal { get; }
+
+ string UserAgent { get; }
+
+ IDictionary<string, Cookie> Cookies { get; }
+
+ /// <summary>
+ /// The expected Response ContentType for this request
+ /// </summary>
+ string ResponseContentType { get; set; }
+
+ /// <summary>
+ /// Attach any data to this request that all filters and services can access.
+ /// </summary>
+ Dictionary<string, object> Items { get; }
+
+ QueryParamCollection Headers { get; }
+
+ QueryParamCollection QueryString { get; }
+
+ Task<QueryParamCollection> GetFormData();
+
+ string RawUrl { get; }
+
+ string AbsoluteUri { get; }
+
+ /// <summary>
+ /// The Remote Ip as reported by Request.UserHostAddress
+ /// </summary>
+ string UserHostAddress { get; }
+
+ /// <summary>
+ /// The Remote Ip as reported by X-Forwarded-For, X-Real-IP or Request.UserHostAddress
+ /// </summary>
+ string RemoteIp { get; }
+
+ /// <summary>
+ /// The value of the Authorization Header used to send the Api Key, null if not available
+ /// </summary>
+ string Authorization { get; }
+
+ /// <summary>
+ /// e.g. is https or not
+ /// </summary>
+ bool IsSecureConnection { get; }
+
+ string[] AcceptTypes { get; }
+
+ string PathInfo { get; }
+
+ Stream InputStream { get; }
+
+ long ContentLength { get; }
+
+ /// <summary>
+ /// Access to the multi-part/formdata files posted on this request
+ /// </summary>
+ IHttpFile[] Files { get; }
+
+ /// <summary>
+ /// The value of the Referrer, null if not available
+ /// </summary>
+ Uri UrlReferrer { get; }
+ }
+
+ public interface IHttpFile
+ {
+ string Name { get; }
+ string FileName { get; }
+ long ContentLength { get; }
+ string ContentType { get; }
+ Stream InputStream { get; }
+ }
+
+ public interface IRequiresRequest
+ {
+ IRequest Request { get; set; }
+ }
+
+ public interface IResponse
+ {
+ IRequest Request { get; }
+
+ int StatusCode { get; set; }
+
+ string StatusDescription { get; set; }
+
+ string ContentType { get; set; }
+
+ void AddHeader(string name, string value);
+
+ string GetHeader(string name);
+
+ void Redirect(string url);
+
+ Stream OutputStream { get; }
+
+ /// <summary>
+ /// Signal that this response has been handled and no more processing should be done.
+ /// When used in a request or response filter, no more filters or processing is done on this request.
+ /// </summary>
+ void Close();
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is closed.
+ /// </summary>
+ bool IsClosed { get; }
+
+ void SetContentLength(long contentLength);
+
+ //Add Metadata to Response
+ Dictionary<string, object> Items { get; }
+
+ QueryParamCollection Headers { get; }
+
+ Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken);
+
+ bool SendChunked { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Services/IRequiresRequestStream.cs b/MediaBrowser.Model/Services/IRequiresRequestStream.cs
new file mode 100644
index 000000000..0b8ac3ed3
--- /dev/null
+++ b/MediaBrowser.Model/Services/IRequiresRequestStream.cs
@@ -0,0 +1,12 @@
+using System.IO;
+
+namespace MediaBrowser.Model.Services
+{
+ public interface IRequiresRequestStream
+ {
+ /// <summary>
+ /// The raw Http Request Input Stream
+ /// </summary>
+ Stream RequestStream { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Services/IService.cs b/MediaBrowser.Model/Services/IService.cs
new file mode 100644
index 000000000..3e0ff280b
--- /dev/null
+++ b/MediaBrowser.Model/Services/IService.cs
@@ -0,0 +1,12 @@
+
+namespace MediaBrowser.Model.Services
+{
+ // marker interface
+ public interface IService
+ {
+ }
+
+ public interface IReturn { }
+ public interface IReturn<T> : IReturn { }
+ public interface IReturnVoid : IReturn { }
+}
diff --git a/MediaBrowser.Model/Services/IStreamWriter.cs b/MediaBrowser.Model/Services/IStreamWriter.cs
new file mode 100644
index 000000000..1fc11049e
--- /dev/null
+++ b/MediaBrowser.Model/Services/IStreamWriter.cs
@@ -0,0 +1,9 @@
+using System.IO;
+
+namespace MediaBrowser.Model.Services
+{
+ public interface IStreamWriter
+ {
+ void WriteTo(Stream responseStream);
+ }
+}
diff --git a/MediaBrowser.Model/Services/QueryParamCollection.cs b/MediaBrowser.Model/Services/QueryParamCollection.cs
new file mode 100644
index 000000000..6f8a76598
--- /dev/null
+++ b/MediaBrowser.Model/Services/QueryParamCollection.cs
@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Extensions;
+
+namespace MediaBrowser.Model.Services
+{
+ public class QueryParamCollection : List<NameValuePair>
+ {
+ public QueryParamCollection()
+ {
+
+ }
+
+ public QueryParamCollection(IDictionary<string, string> headers)
+ {
+ foreach (var pair in headers)
+ {
+ Add(pair.Key, pair.Value);
+ }
+ }
+
+ private StringComparison GetStringComparison()
+ {
+ return StringComparison.OrdinalIgnoreCase;
+ }
+
+ private StringComparer GetStringComparer()
+ {
+ return StringComparer.OrdinalIgnoreCase;
+ }
+
+ public string GetKey(int index)
+ {
+ return this[index].Name;
+ }
+
+ public string Get(int index)
+ {
+ return this[index].Value;
+ }
+
+ public virtual string[] GetValues(int index)
+ {
+ return new[] { Get(index) };
+ }
+
+ /// <summary>
+ /// Adds a new query parameter.
+ /// </summary>
+ public virtual void Add(string key, string value)
+ {
+ Add(new NameValuePair(key, value));
+ }
+
+ public virtual void Set(string key, string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ var parameters = GetItems(key);
+
+ foreach (var p in parameters)
+ {
+ Remove(p);
+ }
+
+ return;
+ }
+
+ foreach (var pair in this)
+ {
+ var stringComparison = GetStringComparison();
+
+ if (string.Equals(key, pair.Name, stringComparison))
+ {
+ pair.Value = value;
+ return;
+ }
+ }
+
+ Add(key, value);
+ }
+
+ /// <summary>
+ /// Removes all parameters of the given name.
+ /// </summary>
+ /// <returns>The number of parameters that were removed</returns>
+ /// <exception cref="ArgumentNullException"><paramref name="name" /> is null.</exception>
+ public virtual int Remove(string name)
+ {
+ return RemoveAll(p => p.Name == name);
+ }
+
+ public string Get(string name)
+ {
+ var stringComparison = GetStringComparison();
+
+ foreach (var pair in this)
+ {
+ if (string.Equals(pair.Name, name, stringComparison))
+ {
+ return pair.Value;
+ }
+ }
+
+ return null;
+ }
+
+ public virtual List<NameValuePair> GetItems(string name)
+ {
+ var stringComparison = GetStringComparison();
+
+ var list = new List<NameValuePair>();
+
+ foreach (var pair in this)
+ {
+ if (string.Equals(pair.Name, name, stringComparison))
+ {
+ list.Add(pair);
+ }
+ }
+
+ return list;
+ }
+
+ public virtual List<string> GetValues(string name)
+ {
+ var stringComparison = GetStringComparison();
+
+ var list = new List<string>();
+
+ foreach (var pair in this)
+ {
+ if (string.Equals(pair.Name, name, stringComparison))
+ {
+ list.Add(pair.Value);
+ }
+ }
+
+ return list;
+ }
+
+ public Dictionary<string, string> ToDictionary()
+ {
+ var stringComparer = GetStringComparer();
+
+ var headers = new Dictionary<string, string>(stringComparer);
+
+ foreach (var pair in this)
+ {
+ headers[pair.Name] = pair.Value;
+ }
+
+ return headers;
+ }
+
+ public IEnumerable<string> Keys
+ {
+ get
+ {
+ var keys = new string[this.Count];
+
+ for (var i = 0; i < keys.Length; i++)
+ {
+ keys[i] = this[i].Name;
+ }
+
+ return keys;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a query parameter value by name. A query may contain multiple values of the same name
+ /// (i.e. "x=1&amp;x=2"), in which case the value is an array, which works for both getting and setting.
+ /// </summary>
+ /// <param name="name">The query parameter name</param>
+ /// <returns>The query parameter value or array of values</returns>
+ public string this[string name]
+ {
+ get { return Get(name); }
+ set
+ {
+ Set(name, value);
+ //var parameters = this.Where(p => p.Name == name).ToArray();
+ //var values = new[] { value };
+
+ //for (int i = 0; ; i++)
+ //{
+ // if (i < parameters.Length && i < values.Length)
+ // {
+ // if (values[i] == null)
+ // Remove(parameters[i]);
+ // else if (values[i] is NameValuePair)
+ // this[IndexOf(parameters[i])] = (NameValuePair)values[i];
+ // else
+ // parameters[i].Value = values[i];
+ // }
+ // else if (i < parameters.Length)
+ // Remove(parameters[i]);
+ // else if (i < values.Length)
+ // {
+ // if (values[i] != null)
+ // {
+ // if (values[i] is NameValuePair)
+ // Add((NameValuePair)values[i]);
+ // else
+ // Add(name, values[i]);
+ // }
+ // }
+ // else
+ // break;
+ //}
+ }
+ }
+
+ private string GetQueryStringValue(NameValuePair pair)
+ {
+ return pair.Name + "=" + pair.Value;
+ }
+
+ public override String ToString()
+ {
+ var vals = this.Select(GetQueryStringValue).ToArray(this.Count);
+
+ return string.Join("&", vals);
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Services/RouteAttribute.cs b/MediaBrowser.Model/Services/RouteAttribute.cs
new file mode 100644
index 000000000..264500e60
--- /dev/null
+++ b/MediaBrowser.Model/Services/RouteAttribute.cs
@@ -0,0 +1,148 @@
+using System;
+
+namespace MediaBrowser.Model.Services
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
+ public class RouteAttribute : Attribute
+ {
+ /// <summary>
+ /// <para>Initializes an instance of the <see cref="RouteAttribute"/> class.</para>
+ /// </summary>
+ /// <param name="path">
+ /// <para>The path template to map to the request. See
+ /// <see cref="Path">RouteAttribute.Path</see>
+ /// for details on the correct format.</para>
+ /// </param>
+ public RouteAttribute(string path)
+ : this(path, null)
+ {
+ }
+
+ /// <summary>
+ /// <para>Initializes an instance of the <see cref="RouteAttribute"/> class.</para>
+ /// </summary>
+ /// <param name="path">
+ /// <para>The path template to map to the request. See
+ /// <see cref="Path">RouteAttribute.Path</see>
+ /// for details on the correct format.</para>
+ /// </param>
+ /// <param name="verbs">A comma-delimited list of HTTP verbs supported by the
+ /// service. If unspecified, all verbs are assumed to be supported.</param>
+ public RouteAttribute(string path, string verbs)
+ {
+ Path = path;
+ Verbs = verbs;
+ }
+
+ /// <summary>
+ /// Gets or sets the path template to be mapped to the request.
+ /// </summary>
+ /// <value>
+ /// A <see cref="String"/> value providing the path mapped to
+ /// the request. Never <see langword="null"/>.
+ /// </value>
+ /// <remarks>
+ /// <para>Some examples of valid paths are:</para>
+ ///
+ /// <list>
+ /// <item>"/Inventory"</item>
+ /// <item>"/Inventory/{Category}/{ItemId}"</item>
+ /// <item>"/Inventory/{ItemPath*}"</item>
+ /// </list>
+ ///
+ /// <para>Variables are specified within "{}"
+ /// brackets. Each variable in the path is mapped to the same-named property
+ /// on the request DTO. At runtime, ServiceStack will parse the
+ /// request URL, extract the variable values, instantiate the request DTO,
+ /// and assign the variable values into the corresponding request properties,
+ /// prior to passing the request DTO to the service object for processing.</para>
+ ///
+ /// <para>It is not necessary to specify all request properties as
+ /// variables in the path. For unspecified properties, callers may provide
+ /// values in the query string. For example: the URL
+ /// "http://services/Inventory?Category=Books&amp;ItemId=12345" causes the same
+ /// request DTO to be processed as "http://services/Inventory/Books/12345",
+ /// provided that the paths "/Inventory" (which supports the first URL) and
+ /// "/Inventory/{Category}/{ItemId}" (which supports the second URL)
+ /// are both mapped to the request DTO.</para>
+ ///
+ /// <para>Please note that while it is possible to specify property values
+ /// in the query string, it is generally considered to be less RESTful and
+ /// less desirable than to specify them as variables in the path. Using the
+ /// query string to specify property values may also interfere with HTTP
+ /// caching.</para>
+ ///
+ /// <para>The final variable in the path may contain a "*" suffix
+ /// to grab all remaining segments in the path portion of the request URL and assign
+ /// them to a single property on the request DTO.
+ /// For example, if the path "/Inventory/{ItemPath*}" is mapped to the request DTO,
+ /// then the request URL "http://services/Inventory/Books/12345" will result
+ /// in a request DTO whose ItemPath property contains "Books/12345".
+ /// You may only specify one such variable in the path, and it must be positioned at
+ /// the end of the path.</para>
+ /// </remarks>
+ public string Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets short summary of what the route does.
+ /// </summary>
+ public string Summary { get; set; }
+
+ public string Description { get; set; }
+
+ public bool IsHidden { get; set; }
+
+ /// <summary>
+ /// Gets or sets longer text to explain the behaviour of the route.
+ /// </summary>
+ public string Notes { get; set; }
+
+ /// <summary>
+ /// Gets or sets a comma-delimited list of HTTP verbs supported by the service, such as
+ /// "GET,PUT,POST,DELETE".
+ /// </summary>
+ /// <value>
+ /// A <see cref="String"/> providing a comma-delimited list of HTTP verbs supported
+ /// by the service, <see langword="null"/> or empty if all verbs are supported.
+ /// </value>
+ public string Verbs { get; set; }
+
+ /// <summary>
+ /// Used to rank the precedences of route definitions in reverse routing.
+ /// i.e. Priorities below 0 are auto-generated have less precedence.
+ /// </summary>
+ public int Priority { get; set; }
+
+ protected bool Equals(RouteAttribute other)
+ {
+ return base.Equals(other)
+ && string.Equals(Path, other.Path)
+ && string.Equals(Summary, other.Summary)
+ && string.Equals(Notes, other.Notes)
+ && string.Equals(Verbs, other.Verbs)
+ && Priority == other.Priority;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != this.GetType()) return false;
+ return Equals((RouteAttribute)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = base.GetHashCode();
+ hashCode = (hashCode * 397) ^ (Path != null ? Path.GetHashCode() : 0);
+ hashCode = (hashCode * 397) ^ (Summary != null ? Summary.GetHashCode() : 0);
+ hashCode = (hashCode * 397) ^ (Notes != null ? Notes.GetHashCode() : 0);
+ hashCode = (hashCode * 397) ^ (Verbs != null ? Verbs.GetHashCode() : 0);
+ hashCode = (hashCode * 397) ^ Priority;
+ return hashCode;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Session/BrowseRequest.cs b/MediaBrowser.Model/Session/BrowseRequest.cs
new file mode 100644
index 000000000..0a13c0549
--- /dev/null
+++ b/MediaBrowser.Model/Session/BrowseRequest.cs
@@ -0,0 +1,27 @@
+
+namespace MediaBrowser.Model.Session
+{
+ /// <summary>
+ /// Class BrowseRequest
+ /// </summary>
+ public class BrowseRequest
+ {
+ /// <summary>
+ /// Artist, Genre, Studio, Person, or any kind of BaseItem
+ /// </summary>
+ /// <value>The type of the item.</value>
+ public string ItemType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item id.
+ /// </summary>
+ /// <value>The item id.</value>
+ public string ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the item.
+ /// </summary>
+ /// <value>The name of the item.</value>
+ public string ItemName { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs
new file mode 100644
index 000000000..0a780b910
--- /dev/null
+++ b/MediaBrowser.Model/Session/ClientCapabilities.cs
@@ -0,0 +1,33 @@
+using MediaBrowser.Model.Dlna;
+using System;
+
+namespace MediaBrowser.Model.Session
+{
+ public class ClientCapabilities
+ {
+ public string[] PlayableMediaTypes { get; set; }
+
+ public string[] SupportedCommands { get; set; }
+
+ public bool SupportsMediaControl { get; set; }
+ public bool SupportsContentUploading { get; set; }
+ public string MessageCallbackUrl { get; set; }
+ public string PushToken { get; set; }
+ public string PushTokenType { get; set; }
+
+ public bool SupportsPersistentIdentifier { get; set; }
+ public bool SupportsSync { get; set; }
+
+ public DeviceProfile DeviceProfile { get; set; }
+
+ public string AppStoreUrl { get; set; }
+ public string IconUrl { get; set; }
+
+ public ClientCapabilities()
+ {
+ PlayableMediaTypes = new string[] {};
+ SupportedCommands = new string[] {};
+ SupportsPersistentIdentifier = true;
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Session/GeneralCommand.cs b/MediaBrowser.Model/Session/GeneralCommand.cs
new file mode 100644
index 000000000..5cfe3e67b
--- /dev/null
+++ b/MediaBrowser.Model/Session/GeneralCommand.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System;
+
+namespace MediaBrowser.Model.Session
+{
+ public class GeneralCommand
+ {
+ public string Name { get; set; }
+
+ public Guid ControllingUserId { get; set; }
+
+ public Dictionary<string, string> Arguments { get; set; }
+
+ public GeneralCommand()
+ {
+ Arguments = new Dictionary<string, string>();
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Session/GeneralCommandType.cs b/MediaBrowser.Model/Session/GeneralCommandType.cs
new file mode 100644
index 000000000..9044dc3ec
--- /dev/null
+++ b/MediaBrowser.Model/Session/GeneralCommandType.cs
@@ -0,0 +1,46 @@
+namespace MediaBrowser.Model.Session
+{
+ /// <summary>
+ /// This exists simply to identify a set of known commands.
+ /// </summary>
+ public enum GeneralCommandType
+ {
+ MoveUp = 0,
+ MoveDown = 1,
+ MoveLeft = 2,
+ MoveRight = 3,
+ PageUp = 4,
+ PageDown = 5,
+ PreviousLetter = 6,
+ NextLetter = 7,
+ ToggleOsd = 8,
+ ToggleContextMenu = 9,
+ Select = 10,
+ Back = 11,
+ TakeScreenshot = 12,
+ SendKey = 13,
+ SendString = 14,
+ GoHome = 15,
+ GoToSettings = 16,
+ VolumeUp = 17,
+ VolumeDown = 18,
+ Mute = 19,
+ Unmute = 20,
+ ToggleMute = 21,
+ SetVolume = 22,
+ SetAudioStreamIndex = 23,
+ SetSubtitleStreamIndex = 24,
+ ToggleFullscreen = 25,
+ DisplayContent = 26,
+ GoToSearch = 27,
+ DisplayMessage = 28,
+ SetRepeatMode = 29,
+ ChannelUp = 30,
+ ChannelDown = 31,
+ SetMaxStreamingBitrate = 31,
+ Guide = 32,
+ ToggleStats = 33,
+ PlayMediaSource = 34,
+ PlayTrailers = 35
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Session/MessageCommand.cs b/MediaBrowser.Model/Session/MessageCommand.cs
new file mode 100644
index 000000000..b028765ed
--- /dev/null
+++ b/MediaBrowser.Model/Session/MessageCommand.cs
@@ -0,0 +1,12 @@
+
+namespace MediaBrowser.Model.Session
+{
+ public class MessageCommand
+ {
+ public string Header { get; set; }
+
+ public string Text { get; set; }
+
+ public long? TimeoutMs { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Session/PlayCommand.cs b/MediaBrowser.Model/Session/PlayCommand.cs
new file mode 100644
index 000000000..3a5a951d7
--- /dev/null
+++ b/MediaBrowser.Model/Session/PlayCommand.cs
@@ -0,0 +1,29 @@
+namespace MediaBrowser.Model.Session
+{
+ /// <summary>
+ /// Enum PlayCommand
+ /// </summary>
+ public enum PlayCommand
+ {
+ /// <summary>
+ /// The play now
+ /// </summary>
+ PlayNow = 0,
+ /// <summary>
+ /// The play next
+ /// </summary>
+ PlayNext = 1,
+ /// <summary>
+ /// The play last
+ /// </summary>
+ PlayLast = 2,
+ /// <summary>
+ /// The play instant mix
+ /// </summary>
+ PlayInstantMix = 3,
+ /// <summary>
+ /// The play shuffle
+ /// </summary>
+ PlayShuffle = 4
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Session/PlayMethod.cs b/MediaBrowser.Model/Session/PlayMethod.cs
new file mode 100644
index 000000000..87b728627
--- /dev/null
+++ b/MediaBrowser.Model/Session/PlayMethod.cs
@@ -0,0 +1,9 @@
+namespace MediaBrowser.Model.Session
+{
+ public enum PlayMethod
+ {
+ Transcode = 0,
+ DirectStream = 1,
+ DirectPlay = 2
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Session/PlayRequest.cs b/MediaBrowser.Model/Session/PlayRequest.cs
new file mode 100644
index 000000000..2ee489f96
--- /dev/null
+++ b/MediaBrowser.Model/Session/PlayRequest.cs
@@ -0,0 +1,43 @@
+using MediaBrowser.Model.Services;
+using System;
+
+namespace MediaBrowser.Model.Session
+{
+ /// <summary>
+ /// Class PlayRequest
+ /// </summary>
+ public class PlayRequest
+ {
+ /// <summary>
+ /// Gets or sets the item ids.
+ /// </summary>
+ /// <value>The item ids.</value>
+ [ApiMember(Name = "ItemIds", Description = "The ids of the items to play, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
+ public Guid[] ItemIds { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start position ticks that the first item should be played at
+ /// </summary>
+ /// <value>The start position ticks.</value>
+ [ApiMember(Name = "StartPositionTicks", Description = "The starting position of the first item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+ public long? StartPositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the play command.
+ /// </summary>
+ /// <value>The play command.</value>
+ [ApiMember(Name = "PlayCommand", Description = "The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
+ public PlayCommand PlayCommand { get; set; }
+
+ /// <summary>
+ /// Gets or sets the controlling user identifier.
+ /// </summary>
+ /// <value>The controlling user identifier.</value>
+ public Guid ControllingUserId { get; set; }
+
+ public int? SubtitleStreamIndex { get; set; }
+ public int? AudioStreamIndex { get; set; }
+ public string MediaSourceId { get; set; }
+ public int? StartIndex { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Session/PlaybackProgressInfo.cs b/MediaBrowser.Model/Session/PlaybackProgressInfo.cs
new file mode 100644
index 000000000..ce6b2875e
--- /dev/null
+++ b/MediaBrowser.Model/Session/PlaybackProgressInfo.cs
@@ -0,0 +1,119 @@
+using MediaBrowser.Model.Dto;
+using System;
+
+namespace MediaBrowser.Model.Session
+{
+ /// <summary>
+ /// Class PlaybackProgressInfo.
+ /// </summary>
+ public class PlaybackProgressInfo
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance can seek.
+ /// </summary>
+ /// <value><c>true</c> if this instance can seek; otherwise, <c>false</c>.</value>
+ public bool CanSeek { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item.
+ /// </summary>
+ /// <value>The item.</value>
+ public BaseItemDto Item { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item identifier.
+ /// </summary>
+ /// <value>The item identifier.</value>
+ public Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the session id.
+ /// </summary>
+ /// <value>The session id.</value>
+ public string SessionId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the media version identifier.
+ /// </summary>
+ /// <value>The media version identifier.</value>
+ public string MediaSourceId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index of the audio stream.
+ /// </summary>
+ /// <value>The index of the audio stream.</value>
+ public int? AudioStreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index of the subtitle stream.
+ /// </summary>
+ /// <value>The index of the subtitle stream.</value>
+ public int? SubtitleStreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is paused.
+ /// </summary>
+ /// <value><c>true</c> if this instance is paused; otherwise, <c>false</c>.</value>
+ public bool IsPaused { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is muted.
+ /// </summary>
+ /// <value><c>true</c> if this instance is muted; otherwise, <c>false</c>.</value>
+ public bool IsMuted { get; set; }
+
+ /// <summary>
+ /// Gets or sets the position ticks.
+ /// </summary>
+ /// <value>The position ticks.</value>
+ public long? PositionTicks { get; set; }
+
+ public long? PlaybackStartTimeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the volume level.
+ /// </summary>
+ /// <value>The volume level.</value>
+ public int? VolumeLevel { get; set; }
+
+ public int? Brightness { get; set; }
+
+ public string AspectRatio { get; set; }
+
+ /// <summary>
+ /// Gets or sets the play method.
+ /// </summary>
+ /// <value>The play method.</value>
+ public PlayMethod PlayMethod { get; set; }
+ /// <summary>
+ /// Gets or sets the live stream identifier.
+ /// </summary>
+ /// <value>The live stream identifier.</value>
+ public string LiveStreamId { get; set; }
+ /// <summary>
+ /// Gets or sets the play session identifier.
+ /// </summary>
+ /// <value>The play session identifier.</value>
+ public string PlaySessionId { get; set; }
+ /// <summary>
+ /// Gets or sets the repeat mode.
+ /// </summary>
+ /// <value>The repeat mode.</value>
+ public RepeatMode RepeatMode { get; set; }
+
+ public QueueItem[] NowPlayingQueue { get; set; }
+ public string PlaylistItemId { get; set; }
+ }
+
+ public enum RepeatMode
+ {
+ RepeatNone = 0,
+ RepeatAll = 1,
+ RepeatOne = 2
+ }
+
+ public class QueueItem {
+ public Guid Id { get; set; }
+ public string PlaylistItemId { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Session/PlaybackStartInfo.cs b/MediaBrowser.Model/Session/PlaybackStartInfo.cs
new file mode 100644
index 000000000..f6f496e4e
--- /dev/null
+++ b/MediaBrowser.Model/Session/PlaybackStartInfo.cs
@@ -0,0 +1,10 @@
+
+namespace MediaBrowser.Model.Session
+{
+ /// <summary>
+ /// Class PlaybackStartInfo.
+ /// </summary>
+ public class PlaybackStartInfo : PlaybackProgressInfo
+ {
+ }
+}
diff --git a/MediaBrowser.Model/Session/PlaybackStopInfo.cs b/MediaBrowser.Model/Session/PlaybackStopInfo.cs
new file mode 100644
index 000000000..6f3351eef
--- /dev/null
+++ b/MediaBrowser.Model/Session/PlaybackStopInfo.cs
@@ -0,0 +1,57 @@
+using MediaBrowser.Model.Dto;
+using System;
+
+namespace MediaBrowser.Model.Session
+{
+ /// <summary>
+ /// Class PlaybackStopInfo.
+ /// </summary>
+ public class PlaybackStopInfo
+ {
+ /// <summary>
+ /// Gets or sets the item.
+ /// </summary>
+ /// <value>The item.</value>
+ public BaseItemDto Item { get; set; }
+ /// <summary>
+ /// Gets or sets the item identifier.
+ /// </summary>
+ /// <value>The item identifier.</value>
+ public Guid ItemId { get; set; }
+ /// <summary>
+ /// Gets or sets the session id.
+ /// </summary>
+ /// <value>The session id.</value>
+ public string SessionId { get; set; }
+ /// <summary>
+ /// Gets or sets the media version identifier.
+ /// </summary>
+ /// <value>The media version identifier.</value>
+ public string MediaSourceId { get; set; }
+ /// <summary>
+ /// Gets or sets the position ticks.
+ /// </summary>
+ /// <value>The position ticks.</value>
+ public long? PositionTicks { get; set; }
+ /// <summary>
+ /// Gets or sets the live stream identifier.
+ /// </summary>
+ /// <value>The live stream identifier.</value>
+ public string LiveStreamId { get; set; }
+ /// <summary>
+ /// Gets or sets the play session identifier.
+ /// </summary>
+ /// <value>The play session identifier.</value>
+ public string PlaySessionId { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="PlaybackStopInfo"/> is failed.
+ /// </summary>
+ /// <value><c>true</c> if failed; otherwise, <c>false</c>.</value>
+ public bool Failed { get; set; }
+
+ public string NextMediaType { get; set; }
+
+ public string PlaylistItemId { get; set; }
+ public QueueItem[] NowPlayingQueue { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Session/PlayerStateInfo.cs b/MediaBrowser.Model/Session/PlayerStateInfo.cs
new file mode 100644
index 000000000..f78842e29
--- /dev/null
+++ b/MediaBrowser.Model/Session/PlayerStateInfo.cs
@@ -0,0 +1,65 @@
+namespace MediaBrowser.Model.Session
+{
+ public class PlayerStateInfo
+ {
+ /// <summary>
+ /// Gets or sets the now playing position ticks.
+ /// </summary>
+ /// <value>The now playing position ticks.</value>
+ public long? PositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance can seek.
+ /// </summary>
+ /// <value><c>true</c> if this instance can seek; otherwise, <c>false</c>.</value>
+ public bool CanSeek { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is paused.
+ /// </summary>
+ /// <value><c>true</c> if this instance is paused; otherwise, <c>false</c>.</value>
+ public bool IsPaused { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is muted.
+ /// </summary>
+ /// <value><c>true</c> if this instance is muted; otherwise, <c>false</c>.</value>
+ public bool IsMuted { get; set; }
+
+ /// <summary>
+ /// Gets or sets the volume level.
+ /// </summary>
+ /// <value>The volume level.</value>
+ public int? VolumeLevel { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index of the now playing audio stream.
+ /// </summary>
+ /// <value>The index of the now playing audio stream.</value>
+ public int? AudioStreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index of the now playing subtitle stream.
+ /// </summary>
+ /// <value>The index of the now playing subtitle stream.</value>
+ public int? SubtitleStreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the now playing media version identifier.
+ /// </summary>
+ /// <value>The now playing media version identifier.</value>
+ public string MediaSourceId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the play method.
+ /// </summary>
+ /// <value>The play method.</value>
+ public PlayMethod? PlayMethod { get; set; }
+
+ /// <summary>
+ /// Gets or sets the repeat mode.
+ /// </summary>
+ /// <value>The repeat mode.</value>
+ public RepeatMode RepeatMode { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Session/PlaystateCommand.cs b/MediaBrowser.Model/Session/PlaystateCommand.cs
new file mode 100644
index 000000000..3b70d5454
--- /dev/null
+++ b/MediaBrowser.Model/Session/PlaystateCommand.cs
@@ -0,0 +1,43 @@
+
+namespace MediaBrowser.Model.Session
+{
+ /// <summary>
+ /// Enum PlaystateCommand
+ /// </summary>
+ public enum PlaystateCommand
+ {
+ /// <summary>
+ /// The stop
+ /// </summary>
+ Stop,
+ /// <summary>
+ /// The pause
+ /// </summary>
+ Pause,
+ /// <summary>
+ /// The unpause
+ /// </summary>
+ Unpause,
+ /// <summary>
+ /// The next track
+ /// </summary>
+ NextTrack,
+ /// <summary>
+ /// The previous track
+ /// </summary>
+ PreviousTrack,
+ /// <summary>
+ /// The seek
+ /// </summary>
+ Seek,
+ /// <summary>
+ /// The rewind
+ /// </summary>
+ Rewind,
+ /// <summary>
+ /// The fast forward
+ /// </summary>
+ FastForward,
+ PlayPause
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Session/PlaystateRequest.cs b/MediaBrowser.Model/Session/PlaystateRequest.cs
new file mode 100644
index 000000000..8a046b503
--- /dev/null
+++ b/MediaBrowser.Model/Session/PlaystateRequest.cs
@@ -0,0 +1,15 @@
+namespace MediaBrowser.Model.Session
+{
+ public class PlaystateRequest
+ {
+ public PlaystateCommand Command { get; set; }
+
+ public long? SeekPositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the controlling user identifier.
+ /// </summary>
+ /// <value>The controlling user identifier.</value>
+ public string ControllingUserId { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Session/SessionInfoDto.cs b/MediaBrowser.Model/Session/SessionInfoDto.cs
new file mode 100644
index 000000000..ca14107df
--- /dev/null
+++ b/MediaBrowser.Model/Session/SessionInfoDto.cs
@@ -0,0 +1,117 @@
+using MediaBrowser.Model.Dto;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Session
+{
+ public class SessionInfoDto
+ {
+ /// <summary>
+ /// Gets or sets the supported commands.
+ /// </summary>
+ /// <value>The supported commands.</value>
+ public string[] SupportedCommands { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playable media types.
+ /// </summary>
+ /// <value>The playable media types.</value>
+ public string[] PlayableMediaTypes { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public string Id { get; set; }
+
+ public string ServerId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ /// <value>The user id.</value>
+ public string UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user primary image tag.
+ /// </summary>
+ /// <value>The user primary image tag.</value>
+ public string UserPrimaryImageTag { get; set; }
+
+ public string RemoteEndPoint { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the user.
+ /// </summary>
+ /// <value>The name of the user.</value>
+ public string UserName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the additional users present.
+ /// </summary>
+ /// <value>The additional users present.</value>
+ public SessionUserInfo[] AdditionalUsers { get; set; }
+
+ /// <summary>
+ /// Gets or sets the application version.
+ /// </summary>
+ /// <value>The application version.</value>
+ public string ApplicationVersion { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type of the client.
+ /// </summary>
+ /// <value>The type of the client.</value>
+ public string Client { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last activity date.
+ /// </summary>
+ /// <value>The last activity date.</value>
+ public DateTime LastActivityDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the device.
+ /// </summary>
+ /// <value>The name of the device.</value>
+ public string DeviceName { get; set; }
+
+ public string DeviceType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the now playing item.
+ /// </summary>
+ /// <value>The now playing item.</value>
+ public BaseItemDto NowPlayingItem { get; set; }
+
+ /// <summary>
+ /// Gets or sets the device id.
+ /// </summary>
+ /// <value>The device id.</value>
+ public string DeviceId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the application icon URL.
+ /// </summary>
+ /// <value>The application icon URL.</value>
+ public string AppIconUrl { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [supports remote control].
+ /// </summary>
+ /// <value><c>true</c> if [supports remote control]; otherwise, <c>false</c>.</value>
+ public bool SupportsRemoteControl { get; set; }
+
+ public PlayerStateInfo PlayState { get; set; }
+
+ public TranscodingInfo TranscodingInfo { get; set; }
+
+ public SessionInfoDto()
+ {
+ AdditionalUsers = new SessionUserInfo[] { };
+
+ PlayableMediaTypes = new string[] {};
+ SupportedCommands = new string[] {};
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Session/SessionUserInfo.cs b/MediaBrowser.Model/Session/SessionUserInfo.cs
new file mode 100644
index 000000000..7746bc2d6
--- /dev/null
+++ b/MediaBrowser.Model/Session/SessionUserInfo.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace MediaBrowser.Model.Session
+{
+ /// <summary>
+ /// Class SessionUserInfo.
+ /// </summary>
+ public class SessionUserInfo
+ {
+ /// <summary>
+ /// Gets or sets the user identifier.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public Guid UserId { get; set; }
+ /// <summary>
+ /// Gets or sets the name of the user.
+ /// </summary>
+ /// <value>The name of the user.</value>
+ public string UserName { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Session/TranscodingInfo.cs b/MediaBrowser.Model/Session/TranscodingInfo.cs
new file mode 100644
index 000000000..ed86d2358
--- /dev/null
+++ b/MediaBrowser.Model/Session/TranscodingInfo.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Session
+{
+ public class TranscodingInfo
+ {
+ public string AudioCodec { get; set; }
+ public string VideoCodec { get; set; }
+ public string Container { get; set; }
+ public bool IsVideoDirect { get; set; }
+ public bool IsAudioDirect { get; set; }
+ public int? Bitrate { get; set; }
+
+ public float? Framerate { get; set; }
+ public double? CompletionPercentage { get; set; }
+
+ public int? Width { get; set; }
+ public int? Height { get; set; }
+ public int? AudioChannels { get; set; }
+
+ public TranscodeReason[] TranscodeReasons { get; set; }
+
+ public TranscodingInfo()
+ {
+ TranscodeReasons = new TranscodeReason[] { };
+ }
+ }
+
+ public enum TranscodeReason
+ {
+ ContainerNotSupported = 0,
+ VideoCodecNotSupported = 1,
+ AudioCodecNotSupported = 2,
+ ContainerBitrateExceedsLimit = 3,
+ AudioBitrateNotSupported = 4,
+ AudioChannelsNotSupported = 5,
+ VideoResolutionNotSupported = 6,
+ UnknownVideoStreamInfo = 7,
+ UnknownAudioStreamInfo = 8,
+ AudioProfileNotSupported = 9,
+ AudioSampleRateNotSupported = 10,
+ AnamorphicVideoNotSupported = 11,
+ InterlacedVideoNotSupported = 12,
+ SecondaryAudioNotSupported = 13,
+ RefFramesNotSupported = 14,
+ VideoBitDepthNotSupported = 15,
+ VideoBitrateNotSupported = 16,
+ VideoFramerateNotSupported = 17,
+ VideoLevelNotSupported = 18,
+ VideoProfileNotSupported = 19,
+ AudioBitDepthNotSupported = 20,
+ SubtitleCodecNotSupported = 21,
+ DirectPlayError = 22
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Session/UserDataChangeInfo.cs b/MediaBrowser.Model/Session/UserDataChangeInfo.cs
new file mode 100644
index 000000000..c6b03200d
--- /dev/null
+++ b/MediaBrowser.Model/Session/UserDataChangeInfo.cs
@@ -0,0 +1,23 @@
+using MediaBrowser.Model.Dto;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Session
+{
+ /// <summary>
+ /// Class UserDataChangeInfo
+ /// </summary>
+ public class UserDataChangeInfo
+ {
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ /// <value>The user id.</value>
+ public string UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user data list.
+ /// </summary>
+ /// <value>The user data list.</value>
+ public UserItemDataDto[] UserDataList { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Sync/SyncCategory.cs b/MediaBrowser.Model/Sync/SyncCategory.cs
new file mode 100644
index 000000000..e0d748685
--- /dev/null
+++ b/MediaBrowser.Model/Sync/SyncCategory.cs
@@ -0,0 +1,19 @@
+
+namespace MediaBrowser.Model.Sync
+{
+ public enum SyncCategory
+ {
+ /// <summary>
+ /// The latest
+ /// </summary>
+ Latest = 0,
+ /// <summary>
+ /// The next up
+ /// </summary>
+ NextUp = 1,
+ /// <summary>
+ /// The resume
+ /// </summary>
+ Resume = 2
+ }
+}
diff --git a/MediaBrowser.Model/Sync/SyncJob.cs b/MediaBrowser.Model/Sync/SyncJob.cs
new file mode 100644
index 000000000..0cf9312a1
--- /dev/null
+++ b/MediaBrowser.Model/Sync/SyncJob.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Sync;
+
+namespace MediaBrowser.Model.Sync
+{
+ public class SyncJob
+ {
+ /// <summary>
+ /// Gets or sets the identifier.
+ /// </summary>
+ /// <value>The identifier.</value>
+ public string Id { get; set; }
+ /// <summary>
+ /// Gets or sets the device identifier.
+ /// </summary>
+ /// <value>The device identifier.</value>
+ public string TargetId { get; set; }
+ /// <summary>
+ /// Gets or sets the name of the target.
+ /// </summary>
+ /// <value>The name of the target.</value>
+ public string TargetName { get; set; }
+ /// <summary>
+ /// Gets or sets the quality.
+ /// </summary>
+ /// <value>The quality.</value>
+ public string Quality { get; set; }
+ /// <summary>
+ /// Gets or sets the bitrate.
+ /// </summary>
+ /// <value>The bitrate.</value>
+ public int? Bitrate { get; set; }
+ /// <summary>
+ /// Gets or sets the profile.
+ /// </summary>
+ /// <value>The profile.</value>
+ public string Profile { get; set; }
+ /// <summary>
+ /// Gets or sets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public SyncCategory? Category { get; set; }
+ /// <summary>
+ /// Gets or sets the parent identifier.
+ /// </summary>
+ /// <value>The parent identifier.</value>
+ public string ParentId { get; set; }
+ /// <summary>
+ /// Gets or sets the current progress.
+ /// </summary>
+ /// <value>The current progress.</value>
+ public double? Progress { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the status.
+ /// </summary>
+ /// <value>The status.</value>
+ public SyncJobStatus Status { get; set; }
+ /// <summary>
+ /// Gets or sets the user identifier.
+ /// </summary>
+ /// <value>The user identifier.</value>
+ public string UserId { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether [unwatched only].
+ /// </summary>
+ /// <value><c>true</c> if [unwatched only]; otherwise, <c>false</c>.</value>
+ public bool UnwatchedOnly { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether [synchronize new content].
+ /// </summary>
+ /// <value><c>true</c> if [synchronize new content]; otherwise, <c>false</c>.</value>
+ public bool SyncNewContent { get; set; }
+ /// <summary>
+ /// Gets or sets the item limit.
+ /// </summary>
+ /// <value>The item limit.</value>
+ public int? ItemLimit { get; set; }
+ /// <summary>
+ /// Gets or sets the requested item ids.
+ /// </summary>
+ /// <value>The requested item ids.</value>
+ public Guid[] RequestedItemIds { get; set; }
+ /// <summary>
+ /// Gets or sets the date created.
+ /// </summary>
+ /// <value>The date created.</value>
+ public DateTime DateCreated { get; set; }
+ /// <summary>
+ /// Gets or sets the date last modified.
+ /// </summary>
+ /// <value>The date last modified.</value>
+ public DateTime DateLastModified { get; set; }
+ /// <summary>
+ /// Gets or sets the item count.
+ /// </summary>
+ /// <value>The item count.</value>
+ public int ItemCount { get; set; }
+
+ public string ParentName { get; set; }
+ public string PrimaryImageItemId { get; set; }
+ public string PrimaryImageTag { get; set; }
+
+ public SyncJob()
+ {
+ RequestedItemIds = new Guid[] { };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Sync/SyncJobStatus.cs b/MediaBrowser.Model/Sync/SyncJobStatus.cs
new file mode 100644
index 000000000..2d1d30802
--- /dev/null
+++ b/MediaBrowser.Model/Sync/SyncJobStatus.cs
@@ -0,0 +1,14 @@
+
+namespace MediaBrowser.Model.Sync
+{
+ public enum SyncJobStatus
+ {
+ Queued = 0,
+ Converting = 1,
+ ReadyToTransfer = 2,
+ Transferring = 3,
+ Completed = 4,
+ CompletedWithError = 5,
+ Failed = 6
+ }
+}
diff --git a/MediaBrowser.Model/Sync/SyncTarget.cs b/MediaBrowser.Model/Sync/SyncTarget.cs
new file mode 100644
index 000000000..8901f0f27
--- /dev/null
+++ b/MediaBrowser.Model/Sync/SyncTarget.cs
@@ -0,0 +1,17 @@
+
+namespace MediaBrowser.Model.Sync
+{
+ public class SyncTarget
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the identifier.
+ /// </summary>
+ /// <value>The identifier.</value>
+ public string Id { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/System/Architecture.cs b/MediaBrowser.Model/System/Architecture.cs
new file mode 100644
index 000000000..73f78cd58
--- /dev/null
+++ b/MediaBrowser.Model/System/Architecture.cs
@@ -0,0 +1,10 @@
+namespace MediaBrowser.Model.System
+{
+ public enum Architecture
+ {
+ X86 = 0,
+ X64 = 1,
+ Arm = 2,
+ Arm64 = 3
+ }
+}
diff --git a/MediaBrowser.Model/System/IEnvironmentInfo.cs b/MediaBrowser.Model/System/IEnvironmentInfo.cs
new file mode 100644
index 000000000..8cf25a365
--- /dev/null
+++ b/MediaBrowser.Model/System/IEnvironmentInfo.cs
@@ -0,0 +1,24 @@
+
+namespace MediaBrowser.Model.System
+{
+ public interface IEnvironmentInfo
+ {
+ MediaBrowser.Model.System.OperatingSystem OperatingSystem { get; }
+ string OperatingSystemName { get; }
+ string OperatingSystemVersion { get; }
+ Architecture SystemArchitecture { get; }
+ string GetEnvironmentVariable(string name);
+ void SetProcessEnvironmentVariable(string name, string value);
+ string StackTrace { get; }
+ char PathSeparator { get; }
+ }
+
+ public enum OperatingSystem
+ {
+ Windows,
+ Linux,
+ OSX,
+ BSD,
+ Android
+ }
+}
diff --git a/MediaBrowser.Model/System/IPowerManagement.cs b/MediaBrowser.Model/System/IPowerManagement.cs
new file mode 100644
index 000000000..03907568c
--- /dev/null
+++ b/MediaBrowser.Model/System/IPowerManagement.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace MediaBrowser.Model.System
+{
+ public interface IPowerManagement
+ {
+ void PreventSystemStandby();
+ void AllowSystemStandby();
+ void ScheduleWake(DateTime wakeTimeUtc, string displayName);
+ }
+}
diff --git a/MediaBrowser.Model/System/ISystemEvents.cs b/MediaBrowser.Model/System/ISystemEvents.cs
new file mode 100644
index 000000000..dec8ed8c0
--- /dev/null
+++ b/MediaBrowser.Model/System/ISystemEvents.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace MediaBrowser.Model.System
+{
+ public interface ISystemEvents
+ {
+ event EventHandler Resume;
+ event EventHandler Suspend;
+ event EventHandler SessionLogoff;
+ event EventHandler SystemShutdown;
+ }
+}
diff --git a/MediaBrowser.Model/System/LogFile.cs b/MediaBrowser.Model/System/LogFile.cs
new file mode 100644
index 000000000..ba409c542
--- /dev/null
+++ b/MediaBrowser.Model/System/LogFile.cs
@@ -0,0 +1,31 @@
+using System;
+
+namespace MediaBrowser.Model.System
+{
+ public class LogFile
+ {
+ /// <summary>
+ /// Gets or sets the date created.
+ /// </summary>
+ /// <value>The date created.</value>
+ public DateTime DateCreated { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date modified.
+ /// </summary>
+ /// <value>The date modified.</value>
+ public DateTime DateModified { get; set; }
+
+ /// <summary>
+ /// Gets or sets the size.
+ /// </summary>
+ /// <value>The size.</value>
+ public long Size { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/System/PublicSystemInfo.cs b/MediaBrowser.Model/System/PublicSystemInfo.cs
new file mode 100644
index 000000000..b9a3260b0
--- /dev/null
+++ b/MediaBrowser.Model/System/PublicSystemInfo.cs
@@ -0,0 +1,41 @@
+namespace MediaBrowser.Model.System
+{
+ public class PublicSystemInfo
+ {
+ /// <summary>
+ /// Gets or sets the local address.
+ /// </summary>
+ /// <value>The local address.</value>
+ public string LocalAddress { get; set; }
+
+ /// <summary>
+ /// Gets or sets the wan address.
+ /// </summary>
+ /// <value>The wan address.</value>
+ public string WanAddress { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name of the server.
+ /// </summary>
+ /// <value>The name of the server.</value>
+ public string ServerName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the version.
+ /// </summary>
+ /// <value>The version.</value>
+ public string Version { get; set; }
+
+ /// <summary>
+ /// Gets or sets the operating sytem.
+ /// </summary>
+ /// <value>The operating sytem.</value>
+ public string OperatingSystem { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public string Id { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs
new file mode 100644
index 000000000..c790731c6
--- /dev/null
+++ b/MediaBrowser.Model/System/SystemInfo.cs
@@ -0,0 +1,140 @@
+using MediaBrowser.Model.Updates;
+using System.Collections.Generic;
+using System;
+
+namespace MediaBrowser.Model.System
+{
+ /// <summary>
+ /// Class SystemInfo
+ /// </summary>
+ public class SystemInfo : PublicSystemInfo
+ {
+ public PackageVersionClass SystemUpdateLevel { get; set; }
+
+ /// <summary>
+ /// Gets or sets the display name of the operating system.
+ /// </summary>
+ /// <value>The display name of the operating system.</value>
+ public string OperatingSystemDisplayName { get; set; }
+
+ public string PackageName { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has pending restart.
+ /// </summary>
+ /// <value><c>true</c> if this instance has pending restart; otherwise, <c>false</c>.</value>
+ public bool HasPendingRestart { get; set; }
+
+ public bool IsShuttingDown { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [supports library monitor].
+ /// </summary>
+ /// <value><c>true</c> if [supports library monitor]; otherwise, <c>false</c>.</value>
+ public bool SupportsLibraryMonitor { get; set; }
+
+ /// <summary>
+ /// Gets or sets the web socket port number.
+ /// </summary>
+ /// <value>The web socket port number.</value>
+ public int WebSocketPortNumber { get; set; }
+
+ /// <summary>
+ /// Gets or sets the completed installations.
+ /// </summary>
+ /// <value>The completed installations.</value>
+ public InstallationInfo[] CompletedInstallations { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance can self restart.
+ /// </summary>
+ /// <value><c>true</c> if this instance can self restart; otherwise, <c>false</c>.</value>
+ public bool CanSelfRestart { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance can self update.
+ /// </summary>
+ /// <value><c>true</c> if this instance can self update; otherwise, <c>false</c>.</value>
+ public bool CanSelfUpdate { get; set; }
+
+ public bool CanLaunchWebBrowser { get; set; }
+
+ /// <summary>
+ /// Gets or sets the program data path.
+ /// </summary>
+ /// <value>The program data path.</value>
+ public string ProgramDataPath { get; set; }
+
+ /// <summary>
+ /// Gets or sets the items by name path.
+ /// </summary>
+ /// <value>The items by name path.</value>
+ public string ItemsByNamePath { get; set; }
+
+ /// <summary>
+ /// Gets or sets the cache path.
+ /// </summary>
+ /// <value>The cache path.</value>
+ public string CachePath { get; set; }
+
+ /// <summary>
+ /// Gets or sets the log path.
+ /// </summary>
+ /// <value>The log path.</value>
+ public string LogPath { get; set; }
+
+ /// <summary>
+ /// Gets or sets the internal metadata path.
+ /// </summary>
+ /// <value>The internal metadata path.</value>
+ public string InternalMetadataPath { get; set; }
+
+ /// <summary>
+ /// Gets or sets the transcoding temporary path.
+ /// </summary>
+ /// <value>The transcoding temporary path.</value>
+ public string TranscodingTempPath { get; set; }
+
+ /// <summary>
+ /// Gets or sets the HTTP server port number.
+ /// </summary>
+ /// <value>The HTTP server port number.</value>
+ public int HttpServerPortNumber { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [enable HTTPS].
+ /// </summary>
+ /// <value><c>true</c> if [enable HTTPS]; otherwise, <c>false</c>.</value>
+ public bool SupportsHttps { get; set; }
+
+ /// <summary>
+ /// Gets or sets the HTTPS server port number.
+ /// </summary>
+ /// <value>The HTTPS server port number.</value>
+ public int HttpsPortNumber { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has update available.
+ /// </summary>
+ /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
+ public bool HasUpdateAvailable { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [supports automatic run at startup].
+ /// </summary>
+ /// <value><c>true</c> if [supports automatic run at startup]; otherwise, <c>false</c>.</value>
+ public bool SupportsAutoRunAtStartup { get; set; }
+
+ public string EncoderLocationType { get; set; }
+
+ public Architecture SystemArchitecture { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SystemInfo" /> class.
+ /// </summary>
+ public SystemInfo()
+ {
+ CompletedInstallations = new InstallationInfo[] { };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/System/WakeOnLanInfo.cs b/MediaBrowser.Model/System/WakeOnLanInfo.cs
new file mode 100644
index 000000000..cde867176
--- /dev/null
+++ b/MediaBrowser.Model/System/WakeOnLanInfo.cs
@@ -0,0 +1,13 @@
+namespace MediaBrowser.Model.System
+{
+ public class WakeOnLanInfo
+ {
+ public string MacAddress { get; set; }
+ public int Port { get; set; }
+
+ public WakeOnLanInfo()
+ {
+ Port = 9;
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Tasks/IConfigurableScheduledTask.cs b/MediaBrowser.Model/Tasks/IConfigurableScheduledTask.cs
new file mode 100644
index 000000000..ed981a905
--- /dev/null
+++ b/MediaBrowser.Model/Tasks/IConfigurableScheduledTask.cs
@@ -0,0 +1,18 @@
+namespace MediaBrowser.Model.Tasks
+{
+ public interface IConfigurableScheduledTask
+ {
+ /// <summary>
+ /// Gets a value indicating whether this instance is hidden.
+ /// </summary>
+ /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value>
+ bool IsHidden { get; }
+ /// <summary>
+ /// Gets a value indicating whether this instance is enabled.
+ /// </summary>
+ /// <value><c>true</c> if this instance is enabled; otherwise, <c>false</c>.</value>
+ bool IsEnabled { get; }
+
+ bool IsLogged { get; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Tasks/IScheduledTask.cs b/MediaBrowser.Model/Tasks/IScheduledTask.cs
new file mode 100644
index 000000000..81ba239ad
--- /dev/null
+++ b/MediaBrowser.Model/Tasks/IScheduledTask.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Model.Tasks
+{
+ /// <summary>
+ /// Interface IScheduledTaskWorker
+ /// </summary>
+ public interface IScheduledTask
+ {
+ /// <summary>
+ /// Gets the name of the task
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ string Key { get; }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ string Description { get; }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ string Category { get; }
+
+ /// <summary>
+ /// Executes the task
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ Task Execute(CancellationToken cancellationToken, IProgress<double> progress);
+
+ /// <summary>
+ /// Gets the default triggers.
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ IEnumerable<TaskTriggerInfo> GetDefaultTriggers();
+ }
+}
diff --git a/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs b/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs
new file mode 100644
index 000000000..415207f8f
--- /dev/null
+++ b/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs
@@ -0,0 +1,76 @@
+using System;
+using MediaBrowser.Model.Events;
+
+namespace MediaBrowser.Model.Tasks
+{
+ /// <summary>
+ /// Interface IScheduledTaskWorker
+ /// </summary>
+ public interface IScheduledTaskWorker : IDisposable
+ {
+ /// <summary>
+ /// Occurs when [task progress].
+ /// </summary>
+ event EventHandler<GenericEventArgs<double>> TaskProgress;
+
+ /// <summary>
+ /// Gets or sets the scheduled task.
+ /// </summary>
+ /// <value>The scheduled task.</value>
+ IScheduledTask ScheduledTask { get; }
+
+ /// <summary>
+ /// Gets the last execution result.
+ /// </summary>
+ /// <value>The last execution result.</value>
+ TaskResult LastExecutionResult { get; }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ string Name { get; }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ string Description { get; }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ string Category { get; }
+
+ /// <summary>
+ /// Gets the state.
+ /// </summary>
+ /// <value>The state.</value>
+ TaskState State { get; }
+
+ /// <summary>
+ /// Gets the current progress.
+ /// </summary>
+ /// <value>The current progress.</value>
+ double? CurrentProgress { get; }
+
+ /// <summary>
+ /// Gets the triggers that define when the task will run
+ /// </summary>
+ /// <value>The triggers.</value>
+ /// <exception cref="ArgumentNullException">value</exception>
+ TaskTriggerInfo[] Triggers { get; set; }
+
+ /// <summary>
+ /// Gets the unique id.
+ /// </summary>
+ /// <value>The unique id.</value>
+ string Id { get; }
+
+ /// <summary>
+ /// Reloads the trigger events.
+ /// </summary>
+ void ReloadTriggerEvents();
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Tasks/ITaskManager.cs b/MediaBrowser.Model/Tasks/ITaskManager.cs
new file mode 100644
index 000000000..cbc18032c
--- /dev/null
+++ b/MediaBrowser.Model/Tasks/ITaskManager.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Events;
+
+namespace MediaBrowser.Model.Tasks
+{
+ public interface ITaskManager : IDisposable
+ {
+ /// <summary>
+ /// Gets the list of Scheduled Tasks
+ /// </summary>
+ /// <value>The scheduled tasks.</value>
+ IScheduledTaskWorker[] ScheduledTasks { get; }
+
+ /// <summary>
+ /// Cancels if running and queue.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="options">Task options.</param>
+ void CancelIfRunningAndQueue<T>(TaskOptions options)
+ where T : IScheduledTask;
+
+ /// <summary>
+ /// Cancels if running and queue.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ void CancelIfRunningAndQueue<T>()
+ where T : IScheduledTask;
+
+ /// <summary>
+ /// Cancels if running.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ void CancelIfRunning<T>()
+ where T : IScheduledTask;
+
+ /// <summary>
+ /// Queues the scheduled task.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="options">Task options.</param>
+ void QueueScheduledTask<T>(TaskOptions options)
+ where T : IScheduledTask;
+
+ /// <summary>
+ /// Queues the scheduled task.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ void QueueScheduledTask<T>()
+ where T : IScheduledTask;
+
+ void QueueIfNotRunning<T>()
+ where T : IScheduledTask;
+
+ /// <summary>
+ /// Queues the scheduled task.
+ /// </summary>
+ void QueueScheduledTask(IScheduledTask task, TaskOptions options);
+
+ /// <summary>
+ /// Adds the tasks.
+ /// </summary>
+ /// <param name="tasks">The tasks.</param>
+ void AddTasks(IEnumerable<IScheduledTask> tasks);
+
+ void Cancel(IScheduledTaskWorker task);
+ Task Execute(IScheduledTaskWorker task, TaskOptions options);
+
+ void Execute<T>()
+ where T : IScheduledTask;
+
+ event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
+ event EventHandler<TaskCompletionEventArgs> TaskCompleted;
+
+ void RunTaskOnNextStartup(string key);
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Tasks/ITaskTrigger.cs b/MediaBrowser.Model/Tasks/ITaskTrigger.cs
new file mode 100644
index 000000000..7c804348a
--- /dev/null
+++ b/MediaBrowser.Model/Tasks/ITaskTrigger.cs
@@ -0,0 +1,32 @@
+using System;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+
+namespace MediaBrowser.Model.Tasks
+{
+ /// <summary>
+ /// Interface ITaskTrigger
+ /// </summary>
+ public interface ITaskTrigger
+ {
+ /// <summary>
+ /// Fires when the trigger condition is satisfied and the task should run
+ /// </summary>
+ event EventHandler<EventArgs> Triggered;
+
+ /// <summary>
+ /// Gets or sets the options of this task.
+ /// </summary>
+ TaskOptions TaskOptions { get; set; }
+
+ /// <summary>
+ /// Stars waiting for the trigger action
+ /// </summary>
+ void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup);
+
+ /// <summary>
+ /// Stops waiting for the trigger action
+ /// </summary>
+ void Stop();
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Tasks/ScheduledTaskHelpers.cs b/MediaBrowser.Model/Tasks/ScheduledTaskHelpers.cs
new file mode 100644
index 000000000..2dec79e93
--- /dev/null
+++ b/MediaBrowser.Model/Tasks/ScheduledTaskHelpers.cs
@@ -0,0 +1,44 @@
+
+namespace MediaBrowser.Model.Tasks
+{
+ /// <summary>
+ /// Class ScheduledTaskHelpers
+ /// </summary>
+ public static class ScheduledTaskHelpers
+ {
+ /// <summary>
+ /// Gets the task info.
+ /// </summary>
+ /// <param name="task">The task.</param>
+ /// <returns>TaskInfo.</returns>
+ public static TaskInfo GetTaskInfo(IScheduledTaskWorker task)
+ {
+ var isHidden = false;
+
+ var configurableTask = task.ScheduledTask as IConfigurableScheduledTask;
+
+ if (configurableTask != null)
+ {
+ isHidden = configurableTask.IsHidden;
+ }
+
+ string key = task.ScheduledTask.Key;
+
+ return new TaskInfo
+ {
+ Name = task.Name,
+ CurrentProgressPercentage = task.CurrentProgress,
+ State = task.State,
+ Id = task.Id,
+ LastExecutionResult = task.LastExecutionResult,
+
+ Triggers = task.Triggers,
+
+ Description = task.Description,
+ Category = task.Category,
+ IsHidden = isHidden,
+ Key = key
+ };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Tasks/SystemEvent.cs b/MediaBrowser.Model/Tasks/SystemEvent.cs
new file mode 100644
index 000000000..4d49a38cc
--- /dev/null
+++ b/MediaBrowser.Model/Tasks/SystemEvent.cs
@@ -0,0 +1,14 @@
+
+namespace MediaBrowser.Model.Tasks
+{
+ /// <summary>
+ /// Enum SystemEvent
+ /// </summary>
+ public enum SystemEvent
+ {
+ /// <summary>
+ /// The wake from sleep
+ /// </summary>
+ WakeFromSleep = 0
+ }
+}
diff --git a/MediaBrowser.Model/Tasks/TaskCompletionEventArgs.cs b/MediaBrowser.Model/Tasks/TaskCompletionEventArgs.cs
new file mode 100644
index 000000000..be9eaa613
--- /dev/null
+++ b/MediaBrowser.Model/Tasks/TaskCompletionEventArgs.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace MediaBrowser.Model.Tasks
+{
+ public class TaskCompletionEventArgs : EventArgs
+ {
+ public IScheduledTaskWorker Task { get; set; }
+
+ public TaskResult Result { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Tasks/TaskCompletionStatus.cs b/MediaBrowser.Model/Tasks/TaskCompletionStatus.cs
new file mode 100644
index 000000000..6ba5ba5e4
--- /dev/null
+++ b/MediaBrowser.Model/Tasks/TaskCompletionStatus.cs
@@ -0,0 +1,29 @@
+
+namespace MediaBrowser.Model.Tasks
+{
+ /// <summary>
+ /// Enum TaskCompletionStatus
+ /// </summary>
+ public enum TaskCompletionStatus
+ {
+ /// <summary>
+ /// The completed
+ /// </summary>
+ Completed,
+
+ /// <summary>
+ /// The failed
+ /// </summary>
+ Failed,
+
+ /// <summary>
+ /// Manually cancelled by the user
+ /// </summary>
+ Cancelled,
+
+ /// <summary>
+ /// Aborted due to a system failure or shutdown
+ /// </summary>
+ Aborted
+ }
+}
diff --git a/MediaBrowser.Model/Tasks/TaskInfo.cs b/MediaBrowser.Model/Tasks/TaskInfo.cs
new file mode 100644
index 000000000..8792ce952
--- /dev/null
+++ b/MediaBrowser.Model/Tasks/TaskInfo.cs
@@ -0,0 +1,78 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Tasks
+{
+ /// <summary>
+ /// Class TaskInfo
+ /// </summary>
+ public class TaskInfo
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the state of the task.
+ /// </summary>
+ /// <value>The state of the task.</value>
+ public TaskState State { get; set; }
+
+ /// <summary>
+ /// Gets or sets the progress.
+ /// </summary>
+ /// <value>The progress.</value>
+ public double? CurrentProgressPercentage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last execution result.
+ /// </summary>
+ /// <value>The last execution result.</value>
+ public TaskResult LastExecutionResult { get; set; }
+
+ /// <summary>
+ /// Gets or sets the triggers.
+ /// </summary>
+ /// <value>The triggers.</value>
+ public TaskTriggerInfo[] Triggers { get; set; }
+
+ /// <summary>
+ /// Gets or sets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public string Description { get; set; }
+
+ /// <summary>
+ /// Gets or sets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public string Category { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is hidden.
+ /// </summary>
+ /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value>
+ public bool IsHidden { get; set; }
+
+ /// <summary>
+ /// Gets or sets the key.
+ /// </summary>
+ /// <value>The key.</value>
+ public string Key { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TaskInfo"/> class.
+ /// </summary>
+ public TaskInfo()
+ {
+ Triggers = new TaskTriggerInfo[]{};
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Tasks/TaskOptions.cs b/MediaBrowser.Model/Tasks/TaskOptions.cs
new file mode 100644
index 000000000..caca154a9
--- /dev/null
+++ b/MediaBrowser.Model/Tasks/TaskOptions.cs
@@ -0,0 +1,8 @@
+
+namespace MediaBrowser.Model.Tasks
+{
+ public class TaskOptions
+ {
+ public long? MaxRuntimeTicks { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Tasks/TaskResult.cs b/MediaBrowser.Model/Tasks/TaskResult.cs
new file mode 100644
index 000000000..39eacdf66
--- /dev/null
+++ b/MediaBrowser.Model/Tasks/TaskResult.cs
@@ -0,0 +1,58 @@
+using System;
+
+namespace MediaBrowser.Model.Tasks
+{
+ /// <summary>
+ /// Class TaskExecutionInfo
+ /// </summary>
+ public class TaskResult
+ {
+ /// <summary>
+ /// Gets or sets the start time UTC.
+ /// </summary>
+ /// <value>The start time UTC.</value>
+ public DateTime StartTimeUtc { get; set; }
+
+ /// <summary>
+ /// Gets or sets the end time UTC.
+ /// </summary>
+ /// <value>The end time UTC.</value>
+ public DateTime EndTimeUtc { get; set; }
+
+ /// <summary>
+ /// Gets or sets the status.
+ /// </summary>
+ /// <value>The status.</value>
+ public TaskCompletionStatus Status { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the key.
+ /// </summary>
+ /// <value>The key.</value>
+ public string Key { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the error message.
+ /// </summary>
+ /// <value>The error message.</value>
+ public string ErrorMessage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the long error message.
+ /// </summary>
+ /// <value>The long error message.</value>
+ public string LongErrorMessage { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Tasks/TaskState.cs b/MediaBrowser.Model/Tasks/TaskState.cs
new file mode 100644
index 000000000..889ce6875
--- /dev/null
+++ b/MediaBrowser.Model/Tasks/TaskState.cs
@@ -0,0 +1,22 @@
+
+namespace MediaBrowser.Model.Tasks
+{
+ /// <summary>
+ /// Enum TaskState
+ /// </summary>
+ public enum TaskState
+ {
+ /// <summary>
+ /// The idle
+ /// </summary>
+ Idle,
+ /// <summary>
+ /// The cancelling
+ /// </summary>
+ Cancelling,
+ /// <summary>
+ /// The running
+ /// </summary>
+ Running
+ }
+}
diff --git a/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs b/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs
new file mode 100644
index 000000000..901a300d0
--- /dev/null
+++ b/MediaBrowser.Model/Tasks/TaskTriggerInfo.cs
@@ -0,0 +1,52 @@
+using System;
+
+namespace MediaBrowser.Model.Tasks
+{
+ /// <summary>
+ /// Class TaskTriggerInfo
+ /// </summary>
+ public class TaskTriggerInfo
+ {
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ public string Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the time of day.
+ /// </summary>
+ /// <value>The time of day.</value>
+ public long? TimeOfDayTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the interval.
+ /// </summary>
+ /// <value>The interval.</value>
+ public long? IntervalTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the system event.
+ /// </summary>
+ /// <value>The system event.</value>
+ public SystemEvent? SystemEvent { get; set; }
+
+ /// <summary>
+ /// Gets or sets the day of week.
+ /// </summary>
+ /// <value>The day of week.</value>
+ public DayOfWeek? DayOfWeek { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum runtime ticks.
+ /// </summary>
+ /// <value>The maximum runtime ticks.</value>
+ public long? MaxRuntimeTicks { get; set; }
+
+ public const string TriggerDaily = "DailyTrigger";
+ public const string TriggerWeekly = "WeeklyTrigger";
+ public const string TriggerInterval = "IntervalTrigger";
+ public const string TriggerSystemEvent = "SystemEventTrigger";
+ public const string TriggerStartup = "StartupTrigger";
+ }
+}
diff --git a/MediaBrowser.Model/Text/ITextEncoding.cs b/MediaBrowser.Model/Text/ITextEncoding.cs
new file mode 100644
index 000000000..619d90a2b
--- /dev/null
+++ b/MediaBrowser.Model/Text/ITextEncoding.cs
@@ -0,0 +1,14 @@
+using System.IO;
+using System.Text;
+
+namespace MediaBrowser.Model.Text
+{
+ public interface ITextEncoding
+ {
+ Encoding GetASCIIEncoding();
+
+ string GetDetectedEncodingName(byte[] bytes, int size, string language, bool enableLanguageDetection);
+ Encoding GetDetectedEncoding(byte[] bytes, int size, string language, bool enableLanguageDetection);
+ Encoding GetEncodingFromCharset(string charset);
+ }
+}
diff --git a/MediaBrowser.Model/Threading/ITimer.cs b/MediaBrowser.Model/Threading/ITimer.cs
new file mode 100644
index 000000000..42090250b
--- /dev/null
+++ b/MediaBrowser.Model/Threading/ITimer.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace MediaBrowser.Model.Threading
+{
+ public interface ITimer : IDisposable
+ {
+ void Change(TimeSpan dueTime, TimeSpan period);
+ void Change(int dueTimeMs, int periodMs);
+ }
+}
diff --git a/MediaBrowser.Model/Threading/ITimerFactory.cs b/MediaBrowser.Model/Threading/ITimerFactory.cs
new file mode 100644
index 000000000..5f3df1738
--- /dev/null
+++ b/MediaBrowser.Model/Threading/ITimerFactory.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace MediaBrowser.Model.Threading
+{
+ public interface ITimerFactory
+ {
+ ITimer Create(Action<object> callback, object state, TimeSpan dueTime, TimeSpan period);
+ ITimer Create(Action<object> callback, object state, int dueTimeMs, int periodMs);
+ }
+}
diff --git a/MediaBrowser.Model/Updates/CheckForUpdateResult.cs b/MediaBrowser.Model/Updates/CheckForUpdateResult.cs
new file mode 100644
index 000000000..ff0bba197
--- /dev/null
+++ b/MediaBrowser.Model/Updates/CheckForUpdateResult.cs
@@ -0,0 +1,30 @@
+
+namespace MediaBrowser.Model.Updates
+{
+ /// <summary>
+ /// Class CheckForUpdateResult
+ /// </summary>
+ public class CheckForUpdateResult
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is update available.
+ /// </summary>
+ /// <value><c>true</c> if this instance is update available; otherwise, <c>false</c>.</value>
+ public bool IsUpdateAvailable { get; set; }
+
+ /// <summary>
+ /// Gets or sets the available version.
+ /// </summary>
+ /// <value>The available version.</value>
+ public string AvailableVersion
+ {
+ get { return Package != null ? Package.versionStr : "0.0.0.1"; }
+ set { } // need this for the serializer
+ }
+
+ /// <summary>
+ /// Get or sets package information for an available update
+ /// </summary>
+ public PackageVersionInfo Package { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Updates/InstallationInfo.cs b/MediaBrowser.Model/Updates/InstallationInfo.cs
new file mode 100644
index 000000000..09b4975a8
--- /dev/null
+++ b/MediaBrowser.Model/Updates/InstallationInfo.cs
@@ -0,0 +1,46 @@
+using System;
+
+namespace MediaBrowser.Model.Updates
+{
+ /// <summary>
+ /// Class InstallationInfo
+ /// </summary>
+ public class InstallationInfo
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the assembly guid.
+ /// </summary>
+ /// <value>The guid of the assembly.</value>
+ public string AssemblyGuid { get; set; }
+
+ /// <summary>
+ /// Gets or sets the version.
+ /// </summary>
+ /// <value>The version.</value>
+ public string Version { get; set; }
+
+ /// <summary>
+ /// Gets or sets the update class.
+ /// </summary>
+ /// <value>The update class.</value>
+ public PackageVersionClass UpdateClass { get; set; }
+
+ /// <summary>
+ /// Gets or sets the percent complete.
+ /// </summary>
+ /// <value>The percent complete.</value>
+ public double? PercentComplete { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Updates/PackageInfo.cs b/MediaBrowser.Model/Updates/PackageInfo.cs
new file mode 100644
index 000000000..e46d59fc0
--- /dev/null
+++ b/MediaBrowser.Model/Updates/PackageInfo.cs
@@ -0,0 +1,176 @@
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Updates
+{
+ /// <summary>
+ /// Class PackageInfo
+ /// </summary>
+ public class PackageInfo
+ {
+ /// <summary>
+ /// The internal id of this package.
+ /// </summary>
+ /// <value>The id.</value>
+ public string id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the short description.
+ /// </summary>
+ /// <value>The short description.</value>
+ public string shortDescription { get; set; }
+
+ /// <summary>
+ /// Gets or sets the overview.
+ /// </summary>
+ /// <value>The overview.</value>
+ public string overview { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is premium.
+ /// </summary>
+ /// <value><c>true</c> if this instance is premium; otherwise, <c>false</c>.</value>
+ public bool isPremium { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is adult only content.
+ /// </summary>
+ /// <value><c>true</c> if this instance is adult; otherwise, <c>false</c>.</value>
+ public bool adult { get; set; }
+
+ /// <summary>
+ /// Gets or sets the rich desc URL.
+ /// </summary>
+ /// <value>The rich desc URL.</value>
+ public string richDescUrl { get; set; }
+
+ /// <summary>
+ /// Gets or sets the thumb image.
+ /// </summary>
+ /// <value>The thumb image.</value>
+ public string thumbImage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the preview image.
+ /// </summary>
+ /// <value>The preview image.</value>
+ public string previewImage { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <value>The type.</value>
+ public string type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the target filename.
+ /// </summary>
+ /// <value>The target filename.</value>
+ public string targetFilename { get; set; }
+
+ /// <summary>
+ /// Gets or sets the owner.
+ /// </summary>
+ /// <value>The owner.</value>
+ public string owner { get; set; }
+
+ /// <summary>
+ /// Gets or sets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public string category { get; set; }
+
+ /// <summary>
+ /// Gets or sets the catalog tile color.
+ /// </summary>
+ /// <value>The owner.</value>
+ public string tileColor { get; set; }
+
+ /// <summary>
+ /// Gets or sets the feature id of this package (if premium).
+ /// </summary>
+ /// <value>The feature id.</value>
+ public string featureId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the registration info for this package (if premium).
+ /// </summary>
+ /// <value>The registration info.</value>
+ public string regInfo { get; set; }
+
+ /// <summary>
+ /// Gets or sets the price for this package (if premium).
+ /// </summary>
+ /// <value>The price.</value>
+ public float price { get; set; }
+
+ /// <summary>
+ /// Gets or sets the target system for this plug-in (Server, MBTheater, MBClassic).
+ /// </summary>
+ /// <value>The target system.</value>
+ public PackageTargetSystem targetSystem { get; set; }
+
+ /// <summary>
+ /// The guid of the assembly associated with this package (if a plug-in).
+ /// This is used to identify the proper item for automatic updates.
+ /// </summary>
+ /// <value>The name.</value>
+ public string guid { get; set; }
+
+ /// <summary>
+ /// Gets or sets the total number of ratings for this package.
+ /// </summary>
+ /// <value>The total ratings.</value>
+ public int? totalRatings { get; set; }
+
+ /// <summary>
+ /// Gets or sets the average rating for this package .
+ /// </summary>
+ /// <value>The rating.</value>
+ public float avgRating { get; set; }
+
+ /// <summary>
+ /// Gets or sets whether or not this package is registered.
+ /// </summary>
+ /// <value>True if registered.</value>
+ public bool isRegistered { get; set; }
+
+ /// <summary>
+ /// Gets or sets the expiration date for this package.
+ /// </summary>
+ /// <value>Expiration Date.</value>
+ public DateTime expDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the versions.
+ /// </summary>
+ /// <value>The versions.</value>
+ public PackageVersionInfo[] versions { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [enable in application store].
+ /// </summary>
+ /// <value><c>true</c> if [enable in application store]; otherwise, <c>false</c>.</value>
+ public bool enableInAppStore { get; set; }
+
+ /// <summary>
+ /// Gets or sets the installs.
+ /// </summary>
+ /// <value>The installs.</value>
+ public int installs { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PackageInfo"/> class.
+ /// </summary>
+ public PackageInfo()
+ {
+ versions = new PackageVersionInfo[] { };
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Updates/PackageTargetSystem.cs b/MediaBrowser.Model/Updates/PackageTargetSystem.cs
new file mode 100644
index 000000000..c80dddde3
--- /dev/null
+++ b/MediaBrowser.Model/Updates/PackageTargetSystem.cs
@@ -0,0 +1,21 @@
+namespace MediaBrowser.Model.Updates
+{
+ /// <summary>
+ /// Enum PackageType
+ /// </summary>
+ public enum PackageTargetSystem
+ {
+ /// <summary>
+ /// Server
+ /// </summary>
+ Server,
+ /// <summary>
+ /// MB Theater
+ /// </summary>
+ MBTheater,
+ /// <summary>
+ /// MB Classic
+ /// </summary>
+ MBClassic
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Updates/PackageVersionClass.cs b/MediaBrowser.Model/Updates/PackageVersionClass.cs
new file mode 100644
index 000000000..3f51e1b3c
--- /dev/null
+++ b/MediaBrowser.Model/Updates/PackageVersionClass.cs
@@ -0,0 +1,21 @@
+namespace MediaBrowser.Model.Updates
+{
+ /// <summary>
+ /// Enum PackageVersionClass
+ /// </summary>
+ public enum PackageVersionClass
+ {
+ /// <summary>
+ /// The release
+ /// </summary>
+ Release = 0,
+ /// <summary>
+ /// The beta
+ /// </summary>
+ Beta = 1,
+ /// <summary>
+ /// The dev
+ /// </summary>
+ Dev = 2
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Updates/PackageVersionInfo.cs b/MediaBrowser.Model/Updates/PackageVersionInfo.cs
new file mode 100644
index 000000000..3ac518187
--- /dev/null
+++ b/MediaBrowser.Model/Updates/PackageVersionInfo.cs
@@ -0,0 +1,95 @@
+using System;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Model.Updates
+{
+ /// <summary>
+ /// Class PackageVersionInfo
+ /// </summary>
+ public class PackageVersionInfo
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the guid.
+ /// </summary>
+ /// <value>The guid.</value>
+ public string guid { get; set; }
+
+ /// <summary>
+ /// Gets or sets the version STR.
+ /// </summary>
+ /// <value>The version STR.</value>
+ public string versionStr { get; set; }
+
+ /// <summary>
+ /// The _version
+ /// </summary>
+ private Version _version;
+ /// <summary>
+ /// Gets or sets the version.
+ /// Had to make this an interpreted property since Protobuf can't handle Version
+ /// </summary>
+ /// <value>The version.</value>
+ [IgnoreDataMember]
+ public Version version
+ {
+ get { return _version ?? (_version = new Version(ValueOrDefault(versionStr, "0.0.0.1"))); }
+ }
+
+ /// <summary>
+ /// Values the or default.
+ /// </summary>
+ /// <param name="str">The STR.</param>
+ /// <param name="def">The def.</param>
+ /// <returns>System.String.</returns>
+ private static string ValueOrDefault(string str, string def)
+ {
+ return string.IsNullOrEmpty(str) ? def : str;
+ }
+
+ /// <summary>
+ /// Gets or sets the classification.
+ /// </summary>
+ /// <value>The classification.</value>
+ public PackageVersionClass classification { get; set; }
+
+ /// <summary>
+ /// Gets or sets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public string description { get; set; }
+
+ /// <summary>
+ /// Gets or sets the required version STR.
+ /// </summary>
+ /// <value>The required version STR.</value>
+ public string requiredVersionStr { get; set; }
+
+ /// <summary>
+ /// Gets or sets the source URL.
+ /// </summary>
+ /// <value>The source URL.</value>
+ public string sourceUrl { get; set; }
+
+ /// <summary>
+ /// Gets or sets the source URL.
+ /// </summary>
+ /// <value>The source URL.</value>
+ public string checksum { get; set; }
+
+ /// <summary>
+ /// Gets or sets the target filename.
+ /// </summary>
+ /// <value>The target filename.</value>
+ public string targetFilename { get; set; }
+
+ public string infoUrl { get; set; }
+
+ public string runtimes { get; set; }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Users/ForgotPasswordAction.cs b/MediaBrowser.Model/Users/ForgotPasswordAction.cs
new file mode 100644
index 000000000..f75b1d74b
--- /dev/null
+++ b/MediaBrowser.Model/Users/ForgotPasswordAction.cs
@@ -0,0 +1,10 @@
+
+namespace MediaBrowser.Model.Users
+{
+ public enum ForgotPasswordAction
+ {
+ ContactAdmin = 0,
+ PinCode = 1,
+ InNetworkRequired = 2
+ }
+}
diff --git a/MediaBrowser.Model/Users/ForgotPasswordResult.cs b/MediaBrowser.Model/Users/ForgotPasswordResult.cs
new file mode 100644
index 000000000..7dbb1e96b
--- /dev/null
+++ b/MediaBrowser.Model/Users/ForgotPasswordResult.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace MediaBrowser.Model.Users
+{
+ public class ForgotPasswordResult
+ {
+ /// <summary>
+ /// Gets or sets the action.
+ /// </summary>
+ /// <value>The action.</value>
+ public ForgotPasswordAction Action { get; set; }
+ /// <summary>
+ /// Gets or sets the pin file.
+ /// </summary>
+ /// <value>The pin file.</value>
+ public string PinFile { get; set; }
+ /// <summary>
+ /// Gets or sets the pin expiration date.
+ /// </summary>
+ /// <value>The pin expiration date.</value>
+ public DateTime? PinExpirationDate { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Users/PinRedeemResult.cs b/MediaBrowser.Model/Users/PinRedeemResult.cs
new file mode 100644
index 000000000..6a01bf2d4
--- /dev/null
+++ b/MediaBrowser.Model/Users/PinRedeemResult.cs
@@ -0,0 +1,17 @@
+
+namespace MediaBrowser.Model.Users
+{
+ public class PinRedeemResult
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="PinRedeemResult"/> is success.
+ /// </summary>
+ /// <value><c>true</c> if success; otherwise, <c>false</c>.</value>
+ public bool Success { get; set; }
+ /// <summary>
+ /// Gets or sets the users reset.
+ /// </summary>
+ /// <value>The users reset.</value>
+ public string[] UsersReset { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Users/UserAction.cs b/MediaBrowser.Model/Users/UserAction.cs
new file mode 100644
index 000000000..5f401b9f0
--- /dev/null
+++ b/MediaBrowser.Model/Users/UserAction.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace MediaBrowser.Model.Users
+{
+ public class UserAction
+ {
+ public string Id { get; set; }
+ public string ServerId { get; set; }
+ public Guid UserId { get; set; }
+ public Guid ItemId { get; set; }
+ public UserActionType Type { get; set; }
+ public DateTime Date { get; set; }
+ public long? PositionTicks { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/Users/UserActionType.cs b/MediaBrowser.Model/Users/UserActionType.cs
new file mode 100644
index 000000000..493de6272
--- /dev/null
+++ b/MediaBrowser.Model/Users/UserActionType.cs
@@ -0,0 +1,8 @@
+
+namespace MediaBrowser.Model.Users
+{
+ public enum UserActionType
+ {
+ PlayedItem = 0
+ }
+}
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
new file mode 100644
index 000000000..8bddafb5a
--- /dev/null
+++ b/MediaBrowser.Model/Users/UserPolicy.cs
@@ -0,0 +1,121 @@
+using MediaBrowser.Model.Configuration;
+using System;
+
+namespace MediaBrowser.Model.Users
+{
+ public class UserPolicy
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is administrator.
+ /// </summary>
+ /// <value><c>true</c> if this instance is administrator; otherwise, <c>false</c>.</value>
+ public bool IsAdministrator { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is hidden.
+ /// </summary>
+ /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value>
+ public bool IsHidden { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is disabled.
+ /// </summary>
+ /// <value><c>true</c> if this instance is disabled; otherwise, <c>false</c>.</value>
+ public bool IsDisabled { get; set; }
+
+ /// <summary>
+ /// Gets or sets the max parental rating.
+ /// </summary>
+ /// <value>The max parental rating.</value>
+ public int? MaxParentalRating { get; set; }
+
+ public string[] BlockedTags { get; set; }
+ public bool EnableUserPreferenceAccess { get; set; }
+ public AccessSchedule[] AccessSchedules { get; set; }
+ public UnratedItem[] BlockUnratedItems { get; set; }
+ public bool EnableRemoteControlOfOtherUsers { get; set; }
+ public bool EnableSharedDeviceControl { get; set; }
+ public bool EnableRemoteAccess { get; set; }
+
+ public bool EnableLiveTvManagement { get; set; }
+ public bool EnableLiveTvAccess { get; set; }
+
+ public bool EnableMediaPlayback { get; set; }
+ public bool EnableAudioPlaybackTranscoding { get; set; }
+ public bool EnableVideoPlaybackTranscoding { get; set; }
+ public bool EnablePlaybackRemuxing { get; set; }
+
+ public bool EnableContentDeletion { get; set; }
+ public string[] EnableContentDeletionFromFolders { get; set; }
+ public bool EnableContentDownloading { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether [enable synchronize].
+ /// </summary>
+ /// <value><c>true</c> if [enable synchronize]; otherwise, <c>false</c>.</value>
+ public bool EnableSyncTranscoding { get; set; }
+ public bool EnableMediaConversion { get; set; }
+
+ public string[] EnabledDevices { get; set; }
+ public bool EnableAllDevices { get; set; }
+
+ public string[] EnabledChannels { get; set; }
+ public bool EnableAllChannels { get; set; }
+
+ public string[] EnabledFolders { get; set; }
+ public bool EnableAllFolders { get; set; }
+
+ public int InvalidLoginAttemptCount { get; set; }
+
+ public bool EnablePublicSharing { get; set; }
+
+ public string[] BlockedMediaFolders { get; set; }
+ public string[] BlockedChannels { get; set; }
+
+ public int RemoteClientBitrateLimit { get; set; }
+ public string AuthenticationProviderId { get; set; }
+
+ public UserPolicy()
+ {
+ EnableContentDeletion = true;
+ EnableContentDeletionFromFolders = new string[] { };
+
+ EnableSyncTranscoding = true;
+ EnableMediaConversion = true;
+
+ EnableMediaPlayback = true;
+ EnableAudioPlaybackTranscoding = true;
+ EnableVideoPlaybackTranscoding = true;
+ EnablePlaybackRemuxing = true;
+
+ EnableLiveTvManagement = true;
+ EnableLiveTvAccess = true;
+
+ // Without this on by default, admins won't be able to do this
+ // Improve in the future
+ EnableLiveTvManagement = true;
+
+ EnableSharedDeviceControl = true;
+
+ BlockedTags = new string[] { };
+ BlockUnratedItems = new UnratedItem[] { };
+
+ EnableUserPreferenceAccess = true;
+
+ AccessSchedules = new AccessSchedule[] { };
+
+ EnableAllChannels = true;
+ EnabledChannels = new string[] { };
+
+ EnableAllFolders = true;
+ EnabledFolders = new string[] { };
+
+ EnabledDevices = new string[] { };
+ EnableAllDevices = true;
+
+ EnableContentDownloading = true;
+ EnablePublicSharing = true;
+ EnableRemoteAccess = true;
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.Model/Xml/IXmlReaderSettingsFactory.cs b/MediaBrowser.Model/Xml/IXmlReaderSettingsFactory.cs
new file mode 100644
index 000000000..b9628ec3e
--- /dev/null
+++ b/MediaBrowser.Model/Xml/IXmlReaderSettingsFactory.cs
@@ -0,0 +1,9 @@
+using System.Xml;
+
+namespace MediaBrowser.Model.Xml
+{
+ public interface IXmlReaderSettingsFactory
+ {
+ XmlReaderSettings Create(bool enableValidation);
+ }
+}
diff --git a/MediaBrowser.Server.Mono/MonoAppHost.cs b/MediaBrowser.Server.Mono/MonoAppHost.cs
index d4614ec00..576eb5c68 100644
--- a/MediaBrowser.Server.Mono/MonoAppHost.cs
+++ b/MediaBrowser.Server.Mono/MonoAppHost.cs
@@ -34,11 +34,6 @@ namespace MediaBrowser.Server.Mono
}
}
- protected override IConnectManager CreateConnectManager()
- {
- return new Emby.Server.Implementations.Library.ConnectManager();
- }
-
//protected override ISyncManager CreateSyncManager()
//{
// return new SyncManager();