diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml index b558d2a6f..4d38a906e 100644 --- a/.ci/azure-pipelines-abi.yml +++ b/.ci/azure-pipelines-abi.yml @@ -62,7 +62,6 @@ jobs: - task: DownloadPipelineArtifact@2 displayName: 'Download Reference Assembly Build Artifact' - enabled: false inputs: source: "specific" artifact: "$(NugetPackageName)" @@ -74,7 +73,6 @@ jobs: - task: CopyFiles@2 displayName: 'Copy Reference Assembly Build Artifact' - enabled: false inputs: sourceFolder: $(System.ArtifactsDirectory)/current-artifacts contents: '**/*.dll' @@ -85,7 +83,6 @@ jobs: - task: DotNetCoreCLI@2 displayName: 'Execute ABI Compatibility Check Tool' - enabled: false inputs: command: custom custom: compat diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index 003d5baf0..cc845afd4 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -42,7 +42,7 @@ jobs: - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)' displayName: 'Run Dockerfile (stable)' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - task: PublishPipelineArtifact@1 displayName: 'Publish Release' @@ -87,7 +87,7 @@ jobs: steps: - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )" displayName: Set release version (stable) - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - task: Docker@2 displayName: 'Push Unstable Image' @@ -104,7 +104,7 @@ jobs: - task: Docker@2 displayName: 'Push Stable Image' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') inputs: repository: 'jellyfin/jellyfin-server' command: buildAndPush @@ -116,8 +116,9 @@ jobs: $(JellyfinVersion)-$(BuildConfiguration) - job: CollectArtifacts - timeoutInMinutes: 10 + timeoutInMinutes: 20 displayName: 'Collect Artifacts' + continueOnError: true dependsOn: - BuildPackage - BuildDocker @@ -129,38 +130,85 @@ jobs: steps: - task: SSH@0 displayName: 'Update Unstable Repository' + continueOnError: true condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') inputs: sshEndpoint: repository runOptions: 'commands' - commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable + commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable & - task: SSH@0 displayName: 'Update Stable Repository' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') + continueOnError: true + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') inputs: sshEndpoint: repository runOptions: 'commands' - commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) - + commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) & + - job: PublishNuget displayName: 'Publish NuGet packages' dependsOn: - BuildPackage - condition: and(succeeded('BuildPackage'), startsWith(variables['Build.SourceBranch'], 'refs/tags')) + condition: succeeded('BuildPackage') pool: vmImage: 'ubuntu-latest' steps: - - task: NuGetCommand@2 + - task: DotNetCoreCLI@2 + displayName: 'Build Stable Nuget packages' + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') inputs: command: 'pack' - packagesToPack: Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj - packDestination: '$(Build.ArtifactStagingDirectory)' + packagesToPack: 'Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj' + versioningScheme: 'off' + + - task: DotNetCoreCLI@2 + displayName: 'Build Unstable Nuget packages' + inputs: + command: 'custom' + projects: | + Jellyfin.Data/Jellyfin.Data.csproj + MediaBrowser.Common/MediaBrowser.Common.csproj + MediaBrowser.Controller/MediaBrowser.Controller.csproj + MediaBrowser.Model/MediaBrowser.Model.csproj + Emby.Naming/Emby.Naming.csproj + custom: 'pack' + arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable' + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Nuget packages' + inputs: + pathToPublish: $(Build.ArtifactStagingDirectory) + artifactName: Jellyfin Nuget Packages + + - task: NuGetAuthenticate@0 + displayName: 'Authenticate to stable Nuget feed' + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') + inputs: + nuGetServiceConnections: 'NugetOrg' - task: NuGetCommand@2 + displayName: 'Push Nuget packages to stable feed' + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') inputs: command: 'push' - packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg' - includeNugetOrg: 'true' + packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg' + nuGetFeedType: 'external' + publishFeedCredentials: 'NugetOrg' + allowPackageConflicts: true # This ignores an error if the version already exists + + - task: NuGetAuthenticate@0 + displayName: 'Authenticate to unstable Nuget feed' + condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') + + - task: NuGetCommand@2 + displayName: 'Push Nuget packages to unstable feed' + condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') + inputs: + command: 'push' + packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg' # No symbols since Azure Artifact does not support it + nuGetFeedType: 'internal' + publishVstsFeed: '7cce6c46-d610-45e3-9fb7-65a6bfd1b671/a5746b79-f369-42db-93ff-59cd066f9327' + allowPackageConflicts: true # This ignores an error if the version already exists diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index 0c86c0171..b417aae67 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -13,15 +13,21 @@ pr: trigger: batch: true + branches: + include: + - '*' + tags: + include: + - 'v*' jobs: -- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}: +- ${{ if not(startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) }}: - template: azure-pipelines-main.yml parameters: LinuxImage: 'ubuntu-latest' RestoreBuildProjects: $(RestoreBuildProjects) -- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}: +- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}: - template: azure-pipelines-test.yml parameters: ImageNames: @@ -29,7 +35,7 @@ jobs: Windows: 'windows-latest' macOS: 'macos-latest' -- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}: +- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}: - template: azure-pipelines-abi.yml parameters: Packages: @@ -47,5 +53,5 @@ jobs: AssemblyFileName: MediaBrowser.Common.dll LinuxImage: 'ubuntu-latest' -- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}: +- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}: - template: azure-pipelines-package.yml diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index c5f35c088..1b4fdc8c4 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -16,6 +16,7 @@ - [bugfixin](https://github.com/bugfixin) - [chaosinnovator](https://github.com/chaosinnovator) - [ckcr4lyf](https://github.com/ckcr4lyf) + - [ConfusedPolarBear](https://github.com/ConfusedPolarBear) - [crankdoofus](https://github.com/crankdoofus) - [crobibero](https://github.com/crobibero) - [cromefire](https://github.com/cromefire) diff --git a/Emby.Dlna/Common/ServiceAction.cs b/Emby.Dlna/Common/ServiceAction.cs index db4f27063..d458d7f3f 100644 --- a/Emby.Dlna/Common/ServiceAction.cs +++ b/Emby.Dlna/Common/ServiceAction.cs @@ -13,7 +13,7 @@ namespace Emby.Dlna.Common public string Name { get; set; } - public List ArgumentList { get; set; } + public List ArgumentList { get; } /// public override string ToString() diff --git a/Emby.Dlna/Common/StateVariable.cs b/Emby.Dlna/Common/StateVariable.cs index a2c2bf5dd..6daf7ab6b 100644 --- a/Emby.Dlna/Common/StateVariable.cs +++ b/Emby.Dlna/Common/StateVariable.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Generic; namespace Emby.Dlna.Common { @@ -17,7 +18,7 @@ namespace Emby.Dlna.Common public bool SendsEvents { get; set; } - public string[] AllowedValues { get; set; } + public IReadOnlyList AllowedValues { get; set; } /// public override string ToString() diff --git a/Emby.Dlna/ConfigurationExtension.cs b/Emby.Dlna/ConfigurationExtension.cs index dba901967..fc02e1751 100644 --- a/Emby.Dlna/ConfigurationExtension.cs +++ b/Emby.Dlna/ConfigurationExtension.cs @@ -1,7 +1,6 @@ #nullable enable #pragma warning disable CS1591 -using System.Collections.Generic; using Emby.Dlna.Configuration; using MediaBrowser.Common.Configuration; @@ -14,19 +13,4 @@ namespace Emby.Dlna return manager.GetConfiguration("dlna"); } } - - public class DlnaConfigurationFactory : IConfigurationFactory - { - public IEnumerable GetConfigurations() - { - return new ConfigurationStore[] - { - new ConfigurationStore - { - Key = "dlna", - ConfigurationType = typeof (DlnaOptions) - } - }; - } - } } diff --git a/Emby.Dlna/ConnectionManager/ConnectionManager.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs similarity index 76% rename from Emby.Dlna/ConnectionManager/ConnectionManager.cs rename to Emby.Dlna/ConnectionManager/ConnectionManagerService.cs index e32cc11bf..12338e2b4 100644 --- a/Emby.Dlna/ConnectionManager/ConnectionManager.cs +++ b/Emby.Dlna/ConnectionManager/ConnectionManagerService.cs @@ -9,22 +9,20 @@ using Microsoft.Extensions.Logging; namespace Emby.Dlna.ConnectionManager { - public class ConnectionManager : BaseService, IConnectionManager + public class ConnectionManagerService : BaseService, IConnectionManager { private readonly IDlnaManager _dlna; - private readonly ILogger _logger; private readonly IServerConfigurationManager _config; - public ConnectionManager( + public ConnectionManagerService( IDlnaManager dlna, IServerConfigurationManager config, - ILogger logger, + ILogger logger, IHttpClient httpClient) : base(logger, httpClient) { _dlna = dlna; _config = config; - _logger = logger; } /// @@ -39,7 +37,7 @@ namespace Emby.Dlna.ConnectionManager var profile = _dlna.GetProfile(request.Headers) ?? _dlna.GetDefaultProfile(); - return new ControlHandler(_config, _logger, profile).ProcessControlRequestAsync(request); + return new ControlHandler(_config, Logger, profile).ProcessControlRequestAsync(request); } } } diff --git a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs index b31d437c3..c8db5a367 100644 --- a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs +++ b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs @@ -44,7 +44,7 @@ namespace Emby.Dlna.ConnectionManager DataType = "string", SendsEvents = false, - AllowedValues = new string[] + AllowedValues = new[] { "OK", "ContentFormatMismatch", @@ -67,7 +67,7 @@ namespace Emby.Dlna.ConnectionManager DataType = "string", SendsEvents = false, - AllowedValues = new string[] + AllowedValues = new[] { "Output", "Input" diff --git a/Emby.Dlna/ContentDirectory/ContentDirectory.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs similarity index 96% rename from Emby.Dlna/ContentDirectory/ContentDirectory.cs rename to Emby.Dlna/ContentDirectory/ContentDirectoryService.cs index b1ce7e8ec..72732823a 100644 --- a/Emby.Dlna/ContentDirectory/ContentDirectory.cs +++ b/Emby.Dlna/ContentDirectory/ContentDirectoryService.cs @@ -19,7 +19,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Dlna.ContentDirectory { - public class ContentDirectory : BaseService, IContentDirectory + public class ContentDirectoryService : BaseService, IContentDirectory { private readonly ILibraryManager _libraryManager; private readonly IImageProcessor _imageProcessor; @@ -33,14 +33,14 @@ namespace Emby.Dlna.ContentDirectory private readonly IMediaEncoder _mediaEncoder; private readonly ITVSeriesManager _tvSeriesManager; - public ContentDirectory( + public ContentDirectoryService( IDlnaManager dlna, IUserDataManager userDataManager, IImageProcessor imageProcessor, ILibraryManager libraryManager, IServerConfigurationManager config, IUserManager userManager, - ILogger logger, + ILogger logger, IHttpClient httpClient, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs index 6db4d7cb6..743dcc516 100644 --- a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs +++ b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs @@ -10,7 +10,8 @@ namespace Emby.Dlna.ContentDirectory { public string GetXml() { - return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(), + return new ServiceXmlBuilder().GetXml( + new ServiceActionListBuilder().GetActions(), GetStateVariables()); } @@ -101,7 +102,7 @@ namespace Emby.Dlna.ContentDirectory DataType = "string", SendsEvents = false, - AllowedValues = new string[] + AllowedValues = new[] { "BrowseMetadata", "BrowseDirectChildren" diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index 00821bf78..be1ed7872 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -40,6 +40,11 @@ namespace Emby.Dlna.ContentDirectory { public class ControlHandler : BaseControlHandler { + private const string NsDc = "http://purl.org/dc/elements/1.1/"; + private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; + private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/"; + private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/"; + private readonly ILibraryManager _libraryManager; private readonly IUserDataManager _userDataManager; private readonly IServerConfigurationManager _config; @@ -47,11 +52,6 @@ namespace Emby.Dlna.ContentDirectory private readonly IUserViewManager _userViewManager; private readonly ITVSeriesManager _tvSeriesManager; - private const string NS_DC = "http://purl.org/dc/elements/1.1/"; - private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; - private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/"; - private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/"; - private readonly int _systemUpdateId; private readonly DidlBuilder _didlBuilder; @@ -181,7 +181,11 @@ namespace Emby.Dlna.ContentDirectory userdata.PlaybackPositionTicks = TimeSpan.FromSeconds(newbookmark).Ticks; - _userDataManager.SaveUserData(_user, item, userdata, UserDataSaveReason.TogglePlayed, + _userDataManager.SaveUserData( + _user, + item, + userdata, + UserDataSaveReason.TogglePlayed, CancellationToken.None); } @@ -253,7 +257,7 @@ namespace Emby.Dlna.ContentDirectory var id = sparams["ObjectID"]; var flag = sparams["BrowseFlag"]; var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*")); - var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", "")); + var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty)); var provided = 0; @@ -286,18 +290,17 @@ namespace Emby.Dlna.ContentDirectory using (var writer = XmlWriter.Create(builder, settings)) { - writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL); + writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl); - writer.WriteAttributeString("xmlns", "dc", null, NS_DC); - writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA); - writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP); + writer.WriteAttributeString("xmlns", "dc", null, NsDc); + writer.WriteAttributeString("xmlns", "dlna", null, NsDlna); + writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp); DidlBuilder.WriteXmlRootAttributes(_profile, writer); var serverItem = GetItemFromObjectId(id); var item = serverItem.Item; - if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal)) { totalCount = 1; @@ -362,8 +365,8 @@ namespace Emby.Dlna.ContentDirectory private void HandleSearch(XmlWriter xmlWriter, IDictionary sparams, string deviceId) { - var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", "")); - var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", "")); + var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", string.Empty)); + var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty)); var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*")); // sort example: dc:title, dc:date @@ -397,11 +400,11 @@ namespace Emby.Dlna.ContentDirectory using (var writer = XmlWriter.Create(builder, settings)) { - writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL); + writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl); - writer.WriteAttributeString("xmlns", "dc", null, NS_DC); - writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA); - writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP); + writer.WriteAttributeString("xmlns", "dc", null, NsDc); + writer.WriteAttributeString("xmlns", "dlna", null, NsDlna); + writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp); DidlBuilder.WriteXmlRootAttributes(_profile, writer); @@ -783,11 +786,14 @@ namespace Emby.Dlna.ContentDirectory }) .ToArray(); - return ApplyPaging(new QueryResult - { - Items = folders, - TotalRecordCount = folders.Length - }, startIndex, limit); + return ApplyPaging( + new QueryResult + { + Items = folders, + TotalRecordCount = folders.Length + }, + startIndex, + limit); } private QueryResult GetTvFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) @@ -1135,14 +1141,16 @@ namespace Emby.Dlna.ContentDirectory { query.OrderBy = Array.Empty<(string, SortOrder)>(); - var items = _userViewManager.GetLatestItems(new LatestItemsQuery - { - UserId = user.Id, - Limit = 50, - IncludeItemTypes = new[] { nameof(Audio) }, - ParentId = parent?.Id ?? Guid.Empty, - GroupItems = true - }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); + var items = _userViewManager.GetLatestItems( + new LatestItemsQuery + { + UserId = user.Id, + Limit = 50, + IncludeItemTypes = new[] { nameof(Audio) }, + ParentId = parent?.Id ?? Guid.Empty, + GroupItems = true + }, + query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); return ToResult(items); } @@ -1151,12 +1159,15 @@ namespace Emby.Dlna.ContentDirectory { query.OrderBy = Array.Empty<(string, SortOrder)>(); - var result = _tvSeriesManager.GetNextUp(new NextUpQuery - { - Limit = query.Limit, - StartIndex = query.StartIndex, - UserId = query.User.Id - }, new[] { parent }, query.DtoOptions); + var result = _tvSeriesManager.GetNextUp( + new NextUpQuery + { + Limit = query.Limit, + StartIndex = query.StartIndex, + UserId = query.User.Id + }, + new[] { parent }, + query.DtoOptions); return ToResult(result); } @@ -1165,14 +1176,16 @@ namespace Emby.Dlna.ContentDirectory { query.OrderBy = Array.Empty<(string, SortOrder)>(); - var items = _userViewManager.GetLatestItems(new LatestItemsQuery - { - UserId = user.Id, - Limit = 50, - IncludeItemTypes = new[] { typeof(Episode).Name }, - ParentId = parent == null ? Guid.Empty : parent.Id, - GroupItems = false - }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); + var items = _userViewManager.GetLatestItems( + new LatestItemsQuery + { + UserId = user.Id, + Limit = 50, + IncludeItemTypes = new[] { typeof(Episode).Name }, + ParentId = parent == null ? Guid.Empty : parent.Id, + GroupItems = false + }, + query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); return ToResult(items); } @@ -1183,13 +1196,14 @@ namespace Emby.Dlna.ContentDirectory var items = _userViewManager.GetLatestItems( new LatestItemsQuery - { - UserId = user.Id, - Limit = 50, - IncludeItemTypes = new[] { nameof(Movie) }, - ParentId = parent?.Id ?? Guid.Empty, - GroupItems = true - }, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); + { + UserId = user.Id, + Limit = 50, + IncludeItemTypes = new[] { nameof(Movie) }, + ParentId = parent?.Id ?? Guid.Empty, + GroupItems = true + }, + query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray(); return ToResult(items); } @@ -1354,44 +1368,4 @@ namespace Emby.Dlna.ContentDirectory return new ServerItem(_libraryManager.GetUserRootFolder()); } } - - internal class ServerItem - { - public BaseItem Item { get; set; } - - public StubType? StubType { get; set; } - - public ServerItem(BaseItem item) - { - Item = item; - - if (item is IItemByName && !(item is Folder)) - { - StubType = Dlna.ContentDirectory.StubType.Folder; - } - } - } - - public enum StubType - { - Folder = 0, - Latest = 2, - Playlists = 3, - Albums = 4, - AlbumArtists = 5, - Artists = 6, - Songs = 7, - Genres = 8, - FavoriteSongs = 9, - FavoriteArtists = 10, - FavoriteAlbums = 11, - ContinueWatching = 12, - Movies = 13, - Collections = 14, - Favorites = 15, - NextUp = 16, - Series = 17, - FavoriteSeries = 18, - FavoriteEpisodes = 19 - } } diff --git a/Emby.Dlna/ContentDirectory/ServerItem.cs b/Emby.Dlna/ContentDirectory/ServerItem.cs new file mode 100644 index 000000000..e40605414 --- /dev/null +++ b/Emby.Dlna/ContentDirectory/ServerItem.cs @@ -0,0 +1,23 @@ +#pragma warning disable CS1591 + +using MediaBrowser.Controller.Entities; + +namespace Emby.Dlna.ContentDirectory +{ + internal class ServerItem + { + public ServerItem(BaseItem item) + { + Item = item; + + if (item is IItemByName && !(item is Folder)) + { + StubType = Dlna.ContentDirectory.StubType.Folder; + } + } + + public BaseItem Item { get; set; } + + public StubType? StubType { get; set; } + } +} diff --git a/Emby.Dlna/ContentDirectory/StubType.cs b/Emby.Dlna/ContentDirectory/StubType.cs new file mode 100644 index 000000000..eee405d3e --- /dev/null +++ b/Emby.Dlna/ContentDirectory/StubType.cs @@ -0,0 +1,28 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1602 + +namespace Emby.Dlna.ContentDirectory +{ + public enum StubType + { + Folder = 0, + Latest = 2, + Playlists = 3, + Albums = 4, + AlbumArtists = 5, + Artists = 6, + Songs = 7, + Genres = 8, + FavoriteSongs = 9, + FavoriteArtists = 10, + FavoriteAlbums = 11, + ContinueWatching = 12, + Movies = 13, + Collections = 14, + Favorites = 15, + NextUp = 16, + Series = 17, + FavoriteSeries = 18, + FavoriteEpisodes = 19 + } +} diff --git a/Emby.Dlna/ControlRequest.cs b/Emby.Dlna/ControlRequest.cs index a6e03b7e6..4ea4e4e48 100644 --- a/Emby.Dlna/ControlRequest.cs +++ b/Emby.Dlna/ControlRequest.cs @@ -7,17 +7,17 @@ namespace Emby.Dlna { public class ControlRequest { - public IHeaderDictionary Headers { get; set; } + public ControlRequest(IHeaderDictionary headers) + { + Headers = headers; + } + + public IHeaderDictionary Headers { get; } public Stream InputXml { get; set; } public string TargetServerUuId { get; set; } public string RequestedUrl { get; set; } - - public ControlRequest() - { - Headers = new HeaderDictionary(); - } } } diff --git a/Emby.Dlna/ControlResponse.cs b/Emby.Dlna/ControlResponse.cs index 243b09786..d827eef26 100644 --- a/Emby.Dlna/ControlResponse.cs +++ b/Emby.Dlna/ControlResponse.cs @@ -11,7 +11,7 @@ namespace Emby.Dlna Headers = new Dictionary(); } - public IDictionary Headers { get; set; } + public IDictionary Headers { get; } public string Xml { get; set; } diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 70e358019..bd09a8051 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -34,12 +34,12 @@ namespace Emby.Dlna.Didl { public class DidlBuilder { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; + private const string NsDc = "http://purl.org/dc/elements/1.1/"; + private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/"; + private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/"; - private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; - private const string NS_DC = "http://purl.org/dc/elements/1.1/"; - private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/"; - private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/"; + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly DeviceProfile _profile; private readonly IImageProcessor _imageProcessor; @@ -100,11 +100,11 @@ namespace Emby.Dlna.Didl { // writer.WriteStartDocument(); - writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL); + writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl); - writer.WriteAttributeString("xmlns", "dc", null, NS_DC); - writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA); - writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP); + writer.WriteAttributeString("xmlns", "dc", null, NsDc); + writer.WriteAttributeString("xmlns", "dlna", null, NsDlna); + writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp); // didl.SetAttribute("xmlns:sec", NS_SEC); WriteXmlRootAttributes(_profile, writer); @@ -147,7 +147,7 @@ namespace Emby.Dlna.Didl { var clientId = GetClientId(item, null); - writer.WriteStartElement(string.Empty, "item", NS_DIDL); + writer.WriteStartElement(string.Empty, "item", NsDidl); writer.WriteAttributeString("restricted", "1"); writer.WriteAttributeString("id", clientId); @@ -207,7 +207,8 @@ namespace Emby.Dlna.Didl var targetWidth = streamInfo.TargetWidth; var targetHeight = streamInfo.TargetHeight; - var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(streamInfo.Container, + var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader( + streamInfo.Container, streamInfo.TargetVideoCodec.FirstOrDefault(), streamInfo.TargetAudioCodec.FirstOrDefault(), targetWidth, @@ -279,7 +280,7 @@ namespace Emby.Dlna.Didl } else if (string.Equals(subtitleMode, "smi", StringComparison.OrdinalIgnoreCase)) { - writer.WriteStartElement(string.Empty, "res", NS_DIDL); + writer.WriteStartElement(string.Empty, "res", NsDidl); writer.WriteAttributeString("protocolInfo", "http-get:*:smi/caption:*"); @@ -288,7 +289,7 @@ namespace Emby.Dlna.Didl } else { - writer.WriteStartElement(string.Empty, "res", NS_DIDL); + writer.WriteStartElement(string.Empty, "res", NsDidl); var protocolInfo = string.Format( CultureInfo.InvariantCulture, "http-get:*:text/{0}:*", @@ -304,7 +305,7 @@ namespace Emby.Dlna.Didl private void AddVideoResource(XmlWriter writer, Filter filter, string contentFeatures, StreamInfo streamInfo) { - writer.WriteStartElement(string.Empty, "res", NS_DIDL); + writer.WriteStartElement(string.Empty, "res", NsDidl); var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken)); @@ -526,7 +527,7 @@ namespace Emby.Dlna.Didl private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null) { - writer.WriteStartElement(string.Empty, "res", NS_DIDL); + writer.WriteStartElement(string.Empty, "res", NsDidl); if (streamInfo == null) { @@ -583,7 +584,8 @@ namespace Emby.Dlna.Didl writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture)); } - var mediaProfile = _profile.GetAudioMediaProfile(streamInfo.Container, + var mediaProfile = _profile.GetAudioMediaProfile( + streamInfo.Container, streamInfo.TargetAudioCodec.FirstOrDefault(), targetChannels, targetAudioBitrate, @@ -596,7 +598,8 @@ namespace Emby.Dlna.Didl ? MimeTypes.GetMimeType(filename) : mediaProfile.MimeType; - var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(streamInfo.Container, + var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader( + streamInfo.Container, streamInfo.TargetAudioCodec.FirstOrDefault(), targetAudioBitrate, targetSampleRate, @@ -627,7 +630,7 @@ namespace Emby.Dlna.Didl public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null) { - writer.WriteStartElement(string.Empty, "container", NS_DIDL); + writer.WriteStartElement(string.Empty, "container", NsDidl); writer.WriteAttributeString("restricted", "1"); writer.WriteAttributeString("searchable", "1"); @@ -714,7 +717,7 @@ namespace Emby.Dlna.Didl // MediaMonkey for example won't display content without a title // if (filter.Contains("dc:title")) { - AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NS_DC); + AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NsDc); } WriteObjectClass(writer, item, itemStubType); @@ -723,7 +726,7 @@ namespace Emby.Dlna.Didl { if (item.PremiereDate.HasValue) { - AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NS_DC); + AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc); } } @@ -731,13 +734,13 @@ namespace Emby.Dlna.Didl { foreach (var genre in item.Genres) { - AddValue(writer, "upnp", "genre", genre, NS_UPNP); + AddValue(writer, "upnp", "genre", genre, NsUpnp); } } foreach (var studio in item.Studios) { - AddValue(writer, "upnp", "publisher", studio, NS_UPNP); + AddValue(writer, "upnp", "publisher", studio, NsUpnp); } if (!(item is Folder)) @@ -748,28 +751,29 @@ namespace Emby.Dlna.Didl if (!string.IsNullOrWhiteSpace(desc)) { - AddValue(writer, "dc", "description", desc, NS_DC); + AddValue(writer, "dc", "description", desc, NsDc); } } + // if (filter.Contains("upnp:longDescription")) - //{ + // { // if (!string.IsNullOrWhiteSpace(item.Overview)) // { - // AddValue(writer, "upnp", "longDescription", item.Overview, NS_UPNP); + // AddValue(writer, "upnp", "longDescription", item.Overview, NsUpnp); // } - //} + // } } if (!string.IsNullOrEmpty(item.OfficialRating)) { if (filter.Contains("dc:rating")) { - AddValue(writer, "dc", "rating", item.OfficialRating, NS_DC); + AddValue(writer, "dc", "rating", item.OfficialRating, NsDc); } if (filter.Contains("upnp:rating")) { - AddValue(writer, "upnp", "rating", item.OfficialRating, NS_UPNP); + AddValue(writer, "upnp", "rating", item.OfficialRating, NsUpnp); } } @@ -781,7 +785,7 @@ namespace Emby.Dlna.Didl // More types here // http://oss.linn.co.uk/repos/Public/LibUpnpCil/DidlLite/UpnpAv/Test/TestDidlLite.cs - writer.WriteStartElement("upnp", "class", NS_UPNP); + writer.WriteStartElement("upnp", "class", NsUpnp); if (item.IsDisplayedAsFolder || stubType.HasValue) { @@ -882,7 +886,7 @@ namespace Emby.Dlna.Didl var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase)) ?? PersonType.Actor; - AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NS_UPNP); + AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp); } } @@ -896,8 +900,8 @@ namespace Emby.Dlna.Didl { foreach (var artist in hasArtists.Artists) { - AddValue(writer, "upnp", "artist", artist, NS_UPNP); - AddValue(writer, "dc", "creator", artist, NS_DC); + AddValue(writer, "upnp", "artist", artist, NsUpnp); + AddValue(writer, "dc", "creator", artist, NsDc); // If it doesn't support album artists (musicvideo), then tag as both if (hasAlbumArtists == null) @@ -917,16 +921,16 @@ namespace Emby.Dlna.Didl if (!string.IsNullOrWhiteSpace(item.Album)) { - AddValue(writer, "upnp", "album", item.Album, NS_UPNP); + AddValue(writer, "upnp", "album", item.Album, NsUpnp); } if (item.IndexNumber.HasValue) { - AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP); + AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp); if (item is Episode) { - AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP); + AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp); } } } @@ -935,7 +939,7 @@ namespace Emby.Dlna.Didl { try { - writer.WriteStartElement("upnp", "artist", NS_UPNP); + writer.WriteStartElement("upnp", "artist", NsUpnp); writer.WriteAttributeString("role", "AlbumArtist"); writer.WriteString(name); @@ -971,14 +975,14 @@ namespace Emby.Dlna.Didl var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg"); - writer.WriteStartElement("upnp", "albumArtURI", NS_UPNP); - writer.WriteAttributeString("dlna", "profileID", NS_DLNA, _profile.AlbumArtPn); - writer.WriteString(albumartUrlInfo.Url); + writer.WriteStartElement("upnp", "albumArtURI", NsUpnp); + writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn); + writer.WriteString(albumartUrlInfo.url); writer.WriteFullEndElement(); // TOOD: Remove these default values var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, "jpg"); - writer.WriteElementString("upnp", "icon", NS_UPNP, iconUrlInfo.Url); + writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url); if (!_profile.EnableAlbumArtInDidl) { @@ -1021,12 +1025,12 @@ namespace Emby.Dlna.Didl var albumartUrlInfo = GetImageUrl(imageInfo, maxWidth, maxHeight, format); - writer.WriteStartElement(string.Empty, "res", NS_DIDL); + writer.WriteStartElement(string.Empty, "res", NsDidl); // Images must have a reported size or many clients (Bubble upnp), will only use the first thumbnail // rather than using a larger one when available - var width = albumartUrlInfo.Width ?? maxWidth; - var height = albumartUrlInfo.Height ?? maxHeight; + var width = albumartUrlInfo.width ?? maxWidth; + var height = albumartUrlInfo.height ?? maxHeight; var contentFeatures = new ContentFeatureBuilder(_profile) .BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn); @@ -1043,7 +1047,7 @@ namespace Emby.Dlna.Didl "resolution", string.Format(CultureInfo.InvariantCulture, "{0}x{1}", width, height)); - writer.WriteString(albumartUrlInfo.Url); + writer.WriteString(albumartUrlInfo.url); writer.WriteFullEndElement(); } @@ -1139,7 +1143,6 @@ namespace Emby.Dlna.Didl if (width == 0 || height == 0) { - // _imageProcessor.GetImageSize(item, imageInfo); width = null; height = null; } @@ -1149,18 +1152,6 @@ namespace Emby.Dlna.Didl height = null; } - // try - //{ - // var size = _imageProcessor.GetImageSize(imageInfo); - - // width = size.Width; - // height = size.Height; - //} - // catch - //{ - - //} - var inputFormat = (Path.GetExtension(imageInfo.Path) ?? string.Empty) .TrimStart('.') .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase); @@ -1177,30 +1168,6 @@ namespace Emby.Dlna.Didl }; } - private class ImageDownloadInfo - { - internal Guid ItemId; - internal string ImageTag; - internal ImageType Type; - - internal int? Width; - internal int? Height; - - internal bool IsDirectStream; - - internal string Format; - - internal ItemImageInfo ItemImageInfo; - } - - private class ImageUrlInfo - { - internal string Url; - - internal int? Width; - internal int? Height; - } - public static string GetClientId(BaseItem item, StubType? stubType) { return GetClientId(item.Id, stubType); @@ -1218,7 +1185,7 @@ namespace Emby.Dlna.Didl return id; } - private ImageUrlInfo GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format) + private (string url, int? width, int? height) GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format) { var url = string.Format( CultureInfo.InvariantCulture, @@ -1256,12 +1223,26 @@ namespace Emby.Dlna.Didl // just lie info.IsDirectStream = true; - return new ImageUrlInfo - { - Url = url, - Width = width, - Height = height - }; + return (url, width, height); + } + + private class ImageDownloadInfo + { + internal Guid ItemId { get; set; } + + internal string ImageTag { get; set; } + + internal ImageType Type { get; set; } + + internal int? Width { get; set; } + + internal int? Height { get; set; } + + internal bool IsDirectStream { get; set; } + + internal string Format { get; set; } + + internal ItemImageInfo ItemImageInfo { get; set; } } } } diff --git a/Emby.Dlna/Didl/Filter.cs b/Emby.Dlna/Didl/Filter.cs index b730d9db2..b58fdff2c 100644 --- a/Emby.Dlna/Didl/Filter.cs +++ b/Emby.Dlna/Didl/Filter.cs @@ -23,9 +23,7 @@ namespace Emby.Dlna.Didl public bool Contains(string field) { - // Don't bother with this. Some clients (media monkey) use the filter and then don't display very well when very little data comes back. - return true; - // return _all || ListHelper.ContainsIgnoreCase(_fields, field); + return _all || Array.Exists(_fields, x => x.Equals(field, StringComparison.OrdinalIgnoreCase)); } } } diff --git a/Emby.Dlna/Didl/StringWriterWithEncoding.cs b/Emby.Dlna/Didl/StringWriterWithEncoding.cs index 896fe992b..2b86ea333 100644 --- a/Emby.Dlna/Didl/StringWriterWithEncoding.cs +++ b/Emby.Dlna/Didl/StringWriterWithEncoding.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +#pragma warning disable CA1305 using System; using System.IO; @@ -29,7 +30,6 @@ namespace Emby.Dlna.Didl { } - public StringWriterWithEncoding(Encoding encoding) { _encoding = encoding; diff --git a/Emby.Dlna/DlnaConfigurationFactory.cs b/Emby.Dlna/DlnaConfigurationFactory.cs new file mode 100644 index 000000000..4c6ca869a --- /dev/null +++ b/Emby.Dlna/DlnaConfigurationFactory.cs @@ -0,0 +1,24 @@ +#nullable enable +#pragma warning disable CS1591 + +using System.Collections.Generic; +using Emby.Dlna.Configuration; +using MediaBrowser.Common.Configuration; + +namespace Emby.Dlna +{ + public class DlnaConfigurationFactory : IConfigurationFactory + { + public IEnumerable GetConfigurations() + { + return new[] + { + new ConfigurationStore + { + Key = "dlna", + ConfigurationType = typeof(DlnaOptions) + } + }; + } + } +} diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index 269f7ee43..d5629684c 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -54,11 +54,15 @@ namespace Emby.Dlna _appHost = appHost; } + private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user"); + + private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system"); + public async Task InitProfilesAsync() { try { - await ExtractSystemProfilesAsync(); + await ExtractSystemProfilesAsync().ConfigureAwait(false); LoadProfiles(); } catch (Exception ex) @@ -240,7 +244,7 @@ namespace Emby.Dlna } else { - var headerString = string.Join(", ", headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray()); + var headerString = string.Join(", ", headers.Select(i => string.Format(CultureInfo.InvariantCulture, "{0}={1}", i.Key, i.Value))); _logger.LogDebug("No matching device profile found. {0}", headerString); } @@ -280,10 +284,6 @@ namespace Emby.Dlna return false; } - private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user"); - - private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system"); - private IEnumerable GetProfiles(string path, DeviceProfileType type) { try @@ -495,8 +495,8 @@ namespace Emby.Dlna /// Recreates the object using serialization, to ensure it's not a subclass. /// If it's a subclass it may not serlialize properly to xml (different root element tag name). /// - /// - /// + /// The device profile. + /// The reserialized device profile. private DeviceProfile ReserializeProfile(DeviceProfile profile) { if (profile.GetType() == typeof(DeviceProfile)) @@ -509,13 +509,6 @@ namespace Emby.Dlna return _jsonSerializer.DeserializeFromString(json); } - private class InternalProfileInfo - { - internal DeviceProfileInfo Info { get; set; } - - internal string Path { get; set; } - } - public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress) { var profile = GetProfile(headers) ?? @@ -540,7 +533,15 @@ namespace Emby.Dlna Stream = _assembly.GetManifestResourceStream(resource) }; } + + private class InternalProfileInfo + { + internal DeviceProfileInfo Info { get; set; } + + internal string Path { get; set; } + } } + /* class DlnaProfileEntryPoint : IServerEntryPoint { diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index 42a5f95c1..8538f580c 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -20,7 +20,7 @@ netstandard2.1 false true - true + true diff --git a/Emby.Dlna/EventSubscriptionResponse.cs b/Emby.Dlna/EventSubscriptionResponse.cs index fd18343e6..1b1bd426c 100644 --- a/Emby.Dlna/EventSubscriptionResponse.cs +++ b/Emby.Dlna/EventSubscriptionResponse.cs @@ -15,6 +15,6 @@ namespace Emby.Dlna public string ContentType { get; set; } - public Dictionary Headers { get; set; } + public Dictionary Headers { get; } } } diff --git a/Emby.Dlna/Eventing/EventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs similarity index 95% rename from Emby.Dlna/Eventing/EventManager.cs rename to Emby.Dlna/Eventing/DlnaEventManager.cs index 7d02f5e96..b66e966df 100644 --- a/Emby.Dlna/Eventing/EventManager.cs +++ b/Emby.Dlna/Eventing/DlnaEventManager.cs @@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Dlna.Eventing { - public class EventManager : IEventManager + public class DlnaEventManager : IDlnaEventManager { private readonly ConcurrentDictionary _subscriptions = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); @@ -22,7 +22,9 @@ namespace Emby.Dlna.Eventing private readonly ILogger _logger; private readonly IHttpClient _httpClient; - public EventManager(ILogger logger, IHttpClient httpClient) + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public DlnaEventManager(ILogger logger, IHttpClient httpClient) { _httpClient = httpClient; _logger = logger; @@ -58,7 +60,8 @@ namespace Emby.Dlna.Eventing var timeout = ParseTimeout(requestedTimeoutString) ?? 300; var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - _logger.LogDebug("Creating event subscription for {0} with timeout of {1} to {2}", + _logger.LogDebug( + "Creating event subscription for {0} with timeout of {1} to {2}", notificationType, timeout, callbackUrl); @@ -94,7 +97,7 @@ namespace Emby.Dlna.Eventing { _logger.LogDebug("Cancelling event subscription {0}", subscriptionId); - _subscriptions.TryRemove(subscriptionId, out EventSubscription sub); + _subscriptions.TryRemove(subscriptionId, out _); return new EventSubscriptionResponse { @@ -103,7 +106,6 @@ namespace Emby.Dlna.Eventing }; } - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds) { var response = new EventSubscriptionResponse diff --git a/Emby.Dlna/IConnectionManager.cs b/Emby.Dlna/IConnectionManager.cs index 7b4a33a98..9f643a9e6 100644 --- a/Emby.Dlna/IConnectionManager.cs +++ b/Emby.Dlna/IConnectionManager.cs @@ -2,7 +2,7 @@ namespace Emby.Dlna { - public interface IConnectionManager : IEventManager, IUpnpService + public interface IConnectionManager : IDlnaEventManager, IUpnpService { } } diff --git a/Emby.Dlna/IContentDirectory.cs b/Emby.Dlna/IContentDirectory.cs index 83ef09c66..10f4d6386 100644 --- a/Emby.Dlna/IContentDirectory.cs +++ b/Emby.Dlna/IContentDirectory.cs @@ -2,7 +2,7 @@ namespace Emby.Dlna { - public interface IContentDirectory : IEventManager, IUpnpService + public interface IContentDirectory : IDlnaEventManager, IUpnpService { } } diff --git a/Emby.Dlna/IEventManager.cs b/Emby.Dlna/IDlnaEventManager.cs similarity index 52% rename from Emby.Dlna/IEventManager.cs rename to Emby.Dlna/IDlnaEventManager.cs index 287203389..33cf0896b 100644 --- a/Emby.Dlna/IEventManager.cs +++ b/Emby.Dlna/IDlnaEventManager.cs @@ -2,22 +2,32 @@ namespace Emby.Dlna { - public interface IEventManager + public interface IDlnaEventManager { /// /// Cancels the event subscription. /// /// The subscription identifier. + /// The response. EventSubscriptionResponse CancelEventSubscription(string subscriptionId); /// /// Renews the event subscription. /// + /// The subscription identifier. + /// The notification type. + /// The requested timeout as a sting. + /// The callback url. + /// The response. EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl); /// /// Creates the event subscription. /// + /// The notification type. + /// The requested timeout as a sting. + /// The callback url. + /// The response. EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl); } } diff --git a/Emby.Dlna/IMediaReceiverRegistrar.cs b/Emby.Dlna/IMediaReceiverRegistrar.cs index b0376b6a9..43e934b53 100644 --- a/Emby.Dlna/IMediaReceiverRegistrar.cs +++ b/Emby.Dlna/IMediaReceiverRegistrar.cs @@ -2,7 +2,7 @@ namespace Emby.Dlna { - public interface IMediaReceiverRegistrar : IEventManager, IUpnpService + public interface IMediaReceiverRegistrar : IDlnaEventManager, IUpnpService { } } diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index a21d4cc11..0ad82022d 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -30,7 +30,7 @@ using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; namespace Emby.Dlna.Main { - public class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup + public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup { private readonly IServerConfigurationManager _config; private readonly ILogger _logger; @@ -54,13 +54,7 @@ namespace Emby.Dlna.Main private SsdpDevicePublisher _publisher; private ISsdpCommunicationsServer _communicationsServer; - public IContentDirectory ContentDirectory { get; private set; } - - public IConnectionManager ConnectionManager { get; private set; } - - public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; } - - public static DlnaEntryPoint Current; + private bool _disposed; public DlnaEntryPoint( IServerConfigurationManager config, @@ -99,14 +93,14 @@ namespace Emby.Dlna.Main _networkManager = networkManager; _logger = loggerFactory.CreateLogger(); - ContentDirectory = new ContentDirectory.ContentDirectory( + ContentDirectory = new ContentDirectory.ContentDirectoryService( dlnaManager, userDataManager, imageProcessor, libraryManager, config, userManager, - loggerFactory.CreateLogger(), + loggerFactory.CreateLogger(), httpClient, localizationManager, mediaSourceManager, @@ -114,19 +108,27 @@ namespace Emby.Dlna.Main mediaEncoder, tvSeriesManager); - ConnectionManager = new ConnectionManager.ConnectionManager( + ConnectionManager = new ConnectionManager.ConnectionManagerService( dlnaManager, config, - loggerFactory.CreateLogger(), + loggerFactory.CreateLogger(), httpClient); - MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar( - loggerFactory.CreateLogger(), + MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService( + loggerFactory.CreateLogger(), httpClient, config); Current = this; } + public static DlnaEntryPoint Current { get; private set; } + + public IContentDirectory ContentDirectory { get; private set; } + + public IConnectionManager ConnectionManager { get; private set; } + + public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; } + public async Task RunAsync() { await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); @@ -399,8 +401,24 @@ namespace Emby.Dlna.Main } } + public void DisposeDevicePublisher() + { + if (_publisher != null) + { + _logger.LogInformation("Disposing SsdpDevicePublisher"); + _publisher.Dispose(); + _publisher = null; + } + } + + /// public void Dispose() { + if (_disposed) + { + return; + } + DisposeDevicePublisher(); DisposePlayToManager(); DisposeDeviceDiscovery(); @@ -416,16 +434,8 @@ namespace Emby.Dlna.Main ConnectionManager = null; MediaReceiverRegistrar = null; Current = null; - } - public void DisposeDevicePublisher() - { - if (_publisher != null) - { - _logger.LogInformation("Disposing SsdpDevicePublisher"); - _publisher.Dispose(); - _publisher = null; - } + _disposed = true; } } } diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs similarity index 82% rename from Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs rename to Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs index 64dfc840a..28de2fef5 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs @@ -8,12 +8,12 @@ using Microsoft.Extensions.Logging; namespace Emby.Dlna.MediaReceiverRegistrar { - public class MediaReceiverRegistrar : BaseService, IMediaReceiverRegistrar + public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar { private readonly IServerConfigurationManager _config; - public MediaReceiverRegistrar( - ILogger logger, + public MediaReceiverRegistrarService( + ILogger logger, IHttpClient httpClient, IServerConfigurationManager config) : base(logger, httpClient) diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs index 849702546..26994925d 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs @@ -10,7 +10,8 @@ namespace Emby.Dlna.MediaReceiverRegistrar { public string GetXml() { - return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(), + return new ServiceXmlBuilder().GetXml( + new ServiceActionListBuilder().GetActions(), GetStateVariables()); } diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index 72834c69d..5462e7abc 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -19,15 +19,40 @@ namespace Emby.Dlna.PlayTo { public class Device : IDisposable { + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + private readonly IHttpClient _httpClient; + + private readonly ILogger _logger; + + private readonly object _timerLock = new object(); private Timer _timer; + private int _muteVol; + private int _volume; + private DateTime _lastVolumeRefresh; + private bool _volumeRefreshActive; + private int _connectFailureCount; + private bool _disposed; + + public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger) + { + Properties = deviceProperties; + _httpClient = httpClient; + _logger = logger; + } + + public event EventHandler PlaybackStart; + + public event EventHandler PlaybackProgress; + + public event EventHandler PlaybackStopped; + + public event EventHandler MediaChanged; public DeviceInfo Properties { get; set; } - private int _muteVol; public bool IsMuted { get; set; } - private int _volume; - public int Volume { get @@ -43,29 +68,21 @@ namespace Emby.Dlna.PlayTo public TimeSpan Position { get; set; } = TimeSpan.FromSeconds(0); - public TRANSPORTSTATE TransportState { get; private set; } + public TransportState TransportState { get; private set; } - public bool IsPlaying => TransportState == TRANSPORTSTATE.PLAYING; + public bool IsPlaying => TransportState == TransportState.Playing; - public bool IsPaused => TransportState == TRANSPORTSTATE.PAUSED || TransportState == TRANSPORTSTATE.PAUSED_PLAYBACK; + public bool IsPaused => TransportState == TransportState.Paused || TransportState == TransportState.PausedPlayback; - public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED; - - private readonly IHttpClient _httpClient; - - private readonly ILogger _logger; - - private readonly IServerConfigurationManager _config; + public bool IsStopped => TransportState == TransportState.Stopped; public Action OnDeviceUnavailable { get; set; } - public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config) - { - Properties = deviceProperties; - _httpClient = httpClient; - _logger = logger; - _config = config; - } + private TransportCommands AvCommands { get; set; } + + private TransportCommands RendererCommands { get; set; } + + public UBaseObject CurrentMediaInfo { get; private set; } public void Start() { @@ -73,8 +90,6 @@ namespace Emby.Dlna.PlayTo _timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite); } - private DateTime _lastVolumeRefresh; - private bool _volumeRefreshActive; private Task RefreshVolumeIfNeeded() { if (_volumeRefreshActive @@ -105,7 +120,6 @@ namespace Emby.Dlna.PlayTo } } - private readonly object _timerLock = new object(); private void RestartTimer(bool immediate = false) { lock (_timerLock) @@ -233,6 +247,9 @@ namespace Emby.Dlna.PlayTo /// /// Sets volume on a scale of 0-100. /// + /// The volume on a scale of 0-100. + /// The cancellation token to cancel operation. + /// A representing the asynchronous operation. public async Task SetVolume(int value, CancellationToken cancellationToken) { var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); @@ -275,7 +292,7 @@ namespace Emby.Dlna.PlayTo throw new InvalidOperationException("Unable to find service"); } - await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME")) + await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME")) .ConfigureAwait(false); RestartTimer(true); @@ -285,7 +302,7 @@ namespace Emby.Dlna.PlayTo { var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - url = url.Replace("&", "&"); + url = url.Replace("&", "&", StringComparison.Ordinal); _logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header); @@ -297,8 +314,8 @@ namespace Emby.Dlna.PlayTo var dictionary = new Dictionary { - {"CurrentURI", url}, - {"CurrentURIMetaData", CreateDidlMeta(metaData)} + { "CurrentURI", url }, + { "CurrentURIMetaData", CreateDidlMeta(metaData) } }; var service = GetAvTransportService(); @@ -401,13 +418,11 @@ namespace Emby.Dlna.PlayTo await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)) .ConfigureAwait(false); - TransportState = TRANSPORTSTATE.PAUSED; + TransportState = TransportState.Paused; RestartTimer(true); } - private int _connectFailureCount; - private async void TimerCallback(object sender) { if (_disposed) @@ -436,7 +451,7 @@ namespace Emby.Dlna.PlayTo if (transportState.HasValue) { // If we're not playing anything no need to get additional data - if (transportState.Value == TRANSPORTSTATE.STOPPED) + if (transportState.Value == TransportState.Stopped) { UpdateMediaInfo(null, transportState.Value); } @@ -465,7 +480,7 @@ namespace Emby.Dlna.PlayTo } // If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive - if (transportState.Value == TRANSPORTSTATE.STOPPED) + if (transportState.Value == TransportState.Stopped) { RestartTimerInactive(); } @@ -539,7 +554,7 @@ namespace Emby.Dlna.PlayTo return; } - var volume = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null); + var volume = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null); var volumeValue = volume?.Value; if (string.IsNullOrWhiteSpace(volumeValue)) @@ -589,14 +604,14 @@ namespace Emby.Dlna.PlayTo return; } - var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse") + var valueNode = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetMuteResponse") .Select(i => i.Element("CurrentMute")) .FirstOrDefault(i => i != null); IsMuted = string.Equals(valueNode?.Value, "1", StringComparison.OrdinalIgnoreCase); } - private async Task GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken) + private async Task GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken) { var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo"); if (command == null) @@ -623,12 +638,12 @@ namespace Emby.Dlna.PlayTo } var transportState = - result.Document.Descendants(uPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null); + result.Document.Descendants(UPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null); var transportStateValue = transportState?.Value; if (transportStateValue != null - && Enum.TryParse(transportStateValue, true, out TRANSPORTSTATE state)) + && Enum.TryParse(transportStateValue, true, out TransportState state)) { return state; } @@ -636,7 +651,7 @@ namespace Emby.Dlna.PlayTo return null; } - private async Task GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken) + private async Task GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken) { var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo"); if (command == null) @@ -671,7 +686,7 @@ namespace Emby.Dlna.PlayTo return null; } - var e = track.Element(uPnpNamespaces.items) ?? track; + var e = track.Element(UPnpNamespaces.Items) ?? track; var elementString = (string)e; @@ -687,13 +702,13 @@ namespace Emby.Dlna.PlayTo return null; } - e = track.Element(uPnpNamespaces.items) ?? track; + e = track.Element(UPnpNamespaces.Items) ?? track; elementString = (string)e; if (!string.IsNullOrWhiteSpace(elementString)) { - return new uBaseObject + return new UBaseObject { Url = elementString }; @@ -702,7 +717,7 @@ namespace Emby.Dlna.PlayTo return null; } - private async Task<(bool, uBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken) + private async Task<(bool, UBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken) { var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo"); if (command == null) @@ -731,11 +746,11 @@ namespace Emby.Dlna.PlayTo return (false, null); } - var trackUriElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null); - var trackUri = trackUriElem == null ? null : trackUriElem.Value; + var trackUriElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null); + var trackUri = trackUriElem?.Value; - var durationElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null); - var duration = durationElem == null ? null : durationElem.Value; + var durationElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null); + var duration = durationElem?.Value; if (!string.IsNullOrWhiteSpace(duration) && !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) @@ -747,8 +762,8 @@ namespace Emby.Dlna.PlayTo Duration = null; } - var positionElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null); - var position = positionElem == null ? null : positionElem.Value; + var positionElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null); + var position = positionElem?.Value; if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) { @@ -787,7 +802,7 @@ namespace Emby.Dlna.PlayTo return (true, null); } - var e = uPnpResponse.Element(uPnpNamespaces.items); + var e = uPnpResponse.Element(UPnpNamespaces.Items); var uTrack = CreateUBaseObject(e, trackUri); @@ -819,7 +834,7 @@ namespace Emby.Dlna.PlayTo // some devices send back invalid xml try { - return XElement.Parse(xml.Replace("&", "&")); + return XElement.Parse(xml.Replace("&", "&", StringComparison.Ordinal)); } catch (XmlException) { @@ -828,27 +843,27 @@ namespace Emby.Dlna.PlayTo return null; } - private static uBaseObject CreateUBaseObject(XElement container, string trackUri) + private static UBaseObject CreateUBaseObject(XElement container, string trackUri) { if (container == null) { throw new ArgumentNullException(nameof(container)); } - var url = container.GetValue(uPnpNamespaces.Res); + var url = container.GetValue(UPnpNamespaces.Res); if (string.IsNullOrWhiteSpace(url)) { url = trackUri; } - return new uBaseObject + return new UBaseObject { - Id = container.GetAttributeValue(uPnpNamespaces.Id), - ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId), - Title = container.GetValue(uPnpNamespaces.title), - IconUrl = container.GetValue(uPnpNamespaces.Artwork), - SecondText = "", + Id = container.GetAttributeValue(UPnpNamespaces.Id), + ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId), + Title = container.GetValue(UPnpNamespaces.Title), + IconUrl = container.GetValue(UPnpNamespaces.Artwork), + SecondText = string.Empty, Url = url, ProtocolInfo = GetProtocolInfo(container), MetaData = container.ToString() @@ -862,11 +877,11 @@ namespace Emby.Dlna.PlayTo throw new ArgumentNullException(nameof(container)); } - var resElement = container.Element(uPnpNamespaces.Res); + var resElement = container.Element(UPnpNamespaces.Res); if (resElement != null) { - var info = resElement.Attribute(uPnpNamespaces.ProtocolInfo); + var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo); if (info != null && !string.IsNullOrWhiteSpace(info.Value)) { @@ -941,12 +956,12 @@ namespace Emby.Dlna.PlayTo return url; } - if (!url.Contains("/")) + if (!url.Contains('/', StringComparison.Ordinal)) { url = "/dmr/" + url; } - if (!url.StartsWith("/")) + if (!url.StartsWith("/", StringComparison.Ordinal)) { url = "/" + url; } @@ -954,11 +969,7 @@ namespace Emby.Dlna.PlayTo return baseUrl + url; } - private TransportCommands AvCommands { get; set; } - - private TransportCommands RendererCommands { get; set; } - - public static async Task CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger, CancellationToken cancellationToken) + public static async Task CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, ILogger logger, CancellationToken cancellationToken) { var ssdpHttpClient = new SsdpHttpClient(httpClient); @@ -966,13 +977,13 @@ namespace Emby.Dlna.PlayTo var friendlyNames = new List(); - var name = document.Descendants(uPnpNamespaces.ud.GetName("friendlyName")).FirstOrDefault(); + var name = document.Descendants(UPnpNamespaces.Ud.GetName("friendlyName")).FirstOrDefault(); if (name != null && !string.IsNullOrWhiteSpace(name.Value)) { friendlyNames.Add(name.Value); } - var room = document.Descendants(uPnpNamespaces.ud.GetName("roomName")).FirstOrDefault(); + var room = document.Descendants(UPnpNamespaces.Ud.GetName("roomName")).FirstOrDefault(); if (room != null && !string.IsNullOrWhiteSpace(room.Value)) { friendlyNames.Add(room.Value); @@ -981,77 +992,77 @@ namespace Emby.Dlna.PlayTo var deviceProperties = new DeviceInfo() { Name = string.Join(" ", friendlyNames), - BaseUrl = string.Format("http://{0}:{1}", url.Host, url.Port) + BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port) }; - var model = document.Descendants(uPnpNamespaces.ud.GetName("modelName")).FirstOrDefault(); + var model = document.Descendants(UPnpNamespaces.Ud.GetName("modelName")).FirstOrDefault(); if (model != null) { deviceProperties.ModelName = model.Value; } - var modelNumber = document.Descendants(uPnpNamespaces.ud.GetName("modelNumber")).FirstOrDefault(); + var modelNumber = document.Descendants(UPnpNamespaces.Ud.GetName("modelNumber")).FirstOrDefault(); if (modelNumber != null) { deviceProperties.ModelNumber = modelNumber.Value; } - var uuid = document.Descendants(uPnpNamespaces.ud.GetName("UDN")).FirstOrDefault(); + var uuid = document.Descendants(UPnpNamespaces.Ud.GetName("UDN")).FirstOrDefault(); if (uuid != null) { deviceProperties.UUID = uuid.Value; } - var manufacturer = document.Descendants(uPnpNamespaces.ud.GetName("manufacturer")).FirstOrDefault(); + var manufacturer = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturer")).FirstOrDefault(); if (manufacturer != null) { deviceProperties.Manufacturer = manufacturer.Value; } - var manufacturerUrl = document.Descendants(uPnpNamespaces.ud.GetName("manufacturerURL")).FirstOrDefault(); + var manufacturerUrl = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturerURL")).FirstOrDefault(); if (manufacturerUrl != null) { deviceProperties.ManufacturerUrl = manufacturerUrl.Value; } - var presentationUrl = document.Descendants(uPnpNamespaces.ud.GetName("presentationURL")).FirstOrDefault(); + var presentationUrl = document.Descendants(UPnpNamespaces.Ud.GetName("presentationURL")).FirstOrDefault(); if (presentationUrl != null) { deviceProperties.PresentationUrl = presentationUrl.Value; } - var modelUrl = document.Descendants(uPnpNamespaces.ud.GetName("modelURL")).FirstOrDefault(); + var modelUrl = document.Descendants(UPnpNamespaces.Ud.GetName("modelURL")).FirstOrDefault(); if (modelUrl != null) { deviceProperties.ModelUrl = modelUrl.Value; } - var serialNumber = document.Descendants(uPnpNamespaces.ud.GetName("serialNumber")).FirstOrDefault(); + var serialNumber = document.Descendants(UPnpNamespaces.Ud.GetName("serialNumber")).FirstOrDefault(); if (serialNumber != null) { deviceProperties.SerialNumber = serialNumber.Value; } - var modelDescription = document.Descendants(uPnpNamespaces.ud.GetName("modelDescription")).FirstOrDefault(); + var modelDescription = document.Descendants(UPnpNamespaces.Ud.GetName("modelDescription")).FirstOrDefault(); if (modelDescription != null) { deviceProperties.ModelDescription = modelDescription.Value; } - var icon = document.Descendants(uPnpNamespaces.ud.GetName("icon")).FirstOrDefault(); + var icon = document.Descendants(UPnpNamespaces.Ud.GetName("icon")).FirstOrDefault(); if (icon != null) { deviceProperties.Icon = CreateIcon(icon); } - foreach (var services in document.Descendants(uPnpNamespaces.ud.GetName("serviceList"))) + foreach (var services in document.Descendants(UPnpNamespaces.Ud.GetName("serviceList"))) { if (services == null) { continue; } - var servicesList = services.Descendants(uPnpNamespaces.ud.GetName("service")); + var servicesList = services.Descendants(UPnpNamespaces.Ud.GetName("service")); if (servicesList == null) { continue; @@ -1068,10 +1079,9 @@ namespace Emby.Dlna.PlayTo } } - return new Device(deviceProperties, httpClient, logger, config); + return new Device(deviceProperties, httpClient, logger); } - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); private static DeviceIcon CreateIcon(XElement element) { if (element == null) @@ -1079,11 +1089,11 @@ namespace Emby.Dlna.PlayTo throw new ArgumentNullException(nameof(element)); } - var mimeType = element.GetDescendantValue(uPnpNamespaces.ud.GetName("mimetype")); - var width = element.GetDescendantValue(uPnpNamespaces.ud.GetName("width")); - var height = element.GetDescendantValue(uPnpNamespaces.ud.GetName("height")); - var depth = element.GetDescendantValue(uPnpNamespaces.ud.GetName("depth")); - var url = element.GetDescendantValue(uPnpNamespaces.ud.GetName("url")); + var mimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype")); + var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width")); + var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height")); + var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth")); + var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url")); var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture); var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture); @@ -1100,11 +1110,11 @@ namespace Emby.Dlna.PlayTo private static DeviceService Create(XElement element) { - var type = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceType")); - var id = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceId")); - var scpdUrl = element.GetDescendantValue(uPnpNamespaces.ud.GetName("SCPDURL")); - var controlURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("controlURL")); - var eventSubURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("eventSubURL")); + var type = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType")); + var id = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId")); + var scpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL")); + var controlURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL")); + var eventSubURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL")); return new DeviceService { @@ -1116,14 +1126,7 @@ namespace Emby.Dlna.PlayTo }; } - public event EventHandler PlaybackStart; - public event EventHandler PlaybackProgress; - public event EventHandler PlaybackStopped; - public event EventHandler MediaChanged; - - public uBaseObject CurrentMediaInfo { get; private set; } - - private void UpdateMediaInfo(uBaseObject mediaInfo, TRANSPORTSTATE state) + private void UpdateMediaInfo(UBaseObject mediaInfo, TransportState state) { TransportState = state; @@ -1132,7 +1135,7 @@ namespace Emby.Dlna.PlayTo if (previousMediaInfo == null && mediaInfo != null) { - if (state != TRANSPORTSTATE.STOPPED) + if (state != TransportState.Stopped) { OnPlaybackStart(mediaInfo); } @@ -1151,7 +1154,7 @@ namespace Emby.Dlna.PlayTo } } - private void OnPlaybackStart(uBaseObject mediaInfo) + private void OnPlaybackStart(UBaseObject mediaInfo) { if (string.IsNullOrWhiteSpace(mediaInfo.Url)) { @@ -1164,7 +1167,7 @@ namespace Emby.Dlna.PlayTo }); } - private void OnPlaybackProgress(uBaseObject mediaInfo) + private void OnPlaybackProgress(UBaseObject mediaInfo) { if (string.IsNullOrWhiteSpace(mediaInfo.Url)) { @@ -1177,7 +1180,7 @@ namespace Emby.Dlna.PlayTo }); } - private void OnPlaybackStop(uBaseObject mediaInfo) + private void OnPlaybackStop(UBaseObject mediaInfo) { PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs { @@ -1185,7 +1188,7 @@ namespace Emby.Dlna.PlayTo }); } - private void OnMediaChanged(uBaseObject old, uBaseObject newMedia) + private void OnMediaChanged(UBaseObject old, UBaseObject newMedia) { MediaChanged?.Invoke(this, new MediaChangedEventArgs { @@ -1194,14 +1197,17 @@ namespace Emby.Dlna.PlayTo }); } - bool _disposed; - + /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } + /// + /// Releases unmanaged and optionally managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (_disposed) @@ -1220,9 +1226,10 @@ namespace Emby.Dlna.PlayTo _disposed = true; } + /// public override string ToString() { - return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl); + return string.Format(CultureInfo.InvariantCulture, "{0} - {1}", Properties.Name, Properties.BaseUrl); } } } diff --git a/Emby.Dlna/PlayTo/DeviceInfo.cs b/Emby.Dlna/PlayTo/DeviceInfo.cs index f3aaaebc4..d3daab9e0 100644 --- a/Emby.Dlna/PlayTo/DeviceInfo.cs +++ b/Emby.Dlna/PlayTo/DeviceInfo.cs @@ -8,6 +8,9 @@ namespace Emby.Dlna.PlayTo { public class DeviceInfo { + private readonly List _services = new List(); + private string _baseUrl = string.Empty; + public DeviceInfo() { Name = "Generic Device"; @@ -33,7 +36,6 @@ namespace Emby.Dlna.PlayTo public string PresentationUrl { get; set; } - private string _baseUrl = string.Empty; public string BaseUrl { get => _baseUrl; @@ -42,7 +44,6 @@ namespace Emby.Dlna.PlayTo public DeviceIcon Icon { get; set; } - private readonly List _services = new List(); public List Services => _services; public DeviceIdentification ToDeviceIdentification() diff --git a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs new file mode 100644 index 000000000..dabd079af --- /dev/null +++ b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs @@ -0,0 +1,13 @@ +#pragma warning disable CS1591 + +using System; + +namespace Emby.Dlna.PlayTo +{ + public class MediaChangedEventArgs : EventArgs + { + public UBaseObject OldMediaInfo { get; set; } + + public UBaseObject NewMediaInfo { get; set; } + } +} diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 92a93d434..328759c5b 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Emby.Dlna.Didl; using Jellyfin.Data.Entities; +using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; @@ -18,7 +19,6 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Session; using Microsoft.AspNetCore.WebUtilities; @@ -31,7 +31,6 @@ namespace Emby.Dlna.PlayTo { private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); - private Device _device; private readonly SessionInfo _session; private readonly ISessionManager _sessionManager; private readonly ILibraryManager _libraryManager; @@ -50,6 +49,7 @@ namespace Emby.Dlna.PlayTo private readonly string _accessToken; private readonly List _playlist = new List(); + private Device _device; private int _currentPlaylistIndex; private bool _disposed; @@ -372,8 +372,13 @@ namespace Emby.Dlna.PlayTo if (!command.ControllingUserId.Equals(Guid.Empty)) { - _sessionManager.LogSessionActivity(_session.Client, _session.ApplicationVersion, _session.DeviceId, - _session.DeviceName, _session.RemoteEndPoint, user); + _sessionManager.LogSessionActivity( + _session.Client, + _session.ApplicationVersion, + _session.DeviceId, + _session.DeviceName, + _session.RemoteEndPoint, + user); } return PlayItems(playlist, cancellationToken); @@ -498,42 +503,44 @@ namespace Emby.Dlna.PlayTo if (streamInfo.MediaType == DlnaProfileType.Audio) { return new ContentFeatureBuilder(profile) - .BuildAudioHeader(streamInfo.Container, - streamInfo.TargetAudioCodec.FirstOrDefault(), - streamInfo.TargetAudioBitrate, - streamInfo.TargetAudioSampleRate, - streamInfo.TargetAudioChannels, - streamInfo.TargetAudioBitDepth, - streamInfo.IsDirectStream, - streamInfo.RunTimeTicks ?? 0, - streamInfo.TranscodeSeekInfo); + .BuildAudioHeader( + streamInfo.Container, + streamInfo.TargetAudioCodec.FirstOrDefault(), + streamInfo.TargetAudioBitrate, + streamInfo.TargetAudioSampleRate, + streamInfo.TargetAudioChannels, + streamInfo.TargetAudioBitDepth, + streamInfo.IsDirectStream, + streamInfo.RunTimeTicks ?? 0, + streamInfo.TranscodeSeekInfo); } if (streamInfo.MediaType == DlnaProfileType.Video) { var list = new ContentFeatureBuilder(profile) - .BuildVideoHeader(streamInfo.Container, - streamInfo.TargetVideoCodec.FirstOrDefault(), - streamInfo.TargetAudioCodec.FirstOrDefault(), - streamInfo.TargetWidth, - streamInfo.TargetHeight, - streamInfo.TargetVideoBitDepth, - streamInfo.TargetVideoBitrate, - streamInfo.TargetTimestamp, - streamInfo.IsDirectStream, - streamInfo.RunTimeTicks ?? 0, - streamInfo.TargetVideoProfile, - streamInfo.TargetVideoLevel, - streamInfo.TargetFramerate ?? 0, - streamInfo.TargetPacketLength, - streamInfo.TranscodeSeekInfo, - streamInfo.IsTargetAnamorphic, - streamInfo.IsTargetInterlaced, - streamInfo.TargetRefFrames, - streamInfo.TargetVideoStreamCount, - streamInfo.TargetAudioStreamCount, - streamInfo.TargetVideoCodecTag, - streamInfo.IsTargetAVC); + .BuildVideoHeader( + streamInfo.Container, + streamInfo.TargetVideoCodec.FirstOrDefault(), + streamInfo.TargetAudioCodec.FirstOrDefault(), + streamInfo.TargetWidth, + streamInfo.TargetHeight, + streamInfo.TargetVideoBitDepth, + streamInfo.TargetVideoBitrate, + streamInfo.TargetTimestamp, + streamInfo.IsDirectStream, + streamInfo.RunTimeTicks ?? 0, + streamInfo.TargetVideoProfile, + streamInfo.TargetVideoLevel, + streamInfo.TargetFramerate ?? 0, + streamInfo.TargetPacketLength, + streamInfo.TranscodeSeekInfo, + streamInfo.IsTargetAnamorphic, + streamInfo.IsTargetInterlaced, + streamInfo.TargetRefFrames, + streamInfo.TargetVideoStreamCount, + streamInfo.TargetAudioStreamCount, + streamInfo.TargetVideoCodecTag, + streamInfo.IsTargetAVC); return list.Count == 0 ? null : list[0]; } @@ -633,6 +640,10 @@ namespace Emby.Dlna.PlayTo GC.SuppressFinalize(this); } + /// + /// Releases unmanaged and optionally managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (_disposed) @@ -673,48 +684,41 @@ namespace Emby.Dlna.PlayTo case GeneralCommandType.ToggleMute: return _device.ToggleMute(cancellationToken); case GeneralCommandType.SetAudioStreamIndex: + if (command.Arguments.TryGetValue("Index", out string index)) { - if (command.Arguments.TryGetValue("Index", out string arg)) + if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val)) { - if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val)) - { - return SetAudioStreamIndex(val); - } - - throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied."); + return SetAudioStreamIndex(val); } - throw new ArgumentException("SetAudioStreamIndex argument cannot be null"); + throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied."); } + + throw new ArgumentException("SetAudioStreamIndex argument cannot be null"); case GeneralCommandType.SetSubtitleStreamIndex: + if (command.Arguments.TryGetValue("Index", out index)) { - if (command.Arguments.TryGetValue("Index", out string arg)) + if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val)) { - if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val)) - { - return SetSubtitleStreamIndex(val); - } - - throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied."); + return SetSubtitleStreamIndex(val); } - throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null"); + throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied."); } + + throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null"); case GeneralCommandType.SetVolume: + if (command.Arguments.TryGetValue("Volume", out string vol)) { - if (command.Arguments.TryGetValue("Volume", out string arg)) + if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume)) { - if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var volume)) - { - return _device.SetVolume(volume, cancellationToken); - } - - throw new ArgumentException("Unsupported volume value supplied."); + return _device.SetVolume(volume, cancellationToken); } - throw new ArgumentException("Volume argument cannot be null"); + throw new ArgumentException("Unsupported volume value supplied."); } + throw new ArgumentException("Volume argument cannot be null"); default: return Task.CompletedTask; } @@ -778,7 +782,7 @@ namespace Emby.Dlna.PlayTo const int maxWait = 15000000; const int interval = 500; var currentWait = 0; - while (_device.TransportState != TRANSPORTSTATE.PLAYING && currentWait < maxWait) + while (_device.TransportState != TransportState.Playing && currentWait < maxWait) { await Task.Delay(interval).ConfigureAwait(false); currentWait += interval; @@ -787,8 +791,67 @@ namespace Emby.Dlna.PlayTo await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false); } + private static int? GetIntValue(IReadOnlyDictionary values, string name) + { + var value = values.GetValueOrDefault(name); + + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) + { + return result; + } + + return null; + } + + private static long GetLongValue(IReadOnlyDictionary values, string name) + { + var value = values.GetValueOrDefault(name); + + if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) + { + return result; + } + + return 0; + } + + /// + public Task SendMessage(string name, Guid messageId, T data, CancellationToken cancellationToken) + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + + if (_device == null) + { + return Task.CompletedTask; + } + + if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase)) + { + return SendPlayCommand(data as PlayRequest, cancellationToken); + } + + if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase)) + { + return SendPlaystateCommand(data as PlaystateRequest, cancellationToken); + } + + if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase)) + { + return SendGeneralCommand(data as GeneralCommand, cancellationToken); + } + + // Not supported or needed right now + return Task.CompletedTask; + } + private class StreamParams { + private MediaSourceInfo mediaSource; + private IMediaSourceManager _mediaSourceManager; + public Guid ItemId { get; set; } public bool IsDirectStream { get; set; } @@ -809,15 +872,11 @@ namespace Emby.Dlna.PlayTo public BaseItem Item { get; set; } - private MediaSourceInfo MediaSource; - - private IMediaSourceManager _mediaSourceManager; - public async Task GetMediaSource(CancellationToken cancellationToken) { - if (MediaSource != null) + if (mediaSource != null) { - return MediaSource; + return mediaSource; } var hasMediaSources = Item as IHasMediaSources; @@ -827,9 +886,9 @@ namespace Emby.Dlna.PlayTo return null; } - MediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false); + mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false); - return MediaSource; + return mediaSource; } private static Guid GetItemId(string url) @@ -901,61 +960,5 @@ namespace Emby.Dlna.PlayTo return request; } } - - private static int? GetIntValue(IReadOnlyDictionary values, string name) - { - var value = values.GetValueOrDefault(name); - - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) - { - return result; - } - - return null; - } - - private static long GetLongValue(IReadOnlyDictionary values, string name) - { - var value = values.GetValueOrDefault(name); - - if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) - { - return result; - } - - return 0; - } - - /// - public Task SendMessage(string name, Guid messageId, T data, CancellationToken cancellationToken) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - - if (_device == null) - { - return Task.CompletedTask; - } - - if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase)) - { - return SendPlayCommand(data as PlayRequest, cancellationToken); - } - - if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase)) - { - return SendPlaystateCommand(data as PlaystateRequest, cancellationToken); - } - - if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase)) - { - return SendGeneralCommand(data as GeneralCommand, cancellationToken); - } - - // Not supported or needed right now - return Task.CompletedTask; - } } } diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index 512589e4d..ff801f263 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -16,7 +17,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -92,7 +92,7 @@ namespace Emby.Dlna.PlayTo // It has to report that it's a media renderer if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 && - nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1) + nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1) { // _logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location); return; @@ -174,7 +174,7 @@ namespace Emby.Dlna.PlayTo if (controller == null) { - var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger, cancellationToken).ConfigureAwait(false); + var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _logger, cancellationToken).ConfigureAwait(false); string deviceName = device.Properties.Name; @@ -192,20 +192,20 @@ namespace Emby.Dlna.PlayTo controller = new PlayToController( sessionInfo, - _sessionManager, - _libraryManager, - _logger, - _dlnaManager, - _userManager, - _imageProcessor, - serverAddress, - null, - _deviceDiscovery, - _userDataManager, - _localization, - _mediaSourceManager, - _config, - _mediaEncoder); + _sessionManager, + _libraryManager, + _logger, + _dlnaManager, + _userManager, + _imageProcessor, + serverAddress, + null, + _deviceDiscovery, + _userDataManager, + _localization, + _mediaSourceManager, + _config, + _mediaEncoder); sessionInfo.AddController(controller); @@ -218,17 +218,17 @@ namespace Emby.Dlna.PlayTo { PlayableMediaTypes = profile.GetSupportedMediaTypes(), - SupportedCommands = new string[] + SupportedCommands = new[] { - GeneralCommandType.VolumeDown.ToString(), - GeneralCommandType.VolumeUp.ToString(), - GeneralCommandType.Mute.ToString(), - GeneralCommandType.Unmute.ToString(), - GeneralCommandType.ToggleMute.ToString(), - GeneralCommandType.SetVolume.ToString(), - GeneralCommandType.SetAudioStreamIndex.ToString(), - GeneralCommandType.SetSubtitleStreamIndex.ToString(), - GeneralCommandType.PlayMediaSource.ToString() + GeneralCommandType.VolumeDown.ToString(), + GeneralCommandType.VolumeUp.ToString(), + GeneralCommandType.Mute.ToString(), + GeneralCommandType.Unmute.ToString(), + GeneralCommandType.ToggleMute.ToString(), + GeneralCommandType.SetVolume.ToString(), + GeneralCommandType.SetAudioStreamIndex.ToString(), + GeneralCommandType.SetSubtitleStreamIndex.ToString(), + GeneralCommandType.PlayMediaSource.ToString() }, SupportsMediaControl = true @@ -247,8 +247,9 @@ namespace Emby.Dlna.PlayTo { _disposeCancellationTokenSource.Cancel(); } - catch + catch (Exception ex) { + _logger.LogDebug(ex, "Error while disposing PlayToManager"); } _sessionLock.Dispose(); diff --git a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs index 795618df2..d14617c8a 100644 --- a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs @@ -6,6 +6,6 @@ namespace Emby.Dlna.PlayTo { public class PlaybackProgressEventArgs : EventArgs { - public uBaseObject MediaInfo { get; set; } + public UBaseObject MediaInfo { get; set; } } } diff --git a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs index 27883ca32..3f8d55263 100644 --- a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs @@ -6,6 +6,6 @@ namespace Emby.Dlna.PlayTo { public class PlaybackStartEventArgs : EventArgs { - public uBaseObject MediaInfo { get; set; } + public UBaseObject MediaInfo { get; set; } } } diff --git a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs index fa42b80e8..deeb47918 100644 --- a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs @@ -6,13 +6,6 @@ namespace Emby.Dlna.PlayTo { public class PlaybackStoppedEventArgs : EventArgs { - public uBaseObject MediaInfo { get; set; } - } - - public class MediaChangedEventArgs : EventArgs - { - public uBaseObject OldMediaInfo { get; set; } - - public uBaseObject NewMediaInfo { get; set; } + public UBaseObject MediaInfo { get; set; } } } diff --git a/Emby.Dlna/PlayTo/TRANSPORTSTATE.cs b/Emby.Dlna/PlayTo/TRANSPORTSTATE.cs deleted file mode 100644 index 7daefeca8..000000000 --- a/Emby.Dlna/PlayTo/TRANSPORTSTATE.cs +++ /dev/null @@ -1,13 +0,0 @@ -#pragma warning disable CS1591 - -namespace Emby.Dlna.PlayTo -{ - public enum TRANSPORTSTATE - { - STOPPED, - PLAYING, - TRANSITIONING, - PAUSED_PLAYBACK, - PAUSED - } -} diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs index c0ce3ab6e..fda17a8b4 100644 --- a/Emby.Dlna/PlayTo/TransportCommands.cs +++ b/Emby.Dlna/PlayTo/TransportCommands.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Xml.Linq; using Emby.Dlna.Common; @@ -11,36 +12,30 @@ namespace Emby.Dlna.PlayTo { public class TransportCommands { + private const string CommandBase = "\r\n" + "" + "" + "" + "{2}" + "" + ""; private List _stateVariables = new List(); - public List StateVariables - { - get => _stateVariables; - set => _stateVariables = value; - } - private List _serviceActions = new List(); - public List ServiceActions - { - get => _serviceActions; - set => _serviceActions = value; - } + + public List StateVariables => _stateVariables; + + public List ServiceActions => _serviceActions; public static TransportCommands Create(XDocument document) { var command = new TransportCommands(); - var actionList = document.Descendants(uPnpNamespaces.svc + "actionList"); + var actionList = document.Descendants(UPnpNamespaces.Svc + "actionList"); - foreach (var container in actionList.Descendants(uPnpNamespaces.svc + "action")) + foreach (var container in actionList.Descendants(UPnpNamespaces.Svc + "action")) { command.ServiceActions.Add(ServiceActionFromXml(container)); } - var stateValues = document.Descendants(uPnpNamespaces.ServiceStateTable).FirstOrDefault(); + var stateValues = document.Descendants(UPnpNamespaces.ServiceStateTable).FirstOrDefault(); if (stateValues != null) { - foreach (var container in stateValues.Elements(uPnpNamespaces.svc + "stateVariable")) + foreach (var container in stateValues.Elements(UPnpNamespaces.Svc + "stateVariable")) { command.StateVariables.Add(FromXml(container)); } @@ -51,19 +46,19 @@ namespace Emby.Dlna.PlayTo private static ServiceAction ServiceActionFromXml(XElement container) { - var argumentList = new List(); + var serviceAction = new ServiceAction + { + Name = container.GetValue(UPnpNamespaces.Svc + "name"), + }; - foreach (var arg in container.Descendants(uPnpNamespaces.svc + "argument")) + var argumentList = serviceAction.ArgumentList; + + foreach (var arg in container.Descendants(UPnpNamespaces.Svc + "argument")) { argumentList.Add(ArgumentFromXml(arg)); } - return new ServiceAction - { - Name = container.GetValue(uPnpNamespaces.svc + "name"), - - ArgumentList = argumentList - }; + return serviceAction; } private static Argument ArgumentFromXml(XElement container) @@ -75,29 +70,29 @@ namespace Emby.Dlna.PlayTo return new Argument { - Name = container.GetValue(uPnpNamespaces.svc + "name"), - Direction = container.GetValue(uPnpNamespaces.svc + "direction"), - RelatedStateVariable = container.GetValue(uPnpNamespaces.svc + "relatedStateVariable") + Name = container.GetValue(UPnpNamespaces.Svc + "name"), + Direction = container.GetValue(UPnpNamespaces.Svc + "direction"), + RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") }; } private static StateVariable FromXml(XElement container) { var allowedValues = new List(); - var element = container.Descendants(uPnpNamespaces.svc + "allowedValueList") + var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList") .FirstOrDefault(); if (element != null) { - var values = element.Descendants(uPnpNamespaces.svc + "allowedValue"); + var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue"); allowedValues.AddRange(values.Select(child => child.Value)); } return new StateVariable { - Name = container.GetValue(uPnpNamespaces.svc + "name"), - DataType = container.GetValue(uPnpNamespaces.svc + "dataType"), + Name = container.GetValue(UPnpNamespaces.Svc + "name"), + DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"), AllowedValues = allowedValues.ToArray() }; } @@ -123,7 +118,7 @@ namespace Emby.Dlna.PlayTo } } - return string.Format(CommandBase, action.Name, xmlNamespace, stateString); + return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString); } public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "") @@ -147,7 +142,7 @@ namespace Emby.Dlna.PlayTo } } - return string.Format(CommandBase, action.Name, xmlNamesapce, stateString); + return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString); } public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary dictionary) @@ -170,7 +165,7 @@ namespace Emby.Dlna.PlayTo } } - return string.Format(CommandBase, action.Name, xmlNamesapce, stateString); + return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString); } private string BuildArgumentXml(Argument argument, string value, string commandParameter = "") @@ -180,15 +175,12 @@ namespace Emby.Dlna.PlayTo if (state != null) { var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ?? - state.AllowedValues.FirstOrDefault() ?? - value; + (state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value); - return string.Format("<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}", argument.Name, state.DataType ?? "string", sendValue); + return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}", argument.Name, state.DataType ?? "string", sendValue); } - return string.Format("<{0}>{1}", argument.Name, value); + return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}", argument.Name, value); } - - private const string CommandBase = "\r\n" + "" + "" + "" + "{2}" + "" + ""; } } diff --git a/Emby.Dlna/PlayTo/TransportState.cs b/Emby.Dlna/PlayTo/TransportState.cs new file mode 100644 index 000000000..7068a5d24 --- /dev/null +++ b/Emby.Dlna/PlayTo/TransportState.cs @@ -0,0 +1,14 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1602 + +namespace Emby.Dlna.PlayTo +{ + public enum TransportState + { + Stopped, + Playing, + Transitioning, + PausedPlayback, + Paused + } +} diff --git a/Emby.Dlna/PlayTo/UpnpContainer.cs b/Emby.Dlna/PlayTo/UpnpContainer.cs index e2d7a10f0..05f27603f 100644 --- a/Emby.Dlna/PlayTo/UpnpContainer.cs +++ b/Emby.Dlna/PlayTo/UpnpContainer.cs @@ -6,22 +6,22 @@ using Emby.Dlna.Ssdp; namespace Emby.Dlna.PlayTo { - public class UpnpContainer : uBaseObject + public class UpnpContainer : UBaseObject { - public static uBaseObject Create(XElement container) + public static UBaseObject Create(XElement container) { if (container == null) { throw new ArgumentNullException(nameof(container)); } - return new uBaseObject + return new UBaseObject { - Id = container.GetAttributeValue(uPnpNamespaces.Id), - ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId), - Title = container.GetValue(uPnpNamespaces.title), - IconUrl = container.GetValue(uPnpNamespaces.Artwork), - UpnpClass = container.GetValue(uPnpNamespaces.uClass) + Id = container.GetAttributeValue(UPnpNamespaces.Id), + ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId), + Title = container.GetValue(UPnpNamespaces.Title), + IconUrl = container.GetValue(UPnpNamespaces.Artwork), + UpnpClass = container.GetValue(UPnpNamespaces.Class) }; } } diff --git a/Emby.Dlna/PlayTo/uBaseObject.cs b/Emby.Dlna/PlayTo/uBaseObject.cs index 05c19299f..0d9478e42 100644 --- a/Emby.Dlna/PlayTo/uBaseObject.cs +++ b/Emby.Dlna/PlayTo/uBaseObject.cs @@ -1,10 +1,11 @@ #pragma warning disable CS1591 using System; +using System.Collections.Generic; namespace Emby.Dlna.PlayTo { - public class uBaseObject + public class UBaseObject { public string Id { get; set; } @@ -20,20 +21,10 @@ namespace Emby.Dlna.PlayTo public string Url { get; set; } - public string[] ProtocolInfo { get; set; } + public IReadOnlyList ProtocolInfo { get; set; } public string UpnpClass { get; set; } - public bool Equals(uBaseObject obj) - { - if (obj == null) - { - throw new ArgumentNullException(nameof(obj)); - } - - return string.Equals(Id, obj.Id); - } - public string MediaType { get @@ -58,5 +49,15 @@ namespace Emby.Dlna.PlayTo return null; } } + + public bool Equals(UBaseObject obj) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + + return string.Equals(Id, obj.Id, StringComparison.Ordinal); + } } } diff --git a/Emby.Dlna/PlayTo/uPnpNamespaces.cs b/Emby.Dlna/PlayTo/uPnpNamespaces.cs index dc65cdf43..5042d4493 100644 --- a/Emby.Dlna/PlayTo/uPnpNamespaces.cs +++ b/Emby.Dlna/PlayTo/uPnpNamespaces.cs @@ -4,38 +4,64 @@ using System.Xml.Linq; namespace Emby.Dlna.PlayTo { - public class uPnpNamespaces + public static class UPnpNamespaces { - public static XNamespace dc = "http://purl.org/dc/elements/1.1/"; - public static XNamespace ns = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; - public static XNamespace svc = "urn:schemas-upnp-org:service-1-0"; - public static XNamespace ud = "urn:schemas-upnp-org:device-1-0"; - public static XNamespace upnp = "urn:schemas-upnp-org:metadata-1-0/upnp/"; - public static XNamespace RenderingControl = "urn:schemas-upnp-org:service:RenderingControl:1"; - public static XNamespace AvTransport = "urn:schemas-upnp-org:service:AVTransport:1"; - public static XNamespace ContentDirectory = "urn:schemas-upnp-org:service:ContentDirectory:1"; + public static XNamespace Dc { get; } = "http://purl.org/dc/elements/1.1/"; - public static XName containers = ns + "container"; - public static XName items = ns + "item"; - public static XName title = dc + "title"; - public static XName creator = dc + "creator"; - public static XName artist = upnp + "artist"; - public static XName Id = "id"; - public static XName ParentId = "parentID"; - public static XName uClass = upnp + "class"; - public static XName Artwork = upnp + "albumArtURI"; - public static XName Description = dc + "description"; - public static XName LongDescription = upnp + "longDescription"; - public static XName Album = upnp + "album"; - public static XName Author = upnp + "author"; - public static XName Director = upnp + "director"; - public static XName PlayCount = upnp + "playbackCount"; - public static XName Tracknumber = upnp + "originalTrackNumber"; - public static XName Res = ns + "res"; - public static XName Duration = "duration"; - public static XName ProtocolInfo = "protocolInfo"; + public static XNamespace Ns { get; } = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; - public static XName ServiceStateTable = svc + "serviceStateTable"; - public static XName StateVariable = svc + "stateVariable"; + public static XNamespace Svc { get; } = "urn:schemas-upnp-org:service-1-0"; + + public static XNamespace Ud { get; } = "urn:schemas-upnp-org:device-1-0"; + + public static XNamespace UPnp { get; } = "urn:schemas-upnp-org:metadata-1-0/upnp/"; + + public static XNamespace RenderingControl { get; } = "urn:schemas-upnp-org:service:RenderingControl:1"; + + public static XNamespace AvTransport { get; } = "urn:schemas-upnp-org:service:AVTransport:1"; + + public static XNamespace ContentDirectory { get; } = "urn:schemas-upnp-org:service:ContentDirectory:1"; + + public static XName Containers { get; } = Ns + "container"; + + public static XName Items { get; } = Ns + "item"; + + public static XName Title { get; } = Dc + "title"; + + public static XName Creator { get; } = Dc + "creator"; + + public static XName Artist { get; } = UPnp + "artist"; + + public static XName Id { get; } = "id"; + + public static XName ParentId { get; } = "parentID"; + + public static XName Class { get; } = UPnp + "class"; + + public static XName Artwork { get; } = UPnp + "albumArtURI"; + + public static XName Description { get; } = Dc + "description"; + + public static XName LongDescription { get; } = UPnp + "longDescription"; + + public static XName Album { get; } = UPnp + "album"; + + public static XName Author { get; } = UPnp + "author"; + + public static XName Director { get; } = UPnp + "director"; + + public static XName PlayCount { get; } = UPnp + "playbackCount"; + + public static XName Tracknumber { get; } = UPnp + "originalTrackNumber"; + + public static XName Res { get; } = Ns + "res"; + + public static XName Duration { get; } = "duration"; + + public static XName ProtocolInfo { get; } = "protocolInfo"; + + public static XName ServiceStateTable { get; } = Svc + "serviceStateTable"; + + public static XName StateVariable { get; } = Svc + "stateVariable"; } } diff --git a/Emby.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs index 90a23a4a2..d4af72b62 100644 --- a/Emby.Dlna/Profiles/DefaultProfile.cs +++ b/Emby.Dlna/Profiles/DefaultProfile.cs @@ -64,14 +64,14 @@ namespace Emby.Dlna.Profiles new DirectPlayProfile { // play all - Container = "", + Container = string.Empty, Type = DlnaProfileType.Video }, new DirectPlayProfile { // play all - Container = "", + Container = string.Empty, Type = DlnaProfileType.Audio } }; diff --git a/Emby.Dlna/Profiles/DishHopperJoeyProfile.cs b/Emby.Dlna/Profiles/DishHopperJoeyProfile.cs index 942e36930..2a7524a6a 100644 --- a/Emby.Dlna/Profiles/DishHopperJoeyProfile.cs +++ b/Emby.Dlna/Profiles/DishHopperJoeyProfile.cs @@ -24,7 +24,7 @@ namespace Emby.Dlna.Profiles { Match = HeaderMatchType.Substring, Name = "User-Agent", - Value ="Zip_" + Value = "Zip_" } } }; @@ -81,7 +81,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -124,7 +124,7 @@ namespace Emby.Dlna.Profiles new CodecProfile { Type = CodecType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -161,7 +161,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "ac3,he-aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -177,7 +177,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -192,7 +192,7 @@ namespace Emby.Dlna.Profiles new CodecProfile { Type = CodecType.VideoAudio, - Conditions = new [] + Conditions = new[] { // The device does not have any audio switching capabilities new ProfileCondition diff --git a/Emby.Dlna/Profiles/LgTvProfile.cs b/Emby.Dlna/Profiles/LgTvProfile.cs index 02301764c..fbb368d3e 100644 --- a/Emby.Dlna/Profiles/LgTvProfile.cs +++ b/Emby.Dlna/Profiles/LgTvProfile.cs @@ -84,7 +84,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -191,7 +191,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] + ResponseProfiles = new[] { new ResponseProfile { diff --git a/Emby.Dlna/Profiles/LinksysDMA2100Profile.cs b/Emby.Dlna/Profiles/LinksysDMA2100Profile.cs index 1b1423520..1a510dfec 100644 --- a/Emby.Dlna/Profiles/LinksysDMA2100Profile.cs +++ b/Emby.Dlna/Profiles/LinksysDMA2100Profile.cs @@ -32,7 +32,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] + ResponseProfiles = new[] { new ResponseProfile { diff --git a/Emby.Dlna/Profiles/PanasonicVieraProfile.cs b/Emby.Dlna/Profiles/PanasonicVieraProfile.cs index 44c35e142..0d536acf3 100644 --- a/Emby.Dlna/Profiles/PanasonicVieraProfile.cs +++ b/Emby.Dlna/Profiles/PanasonicVieraProfile.cs @@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/PopcornHourProfile.cs b/Emby.Dlna/Profiles/PopcornHourProfile.cs index 9e9f6966f..7fbf8c164 100644 --- a/Emby.Dlna/Profiles/PopcornHourProfile.cs +++ b/Emby.Dlna/Profiles/PopcornHourProfile.cs @@ -93,8 +93,8 @@ namespace Emby.Dlna.Profiles new CodecProfile { Type = CodecType.Video, - Codec="h264", - Conditions = new [] + Codec = "h264", + Conditions = new[] { new ProfileCondition(ProfileConditionType.EqualsAny, ProfileConditionValue.VideoProfile, "baseline|constrained baseline"), new ProfileCondition @@ -122,7 +122,7 @@ namespace Emby.Dlna.Profiles new CodecProfile { Type = CodecType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -150,7 +150,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -166,7 +166,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Audio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -182,7 +182,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Audio, Codec = "mp3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -202,7 +202,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] + ResponseProfiles = new[] { new ResponseProfile { diff --git a/Emby.Dlna/Profiles/SamsungSmartTvProfile.cs b/Emby.Dlna/Profiles/SamsungSmartTvProfile.cs index 4ff2ab9be..ddbebba2a 100644 --- a/Emby.Dlna/Profiles/SamsungSmartTvProfile.cs +++ b/Emby.Dlna/Profiles/SamsungSmartTvProfile.cs @@ -139,7 +139,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs index 238fe9f6b..765c12504 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs @@ -150,7 +150,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -178,7 +178,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -197,7 +197,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs index 812a48151..390c8a3e4 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs @@ -150,7 +150,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -178,7 +178,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -197,7 +197,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs index 6bfff322e..25adc4d02 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs @@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -166,7 +166,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -185,7 +185,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs index ec2529574..0a39a5f40 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs @@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -166,7 +166,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -185,7 +185,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs b/Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs index ecdd2e7a4..05c8ab1c1 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs @@ -114,7 +114,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -156,7 +156,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -172,7 +172,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -191,7 +191,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -217,7 +217,7 @@ namespace Emby.Dlna.Profiles VideoCodec = "h264,mpeg4,vc1", AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", + OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", Type = DlnaProfileType.Video }, diff --git a/Emby.Dlna/Profiles/SonyBravia2010Profile.cs b/Emby.Dlna/Profiles/SonyBravia2010Profile.cs index 68365ba4a..8ab4acd1b 100644 --- a/Emby.Dlna/Profiles/SonyBravia2010Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2010Profile.cs @@ -102,13 +102,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", + OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -128,13 +128,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/mpeg", - OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", + OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -148,28 +148,28 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", + OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "ts,mpegts", - VideoCodec="mpeg2video", + VideoCodec = "mpeg2video", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", + OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "mpeg", - VideoCodec="mpeg1video,mpeg2video", + VideoCodec = "mpeg1video,mpeg2video", MimeType = "video/mpeg", - OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL", + OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL", Type = DlnaProfileType.Video } }; @@ -180,7 +180,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -204,7 +204,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -243,7 +243,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "mpeg2video", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -275,7 +275,7 @@ namespace Emby.Dlna.Profiles new CodecProfile { Type = CodecType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -303,7 +303,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -319,7 +319,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -341,7 +341,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "mp3,mp2", - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/SonyBravia2011Profile.cs b/Emby.Dlna/Profiles/SonyBravia2011Profile.cs index b34af04a5..42d253394 100644 --- a/Emby.Dlna/Profiles/SonyBravia2011Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2011Profile.cs @@ -120,7 +120,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -143,13 +143,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", + OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -169,13 +169,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/mpeg", - OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", + OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -189,28 +189,28 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", + OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "ts,mpegts", - VideoCodec="mpeg2video", + VideoCodec = "mpeg2video", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", + OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "mpeg", - VideoCodec="mpeg1video,mpeg2video", + VideoCodec = "mpeg1video,mpeg2video", MimeType = "video/mpeg", - OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL", + OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL", Type = DlnaProfileType.Video }, new ResponseProfile @@ -227,7 +227,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -266,7 +266,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "mpeg2video", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -298,7 +298,7 @@ namespace Emby.Dlna.Profiles new CodecProfile { Type = CodecType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -326,7 +326,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -364,7 +364,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "mp3,mp2", - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/SonyBravia2012Profile.cs b/Emby.Dlna/Profiles/SonyBravia2012Profile.cs index 0e75d0cb5..0598e8342 100644 --- a/Emby.Dlna/Profiles/SonyBravia2012Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2012Profile.cs @@ -131,13 +131,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", + OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -157,13 +157,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/mpeg", - OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", + OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -177,28 +177,28 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", + OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "ts,mpegts", - VideoCodec="mpeg2video", + VideoCodec = "mpeg2video", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", + OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "mpeg", - VideoCodec="mpeg1video,mpeg2video", + VideoCodec = "mpeg1video,mpeg2video", MimeType = "video/mpeg", - OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL", + OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL", Type = DlnaProfileType.Video }, new ResponseProfile @@ -215,7 +215,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -282,7 +282,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "mp3,mp2", - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/SonyBravia2013Profile.cs b/Emby.Dlna/Profiles/SonyBravia2013Profile.cs index 3300863c9..3d90a1e72 100644 --- a/Emby.Dlna/Profiles/SonyBravia2013Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2013Profile.cs @@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -187,13 +187,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", + OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -213,13 +213,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/mpeg", - OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", + OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -233,28 +233,28 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", + OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "ts,mpegts", - VideoCodec="mpeg2video", + VideoCodec = "mpeg2video", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", + OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "mpeg", - VideoCodec="mpeg1video,mpeg2video", + VideoCodec = "mpeg1video,mpeg2video", MimeType = "video/mpeg", - OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL", + OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL", Type = DlnaProfileType.Video }, new ResponseProfile @@ -265,14 +265,13 @@ namespace Emby.Dlna.Profiles } }; - CodecProfiles = new[] { new CodecProfile { Type = CodecType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -300,7 +299,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "mp3,mp2", - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/SonyBravia2014Profile.cs b/Emby.Dlna/Profiles/SonyBravia2014Profile.cs index 4e833441c..9188f73ef 100644 --- a/Emby.Dlna/Profiles/SonyBravia2014Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2014Profile.cs @@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -187,13 +187,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", + OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -213,13 +213,13 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/mpeg", - OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", + OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO", Type = DlnaProfileType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -233,28 +233,28 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "ts,mpegts", - VideoCodec="h264", - AudioCodec="ac3,aac,mp3", + VideoCodec = "h264", + AudioCodec = "ac3,aac,mp3", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", + OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "ts,mpegts", - VideoCodec="mpeg2video", + VideoCodec = "mpeg2video", MimeType = "video/vnd.dlna.mpeg-tts", - OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", + OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO", Type = DlnaProfileType.Video }, new ResponseProfile { Container = "mpeg", - VideoCodec="mpeg1video,mpeg2video", + VideoCodec = "mpeg1video,mpeg2video", MimeType = "video/mpeg", - OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL", + OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL", Type = DlnaProfileType.Video }, new ResponseProfile @@ -265,14 +265,13 @@ namespace Emby.Dlna.Profiles } }; - CodecProfiles = new[] { new CodecProfile { Type = CodecType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -300,7 +299,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "mp3,mp2", - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/SonyPs3Profile.cs b/Emby.Dlna/Profiles/SonyPs3Profile.cs index 7f72356bd..d56b1df50 100644 --- a/Emby.Dlna/Profiles/SonyPs3Profile.cs +++ b/Emby.Dlna/Profiles/SonyPs3Profile.cs @@ -108,7 +108,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -133,7 +133,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -176,7 +176,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -201,7 +201,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "wmapro", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -217,7 +217,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -235,7 +235,7 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "mp4,mov", - AudioCodec="aac", + AudioCodec = "aac", MimeType = "video/mp4", Type = DlnaProfileType.Video }, @@ -244,7 +244,7 @@ namespace Emby.Dlna.Profiles { Container = "avi", MimeType = "video/divx", - OrgPn="AVI", + OrgPn = "AVI", Type = DlnaProfileType.Video }, diff --git a/Emby.Dlna/Profiles/SonyPs4Profile.cs b/Emby.Dlna/Profiles/SonyPs4Profile.cs index 411bfe2b0..db56094e2 100644 --- a/Emby.Dlna/Profiles/SonyPs4Profile.cs +++ b/Emby.Dlna/Profiles/SonyPs4Profile.cs @@ -110,7 +110,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -135,7 +135,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -178,7 +178,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "ac3", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -203,7 +203,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "wmapro", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -219,7 +219,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -237,7 +237,7 @@ namespace Emby.Dlna.Profiles new ResponseProfile { Container = "mp4,mov", - AudioCodec="aac", + AudioCodec = "aac", MimeType = "video/mp4", Type = DlnaProfileType.Video }, @@ -246,7 +246,7 @@ namespace Emby.Dlna.Profiles { Container = "avi", MimeType = "video/divx", - OrgPn="AVI", + OrgPn = "AVI", Type = DlnaProfileType.Video }, diff --git a/Emby.Dlna/Profiles/WdtvLiveProfile.cs b/Emby.Dlna/Profiles/WdtvLiveProfile.cs index 2de9a8cd9..937ca0f42 100644 --- a/Emby.Dlna/Profiles/WdtvLiveProfile.cs +++ b/Emby.Dlna/Profiles/WdtvLiveProfile.cs @@ -20,7 +20,7 @@ namespace Emby.Dlna.Profiles Headers = new[] { - new HttpHeaderInfo {Name = "User-Agent", Value = "alphanetworks", Match = HeaderMatchType.Substring}, + new HttpHeaderInfo { Name = "User-Agent", Value = "alphanetworks", Match = HeaderMatchType.Substring }, new HttpHeaderInfo { Name = "User-Agent", @@ -168,7 +168,7 @@ namespace Emby.Dlna.Profiles { Type = DlnaProfileType.Photo, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -193,7 +193,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -221,7 +221,7 @@ namespace Emby.Dlna.Profiles Type = CodecType.VideoAudio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Profiles/XboxOneProfile.cs b/Emby.Dlna/Profiles/XboxOneProfile.cs index 2cbe4e6ac..84d8184a2 100644 --- a/Emby.Dlna/Profiles/XboxOneProfile.cs +++ b/Emby.Dlna/Profiles/XboxOneProfile.cs @@ -119,7 +119,7 @@ namespace Emby.Dlna.Profiles Type = DlnaProfileType.Video, Container = "mp4,mov", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "mpeg4", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -187,7 +187,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "h264", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -236,7 +236,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.Video, Codec = "wmv2,wmv3,vc1", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -284,7 +284,7 @@ namespace Emby.Dlna.Profiles new CodecProfile { Type = CodecType.Video, - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -307,7 +307,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "ac3,wmav2,wmapro", - Conditions = new [] + Conditions = new[] { new ProfileCondition { @@ -323,7 +323,7 @@ namespace Emby.Dlna.Profiles { Type = CodecType.VideoAudio, Codec = "aac", - Conditions = new [] + Conditions = new[] { new ProfileCondition { diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs index 699d325ea..d160e3339 100644 --- a/Emby.Dlna/Service/BaseControlHandler.cs +++ b/Emby.Dlna/Service/BaseControlHandler.cs @@ -15,11 +15,7 @@ namespace Emby.Dlna.Service { public abstract class BaseControlHandler { - private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/"; - - protected IServerConfigurationManager Config { get; } - - protected ILogger Logger { get; } + private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/"; protected BaseControlHandler(IServerConfigurationManager config, ILogger logger) { @@ -27,6 +23,10 @@ namespace Emby.Dlna.Service Logger = logger; } + protected IServerConfigurationManager Config { get; } + + protected ILogger Logger { get; } + public async Task ProcessControlRequestAsync(ControlRequest request) { try @@ -80,10 +80,10 @@ namespace Emby.Dlna.Service { writer.WriteStartDocument(true); - writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV); - writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/"); + writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv); + writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/"); - writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV); + writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv); writer.WriteStartElement("u", requestInfo.LocalName + "Response", requestInfo.NamespaceURI); WriteResult(requestInfo.LocalName, requestInfo.Headers, writer); @@ -210,15 +210,6 @@ namespace Emby.Dlna.Service } } - private class ControlRequestInfo - { - public string LocalName { get; set; } - - public string NamespaceURI { get; set; } - - public Dictionary Headers { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - protected abstract void WriteResult(string methodName, IDictionary methodParams, XmlWriter xmlWriter); private void LogRequest(ControlRequest request) @@ -240,5 +231,14 @@ namespace Emby.Dlna.Service Logger.LogDebug("Control response. Headers: {@Headers}\n{Xml}", response.Headers, response.Xml); } + + private class ControlRequestInfo + { + public string LocalName { get; set; } + + public string NamespaceURI { get; set; } + + public Dictionary Headers { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + } } } diff --git a/Emby.Dlna/Service/BaseService.cs b/Emby.Dlna/Service/BaseService.cs index 8794ec26a..40d069e7c 100644 --- a/Emby.Dlna/Service/BaseService.cs +++ b/Emby.Dlna/Service/BaseService.cs @@ -6,20 +6,22 @@ using Microsoft.Extensions.Logging; namespace Emby.Dlna.Service { - public class BaseService : IEventManager + public class BaseService : IDlnaEventManager { - protected IEventManager EventManager; - protected IHttpClient HttpClient; - protected ILogger Logger; - protected BaseService(ILogger logger, IHttpClient httpClient) { Logger = logger; HttpClient = httpClient; - EventManager = new EventManager(logger, HttpClient); + EventManager = new DlnaEventManager(logger, HttpClient); } + protected IDlnaEventManager EventManager { get; } + + protected IHttpClient HttpClient { get; } + + protected ILogger Logger { get; } + public EventSubscriptionResponse CancelEventSubscription(string subscriptionId) { return EventManager.CancelEventSubscription(subscriptionId); diff --git a/Emby.Dlna/Service/ControlErrorHandler.cs b/Emby.Dlna/Service/ControlErrorHandler.cs index 047e9f014..f2b5dd9ca 100644 --- a/Emby.Dlna/Service/ControlErrorHandler.cs +++ b/Emby.Dlna/Service/ControlErrorHandler.cs @@ -10,7 +10,7 @@ namespace Emby.Dlna.Service { public static class ControlErrorHandler { - private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/"; + private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/"; public static ControlResponse GetResponse(Exception ex) { @@ -26,11 +26,11 @@ namespace Emby.Dlna.Service { writer.WriteStartDocument(true); - writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV); - writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/"); + writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv); + writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/"); - writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV); - writer.WriteStartElement("SOAP-ENV", "Fault", NS_SOAPENV); + writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv); + writer.WriteStartElement("SOAP-ENV", "Fault", NsSoapEnv); writer.WriteElementString("faultcode", "500"); writer.WriteElementString("faultstring", ex.Message); diff --git a/Emby.Dlna/Service/ServiceXmlBuilder.cs b/Emby.Dlna/Service/ServiceXmlBuilder.cs index 6c7d6f846..1e56d09b2 100644 --- a/Emby.Dlna/Service/ServiceXmlBuilder.cs +++ b/Emby.Dlna/Service/ServiceXmlBuilder.cs @@ -87,7 +87,7 @@ namespace Emby.Dlna.Service .Append(SecurityElement.Escape(item.DataType ?? string.Empty)) .Append(""); - if (item.AllowedValues.Length > 0) + if (item.AllowedValues.Count > 0) { builder.Append(""); foreach (var allowedValue in item.AllowedValues) diff --git a/Emby.Dlna/Ssdp/DeviceDiscovery.cs b/Emby.Dlna/Ssdp/DeviceDiscovery.cs index 7daac96d1..8c7d961f3 100644 --- a/Emby.Dlna/Ssdp/DeviceDiscovery.cs +++ b/Emby.Dlna/Ssdp/DeviceDiscovery.cs @@ -3,9 +3,9 @@ using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Events; using Rssdp; using Rssdp.Infrastructure; @@ -17,9 +17,17 @@ namespace Emby.Dlna.Ssdp private readonly IServerConfigurationManager _config; + private SsdpDeviceLocator _deviceLocator; + private ISsdpCommunicationsServer _commsServer; + private int _listenerCount; private bool _disposed; + public DeviceDiscovery(IServerConfigurationManager config) + { + _config = config; + } + private event EventHandler> DeviceDiscoveredInternal; /// @@ -49,15 +57,6 @@ namespace Emby.Dlna.Ssdp /// public event EventHandler> DeviceLeft; - private SsdpDeviceLocator _deviceLocator; - - private ISsdpCommunicationsServer _commsServer; - - public DeviceDiscovery(IServerConfigurationManager config) - { - _config = config; - } - // Call this method from somewhere in your code to start the search. public void Start(ISsdpCommunicationsServer communicationsServer) { diff --git a/Emby.Dlna/Ssdp/Extensions.cs b/Emby.Dlna/Ssdp/SsdpExtensions.cs similarity index 94% rename from Emby.Dlna/Ssdp/Extensions.cs rename to Emby.Dlna/Ssdp/SsdpExtensions.cs index 613d332b2..e7a52f168 100644 --- a/Emby.Dlna/Ssdp/Extensions.cs +++ b/Emby.Dlna/Ssdp/SsdpExtensions.cs @@ -5,7 +5,7 @@ using System.Xml.Linq; namespace Emby.Dlna.Ssdp { - public static class Extensions + public static class SsdpExtensions { public static string GetValue(this XElement container, XName name) { diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index c017e76c7..6857f9952 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -10,6 +10,15 @@ false true true + true + true + true + snupkg + + + + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb @@ -23,10 +32,15 @@ Jellyfin Contributors Jellyfin.Naming - https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt + 10.7.0 https://github.com/jellyfin/jellyfin + GPL-3.0-only + + + + diff --git a/Emby.Notifications/NotificationEntryPoint.cs b/Emby.Notifications/NotificationEntryPoint.cs index b923fd26c..ded22d26c 100644 --- a/Emby.Notifications/NotificationEntryPoint.cs +++ b/Emby.Notifications/NotificationEntryPoint.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; @@ -13,7 +14,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Notifications; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Activity; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Notifications; using Microsoft.Extensions.Logging; diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs deleted file mode 100644 index 84bec9201..000000000 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ /dev/null @@ -1,590 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Jellyfin.Data.Entities; -using MediaBrowser.Common.Plugins; -using MediaBrowser.Common.Updates; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Session; -using MediaBrowser.Controller.Subtitles; -using MediaBrowser.Model.Activity; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Notifications; -using MediaBrowser.Model.Tasks; -using MediaBrowser.Model.Updates; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Activity -{ - /// - /// Entry point for the activity logger. - /// - public sealed class ActivityLogEntryPoint : IServerEntryPoint - { - private readonly ILogger _logger; - private readonly IInstallationManager _installationManager; - private readonly ISessionManager _sessionManager; - private readonly ITaskManager _taskManager; - private readonly IActivityManager _activityManager; - private readonly ILocalizationManager _localization; - private readonly ISubtitleManager _subManager; - private readonly IUserManager _userManager; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The session manager. - /// The task manager. - /// The activity manager. - /// The localization manager. - /// The installation manager. - /// The subtitle manager. - /// The user manager. - public ActivityLogEntryPoint( - ILogger logger, - ISessionManager sessionManager, - ITaskManager taskManager, - IActivityManager activityManager, - ILocalizationManager localization, - IInstallationManager installationManager, - ISubtitleManager subManager, - IUserManager userManager) - { - _logger = logger; - _sessionManager = sessionManager; - _taskManager = taskManager; - _activityManager = activityManager; - _localization = localization; - _installationManager = installationManager; - _subManager = subManager; - _userManager = userManager; - } - - /// - public Task RunAsync() - { - _taskManager.TaskCompleted += OnTaskCompleted; - - _installationManager.PluginInstalled += OnPluginInstalled; - _installationManager.PluginUninstalled += OnPluginUninstalled; - _installationManager.PluginUpdated += OnPluginUpdated; - _installationManager.PackageInstallationFailed += OnPackageInstallationFailed; - - _sessionManager.SessionStarted += OnSessionStarted; - _sessionManager.AuthenticationFailed += OnAuthenticationFailed; - _sessionManager.AuthenticationSucceeded += OnAuthenticationSucceeded; - _sessionManager.SessionEnded += OnSessionEnded; - _sessionManager.PlaybackStart += OnPlaybackStart; - _sessionManager.PlaybackStopped += OnPlaybackStopped; - - _subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure; - - _userManager.OnUserCreated += OnUserCreated; - _userManager.OnUserPasswordChanged += OnUserPasswordChanged; - _userManager.OnUserDeleted += OnUserDeleted; - _userManager.OnUserLockedOut += OnUserLockedOut; - - return Task.CompletedTask; - } - - private async void OnUserLockedOut(object sender, GenericEventArgs e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserLockedOutWithName"), - e.Argument.Username), - NotificationType.UserLockedOut.ToString(), - e.Argument.Id) - { - LogSeverity = LogLevel.Error - }).ConfigureAwait(false); - } - - private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"), - e.Provider, - Notifications.NotificationEntryPoint.GetItemName(e.Item)), - "SubtitleDownloadFailure", - Guid.Empty) - { - ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture), - ShortOverview = e.Exception.Message - }).ConfigureAwait(false); - } - - private async void OnPlaybackStopped(object sender, PlaybackStopEventArgs e) - { - var item = e.MediaInfo; - - if (item == null) - { - _logger.LogWarning("PlaybackStopped reported with null media info."); - return; - } - - if (e.Item != null && e.Item.IsThemeMedia) - { - // Don't report theme song or local trailer playback - return; - } - - if (e.Users.Count == 0) - { - return; - } - - var user = e.Users[0]; - - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), - user.Username, - GetItemName(item), - e.DeviceName), - GetPlaybackStoppedNotificationType(item.MediaType), - user.Id)) - .ConfigureAwait(false); - } - - private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e) - { - var item = e.MediaInfo; - - if (item == null) - { - _logger.LogWarning("PlaybackStart reported with null media info."); - return; - } - - if (e.Item != null && e.Item.IsThemeMedia) - { - // Don't report theme song or local trailer playback - return; - } - - if (e.Users.Count == 0) - { - return; - } - - var user = e.Users.First(); - - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserStartedPlayingItemWithValues"), - user.Username, - GetItemName(item), - e.DeviceName), - GetPlaybackNotificationType(item.MediaType), - user.Id)) - .ConfigureAwait(false); - } - - private static string GetItemName(BaseItemDto item) - { - var name = item.Name; - - if (!string.IsNullOrEmpty(item.SeriesName)) - { - name = item.SeriesName + " - " + name; - } - - if (item.Artists != null && item.Artists.Count > 0) - { - name = item.Artists[0] + " - " + name; - } - - return name; - } - - private static string GetPlaybackNotificationType(string mediaType) - { - if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) - { - return NotificationType.AudioPlayback.ToString(); - } - - if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) - { - return NotificationType.VideoPlayback.ToString(); - } - - return null; - } - - private static string GetPlaybackStoppedNotificationType(string mediaType) - { - if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) - { - return NotificationType.AudioPlaybackStopped.ToString(); - } - - if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) - { - return NotificationType.VideoPlaybackStopped.ToString(); - } - - return null; - } - - private async void OnSessionEnded(object sender, SessionEventArgs e) - { - var session = e.SessionInfo; - - if (string.IsNullOrEmpty(session.UserName)) - { - return; - } - - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserOfflineFromDevice"), - session.UserName, - session.DeviceName), - "SessionEnded", - session.UserId) - { - ShortOverview = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("LabelIpAddressValue"), - session.RemoteEndPoint), - }).ConfigureAwait(false); - } - - private async void OnAuthenticationSucceeded(object sender, GenericEventArgs e) - { - var user = e.Argument.User; - - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("AuthenticationSucceededWithUserName"), - user.Name), - "AuthenticationSucceeded", - user.Id) - { - ShortOverview = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("LabelIpAddressValue"), - e.Argument.SessionInfo.RemoteEndPoint), - }).ConfigureAwait(false); - } - - private async void OnAuthenticationFailed(object sender, GenericEventArgs e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("FailedLoginAttemptWithUserName"), - e.Argument.Username), - "AuthenticationFailed", - Guid.Empty) - { - LogSeverity = LogLevel.Error, - ShortOverview = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("LabelIpAddressValue"), - e.Argument.RemoteEndPoint), - }).ConfigureAwait(false); - } - - private async void OnUserDeleted(object sender, GenericEventArgs e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserDeletedWithName"), - e.Argument.Username), - "UserDeleted", - Guid.Empty)) - .ConfigureAwait(false); - } - - private async void OnUserPasswordChanged(object sender, GenericEventArgs e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserPasswordChangedWithName"), - e.Argument.Username), - "UserPasswordChanged", - e.Argument.Id)) - .ConfigureAwait(false); - } - - private async void OnUserCreated(object sender, GenericEventArgs e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserCreatedWithName"), - e.Argument.Username), - "UserCreated", - e.Argument.Id)) - .ConfigureAwait(false); - } - - private async void OnSessionStarted(object sender, SessionEventArgs e) - { - var session = e.SessionInfo; - - if (string.IsNullOrEmpty(session.UserName)) - { - return; - } - - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserOnlineFromDevice"), - session.UserName, - session.DeviceName), - "SessionStarted", - session.UserId) - { - ShortOverview = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("LabelIpAddressValue"), - session.RemoteEndPoint) - }).ConfigureAwait(false); - } - - private async void OnPluginUpdated(object sender, InstallationInfo e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("PluginUpdatedWithName"), - e.Name), - NotificationType.PluginUpdateInstalled.ToString(), - Guid.Empty) - { - ShortOverview = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("VersionNumber"), - e.Version), - Overview = e.Changelog - }).ConfigureAwait(false); - } - - private async void OnPluginUninstalled(object sender, IPlugin e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("PluginUninstalledWithName"), - e.Name), - NotificationType.PluginUninstalled.ToString(), - Guid.Empty)) - .ConfigureAwait(false); - } - - private async void OnPluginInstalled(object sender, InstallationInfo e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("PluginInstalledWithName"), - e.Name), - NotificationType.PluginInstalled.ToString(), - Guid.Empty) - { - ShortOverview = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("VersionNumber"), - e.Version) - }).ConfigureAwait(false); - } - - private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e) - { - var installationInfo = e.InstallationInfo; - - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("NameInstallFailed"), - installationInfo.Name), - NotificationType.InstallationFailed.ToString(), - Guid.Empty) - { - ShortOverview = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("VersionNumber"), - installationInfo.Version), - Overview = e.Exception.Message - }).ConfigureAwait(false); - } - - private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e) - { - var result = e.Result; - var task = e.Task; - - if (task.ScheduledTask is IConfigurableScheduledTask activityTask - && !activityTask.IsLogged) - { - return; - } - - var time = result.EndTimeUtc - result.StartTimeUtc; - var runningTime = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("LabelRunningTimeValue"), - ToUserFriendlyString(time)); - - if (result.Status == TaskCompletionStatus.Failed) - { - var vals = new List(); - - if (!string.IsNullOrEmpty(e.Result.ErrorMessage)) - { - vals.Add(e.Result.ErrorMessage); - } - - if (!string.IsNullOrEmpty(e.Result.LongErrorMessage)) - { - vals.Add(e.Result.LongErrorMessage); - } - - await CreateLogEntry(new ActivityLog( - string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name), - NotificationType.TaskFailed.ToString(), - Guid.Empty) - { - LogSeverity = LogLevel.Error, - Overview = string.Join(Environment.NewLine, vals), - ShortOverview = runningTime - }).ConfigureAwait(false); - } - } - - private async Task CreateLogEntry(ActivityLog entry) - => await _activityManager.CreateAsync(entry).ConfigureAwait(false); - - /// - public void Dispose() - { - _taskManager.TaskCompleted -= OnTaskCompleted; - - _installationManager.PluginInstalled -= OnPluginInstalled; - _installationManager.PluginUninstalled -= OnPluginUninstalled; - _installationManager.PluginUpdated -= OnPluginUpdated; - _installationManager.PackageInstallationFailed -= OnPackageInstallationFailed; - - _sessionManager.SessionStarted -= OnSessionStarted; - _sessionManager.AuthenticationFailed -= OnAuthenticationFailed; - _sessionManager.AuthenticationSucceeded -= OnAuthenticationSucceeded; - _sessionManager.SessionEnded -= OnSessionEnded; - - _sessionManager.PlaybackStart -= OnPlaybackStart; - _sessionManager.PlaybackStopped -= OnPlaybackStopped; - - _subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure; - - _userManager.OnUserCreated -= OnUserCreated; - _userManager.OnUserPasswordChanged -= OnUserPasswordChanged; - _userManager.OnUserDeleted -= OnUserDeleted; - _userManager.OnUserLockedOut -= OnUserLockedOut; - } - - /// - /// Constructs a user-friendly string for this TimeSpan instance. - /// - private static string ToUserFriendlyString(TimeSpan span) - { - const int DaysInYear = 365; - const int DaysInMonth = 30; - - // Get each non-zero value from TimeSpan component - var values = new List(); - - // Number of years - int days = span.Days; - if (days >= DaysInYear) - { - int years = days / DaysInYear; - values.Add(CreateValueString(years, "year")); - days %= DaysInYear; - } - - // Number of months - if (days >= DaysInMonth) - { - int months = days / DaysInMonth; - values.Add(CreateValueString(months, "month")); - days = days % DaysInMonth; - } - - // Number of days - if (days >= 1) - { - values.Add(CreateValueString(days, "day")); - } - - // Number of hours - if (span.Hours >= 1) - { - values.Add(CreateValueString(span.Hours, "hour")); - } - - // Number of minutes - if (span.Minutes >= 1) - { - values.Add(CreateValueString(span.Minutes, "minute")); - } - - // Number of seconds (include when 0 if no other components included) - if (span.Seconds >= 1 || values.Count == 0) - { - values.Add(CreateValueString(span.Seconds, "second")); - } - - // Combine values into string - var builder = new StringBuilder(); - for (int i = 0; i < values.Count; i++) - { - if (builder.Length > 0) - { - builder.Append(i == values.Count - 1 ? " and " : ", "); - } - - builder.Append(values[i]); - } - - // Return result - return builder.ToString(); - } - - /// - /// Constructs a string description of a time-span value. - /// - /// The value of this item. - /// The name of this item (singular form). - private static string CreateValueString(int value, string description) - { - return string.Format( - CultureInfo.InvariantCulture, - "{0:#,##0} {1}", - value, - value == 1 ? description : string.Format(CultureInfo.InvariantCulture, "{0}s", description)); - } - } -} diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 0201ed7a3..8e9a581ea 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -37,10 +37,10 @@ using Emby.Server.Implementations.LiveTv; using Emby.Server.Implementations.Localization; using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Playlists; +using Emby.Server.Implementations.QuickConnect; using Emby.Server.Implementations.ScheduledTasks; using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Serialization; -using Emby.Server.Implementations.Services; using Emby.Server.Implementations.Session; using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; @@ -53,7 +53,6 @@ using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Controller; -using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Collections; @@ -72,6 +71,7 @@ using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; @@ -89,7 +89,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; using MediaBrowser.Model.System; using MediaBrowser.Model.Tasks; using MediaBrowser.Providers.Chapters; @@ -97,11 +96,12 @@ using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.TheTvdb; using MediaBrowser.Providers.Subtitles; using MediaBrowser.XbmcMetadata.Providers; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; +using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager; namespace Emby.Server.Implementations { @@ -122,14 +122,18 @@ namespace Emby.Server.Implementations private IMediaEncoder _mediaEncoder; private ISessionManager _sessionManager; - private IHttpServer _httpServer; + private IWebSocketManager _webSocketManager; private IHttpClient _httpClient; + private string[] _urlPrefixes; + /// /// Gets a value indicating whether this instance can self restart. /// public bool CanSelfRestart => _startupOptions.RestartPath != null; + public bool CoreStartupHasCompleted { get; private set; } + public virtual bool CanLaunchWebBrowser { get @@ -173,6 +177,8 @@ namespace Emby.Server.Implementations /// protected ILogger Logger { get; } + protected IServiceCollection ServiceCollection { get; } + private IPlugin[] _plugins; /// @@ -238,9 +244,11 @@ namespace Emby.Server.Implementations ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, - INetworkManager networkManager) + INetworkManager networkManager, + IServiceCollection serviceCollection) { _xmlSerializer = new MyXmlSerializer(); + ServiceCollection = serviceCollection; _networkManager = networkManager; networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets; @@ -440,8 +448,7 @@ namespace Emby.Server.Implementations Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed); Logger.LogInformation("Core startup complete"); - _httpServer.GlobalResponse = null; - + CoreStartupHasCompleted = true; stopWatch.Restart(); await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed); @@ -464,7 +471,7 @@ namespace Emby.Server.Implementations } /// - public void Init(IServiceCollection serviceCollection) + public void Init() { HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber; HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber; @@ -493,145 +500,143 @@ namespace Emby.Server.Implementations DiscoverTypes(); - RegisterServices(serviceCollection); + RegisterServices(); } - public Task ExecuteHttpHandlerAsync(HttpContext context, Func next) - => _httpServer.RequestHandler(context); - /// /// Registers services/resources with the service collection that will be available via DI. /// - protected virtual void RegisterServices(IServiceCollection serviceCollection) + protected virtual void RegisterServices() { - serviceCollection.AddSingleton(_startupOptions); + ServiceCollection.AddSingleton(_startupOptions); - serviceCollection.AddMemoryCache(); + ServiceCollection.AddMemoryCache(); - serviceCollection.AddSingleton(ConfigurationManager); - serviceCollection.AddSingleton(this); + ServiceCollection.AddSingleton(ConfigurationManager); + ServiceCollection.AddSingleton(this); - serviceCollection.AddSingleton(ApplicationPaths); + ServiceCollection.AddSingleton(ApplicationPaths); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(_fileSystemManager); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(_fileSystemManager); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(_networkManager); + ServiceCollection.AddSingleton(_networkManager); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(_xmlSerializer); + ServiceCollection.AddSingleton(_xmlSerializer); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(this); + ServiceCollection.AddSingleton(ApplicationPaths); - serviceCollection.AddSingleton(this); - serviceCollection.AddSingleton(ApplicationPaths); + ServiceCollection.AddSingleton(ServerConfigurationManager); - serviceCollection.AddSingleton(ServerConfigurationManager); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required - serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); + ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required - serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); - serviceCollection.AddSingleton(); + ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); + ServiceCollection.AddSingleton(); // TODO: Refactor to eliminate the circular dependencies here so that Lazy isn't required - serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); - serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); - serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); - serviceCollection.AddSingleton(); + ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); + ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); + ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); // TODO: Refactor to eliminate the circular dependency here so that Lazy isn't required - serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); - serviceCollection.AddSingleton(); + ServiceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); + ServiceCollection.AddScoped(); + ServiceCollection.AddScoped(); + ServiceCollection.AddScoped(); } /// @@ -645,7 +650,7 @@ namespace Emby.Server.Implementations _mediaEncoder = Resolve(); _sessionManager = Resolve(); - _httpServer = Resolve(); + _webSocketManager = Resolve(); _httpClient = Resolve(); ((AuthenticationRepository)Resolve()).Initialize(); @@ -747,7 +752,6 @@ namespace Emby.Server.Implementations CollectionFolder.XmlSerializer = _xmlSerializer; CollectionFolder.JsonSerializer = Resolve(); CollectionFolder.ApplicationHost = this; - AuthenticatedAttribute.AuthService = Resolve(); } /// @@ -767,7 +771,8 @@ namespace Emby.Server.Implementations .Where(i => i != null) .ToArray(); - _httpServer.Init(GetExportTypes(), GetExports(), GetUrlPrefixes()); + _urlPrefixes = GetUrlPrefixes().ToArray(); + _webSocketManager.Init(GetExports()); Resolve().AddParts( GetExports(), @@ -831,6 +836,8 @@ namespace Emby.Server.Implementations { hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s)); } + + plugin.RegisterServices(ServiceCollection); } catch (Exception ex) { @@ -931,7 +938,7 @@ namespace Emby.Server.Implementations } } - if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase)) + if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase)) { requiresRestart = true; } @@ -1385,6 +1392,20 @@ namespace Emby.Server.Implementations _plugins = list.ToArray(); } + public IEnumerable GetApiPluginAssemblies() + { + var assemblies = _allConcreteTypes + .Where(i => typeof(ControllerBase).IsAssignableFrom(i)) + .Select(i => i.Assembly) + .Distinct(); + + foreach (var assembly in assemblies) + { + Logger.LogDebug("Found API endpoints in plugin {name}", assembly.FullName); + yield return assembly; + } + } + public virtual void LaunchUrl(string url) { if (!CanLaunchWebBrowser) diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index d8ab1f1a1..26fc1bee4 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -746,12 +746,21 @@ namespace Emby.Server.Implementations.Channels // null if came from cache if (itemsResult != null) { - var internalItems = itemsResult.Items - .Select(i => GetChannelItemEntity(i, channelProvider, channel.Id, parentItem, cancellationToken)) - .ToArray(); + var items = itemsResult.Items; + var itemsLen = items.Count; + var internalItems = new Guid[itemsLen]; + for (int i = 0; i < itemsLen; i++) + { + internalItems[i] = (await GetChannelItemEntityAsync( + items[i], + channelProvider, + channel.Id, + parentItem, + cancellationToken).ConfigureAwait(false)).Id; + } var existingIds = _libraryManager.GetItemIds(query); - var deadIds = existingIds.Except(internalItems.Select(i => i.Id)) + var deadIds = existingIds.Except(internalItems) .ToArray(); foreach (var deadId in deadIds) @@ -963,7 +972,7 @@ namespace Emby.Server.Implementations.Channels return item; } - private BaseItem GetChannelItemEntity(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, BaseItem parentFolder, CancellationToken cancellationToken) + private async Task GetChannelItemEntityAsync(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, BaseItem parentFolder, CancellationToken cancellationToken) { var parentFolderId = parentFolder.Id; @@ -1165,7 +1174,7 @@ namespace Emby.Server.Implementations.Channels } else if (forceUpdate) { - item.UpdateToRepository(ItemUpdateType.None, cancellationToken); + await item.UpdateToRepositoryAsync(ItemUpdateType.None, cancellationToken).ConfigureAwait(false); } if ((isNew || forceUpdate) && info.Type == ChannelItemType.Media) diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index ac2edc1e2..3011a37e3 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.Collections } /// - public BoxSet CreateCollection(CollectionCreationOptions options) + public async Task CreateCollectionAsync(CollectionCreationOptions options) { var name = options.Name; @@ -141,7 +141,7 @@ namespace Emby.Server.Implementations.Collections // This could cause it to get re-resolved as a plain folder var folderName = _fileSystem.GetValidFilename(name) + " [boxset]"; - var parentFolder = GetCollectionsFolder(true).GetAwaiter().GetResult(); + var parentFolder = await GetCollectionsFolder(true).ConfigureAwait(false); if (parentFolder == null) { @@ -169,12 +169,16 @@ namespace Emby.Server.Implementations.Collections if (options.ItemIdList.Length > 0) { - AddToCollection(collection.Id, options.ItemIdList, false, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - // The initial adding of items is going to create a local metadata file - // This will cause internet metadata to be skipped as a result - MetadataRefreshMode = MetadataRefreshMode.FullRefresh - }); + await AddToCollectionAsync( + collection.Id, + options.ItemIdList.Select(x => new Guid(x)), + false, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + // The initial adding of items is going to create a local metadata file + // This will cause internet metadata to be skipped as a result + MetadataRefreshMode = MetadataRefreshMode.FullRefresh + }).ConfigureAwait(false); } else { @@ -197,18 +201,10 @@ namespace Emby.Server.Implementations.Collections } /// - public void AddToCollection(Guid collectionId, IEnumerable ids) - { - AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem))); - } + public Task AddToCollectionAsync(Guid collectionId, IEnumerable ids) + => AddToCollectionAsync(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem))); - /// - public void AddToCollection(Guid collectionId, IEnumerable ids) - { - AddToCollection(collectionId, ids.Select(i => i.ToString("N", CultureInfo.InvariantCulture)), true, new MetadataRefreshOptions(new DirectoryService(_fileSystem))); - } - - private void AddToCollection(Guid collectionId, IEnumerable ids, bool fireEvent, MetadataRefreshOptions refreshOptions) + private async Task AddToCollectionAsync(Guid collectionId, IEnumerable ids, bool fireEvent, MetadataRefreshOptions refreshOptions) { var collection = _libraryManager.GetItemById(collectionId) as BoxSet; if (collection == null) @@ -224,15 +220,14 @@ namespace Emby.Server.Implementations.Collections foreach (var id in ids) { - var guidId = new Guid(id); - var item = _libraryManager.GetItemById(guidId); + var item = _libraryManager.GetItemById(id); if (item == null) { throw new ArgumentException("No item exists with the supplied Id"); } - if (!currentLinkedChildrenIds.Contains(guidId)) + if (!currentLinkedChildrenIds.Contains(id)) { itemList.Add(item); @@ -249,7 +244,7 @@ namespace Emby.Server.Implementations.Collections collection.UpdateRatingToItems(linkedChildrenList); - collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); refreshOptions.ForceSave = true; _providerManager.QueueRefresh(collection.Id, refreshOptions, RefreshPriority.High); @@ -266,13 +261,7 @@ namespace Emby.Server.Implementations.Collections } /// - public void RemoveFromCollection(Guid collectionId, IEnumerable itemIds) - { - RemoveFromCollection(collectionId, itemIds.Select(i => new Guid(i))); - } - - /// - public void RemoveFromCollection(Guid collectionId, IEnumerable itemIds) + public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable itemIds) { var collection = _libraryManager.GetItemById(collectionId) as BoxSet; @@ -309,7 +298,7 @@ namespace Emby.Server.Implementations.Collections collection.LinkedChildren = collection.LinkedChildren.Except(list).ToArray(); } - collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); _providerManager.QueueRefresh( collection.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index a15295fca..f05a30a89 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -2,11 +2,11 @@ using System; using System.Globalization; using System.IO; using Emby.Server.Implementations.AppBase; +using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Events; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 64ccff53b..fde6fa115 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -15,7 +15,7 @@ namespace Emby.Server.Implementations public static Dictionary DefaultConfiguration => new Dictionary { { HostWebClientKey, bool.TrueString }, - { HttpListenerHost.DefaultRedirectKey, "web/index.html" }, + { DefaultRedirectKey, "web/index.html" }, { FfmpegProbeSizeKey, "1G" }, { FfmpegAnalyzeDurationKey, "200M" }, { PlaylistsAllowDuplicatesKey, bool.TrueString }, diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index d11e5e62e..5bf740cfc 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -4308,7 +4308,7 @@ namespace Emby.Server.Implementations.Data whereClauses.Add("ProductionYear=@Years"); if (statement != null) { - statement.TryBind("@Years", query.Years[0].ToString()); + statement.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture)); } } else if (query.Years.Length > 1) @@ -4560,13 +4560,13 @@ namespace Emby.Server.Implementations.Data if (query.AncestorIds.Length > 1) { var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); - whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause)); + whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause)); } if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey)) { var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey"; - whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause)); + whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause)); if (statement != null) { statement.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey); @@ -5170,7 +5170,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type insertText.Append(','); } - insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture)); + insertText.AppendFormat( + CultureInfo.InvariantCulture, + "(@ItemId, @AncestorId{0}, @AncestorIdText{0})", + i.ToString(CultureInfo.InvariantCulture)); } using (var statement = PrepareStatement(db, insertText.ToString())) diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs index cc4b407f5..f98c694c4 100644 --- a/Emby.Server.Implementations/Devices/DeviceManager.cs +++ b/Emby.Server.Implementations/Devices/DeviceManager.cs @@ -7,13 +7,13 @@ using System.IO; using System.Linq; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Data.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Security; using MediaBrowser.Model.Devices; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Session; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 1adef68aa..56fc57327 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -22,7 +22,7 @@ - + @@ -37,11 +37,11 @@ - + - + diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index 9fce49425..2e8cc76d2 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -7,11 +7,11 @@ using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Events; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Events; using Microsoft.Extensions.Logging; using Mono.Nat; diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 1deef7f72..c9d21d963 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -15,7 +16,6 @@ using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs index 632735910..44d2580d6 100644 --- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Plugins; @@ -43,22 +44,22 @@ namespace Emby.Server.Implementations.EntryPoints return Task.CompletedTask; } - private async void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs e) + private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs e) { await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false); } - private async void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs e) + private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs e) { await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false); } - private async void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs e) + private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs e) { await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false); } - private async void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs e) + private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs e) { await SendMessage("TimerCancelled", e.Argument).ConfigureAwait(false); } diff --git a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs b/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs deleted file mode 100644 index 826d4d8dc..000000000 --- a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Data.Entities; -using MediaBrowser.Common.Plugins; -using MediaBrowser.Common.Updates; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Tasks; -using MediaBrowser.Model.Updates; - -namespace Emby.Server.Implementations.EntryPoints -{ - /// - /// Class WebSocketEvents. - /// - public class ServerEventNotifier : IServerEntryPoint - { - /// - /// The user manager. - /// - private readonly IUserManager _userManager; - - /// - /// The installation manager. - /// - private readonly IInstallationManager _installationManager; - - /// - /// The kernel. - /// - private readonly IServerApplicationHost _appHost; - - /// - /// The task manager. - /// - private readonly ITaskManager _taskManager; - - private readonly ISessionManager _sessionManager; - - /// - /// Initializes a new instance of the class. - /// - /// The application host. - /// The user manager. - /// The installation manager. - /// The task manager. - /// The session manager. - public ServerEventNotifier( - IServerApplicationHost appHost, - IUserManager userManager, - IInstallationManager installationManager, - ITaskManager taskManager, - ISessionManager sessionManager) - { - _userManager = userManager; - _installationManager = installationManager; - _appHost = appHost; - _taskManager = taskManager; - _sessionManager = sessionManager; - } - - /// - public Task RunAsync() - { - _userManager.OnUserDeleted += OnUserDeleted; - _userManager.OnUserUpdated += OnUserUpdated; - - _appHost.HasPendingRestartChanged += OnHasPendingRestartChanged; - - _installationManager.PluginUninstalled += OnPluginUninstalled; - _installationManager.PackageInstalling += OnPackageInstalling; - _installationManager.PackageInstallationCancelled += OnPackageInstallationCancelled; - _installationManager.PackageInstallationCompleted += OnPackageInstallationCompleted; - _installationManager.PackageInstallationFailed += OnPackageInstallationFailed; - - _taskManager.TaskCompleted += OnTaskCompleted; - - return Task.CompletedTask; - } - - private async void OnPackageInstalling(object sender, InstallationInfo e) - { - await SendMessageToAdminSessions("PackageInstalling", e).ConfigureAwait(false); - } - - private async void OnPackageInstallationCancelled(object sender, InstallationInfo e) - { - await SendMessageToAdminSessions("PackageInstallationCancelled", e).ConfigureAwait(false); - } - - private async void OnPackageInstallationCompleted(object sender, InstallationInfo e) - { - await SendMessageToAdminSessions("PackageInstallationCompleted", e).ConfigureAwait(false); - } - - private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e) - { - await SendMessageToAdminSessions("PackageInstallationFailed", e.InstallationInfo).ConfigureAwait(false); - } - - private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e) - { - await SendMessageToAdminSessions("ScheduledTaskEnded", e.Result).ConfigureAwait(false); - } - - /// - /// Installations the manager_ plugin uninstalled. - /// - /// The sender. - /// The e. - private async void OnPluginUninstalled(object sender, IPlugin e) - { - await SendMessageToAdminSessions("PluginUninstalled", e).ConfigureAwait(false); - } - - /// - /// Handles the HasPendingRestartChanged event of the kernel control. - /// - /// The source of the event. - /// The instance containing the event data. - private async void OnHasPendingRestartChanged(object sender, EventArgs e) - { - await _sessionManager.SendRestartRequiredNotification(CancellationToken.None).ConfigureAwait(false); - } - - /// - /// Users the manager_ user updated. - /// - /// The sender. - /// The e. - private async void OnUserUpdated(object sender, GenericEventArgs e) - { - var dto = _userManager.GetUserDto(e.Argument); - - await SendMessageToUserSession(e.Argument, "UserUpdated", dto).ConfigureAwait(false); - } - - /// - /// Users the manager_ user deleted. - /// - /// The sender. - /// The e. - private async void OnUserDeleted(object sender, GenericEventArgs e) - { - await SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture)).ConfigureAwait(false); - } - - private async Task SendMessageToAdminSessions(string name, T data) - { - try - { - await _sessionManager.SendMessageToAdminSessions(name, data, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception) - { - } - } - - private async Task SendMessageToUserSession(User user, string name, T data) - { - try - { - await _sessionManager.SendMessageToUserSessions( - new List { user.Id }, - name, - data, - CancellationToken.None).ConfigureAwait(false); - } - catch (Exception) - { - } - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool dispose) - { - if (dispose) - { - _userManager.OnUserDeleted -= OnUserDeleted; - _userManager.OnUserUpdated -= OnUserUpdated; - - _installationManager.PluginUninstalled -= OnPluginUninstalled; - _installationManager.PackageInstalling -= OnPackageInstalling; - _installationManager.PackageInstallationCancelled -= OnPackageInstallationCancelled; - _installationManager.PackageInstallationCompleted -= OnPackageInstallationCompleted; - _installationManager.PackageInstallationFailed -= OnPackageInstallationFailed; - - _appHost.HasPendingRestartChanged -= OnHasPendingRestartChanged; - - _taskManager.TaskCompleted -= OnTaskCompleted; - } - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs deleted file mode 100644 index 6fce8de44..000000000 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ /dev/null @@ -1,250 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpServer -{ - public class FileWriter : IHttpResult - { - private static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); - - private static readonly string[] _skipLogExtensions = { - ".js", - ".html", - ".css" - }; - - private readonly IStreamHelper _streamHelper; - private readonly ILogger _logger; - - /// - /// The _options. - /// - private readonly IDictionary _options = new Dictionary(); - - /// - /// The _requested ranges. - /// - private List> _requestedRanges; - - public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem, IStreamHelper streamHelper) - { - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - _streamHelper = streamHelper; - - Path = path; - _logger = logger; - RangeHeader = rangeHeader; - - Headers[HeaderNames.ContentType] = contentType; - - TotalContentLength = fileSystem.GetFileInfo(path).Length; - Headers[HeaderNames.AcceptRanges] = "bytes"; - - if (string.IsNullOrWhiteSpace(rangeHeader)) - { - Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture); - StatusCode = HttpStatusCode.OK; - } - else - { - StatusCode = HttpStatusCode.PartialContent; - SetRangeValues(); - } - - FileShare = FileShare.Read; - Cookies = new List(); - } - - private string RangeHeader { get; set; } - - private bool IsHeadRequest { get; set; } - - private long RangeStart { get; set; } - - private long RangeEnd { get; set; } - - private long RangeLength { get; set; } - - public long TotalContentLength { get; set; } - - public Action OnComplete { get; set; } - - public Action OnError { get; set; } - - public List Cookies { get; private set; } - - public FileShare FileShare { get; set; } - - /// - /// Gets the options. - /// - /// The options. - public IDictionary Headers => _options; - - public string Path { get; set; } - - /// - /// Gets the requested ranges. - /// - /// The requested ranges. - protected List> RequestedRanges - { - get - { - if (_requestedRanges == null) - { - _requestedRanges = new List>(); - - // Example: bytes=0-,32-63 - var ranges = RangeHeader.Split('=')[1].Split(','); - - foreach (var range in ranges) - { - var vals = range.Split('-'); - - long start = 0; - long? end = null; - - if (!string.IsNullOrEmpty(vals[0])) - { - start = long.Parse(vals[0], UsCulture); - } - - if (!string.IsNullOrEmpty(vals[1])) - { - end = long.Parse(vals[1], UsCulture); - } - - _requestedRanges.Add(new KeyValuePair(start, end)); - } - } - - return _requestedRanges; - } - } - - public string ContentType { get; set; } - - public IRequest RequestContext { get; set; } - - public object Response { get; set; } - - public int Status { get; set; } - - public HttpStatusCode StatusCode - { - get => (HttpStatusCode)Status; - set => Status = (int)value; - } - - /// - /// Sets the range values. - /// - private void SetRangeValues() - { - var requestedRange = RequestedRanges[0]; - - // If the requested range is "0-", we can optimize by just doing a stream copy - if (!requestedRange.Value.HasValue) - { - RangeEnd = TotalContentLength - 1; - } - else - { - RangeEnd = requestedRange.Value.Value; - } - - RangeStart = requestedRange.Key; - RangeLength = 1 + RangeEnd - RangeStart; - - // Content-Length is the length of what we're serving, not the original content - var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture); - Headers[HeaderNames.ContentLength] = lengthString; - var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; - Headers[HeaderNames.ContentRange] = rangeString; - - _logger.LogDebug("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString); - } - - public async Task WriteToAsync(HttpResponse response, CancellationToken cancellationToken) - { - try - { - // Headers only - if (IsHeadRequest) - { - return; - } - - var path = Path; - var offset = RangeStart; - var count = RangeLength; - - if (string.IsNullOrWhiteSpace(RangeHeader) || RangeStart <= 0 && RangeEnd >= TotalContentLength - 1) - { - var extension = System.IO.Path.GetExtension(path); - - if (extension == null || !_skipLogExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) - { - _logger.LogDebug("Transmit file {0}", path); - } - - offset = 0; - count = 0; - } - - await TransmitFile(response.Body, path, offset, count, FileShare, cancellationToken).ConfigureAwait(false); - } - finally - { - OnComplete?.Invoke(); - } - } - - public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShare fileShare, CancellationToken cancellationToken) - { - var fileOptions = FileOptions.SequentialScan; - - // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - fileOptions |= FileOptions.Asynchronous; - } - - using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, fileOptions)) - { - if (offset > 0) - { - fs.Position = offset; - } - - if (count > 0) - { - await _streamHelper.CopyToAsync(fs, stream, count, cancellationToken).ConfigureAwait(false); - } - else - { - await fs.CopyToAsync(stream, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false); - } - } - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs deleted file mode 100644 index dafdd5b7b..000000000 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ /dev/null @@ -1,766 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net.Sockets; -using System.Net.WebSockets; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.Services; -using Emby.Server.Implementations.SocketSharp; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; -using ServiceStack.Text.Jsv; - -namespace Emby.Server.Implementations.HttpServer -{ - public class HttpListenerHost : IHttpServer - { - /// - /// The key for a setting that specifies the default redirect path - /// to use for requests where the URL base prefix is invalid or missing. - /// - public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath"; - - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IServerConfigurationManager _config; - private readonly INetworkManager _networkManager; - private readonly IServerApplicationHost _appHost; - private readonly IJsonSerializer _jsonSerializer; - private readonly IXmlSerializer _xmlSerializer; - private readonly Func> _funcParseFn; - private readonly string _defaultRedirectPath; - private readonly string _baseUrlPrefix; - - private readonly Dictionary _serviceOperationsMap = new Dictionary(); - private readonly IHostEnvironment _hostEnvironment; - - private IWebSocketListener[] _webSocketListeners = Array.Empty(); - private bool _disposed = false; - - public HttpListenerHost( - IServerApplicationHost applicationHost, - ILogger logger, - IServerConfigurationManager config, - IConfiguration configuration, - INetworkManager networkManager, - IJsonSerializer jsonSerializer, - IXmlSerializer xmlSerializer, - ILocalizationManager localizationManager, - ServiceController serviceController, - IHostEnvironment hostEnvironment, - ILoggerFactory loggerFactory) - { - _appHost = applicationHost; - _logger = logger; - _config = config; - _defaultRedirectPath = configuration[DefaultRedirectKey]; - _baseUrlPrefix = _config.Configuration.BaseUrl; - _networkManager = networkManager; - _jsonSerializer = jsonSerializer; - _xmlSerializer = xmlSerializer; - ServiceController = serviceController; - _hostEnvironment = hostEnvironment; - _loggerFactory = loggerFactory; - - _funcParseFn = t => s => JsvReader.GetParseFn(t)(s); - - Instance = this; - ResponseFilters = Array.Empty>(); - GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); - } - - public event EventHandler> WebSocketConnected; - - public Action[] ResponseFilters { get; set; } - - public static HttpListenerHost Instance { get; protected set; } - - public string[] UrlPrefixes { get; private set; } - - public string GlobalResponse { get; set; } - - public ServiceController ServiceController { get; } - - public object CreateInstance(Type type) - { - return _appHost.CreateInstance(type); - } - - private static string NormalizeUrlPath(string path) - { - if (path.Length > 0 && path[0] == '/') - { - // If the path begins with a leading slash, just return it as-is - return path; - } - else - { - // If the path does not begin with a leading slash, append one for consistency - return "/" + path; - } - } - - /// - /// Applies the request filters. Returns whether or not the request has been handled - /// and no more processing should be done. - /// - /// - public void ApplyRequestFilters(IRequest req, HttpResponse res, object requestDto) - { - // Exec all RequestFilter attributes with Priority < 0 - var attributes = GetRequestFilterAttributes(requestDto.GetType()); - - int count = attributes.Count; - int i = 0; - for (; i < count && attributes[i].Priority < 0; i++) - { - var attribute = attributes[i]; - attribute.RequestFilter(req, res, requestDto); - } - - // Exec remaining RequestFilter attributes with Priority >= 0 - for (; i < count && attributes[i].Priority >= 0; i++) - { - var attribute = attributes[i]; - attribute.RequestFilter(req, res, requestDto); - } - } - - public Type GetServiceTypeByRequest(Type requestType) - { - _serviceOperationsMap.TryGetValue(requestType, out var serviceType); - return serviceType; - } - - public void AddServiceInfo(Type serviceType, Type requestType) - { - _serviceOperationsMap[requestType] = serviceType; - } - - private List GetRequestFilterAttributes(Type requestDtoType) - { - var attributes = requestDtoType.GetCustomAttributes(true).OfType().ToList(); - - var serviceType = GetServiceTypeByRequest(requestDtoType); - if (serviceType != null) - { - attributes.AddRange(serviceType.GetCustomAttributes(true).OfType()); - } - - attributes.Sort((x, y) => x.Priority - y.Priority); - - return attributes; - } - - private static Exception GetActualException(Exception ex) - { - if (ex is AggregateException agg) - { - var inner = agg.InnerException; - if (inner != null) - { - return GetActualException(inner); - } - else - { - var inners = agg.InnerExceptions; - if (inners.Count > 0) - { - return GetActualException(inners[0]); - } - } - } - - return ex; - } - - private int GetStatusCode(Exception ex) - { - switch (ex) - { - case ArgumentException _: return 400; - case AuthenticationException _: return 401; - case SecurityException _: return 403; - case DirectoryNotFoundException _: - case FileNotFoundException _: - case ResourceNotFoundException _: return 404; - case MethodNotAllowedException _: return 405; - default: return 500; - } - } - - private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace) - { - if (ignoreStackTrace) - { - _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog); - } - else - { - _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog); - } - - var httpRes = httpReq.Response; - - if (httpRes.HasStarted) - { - return; - } - - httpRes.StatusCode = statusCode; - - var errContent = _hostEnvironment.IsDevelopment() - ? (NormalizeExceptionMessage(ex) ?? string.Empty) - : "Error processing request."; - httpRes.ContentType = "text/plain"; - httpRes.ContentLength = errContent.Length; - await httpRes.WriteAsync(errContent).ConfigureAwait(false); - } - - private string NormalizeExceptionMessage(Exception ex) - { - // Do not expose the exception message for AuthenticationException - if (ex is AuthenticationException) - { - return null; - } - - // Strip any information we don't want to reveal - return ex.Message - ?.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase) - .Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase); - } - - public static string RemoveQueryStringByKey(string url, string key) - { - var uri = new Uri(url); - - // this gets all the query string key value pairs as a collection - var newQueryString = QueryHelpers.ParseQuery(uri.Query); - - var originalCount = newQueryString.Count; - - if (originalCount == 0) - { - return url; - } - - // this removes the key if exists - newQueryString.Remove(key); - - if (originalCount == newQueryString.Count) - { - return url; - } - - // this gets the page path from root without QueryString - string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0]; - - return newQueryString.Count > 0 - ? QueryHelpers.AddQueryString(pagePathWithoutQueryString, newQueryString.ToDictionary(kv => kv.Key, kv => kv.Value.ToString())) - : pagePathWithoutQueryString; - } - - private static string GetUrlToLog(string url) - { - url = RemoveQueryStringByKey(url, "api_key"); - - return url; - } - - private static string NormalizeConfiguredLocalAddress(string address) - { - var add = address.AsSpan().Trim('/'); - int index = add.IndexOf('/'); - if (index != -1) - { - add = add.Slice(index + 1); - } - - return add.TrimStart('/').ToString(); - } - - private bool ValidateHost(string host) - { - var hosts = _config - .Configuration - .LocalNetworkAddresses - .Select(NormalizeConfiguredLocalAddress) - .ToList(); - - if (hosts.Count == 0) - { - return true; - } - - host ??= string.Empty; - - if (_networkManager.IsInPrivateAddressSpace(host)) - { - hosts.Add("localhost"); - hosts.Add("127.0.0.1"); - - return hosts.Any(i => host.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1); - } - - return true; - } - - private bool ValidateRequest(string remoteIp, bool isLocal) - { - if (isLocal) - { - return true; - } - - if (_config.Configuration.EnableRemoteAccess) - { - var addressFilter = _config.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); - - if (addressFilter.Length > 0 && !_networkManager.IsInLocalNetwork(remoteIp)) - { - if (_config.Configuration.IsRemoteIPFilterBlacklist) - { - return !_networkManager.IsAddressInSubnets(remoteIp, addressFilter); - } - else - { - return _networkManager.IsAddressInSubnets(remoteIp, addressFilter); - } - } - } - else - { - if (!_networkManager.IsInLocalNetwork(remoteIp)) - { - return false; - } - } - - return true; - } - - /// - /// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required. - /// - /// True if the request is valid, or false if the request is not valid and an HTTPS redirect is required. - private bool ValidateSsl(string remoteIp, string urlString) - { - if (_config.Configuration.RequireHttps - && _appHost.ListenWithHttps - && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase)) - { - // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected - if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 - || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1) - { - return true; - } - - if (!_networkManager.IsInLocalNetwork(remoteIp)) - { - return false; - } - } - - return true; - } - - /// - public Task RequestHandler(HttpContext context) - { - if (context.WebSockets.IsWebSocketRequest) - { - return WebSocketRequestHandler(context); - } - - var request = context.Request; - var response = context.Response; - var localPath = context.Request.Path.ToString(); - - var req = new WebSocketSharpRequest(request, response, request.Path); - return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted); - } - - /// - /// Overridable method that can be used to implement a custom handler. - /// - private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken) - { - var stopWatch = new Stopwatch(); - stopWatch.Start(); - var httpRes = httpReq.Response; - string urlToLog = GetUrlToLog(urlString); - string remoteIp = httpReq.RemoteIp; - - try - { - if (_disposed) - { - httpRes.StatusCode = 503; - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false); - return; - } - - if (!ValidateHost(host)) - { - httpRes.StatusCode = 400; - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false); - return; - } - - if (!ValidateRequest(remoteIp, httpReq.IsLocal)) - { - httpRes.StatusCode = 403; - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false); - return; - } - - if (!ValidateSsl(httpReq.RemoteIp, urlString)) - { - RedirectToSecureUrl(httpReq, httpRes, urlString); - return; - } - - if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase)) - { - httpRes.StatusCode = 200; - foreach (var (key, value) in GetDefaultCorsHeaders(httpReq)) - { - httpRes.Headers.Add(key, value); - } - - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false); - return; - } - - if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) - || string.IsNullOrEmpty(localPath) - || !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase)) - { - // Always redirect back to the default path if the base prefix is invalid or missing - _logger.LogDebug("Normalizing a URL at {0}", localPath); - httpRes.Redirect(_baseUrlPrefix + "/" + _defaultRedirectPath); - return; - } - - if (!string.IsNullOrEmpty(GlobalResponse)) - { - // We don't want the address pings in ApplicationHost to fail - if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1) - { - httpRes.StatusCode = 503; - httpRes.ContentType = "text/html"; - await httpRes.WriteAsync(GlobalResponse, cancellationToken).ConfigureAwait(false); - return; - } - } - - var handler = GetServiceHandler(httpReq); - if (handler != null) - { - await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false); - } - else - { - throw new FileNotFoundException(); - } - } - catch (Exception requestEx) - { - try - { - var requestInnerEx = GetActualException(requestEx); - var statusCode = GetStatusCode(requestInnerEx); - - foreach (var (key, value) in GetDefaultCorsHeaders(httpReq)) - { - if (!httpRes.Headers.ContainsKey(key)) - { - httpRes.Headers.Add(key, value); - } - } - - bool ignoreStackTrace = - requestInnerEx is SocketException - || requestInnerEx is IOException - || requestInnerEx is OperationCanceledException - || requestInnerEx is SecurityException - || requestInnerEx is AuthenticationException - || requestInnerEx is FileNotFoundException; - - // Do not handle 500 server exceptions manually when in development mode. - // Instead, re-throw the exception so it can be handled by the DeveloperExceptionPageMiddleware. - // However, do not use the DeveloperExceptionPageMiddleware when the stack trace should be ignored, - // because it will log the stack trace when it handles the exception. - if (statusCode == 500 && !ignoreStackTrace && _hostEnvironment.IsDevelopment()) - { - throw; - } - - await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false); - } - catch (Exception handlerException) - { - var aggregateEx = new AggregateException("Error while handling request exception", requestEx, handlerException); - _logger.LogError(aggregateEx, "Error while handling exception in response to {Url}", urlToLog); - - if (_hostEnvironment.IsDevelopment()) - { - throw aggregateEx; - } - } - } - finally - { - if (httpRes.StatusCode >= 500) - { - _logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog); - } - - stopWatch.Stop(); - var elapsed = stopWatch.Elapsed; - if (elapsed.TotalMilliseconds > 500) - { - _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); - } - } - } - - private async Task WebSocketRequestHandler(HttpContext context) - { - if (_disposed) - { - return; - } - - try - { - _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); - - WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); - - using var connection = new WebSocketConnection( - _loggerFactory.CreateLogger(), - webSocket, - context.Connection.RemoteIpAddress, - context.Request.Query) - { - OnReceive = ProcessWebSocketMessageReceived - }; - - WebSocketConnected?.Invoke(this, new GenericEventArgs(connection)); - - await connection.ProcessAsync().ConfigureAwait(false); - _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress); - } - catch (Exception ex) // Otherwise ASP.Net will ignore the exception - { - _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress); - if (!context.Response.HasStarted) - { - context.Response.StatusCode = 500; - } - } - } - - /// - /// Get the default CORS headers. - /// - /// - /// - public IDictionary GetDefaultCorsHeaders(IRequest req) - { - var origin = req.Headers["Origin"]; - if (origin == StringValues.Empty) - { - origin = req.Headers["Host"]; - if (origin == StringValues.Empty) - { - origin = "*"; - } - } - - var headers = new Dictionary(); - headers.Add("Access-Control-Allow-Origin", origin); - headers.Add("Access-Control-Allow-Credentials", "true"); - headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); - headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie"); - return headers; - } - - // Entry point for HttpListener - public ServiceHandler GetServiceHandler(IHttpRequest httpReq) - { - var pathInfo = httpReq.PathInfo; - - pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType); - var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo); - if (restPath != null) - { - return new ServiceHandler(restPath, contentType); - } - - _logger.LogError("Could not find handler for {PathInfo}", pathInfo); - return null; - } - - private void RedirectToSecureUrl(IHttpRequest httpReq, HttpResponse httpRes, string url) - { - if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) - { - var builder = new UriBuilder(uri) - { - Port = _config.Configuration.PublicHttpsPort, - Scheme = "https" - }; - url = builder.Uri.ToString(); - } - - httpRes.Redirect(url); - } - - /// - /// Adds the rest handlers. - /// - /// The service types to register with the . - /// The web socket listeners. - /// The URL prefixes. See . - public void Init(IEnumerable serviceTypes, IEnumerable listeners, IEnumerable urlPrefixes) - { - _webSocketListeners = listeners.ToArray(); - UrlPrefixes = urlPrefixes.ToArray(); - - ServiceController.Init(this, serviceTypes); - - ResponseFilters = new Action[] - { - new ResponseFilter(this, _logger).FilterResponse - }; - } - - public RouteAttribute[] GetRouteAttributes(Type requestType) - { - var routes = requestType.GetTypeInfo().GetCustomAttributes(true).ToList(); - var clone = routes.ToList(); - - foreach (var route in clone) - { - routes.Add(new RouteAttribute(NormalizeCustomRoutePath(route.Path), route.Verbs) - { - Notes = route.Notes, - Priority = route.Priority, - Summary = route.Summary - }); - - routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs) - { - Notes = route.Notes, - Priority = route.Priority, - Summary = route.Summary - }); - - routes.Add(new RouteAttribute(NormalizeMediaBrowserRoutePath(route.Path), route.Verbs) - { - Notes = route.Notes, - Priority = route.Priority, - Summary = route.Summary - }); - } - - return routes.ToArray(); - } - - public Func GetParseFn(Type propertyType) - { - return _funcParseFn(propertyType); - } - - public void SerializeToJson(object o, Stream stream) - { - _jsonSerializer.SerializeToStream(o, stream); - } - - public void SerializeToXml(object o, Stream stream) - { - _xmlSerializer.SerializeToStream(o, stream); - } - - public Task DeserializeXml(Type type, Stream stream) - { - return Task.FromResult(_xmlSerializer.DeserializeFromStream(type, stream)); - } - - public Task DeserializeJson(Type type, Stream stream) - { - return _jsonSerializer.DeserializeFromStreamAsync(stream, type); - } - - private string NormalizeEmbyRoutePath(string path) - { - _logger.LogDebug("Normalizing /emby route"); - return _baseUrlPrefix + "/emby" + NormalizeUrlPath(path); - } - - private string NormalizeMediaBrowserRoutePath(string path) - { - _logger.LogDebug("Normalizing /mediabrowser route"); - return _baseUrlPrefix + "/mediabrowser" + NormalizeUrlPath(path); - } - - private string NormalizeCustomRoutePath(string path) - { - _logger.LogDebug("Normalizing custom route {0}", path); - return _baseUrlPrefix + NormalizeUrlPath(path); - } - - /// - /// Processes the web socket message received. - /// - /// The result. - private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result) - { - if (_disposed) - { - return Task.CompletedTask; - } - - IEnumerable GetTasks() - { - foreach (var x in _webSocketListeners) - { - yield return x.ProcessMessageAsync(result); - } - } - - return Task.WhenAll(GetTasks()); - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs deleted file mode 100644 index 688216373..000000000 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ /dev/null @@ -1,721 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.IO.Compression; -using System.Net; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; -using System.Xml; -using Emby.Server.Implementations.Services; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using IRequest = MediaBrowser.Model.Services.IRequest; -using MimeTypes = MediaBrowser.Model.Net.MimeTypes; - -namespace Emby.Server.Implementations.HttpServer -{ - /// - /// Class HttpResultFactory. - /// - public class HttpResultFactory : IHttpResultFactory - { - // Last-Modified and If-Modified-Since must follow strict date format, - // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since - private const string HttpDateFormat = "ddd, dd MMM yyyy HH:mm:ss \"GMT\""; - // We specifically use en-US culture because both day of week and month names require it - private static readonly CultureInfo _enUSculture = new CultureInfo("en-US", false); - - /// - /// The logger. - /// - private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; - private readonly IJsonSerializer _jsonSerializer; - private readonly IStreamHelper _streamHelper; - - /// - /// Initializes a new instance of the class. - /// - public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IStreamHelper streamHelper) - { - _fileSystem = fileSystem; - _jsonSerializer = jsonSerializer; - _streamHelper = streamHelper; - _logger = loggerfactory.CreateLogger(); - } - - /// - /// Gets the result. - /// - /// The request context. - /// The content. - /// Type of the content. - /// The response headers. - /// System.Object. - public object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary responseHeaders = null) - { - return GetHttpResult(requestContext, content, contentType, true, responseHeaders); - } - - public object GetResult(string content, string contentType, IDictionary responseHeaders = null) - { - return GetHttpResult(null, content, contentType, true, responseHeaders); - } - - public object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary responseHeaders = null) - { - return GetHttpResult(requestContext, content, contentType, true, responseHeaders); - } - - public object GetResult(IRequest requestContext, string content, string contentType, IDictionary responseHeaders = null) - { - return GetHttpResult(requestContext, content, contentType, true, responseHeaders); - } - - public object GetRedirectResult(string url) - { - var responseHeaders = new Dictionary(); - responseHeaders[HeaderNames.Location] = url; - - var result = new HttpResult(Array.Empty(), "text/plain", HttpStatusCode.Redirect); - - AddResponseHeaders(result, responseHeaders); - - return result; - } - - /// - /// Gets the HTTP result. - /// - private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary responseHeaders = null) - { - var result = new StreamWriter(content, contentType); - - if (responseHeaders == null) - { - responseHeaders = new Dictionary(); - } - - if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out _)) - { - responseHeaders[HeaderNames.Expires] = "0"; - } - - AddResponseHeaders(result, responseHeaders); - - return result; - } - - /// - /// Gets the HTTP result. - /// - private IHasHeaders GetHttpResult(IRequest requestContext, byte[] content, string contentType, bool addCachePrevention, IDictionary responseHeaders = null) - { - string compressionType = null; - bool isHeadRequest = false; - - if (requestContext != null) - { - compressionType = GetCompressionType(requestContext, content, contentType); - isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase); - } - - IHasHeaders result; - if (string.IsNullOrEmpty(compressionType)) - { - var contentLength = content.Length; - - if (isHeadRequest) - { - content = Array.Empty(); - } - - result = new StreamWriter(content, contentType, contentLength); - } - else - { - result = GetCompressedResult(content, compressionType, responseHeaders, isHeadRequest, contentType); - } - - if (responseHeaders == null) - { - responseHeaders = new Dictionary(); - } - - if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _)) - { - responseHeaders[HeaderNames.Expires] = "0"; - } - - AddResponseHeaders(result, responseHeaders); - - return result; - } - - /// - /// Gets the HTTP result. - /// - private IHasHeaders GetHttpResult(IRequest requestContext, string content, string contentType, bool addCachePrevention, IDictionary responseHeaders = null) - { - IHasHeaders result; - - var bytes = Encoding.UTF8.GetBytes(content); - - var compressionType = requestContext == null ? null : GetCompressionType(requestContext, bytes, contentType); - - var isHeadRequest = requestContext == null ? false : string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase); - - if (string.IsNullOrEmpty(compressionType)) - { - var contentLength = bytes.Length; - - if (isHeadRequest) - { - bytes = Array.Empty(); - } - - result = new StreamWriter(bytes, contentType, contentLength); - } - else - { - result = GetCompressedResult(bytes, compressionType, responseHeaders, isHeadRequest, contentType); - } - - if (responseHeaders == null) - { - responseHeaders = new Dictionary(); - } - - if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _)) - { - responseHeaders[HeaderNames.Expires] = "0"; - } - - AddResponseHeaders(result, responseHeaders); - - return result; - } - - /// - /// Gets the optimized result. - /// - /// - public object GetResult(IRequest requestContext, T result, IDictionary responseHeaders = null) - where T : class - { - if (result == null) - { - throw new ArgumentNullException(nameof(result)); - } - - if (responseHeaders == null) - { - responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - responseHeaders[HeaderNames.Expires] = "0"; - - return ToOptimizedResultInternal(requestContext, result, responseHeaders); - } - - private string GetCompressionType(IRequest request, byte[] content, string responseContentType) - { - if (responseContentType == null) - { - return null; - } - - // Per apple docs, hls manifests must be compressed - if (!responseContentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) && - responseContentType.IndexOf("json", StringComparison.OrdinalIgnoreCase) == -1 && - responseContentType.IndexOf("javascript", StringComparison.OrdinalIgnoreCase) == -1 && - responseContentType.IndexOf("xml", StringComparison.OrdinalIgnoreCase) == -1 && - responseContentType.IndexOf("application/x-mpegURL", StringComparison.OrdinalIgnoreCase) == -1) - { - return null; - } - - if (content.Length < 1024) - { - return null; - } - - return GetCompressionType(request); - } - - private static string GetCompressionType(IRequest request) - { - var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString(); - - if (!string.IsNullOrEmpty(acceptEncoding)) - { - // if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1) - // return "br"; - - if (acceptEncoding.Contains("deflate", StringComparison.OrdinalIgnoreCase)) - { - return "deflate"; - } - - if (acceptEncoding.Contains("gzip", StringComparison.OrdinalIgnoreCase)) - { - return "gzip"; - } - } - - return null; - } - - /// - /// Returns the optimized result for the IRequestContext. - /// Does not use or store results in any cache. - /// - /// - /// - /// - public object ToOptimizedResult(IRequest request, T dto) - { - return ToOptimizedResultInternal(request, dto); - } - - private object ToOptimizedResultInternal(IRequest request, T dto, IDictionary responseHeaders = null) - { - // TODO: @bond use Span and .Equals - var contentType = request.ResponseContentType?.Split(';')[0].Trim().ToLowerInvariant(); - - switch (contentType) - { - case "application/xml": - case "text/xml": - case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml - return GetHttpResult(request, SerializeToXmlString(dto), contentType, false, responseHeaders); - - case "application/json": - case "text/json": - return GetHttpResult(request, _jsonSerializer.SerializeToString(dto), contentType, false, responseHeaders); - default: - break; - } - - var isHeadRequest = string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase); - - var ms = new MemoryStream(); - var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType); - - writerFn(dto, ms); - - ms.Position = 0; - - if (isHeadRequest) - { - using (ms) - { - return GetHttpResult(request, Array.Empty(), contentType, true, responseHeaders); - } - } - - return GetHttpResult(request, ms, contentType, true, responseHeaders); - } - - private IHasHeaders GetCompressedResult( - byte[] content, - string requestedCompressionType, - IDictionary responseHeaders, - bool isHeadRequest, - string contentType) - { - if (responseHeaders == null) - { - responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - content = Compress(content, requestedCompressionType); - responseHeaders[HeaderNames.ContentEncoding] = requestedCompressionType; - - responseHeaders[HeaderNames.Vary] = HeaderNames.AcceptEncoding; - - var contentLength = content.Length; - - if (isHeadRequest) - { - var result = new StreamWriter(Array.Empty(), contentType, contentLength); - AddResponseHeaders(result, responseHeaders); - return result; - } - else - { - var result = new StreamWriter(content, contentType, contentLength); - AddResponseHeaders(result, responseHeaders); - return result; - } - } - - private byte[] Compress(byte[] bytes, string compressionType) - { - if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase)) - { - return Deflate(bytes); - } - - if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase)) - { - return GZip(bytes); - } - - throw new NotSupportedException(compressionType); - } - - private static byte[] Deflate(byte[] bytes) - { - // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream - // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream - using (var ms = new MemoryStream()) - using (var zipStream = new DeflateStream(ms, CompressionMode.Compress)) - { - zipStream.Write(bytes, 0, bytes.Length); - zipStream.Dispose(); - - return ms.ToArray(); - } - } - - private static byte[] GZip(byte[] buffer) - { - using (var ms = new MemoryStream()) - using (var zipStream = new GZipStream(ms, CompressionMode.Compress)) - { - zipStream.Write(buffer, 0, buffer.Length); - zipStream.Dispose(); - - return ms.ToArray(); - } - } - - private static string SerializeToXmlString(object from) - { - using (var ms = new MemoryStream()) - { - var xwSettings = new XmlWriterSettings(); - xwSettings.Encoding = new UTF8Encoding(false); - xwSettings.OmitXmlDeclaration = false; - - using (var xw = XmlWriter.Create(ms, xwSettings)) - { - var serializer = new DataContractSerializer(from.GetType()); - serializer.WriteObject(xw, from); - xw.Flush(); - ms.Seek(0, SeekOrigin.Begin); - using (var reader = new StreamReader(ms)) - { - return reader.ReadToEnd(); - } - } - } - } - - /// - /// Pres the process optimized result. - /// - private object GetCachedResult(IRequest requestContext, IDictionary responseHeaders, StaticResultOptions options) - { - bool noCache = requestContext.Headers[HeaderNames.CacheControl].ToString().IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1; - AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified); - - if (!noCache) - { - if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ifModifiedSinceHeader)) - { - _logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]); - return null; - } - - if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified)) - { - AddAgeHeader(responseHeaders, options.DateLastModified); - - var result = new HttpResult(Array.Empty(), options.ContentType ?? "text/html", HttpStatusCode.NotModified); - - AddResponseHeaders(result, responseHeaders); - - return result; - } - } - - return null; - } - - public Task GetStaticFileResult(IRequest requestContext, - string path, - FileShare fileShare = FileShare.Read) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - return GetStaticFileResult(requestContext, new StaticFileResultOptions - { - Path = path, - FileShare = fileShare - }); - } - - public Task GetStaticFileResult(IRequest requestContext, StaticFileResultOptions options) - { - var path = options.Path; - var fileShare = options.FileShare; - - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentException("Path can't be empty.", nameof(options)); - } - - if (fileShare != FileShare.Read && fileShare != FileShare.ReadWrite) - { - throw new ArgumentException("FileShare must be either Read or ReadWrite"); - } - - if (string.IsNullOrEmpty(options.ContentType)) - { - options.ContentType = MimeTypes.GetMimeType(path); - } - - if (!options.DateLastModified.HasValue) - { - options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path); - } - - options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare)); - - options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary(StringComparer.OrdinalIgnoreCase); - - return GetStaticResult(requestContext, options); - } - - /// - /// Gets the file stream. - /// - /// The path. - /// The file share. - /// Stream. - private Stream GetFileStream(string path, FileShare fileShare) - { - return new FileStream(path, FileMode.Open, FileAccess.Read, fileShare); - } - - public Task GetStaticResult(IRequest requestContext, - Guid cacheKey, - DateTime? lastDateModified, - TimeSpan? cacheDuration, - string contentType, - Func> factoryFn, - IDictionary responseHeaders = null, - bool isHeadRequest = false) - { - return GetStaticResult(requestContext, new StaticResultOptions - { - CacheDuration = cacheDuration, - ContentFactory = factoryFn, - ContentType = contentType, - DateLastModified = lastDateModified, - IsHeadRequest = isHeadRequest, - ResponseHeaders = responseHeaders - }); - } - - public async Task GetStaticResult(IRequest requestContext, StaticResultOptions options) - { - options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary(StringComparer.OrdinalIgnoreCase); - - var contentType = options.ContentType; - if (!StringValues.IsNullOrEmpty(requestContext.Headers[HeaderNames.IfModifiedSince])) - { - // See if the result is already cached in the browser - var result = GetCachedResult(requestContext, options.ResponseHeaders, options); - - if (result != null) - { - return result; - } - } - - // TODO: We don't really need the option value - var isHeadRequest = options.IsHeadRequest || string.Equals(requestContext.Verb, "HEAD", StringComparison.OrdinalIgnoreCase); - var factoryFn = options.ContentFactory; - var responseHeaders = options.ResponseHeaders; - AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified); - AddAgeHeader(responseHeaders, options.DateLastModified); - - var rangeHeader = requestContext.Headers[HeaderNames.Range]; - - if (!isHeadRequest && !string.IsNullOrEmpty(options.Path)) - { - var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem, _streamHelper) - { - OnComplete = options.OnComplete, - OnError = options.OnError, - FileShare = options.FileShare - }; - - AddResponseHeaders(hasHeaders, options.ResponseHeaders); - return hasHeaders; - } - - var stream = await factoryFn().ConfigureAwait(false); - - var totalContentLength = options.ContentLength; - if (!totalContentLength.HasValue) - { - try - { - totalContentLength = stream.Length; - } - catch (NotSupportedException) - { - } - } - - if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue) - { - var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest) - { - OnComplete = options.OnComplete - }; - - AddResponseHeaders(hasHeaders, options.ResponseHeaders); - return hasHeaders; - } - else - { - if (totalContentLength.HasValue) - { - responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture); - } - - if (isHeadRequest) - { - using (stream) - { - return GetHttpResult(requestContext, Array.Empty(), contentType, true, responseHeaders); - } - } - - var hasHeaders = new StreamWriter(stream, contentType) - { - OnComplete = options.OnComplete, - OnError = options.OnError - }; - - AddResponseHeaders(hasHeaders, options.ResponseHeaders); - return hasHeaders; - } - } - - /// - /// Adds the caching responseHeaders. - /// - private void AddCachingHeaders( - IDictionary responseHeaders, - TimeSpan? cacheDuration, - bool noCache, - DateTime? lastModifiedDate) - { - if (noCache) - { - responseHeaders[HeaderNames.CacheControl] = "no-cache, no-store, must-revalidate"; - responseHeaders[HeaderNames.Pragma] = "no-cache, no-store, must-revalidate"; - return; - } - - if (cacheDuration.HasValue) - { - responseHeaders[HeaderNames.CacheControl] = "public, max-age=" + cacheDuration.Value.TotalSeconds; - } - else - { - responseHeaders[HeaderNames.CacheControl] = "public"; - } - - if (lastModifiedDate.HasValue) - { - responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture); - } - } - - /// - /// Adds the age header. - /// - /// The responseHeaders. - /// The last date modified. - private static void AddAgeHeader(IDictionary responseHeaders, DateTime? lastDateModified) - { - if (lastDateModified.HasValue) - { - responseHeaders[HeaderNames.Age] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture); - } - } - - /// - /// Determines whether [is not modified] [the specified if modified since]. - /// - /// If modified since. - /// Duration of the cache. - /// The date modified. - /// true if [is not modified] [the specified if modified since]; otherwise, false. - private bool IsNotModified(DateTime ifModifiedSince, TimeSpan? cacheDuration, DateTime? dateModified) - { - if (dateModified.HasValue) - { - var lastModified = NormalizeDateForComparison(dateModified.Value); - ifModifiedSince = NormalizeDateForComparison(ifModifiedSince); - - return lastModified <= ifModifiedSince; - } - - if (cacheDuration.HasValue) - { - var cacheExpirationDate = ifModifiedSince.Add(cacheDuration.Value); - - if (DateTime.UtcNow < cacheExpirationDate) - { - return true; - } - } - - return false; - } - - - /// - /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that. - /// - /// The date. - /// DateTime. - private static DateTime NormalizeDateForComparison(DateTime date) - { - return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind); - } - - /// - /// Adds the response headers. - /// - /// The has options. - /// The response headers. - private static void AddResponseHeaders(IHasHeaders hasHeaders, IEnumerable> responseHeaders) - { - foreach (var item in responseHeaders) - { - hasHeaders.Headers[item.Key] = item.Value; - } - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs deleted file mode 100644 index 980c2cd3a..000000000 --- a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ /dev/null @@ -1,212 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpServer -{ - public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult - { - private const int BufferSize = 81920; - - private readonly Dictionary _options = new Dictionary(); - - private List> _requestedRanges; - - /// - /// Initializes a new instance of the class. - /// - /// The range header. - /// The content length. - /// The source. - /// Type of the content. - /// if set to true [is head request]. - public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest) - { - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - RangeHeader = rangeHeader; - SourceStream = source; - IsHeadRequest = isHeadRequest; - - ContentType = contentType; - Headers[HeaderNames.ContentType] = contentType; - Headers[HeaderNames.AcceptRanges] = "bytes"; - StatusCode = HttpStatusCode.PartialContent; - - SetRangeValues(contentLength); - } - - /// - /// Gets or sets the source stream. - /// - /// The source stream. - private Stream SourceStream { get; set; } - private string RangeHeader { get; set; } - private bool IsHeadRequest { get; set; } - - private long RangeStart { get; set; } - private long RangeEnd { get; set; } - private long RangeLength { get; set; } - private long TotalContentLength { get; set; } - - public Action OnComplete { get; set; } - - /// - /// Additional HTTP Headers - /// - /// The headers. - public IDictionary Headers => _options; - - /// - /// Gets the requested ranges. - /// - /// The requested ranges. - protected List> RequestedRanges - { - get - { - if (_requestedRanges == null) - { - _requestedRanges = new List>(); - - // Example: bytes=0-,32-63 - var ranges = RangeHeader.Split('=')[1].Split(','); - - foreach (var range in ranges) - { - var vals = range.Split('-'); - - long start = 0; - long? end = null; - - if (!string.IsNullOrEmpty(vals[0])) - { - start = long.Parse(vals[0], CultureInfo.InvariantCulture); - } - - if (!string.IsNullOrEmpty(vals[1])) - { - end = long.Parse(vals[1], CultureInfo.InvariantCulture); - } - - _requestedRanges.Add(new KeyValuePair(start, end)); - } - } - - return _requestedRanges; - } - } - - public string ContentType { get; set; } - - public IRequest RequestContext { get; set; } - - public object Response { get; set; } - - public int Status { get; set; } - - public HttpStatusCode StatusCode - { - get => (HttpStatusCode)Status; - set => Status = (int)value; - } - - /// - /// Sets the range values. - /// - private void SetRangeValues(long contentLength) - { - var requestedRange = RequestedRanges[0]; - - TotalContentLength = contentLength; - - // If the requested range is "0-", we can optimize by just doing a stream copy - if (!requestedRange.Value.HasValue) - { - RangeEnd = TotalContentLength - 1; - } - else - { - RangeEnd = requestedRange.Value.Value; - } - - RangeStart = requestedRange.Key; - RangeLength = 1 + RangeEnd - RangeStart; - - Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture); - Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; - - if (RangeStart > 0 && SourceStream.CanSeek) - { - SourceStream.Position = RangeStart; - } - } - - public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) - { - try - { - // Headers only - if (IsHeadRequest) - { - return; - } - - using (var source = SourceStream) - { - // If the requested range is "0-", we can optimize by just doing a stream copy - if (RangeEnd >= TotalContentLength - 1) - { - await source.CopyToAsync(responseStream, BufferSize, cancellationToken).ConfigureAwait(false); - } - else - { - await CopyToInternalAsync(source, responseStream, RangeLength, cancellationToken).ConfigureAwait(false); - } - } - } - finally - { - OnComplete?.Invoke(); - } - } - - private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) - { - var array = ArrayPool.Shared.Rent(BufferSize); - try - { - int bytesRead; - while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0) - { - var bytesToCopy = Math.Min(bytesRead, copyLength); - - await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy), cancellationToken).ConfigureAwait(false); - - copyLength -= bytesToCopy; - - if (copyLength <= 0) - { - break; - } - } - } - finally - { - ArrayPool.Shared.Return(array); - } - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs deleted file mode 100644 index a8cd2ac8f..000000000 --- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Globalization; -using System.Text; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpServer -{ - /// - /// Class ResponseFilter. - /// - public class ResponseFilter - { - private readonly IHttpServer _server; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP server. - /// The logger. - public ResponseFilter(IHttpServer server, ILogger logger) - { - _server = server; - _logger = logger; - } - - /// - /// Filters the response. - /// - /// The req. - /// The res. - /// The dto. - public void FilterResponse(IRequest req, HttpResponse res, object dto) - { - foreach(var (key, value) in _server.GetDefaultCorsHeaders(req)) - { - res.Headers.Add(key, value); - } - // Try to prevent compatibility view - res.Headers["Access-Control-Allow-Headers"] = "Accept, Accept-Language, Authorization, Cache-Control, " + - "Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " + - "Content-Type, Cookie, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, " + - "Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, " + - "X-Emby-Authorization"; - - if (dto is Exception exception) - { - _logger.LogError(exception, "Error processing request for {RawUrl}", req.RawUrl); - - if (!string.IsNullOrEmpty(exception.Message)) - { - var error = exception.Message.Replace(Environment.NewLine, " ", StringComparison.Ordinal); - error = RemoveControlCharacters(error); - - res.Headers.Add("X-Application-Error-Code", error); - } - } - - if (dto is IHasHeaders hasHeaders) - { - if (!hasHeaders.Headers.ContainsKey(HeaderNames.Server)) - { - hasHeaders.Headers[HeaderNames.Server] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50"; - } - - // Content length has to be explicitly set on on HttpListenerResponse or it won't be happy - if (hasHeaders.Headers.TryGetValue(HeaderNames.ContentLength, out string contentLength) - && !string.IsNullOrEmpty(contentLength)) - { - var length = long.Parse(contentLength, CultureInfo.InvariantCulture); - - if (length > 0) - { - res.ContentLength = length; - } - } - } - } - - /// - /// Removes the control characters. - /// - /// The in string. - /// System.String. - public static string RemoveControlCharacters(string inString) - { - if (inString == null) - { - return null; - } - else if (inString.Length == 0) - { - return inString; - } - - var newString = new StringBuilder(inString.Length); - - foreach (var ch in inString) - { - if (!char.IsControl(ch)) - { - newString.Append(ch); - } - } - - return newString.ToString(); - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 76c1d9bac..68d981ad1 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -1,17 +1,7 @@ #pragma warning disable CS1591 -using System; -using System.Linq; -using Emby.Server.Implementations.SocketSharp; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Security; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; namespace Emby.Server.Implementations.HttpServer.Security @@ -19,32 +9,11 @@ namespace Emby.Server.Implementations.HttpServer.Security public class AuthService : IAuthService { private readonly IAuthorizationContext _authorizationContext; - private readonly ISessionManager _sessionManager; - private readonly IServerConfigurationManager _config; - private readonly INetworkManager _networkManager; public AuthService( - IAuthorizationContext authorizationContext, - IServerConfigurationManager config, - ISessionManager sessionManager, - INetworkManager networkManager) + IAuthorizationContext authorizationContext) { _authorizationContext = authorizationContext; - _config = config; - _sessionManager = sessionManager; - _networkManager = networkManager; - } - - public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes) - { - ValidateUser(request, authAttributes); - } - - public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes) - { - var req = new WebSocketSharpRequest(request, null, request.Path); - var user = ValidateUser(req, authAttributes); - return user; } public AuthorizationInfo Authenticate(HttpRequest request) @@ -62,185 +31,5 @@ namespace Emby.Server.Implementations.HttpServer.Security return auth; } - - private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes) - { - // This code is executed before the service - var auth = _authorizationContext.GetAuthorizationInfo(request); - - if (!IsExemptFromAuthenticationToken(authAttributes, request)) - { - ValidateSecurityToken(request, auth.Token); - } - - if (authAttributes.AllowLocalOnly && !request.IsLocal) - { - throw new SecurityException("Operation not found."); - } - - var user = auth.User; - - if (user == null && auth.UserId != Guid.Empty) - { - throw new AuthenticationException("User with Id " + auth.UserId + " not found"); - } - - if (user != null) - { - ValidateUserAccess(user, request, authAttributes); - } - - var info = GetTokenInfo(request); - - if (!IsExemptFromRoles(auth, authAttributes, request, info)) - { - var roles = authAttributes.GetRoles(); - - ValidateRoles(roles, user); - } - - if (!string.IsNullOrEmpty(auth.DeviceId) && - !string.IsNullOrEmpty(auth.Client) && - !string.IsNullOrEmpty(auth.Device)) - { - _sessionManager.LogSessionActivity( - auth.Client, - auth.Version, - auth.DeviceId, - auth.Device, - request.RemoteIp, - user); - } - - return user; - } - - private void ValidateUserAccess( - User user, - IRequest request, - IAuthenticationAttributes authAttributes) - { - if (user.HasPermission(PermissionKind.IsDisabled)) - { - throw new SecurityException("User account has been disabled."); - } - - if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(request.RemoteIp)) - { - throw new SecurityException("User account has been disabled."); - } - - if (!user.HasPermission(PermissionKind.IsAdministrator) - && !authAttributes.EscapeParentalControl - && !user.IsParentalScheduleAllowed()) - { - request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl"); - - throw new SecurityException("This user account is not allowed access at this time."); - } - } - - private bool IsExemptFromAuthenticationToken(IAuthenticationAttributes authAttribtues, IRequest request) - { - if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard) - { - return true; - } - - if (authAttribtues.AllowLocal && request.IsLocal) - { - return true; - } - - if (authAttribtues.AllowLocalOnly && request.IsLocal) - { - return true; - } - - if (authAttribtues.IgnoreLegacyAuth) - { - return true; - } - - return false; - } - - private bool IsExemptFromRoles(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, IRequest request, AuthenticationInfo tokenInfo) - { - if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard) - { - return true; - } - - if (authAttribtues.AllowLocal && request.IsLocal) - { - return true; - } - - if (authAttribtues.AllowLocalOnly && request.IsLocal) - { - return true; - } - - if (string.IsNullOrEmpty(auth.Token)) - { - return true; - } - - if (tokenInfo != null && tokenInfo.UserId.Equals(Guid.Empty)) - { - return true; - } - - return false; - } - - private static void ValidateRoles(string[] roles, User user) - { - if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase)) - { - if (user == null || !user.HasPermission(PermissionKind.IsAdministrator)) - { - throw new SecurityException("User does not have admin access."); - } - } - - if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase)) - { - if (user == null || !user.HasPermission(PermissionKind.EnableContentDeletion)) - { - throw new SecurityException("User does not have delete access."); - } - } - - if (roles.Contains("download", StringComparer.OrdinalIgnoreCase)) - { - if (user == null || !user.HasPermission(PermissionKind.EnableContentDownloading)) - { - throw new SecurityException("User does not have download access."); - } - } - } - - private static AuthenticationInfo GetTokenInfo(IRequest request) - { - request.Items.TryGetValue("OriginalAuthenticationInfo", out var info); - return info as AuthenticationInfo; - } - - private void ValidateSecurityToken(IRequest request, string token) - { - if (string.IsNullOrEmpty(token)) - { - throw new AuthenticationException("Access token is required."); - } - - var info = GetTokenInfo(request); - - if (info == null) - { - throw new AuthenticationException("Access token is invalid or expired."); - } - } } } diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index fb93fae3e..4b407dd9d 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -7,7 +7,6 @@ using System.Net; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; @@ -24,14 +23,9 @@ namespace Emby.Server.Implementations.HttpServer.Security _userManager = userManager; } - public AuthorizationInfo GetAuthorizationInfo(object requestContext) + public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext) { - return GetAuthorizationInfo((IRequest)requestContext); - } - - public AuthorizationInfo GetAuthorizationInfo(IRequest requestContext) - { - if (requestContext.Items.TryGetValue("AuthorizationInfo", out var cached)) + if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached)) { return (AuthorizationInfo)cached; } @@ -52,18 +46,18 @@ namespace Emby.Server.Implementations.HttpServer.Security /// /// The HTTP req. /// Dictionary{System.StringSystem.String}. - private AuthorizationInfo GetAuthorization(IRequest httpReq) + private AuthorizationInfo GetAuthorization(HttpContext httpReq) { var auth = GetAuthorizationDictionary(httpReq); var (authInfo, originalAuthInfo) = - GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString); + GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query); if (originalAuthInfo != null) { - httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo; + httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo; } - httpReq.Items["AuthorizationInfo"] = authInfo; + httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo; return authInfo; } @@ -203,13 +197,13 @@ namespace Emby.Server.Implementations.HttpServer.Security /// /// The HTTP req. /// Dictionary{System.StringSystem.String}. - private Dictionary GetAuthorizationDictionary(IRequest httpReq) + private Dictionary GetAuthorizationDictionary(HttpContext httpReq) { - var auth = httpReq.Headers["X-Emby-Authorization"]; + var auth = httpReq.Request.Headers["X-Emby-Authorization"]; if (string.IsNullOrEmpty(auth)) { - auth = httpReq.Headers[HeaderNames.Authorization]; + auth = httpReq.Request.Headers[HeaderNames.Authorization]; } return GetAuthorization(auth); diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs index 03fcfa53d..8777c59b7 100644 --- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -2,11 +2,11 @@ using System; using Jellyfin.Data.Entities; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace Emby.Server.Implementations.HttpServer.Security { @@ -23,26 +23,20 @@ namespace Emby.Server.Implementations.HttpServer.Security _sessionManager = sessionManager; } - public SessionInfo GetSession(IRequest requestContext) + public SessionInfo GetSession(HttpContext requestContext) { var authorization = _authContext.GetAuthorizationInfo(requestContext); var user = authorization.User; - return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user); - } - - private AuthenticationInfo GetTokenInfo(IRequest request) - { - request.Items.TryGetValue("OriginalAuthenticationInfo", out var info); - return info as AuthenticationInfo; + return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.Request.RemoteIp(), user); } public SessionInfo GetSession(object requestContext) { - return GetSession((IRequest)requestContext); + return GetSession((HttpContext)requestContext); } - public User GetUser(IRequest requestContext) + public User GetUser(HttpContext requestContext) { var session = GetSession(requestContext); @@ -51,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security public User GetUser(object requestContext) { - return GetUser((IRequest)requestContext); + return GetUser((HttpContext)requestContext); } } } diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs deleted file mode 100644 index 00e3ab8fe..000000000 --- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpServer -{ - /// - /// Class StreamWriter. - /// - public class StreamWriter : IAsyncStreamWriter, IHasHeaders - { - /// - /// The options. - /// - private readonly IDictionary _options = new Dictionary(); - - /// - /// Initializes a new instance of the class. - /// - /// The source. - /// Type of the content. - public StreamWriter(Stream source, string contentType) - { - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - SourceStream = source; - - Headers["Content-Type"] = contentType; - - if (source.CanSeek) - { - Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture); - } - - Headers[HeaderNames.ContentType] = contentType; - } - - /// - /// Initializes a new instance of the class. - /// - /// The source. - /// Type of the content. - /// The content length. - public StreamWriter(byte[] source, string contentType, int contentLength) - { - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - SourceBytes = source; - - Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture); - Headers[HeaderNames.ContentType] = contentType; - } - - /// - /// Gets or sets the source stream. - /// - /// The source stream. - private Stream SourceStream { get; set; } - - private byte[] SourceBytes { get; set; } - - /// - /// Gets the options. - /// - /// The options. - public IDictionary Headers => _options; - - /// - /// Fires when complete. - /// - public Action OnComplete { get; set; } - - /// - /// Fires when an error occours. - /// - public Action OnError { get; set; } - - /// - public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) - { - try - { - var bytes = SourceBytes; - - if (bytes != null) - { - await responseStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false); - } - else - { - using (var src = SourceStream) - { - await src.CopyToAsync(responseStream, cancellationToken).ConfigureAwait(false); - } - } - } - catch - { - OnError?.Invoke(); - - throw; - } - finally - { - OnComplete?.Invoke(); - } - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index d738047e0..7eae4e764 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -179,7 +179,7 @@ namespace Emby.Server.Implementations.HttpServer return; } - WebSocketMessage stub; + WebSocketMessage? stub; try { @@ -209,6 +209,12 @@ namespace Emby.Server.Implementations.HttpServer return; } + if (stub == null) + { + _logger.LogError("Error processing web socket message"); + return; + } + // Tell the PipeReader how much of the buffer we have consumed reader.AdvanceTo(buffer.End); diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs new file mode 100644 index 000000000..89c1b7ea0 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs @@ -0,0 +1,102 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Threading.Tasks; +using Jellyfin.Data.Events; +using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.HttpServer +{ + public class WebSocketManager : IWebSocketManager + { + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + + private IWebSocketListener[] _webSocketListeners = Array.Empty(); + private bool _disposed = false; + + public WebSocketManager( + ILogger logger, + ILoggerFactory loggerFactory) + { + _logger = logger; + _loggerFactory = loggerFactory; + } + + public event EventHandler> WebSocketConnected; + + /// + public async Task WebSocketRequestHandler(HttpContext context) + { + if (_disposed) + { + return; + } + + try + { + _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); + + WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); + + using var connection = new WebSocketConnection( + _loggerFactory.CreateLogger(), + webSocket, + context.Connection.RemoteIpAddress, + context.Request.Query) + { + OnReceive = ProcessWebSocketMessageReceived + }; + + WebSocketConnected?.Invoke(this, new GenericEventArgs(connection)); + + await connection.ProcessAsync().ConfigureAwait(false); + _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress); + } + catch (Exception ex) // Otherwise ASP.Net will ignore the exception + { + _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress); + if (!context.Response.HasStarted) + { + context.Response.StatusCode = 500; + } + } + } + + /// + /// Adds the rest handlers. + /// + /// The web socket listeners. + public void Init(IEnumerable listeners) + { + _webSocketListeners = listeners.ToArray(); + } + + /// + /// Processes the web socket message received. + /// + /// The result. + private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result) + { + if (_disposed) + { + return Task.CompletedTask; + } + + IEnumerable GetTasks() + { + foreach (var x in _webSocketListeners) + { + yield return x.ProcessMessageAsync(result); + } + } + + return Task.WhenAll(GetTasks()); + } + } +} diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index a32b03aaa..9290dfcd0 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -6,12 +6,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using Emby.Server.Implementations.Library; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.IO; -using Emby.Server.Implementations.Library; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.IO @@ -38,6 +37,8 @@ namespace Emby.Server.Implementations.IO /// private readonly ConcurrentDictionary _tempIgnoredPaths = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private bool _disposed = false; + /// /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope. /// @@ -492,8 +493,6 @@ namespace Emby.Server.Implementations.IO } } - private bool _disposed = false; - /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// @@ -522,24 +521,4 @@ namespace Emby.Server.Implementations.IO _disposed = true; } } - - public class LibraryMonitorStartup : IServerEntryPoint - { - private readonly ILibraryMonitor _monitor; - - public LibraryMonitorStartup(ILibraryMonitor monitor) - { - _monitor = monitor; - } - - public Task RunAsync() - { - _monitor.Start(); - return Task.CompletedTask; - } - - public void Dispose() - { - } - } } diff --git a/Emby.Server.Implementations/IO/LibraryMonitorStartup.cs b/Emby.Server.Implementations/IO/LibraryMonitorStartup.cs new file mode 100644 index 000000000..c51cf0545 --- /dev/null +++ b/Emby.Server.Implementations/IO/LibraryMonitorStartup.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Plugins; + +namespace Emby.Server.Implementations.IO +{ + /// + /// which is responsible for starting the library monitor. + /// + public sealed class LibraryMonitorStartup : IServerEntryPoint + { + private readonly ILibraryMonitor _monitor; + + /// + /// Initializes a new instance of the class. + /// + /// The library monitor. + public LibraryMonitorStartup(ILibraryMonitor monitor) + { + _monitor = monitor; + } + + /// + public Task RunAsync() + { + _monitor.Start(); + return Task.CompletedTask; + } + + /// + public void Dispose() + { + } + } +} diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 7b770d940..375f09f5b 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -729,7 +729,7 @@ namespace Emby.Server.Implementations.Library Directory.CreateDirectory(rootFolderPath); var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? - ((Folder) ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath))) + ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath))) .DeepCopy(); // In case program data folder was moved @@ -771,7 +771,7 @@ namespace Emby.Server.Implementations.Library if (folder.ParentId != rootFolder.Id) { folder.ParentId = rootFolder.Id; - folder.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); + folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult(); } rootFolder.AddVirtualChild(folder); @@ -1868,7 +1868,8 @@ namespace Emby.Server.Implementations.Library return image.Path != null && !image.IsLocalFile; } - public void UpdateImages(BaseItem item, bool forceUpdate = false) + /// + public async Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false) { if (item == null) { @@ -1891,7 +1892,7 @@ namespace Emby.Server.Implementations.Library try { var index = item.GetImageIndex(img); - image = ConvertImageToLocal(item, img, index).ConfigureAwait(false).GetAwaiter().GetResult(); + image = await ConvertImageToLocal(item, img, index).ConfigureAwait(false); } catch (ArgumentException) { @@ -1913,7 +1914,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Cannnot get image dimensions for {0}", image.Path); + _logger.LogError(ex, "Cannot get image dimensions for {0}", image.Path); image.Width = 0; image.Height = 0; continue; @@ -1943,10 +1944,8 @@ namespace Emby.Server.Implementations.Library RegisterItem(item); } - /// - /// Updates the item. - /// - public void UpdateItems(IReadOnlyList items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) + /// + public async Task UpdateItemsAsync(IReadOnlyList items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) { foreach (var item in items) { @@ -1957,7 +1956,7 @@ namespace Emby.Server.Implementations.Library item.DateLastSaved = DateTime.UtcNow; - UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate); + await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false); } _itemRepository.SaveItems(items, cancellationToken); @@ -1991,17 +1990,9 @@ namespace Emby.Server.Implementations.Library } } - /// - /// Updates the item. - /// - /// The item. - /// The parent item. - /// The update reason. - /// The cancellation token. - public void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) - { - UpdateItems(new[] { item }, parent, updateReason, cancellationToken); - } + /// + public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) + => UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken); /// /// Reports the item removed. @@ -2233,7 +2224,7 @@ namespace Emby.Server.Implementations.Library if (refresh) { - item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); + item.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult(); ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal); } @@ -2420,7 +2411,7 @@ namespace Emby.Server.Implementations.Library if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase)) { item.ViewType = viewType; - item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult(); } var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; @@ -2902,7 +2893,7 @@ namespace Emby.Server.Implementations.Library await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).ConfigureAwait(false); - item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); return item.GetImageInfo(image.Type, imageIndex); } @@ -2920,7 +2911,7 @@ namespace Emby.Server.Implementations.Library // Remove this image to prevent it from retrying over and over item.RemoveImage(image); - item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); throw new InvalidOperationException(); } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 80e09f0a3..09c52d95b 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using System.Xml; using Emby.Server.Implementations.Library; using Jellyfin.Data.Enums; +using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -29,7 +30,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index d8ec107ec..612dc5238 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -230,7 +230,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (filters.Count > 0) { - output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray())); + output += string.Format(CultureInfo.InvariantCulture, " -vf \"{0}\"", string.Join(",", filters.ToArray())); } return output; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs index 69a9cb78a..a2ec2df37 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs @@ -5,7 +5,7 @@ using MediaBrowser.Controller.Plugins; namespace Emby.Server.Implementations.LiveTv.EmbyTV { - public class EntryPoint : IServerEntryPoint + public sealed class EntryPoint : IServerEntryPoint { /// public Task RunAsync() diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index 285a59a24..dd479b7d1 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -5,8 +5,8 @@ using System.Collections.Concurrent; using System.Globalization; using System.Linq; using System.Threading; +using Jellyfin.Data.Events; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Events; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 77a7069eb..c4d5cc58a 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -929,7 +929,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings private static string NormalizeName(string value) { - return value.Replace(" ", string.Empty).Replace("-", string.Empty); + return value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("-", string.Empty, StringComparison.Ordinal); } public class ScheduleDirect diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index 0a93c4674..f33d07174 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -237,7 +237,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings && !programInfo.IsRepeat && (programInfo.EpisodeNumber ?? 0) == 0) { - programInfo.ShowId = programInfo.ShowId + programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture); + programInfo.ShowId += programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture); } } else @@ -246,7 +246,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings } // Construct an id from the channel and start date - programInfo.Id = string.Format("{0}_{1:O}", program.ChannelId, program.StartDate); + programInfo.Id = string.Format(CultureInfo.InvariantCulture, "{0}_{1:O}", program.ChannelId, program.StartDate); if (programInfo.IsMovie) { @@ -296,7 +296,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings Name = c.DisplayName, ImageUrl = c.Icon != null && !string.IsNullOrEmpty(c.Icon.Source) ? c.Icon.Source : null, Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number - }).ToList(); } } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 1b075d86a..a898a564f 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Emby.Server.Implementations.Library; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; @@ -24,7 +25,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; @@ -41,6 +41,7 @@ namespace Emby.Server.Implementations.LiveTv /// public class LiveTvManager : ILiveTvManager, IDisposable { + private const int MaxGuideDays = 14; private const string ExternalServiceTag = "ExternalServiceId"; private const string EtagKey = "ProgramEtag"; @@ -421,7 +422,7 @@ namespace Emby.Server.Implementations.LiveTv } } - private LiveTvChannel GetChannel(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken) + private async Task GetChannelAsync(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken) { var parentFolderId = parentFolder.Id; var isNew = false; @@ -511,7 +512,7 @@ namespace Emby.Server.Implementations.LiveTv } else if (forceUpdate) { - _libraryManager.UpdateItem(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken); + await _libraryManager.UpdateItemAsync(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); } return item; @@ -560,7 +561,7 @@ namespace Emby.Server.Implementations.LiveTv item.Audio = info.Audio; item.ChannelId = channel.Id; - item.CommunityRating = item.CommunityRating ?? info.CommunityRating; + item.CommunityRating ??= info.CommunityRating; if ((item.CommunityRating ?? 0).Equals(0)) { item.CommunityRating = null; @@ -645,8 +646,8 @@ namespace Emby.Server.Implementations.LiveTv item.IsSeries = isSeries; item.Name = info.Name; - item.OfficialRating = item.OfficialRating ?? info.OfficialRating; - item.Overview = item.Overview ?? info.Overview; + item.OfficialRating ??= info.OfficialRating; + item.Overview ??= info.Overview; item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks; item.ProviderIds = info.ProviderIds; @@ -683,19 +684,23 @@ namespace Emby.Server.Implementations.LiveTv { if (!string.IsNullOrWhiteSpace(info.ImagePath)) { - item.SetImage(new ItemImageInfo - { - Path = info.ImagePath, - Type = ImageType.Primary - }, 0); + item.SetImage( + new ItemImageInfo + { + Path = info.ImagePath, + Type = ImageType.Primary + }, + 0); } else if (!string.IsNullOrWhiteSpace(info.ImageUrl)) { - item.SetImage(new ItemImageInfo - { - Path = info.ImageUrl, - Type = ImageType.Primary - }, 0); + item.SetImage( + new ItemImageInfo + { + Path = info.ImageUrl, + Type = ImageType.Primary + }, + 0); } } @@ -703,11 +708,13 @@ namespace Emby.Server.Implementations.LiveTv { if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl)) { - item.SetImage(new ItemImageInfo - { - Path = info.ThumbImageUrl, - Type = ImageType.Thumb - }, 0); + item.SetImage( + new ItemImageInfo + { + Path = info.ThumbImageUrl, + Type = ImageType.Thumb + }, + 0); } } @@ -715,11 +722,13 @@ namespace Emby.Server.Implementations.LiveTv { if (!string.IsNullOrWhiteSpace(info.LogoImageUrl)) { - item.SetImage(new ItemImageInfo - { - Path = info.LogoImageUrl, - Type = ImageType.Logo - }, 0); + item.SetImage( + new ItemImageInfo + { + Path = info.LogoImageUrl, + Type = ImageType.Logo + }, + 0); } } @@ -727,11 +736,13 @@ namespace Emby.Server.Implementations.LiveTv { if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl)) { - item.SetImage(new ItemImageInfo - { - Path = info.BackdropImageUrl, - Type = ImageType.Backdrop - }, 0); + item.SetImage( + new ItemImageInfo + { + Path = info.BackdropImageUrl, + Type = ImageType.Backdrop + }, + 0); } } @@ -786,7 +797,6 @@ namespace Emby.Server.Implementations.LiveTv if (query.OrderBy.Count == 0) { - // Unless something else was specified, order by start date to take advantage of a specialized index query.OrderBy = new[] { @@ -824,7 +834,7 @@ namespace Emby.Server.Implementations.LiveTv if (!string.IsNullOrWhiteSpace(query.SeriesTimerId)) { - var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery { }, cancellationToken).ConfigureAwait(false); + var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false); var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.Id).ToString("N", CultureInfo.InvariantCulture), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase)); if (seriesTimer != null) { @@ -847,13 +857,11 @@ namespace Emby.Server.Implementations.LiveTv var returnArray = _dtoService.GetBaseItemDtos(queryResult.Items, options, user); - var result = new QueryResult + return new QueryResult { Items = returnArray, TotalRecordCount = queryResult.TotalRecordCount }; - - return result; } public QueryResult GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken) @@ -1121,7 +1129,7 @@ namespace Emby.Server.Implementations.LiveTv try { - var item = GetChannel(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken); + var item = await GetChannelAsync(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken).ConfigureAwait(false); list.Add(item); } @@ -1138,7 +1146,7 @@ namespace Emby.Server.Implementations.LiveTv double percent = numComplete; percent /= allChannelsList.Count; - progress.Report(5 * percent + 10); + progress.Report((5 * percent) + 10); } progress.Report(15); @@ -1173,7 +1181,6 @@ namespace Emby.Server.Implementations.LiveTv var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, ChannelIds = new Guid[] { currentChannel.Id }, DtoOptions = new DtoOptions(true) @@ -1214,7 +1221,11 @@ namespace Emby.Server.Implementations.LiveTv if (updatedPrograms.Count > 0) { - _libraryManager.UpdateItems(updatedPrograms, currentChannel, ItemUpdateType.MetadataImport, cancellationToken); + await _libraryManager.UpdateItemsAsync( + updatedPrograms, + currentChannel, + ItemUpdateType.MetadataImport, + cancellationToken).ConfigureAwait(false); } currentChannel.IsMovie = isMovie; @@ -1227,7 +1238,7 @@ namespace Emby.Server.Implementations.LiveTv currentChannel.AddTag("Kids"); } - currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken); + await currentChannel.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); await currentChannel.RefreshMetadata( new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { @@ -1298,8 +1309,6 @@ namespace Emby.Server.Implementations.LiveTv } } - private const int MaxGuideDays = 14; - private double GetGuideDays() { var config = GetConfiguration(); @@ -1712,7 +1721,7 @@ namespace Emby.Server.Implementations.LiveTv if (timer == null) { - throw new ResourceNotFoundException(string.Format("Timer with Id {0} not found", id)); + throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "Timer with Id {0} not found", id)); } var service = GetService(timer.ServiceName); @@ -1731,7 +1740,7 @@ namespace Emby.Server.Implementations.LiveTv if (timer == null) { - throw new ResourceNotFoundException(string.Format("SeriesTimer with Id {0} not found", id)); + throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "SeriesTimer with Id {0} not found", id)); } var service = GetService(timer.ServiceName); @@ -1743,10 +1752,12 @@ namespace Emby.Server.Implementations.LiveTv public async Task GetTimer(string id, CancellationToken cancellationToken) { - var results = await GetTimers(new TimerQuery - { - Id = id - }, cancellationToken).ConfigureAwait(false); + var results = await GetTimers( + new TimerQuery + { + Id = id + }, + cancellationToken).ConfigureAwait(false); return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); } @@ -1794,10 +1805,7 @@ namespace Emby.Server.Implementations.LiveTv } var returnArray = timers - .Select(i => - { - return i.Item1; - }) + .Select(i => i.Item1) .ToArray(); return new QueryResult @@ -1968,7 +1976,7 @@ namespace Emby.Server.Implementations.LiveTv if (service == null) { - service = _services.First(); + service = _services[0]; } var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false); @@ -1994,9 +2002,7 @@ namespace Emby.Server.Implementations.LiveTv { var info = await GetNewTimerDefaultsInternal(cancellationToken).ConfigureAwait(false); - var obj = _tvDtoService.GetSeriesTimerInfoDto(info.Item1, info.Item2, null); - - return obj; + return _tvDtoService.GetSeriesTimerInfoDto(info.Item1, info.Item2, null); } public async Task GetNewTimerDefaults(string programId, CancellationToken cancellationToken) @@ -2125,6 +2131,7 @@ namespace Emby.Server.Implementations.LiveTv public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } private bool _disposed = false; @@ -2447,8 +2454,7 @@ namespace Emby.Server.Implementations.LiveTv .SelectMany(i => i.Locations) .Distinct(StringComparer.OrdinalIgnoreCase) .Select(i => _libraryManager.FindByPath(i, true)) - .Where(i => i != null) - .Where(i => i.IsVisibleStandalone(user)) + .Where(i => i != null && i.IsVisibleStandalone(user)) .SelectMany(i => _libraryManager.GetCollectionFolders(i)) .GroupBy(x => x.Id) .Select(x => x.First()) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index c61189c0a..2b5f69d41 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -37,6 +37,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private readonly INetworkManager _networkManager; private readonly IStreamHelper _streamHelper; + private readonly Dictionary _modelCache = new Dictionary(); + public HdHomerunHost( IServerConfigurationManager config, ILogger logger, @@ -114,7 +116,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun }).Cast().ToList(); } - private readonly Dictionary _modelCache = new Dictionary(); private async Task GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken) { var cacheKey = info.Id; @@ -157,10 +158,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) { - var defaultValue = "HDHR"; + const string DefaultValue = "HDHR"; var response = new DiscoverResponse { - ModelNumber = defaultValue + ModelNumber = DefaultValue }; if (!string.IsNullOrEmpty(cacheKey)) { @@ -182,12 +183,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - using (var response = await _httpClient.SendAsync(new HttpRequestOptions() - { - Url = string.Format("{0}/tuners.html", GetApiUrl(info)), - CancellationToken = cancellationToken, - BufferContent = false - }, HttpMethod.Get).ConfigureAwait(false)) + using (var response = await _httpClient.SendAsync( + new HttpRequestOptions() + { + Url = string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), + CancellationToken = cancellationToken, + BufferContent = false + }, + HttpMethod.Get).ConfigureAwait(false)) using (var stream = response.Content) using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8)) { @@ -730,7 +733,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun // Need a way to set the Receive timeout on the socket otherwise this might never timeout? try { - await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken); + await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken).ConfigureAwait(false); var receiveBuffer = new byte[8192]; while (!cancellationToken.IsCancellationRequested) diff --git a/Emby.Server.Implementations/Localization/Core/es_DO.json b/Emby.Server.Implementations/Localization/Core/es_DO.json index 0ef16542f..26732eb3f 100644 --- a/Emby.Server.Implementations/Localization/Core/es_DO.json +++ b/Emby.Server.Implementations/Localization/Core/es_DO.json @@ -17,5 +17,8 @@ "Genres": "Géneros", "Folders": "Carpetas", "Favorites": "Favoritos", - "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}" + "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}", + "HeaderFavoriteSongs": "Canciones Favoritas", + "HeaderFavoriteEpisodes": "Episodios Favoritos", + "HeaderFavoriteArtists": "Artistas Favoritos" } diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json index ccb72ff93..b0dfc312e 100644 --- a/Emby.Server.Implementations/Localization/Core/id.json +++ b/Emby.Server.Implementations/Localization/Core/id.json @@ -1,7 +1,7 @@ { "Albums": "Album", "AuthenticationSucceededWithUserName": "{0} berhasil diautentikasi", - "AppDeviceValues": "Aplikasi: {0}, Alat: {1}", + "AppDeviceValues": "Aplikasi : {0}, Alat : {1}", "LabelRunningTimeValue": "Waktu berjalan: {0}", "MessageApplicationUpdatedTo": "Jellyfin Server sudah diperbarui ke {0}", "MessageApplicationUpdated": "Jellyfin Server sudah diperbarui", @@ -22,7 +22,7 @@ "HeaderContinueWatching": "Lanjutkan Menonton", "HeaderCameraUploads": "Unggahan Kamera", "HeaderAlbumArtists": "Album Artis", - "Genres": "Genre", + "Genres": "Aliran", "Folders": "Folder", "Favorites": "Favorit", "Collections": "Koleksi", diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index 1b55c2e38..d4341f2e8 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -45,7 +45,7 @@ "NameSeasonNumber": "Sesong {0}", "NameSeasonUnknown": "Sesong ukjent", "NewVersionIsAvailable": "En ny versjon av Jellyfin Server er tilgjengelig for nedlasting.", - "NotificationOptionApplicationUpdateAvailable": "Programvareoppdatering er tilgjengelig", + "NotificationOptionApplicationUpdateAvailable": "En programvareoppdatering er tilgjengelig", "NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering installert", "NotificationOptionAudioPlayback": "Lydavspilling startet", "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppet", diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 71ee6446c..648aa384b 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -21,7 +21,7 @@ "HeaderFavoriteAlbums": "Избранные альбомы", "HeaderFavoriteArtists": "Избранные исполнители", "HeaderFavoriteEpisodes": "Избранные эпизоды", - "HeaderFavoriteShows": "Избранные передачи", + "HeaderFavoriteShows": "Избранные сериалы", "HeaderFavoriteSongs": "Избранные композиции", "HeaderLiveTV": "Эфир", "HeaderNextUp": "Очередное", diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json index 576aaeb1b..3f6f3b23c 100644 --- a/Emby.Server.Implementations/Localization/Core/th.json +++ b/Emby.Server.Implementations/Localization/Core/th.json @@ -1,73 +1,117 @@ { "ProviderValue": "ผู้ให้บริการ: {0}", - "PluginUpdatedWithName": "{0} ได้รับการ update แล้ว", - "PluginUninstalledWithName": "ถอนการติดตั้ง {0}", - "PluginInstalledWithName": "{0} ได้รับการติดตั้ง", - "Plugin": "Plugin", - "Playlists": "รายการ", + "PluginUpdatedWithName": "อัปเดต {0} แล้ว", + "PluginUninstalledWithName": "ถอนการติดตั้ง {0} แล้ว", + "PluginInstalledWithName": "ติดตั้ง {0} แล้ว", + "Plugin": "ปลั๊กอิน", + "Playlists": "เพลย์ลิสต์", "Photos": "รูปภาพ", - "NotificationOptionVideoPlaybackStopped": "หยุดการเล่น Video", - "NotificationOptionVideoPlayback": "เริ่มแสดง Video", - "NotificationOptionUserLockedOut": "ผู้ใช้ Locked Out", - "NotificationOptionTaskFailed": "ตารางการทำงานล้มเหลว", - "NotificationOptionServerRestartRequired": "ควร Restart Server", - "NotificationOptionPluginUpdateInstalled": "Update Plugin แล้ว", - "NotificationOptionPluginUninstalled": "ถอด Plugin", - "NotificationOptionPluginInstalled": "ติดตั้ง Plugin แล้ว", - "NotificationOptionPluginError": "Plugin ล้มเหลว", - "NotificationOptionNewLibraryContent": "เพิ่มข้อมูลใหม่แล้ว", - "NotificationOptionInstallationFailed": "ติดตั้งล้มเหลว", - "NotificationOptionCameraImageUploaded": "รูปภาพถูก upload", - "NotificationOptionAudioPlaybackStopped": "หยุดการเล่นเสียง", + "NotificationOptionVideoPlaybackStopped": "หยุดเล่นวิดีโอ", + "NotificationOptionVideoPlayback": "เริ่มเล่นวิดีโอ", + "NotificationOptionUserLockedOut": "ผู้ใช้ถูกล็อก", + "NotificationOptionTaskFailed": "งานตามกำหนดการล้มเหลว", + "NotificationOptionServerRestartRequired": "จำเป็นต้องรีสตาร์ทเซิร์ฟเวอร์", + "NotificationOptionPluginUpdateInstalled": "ติดตั้งการอัปเดตปลั๊กอินแล้ว", + "NotificationOptionPluginUninstalled": "ถอนการติดตั้งปลั๊กอินแล้ว", + "NotificationOptionPluginInstalled": "ติดตั้งปลั๊กอินแล้ว", + "NotificationOptionPluginError": "ปลั๊กอินล้มเหลว", + "NotificationOptionNewLibraryContent": "เพิ่มเนื้อหาใหม่แล้ว", + "NotificationOptionInstallationFailed": "การติดตั้งล้มเหลว", + "NotificationOptionCameraImageUploaded": "อัปโหลดภาพถ่ายแล้ว", + "NotificationOptionAudioPlaybackStopped": "หยุดเล่นเสียง", "NotificationOptionAudioPlayback": "เริ่มเล่นเสียง", - "NotificationOptionApplicationUpdateInstalled": "Update ระบบแล้ว", - "NotificationOptionApplicationUpdateAvailable": "ระบบ update สามารถใช้ได้แล้ว", - "NewVersionIsAvailable": "ตรวจพบ Jellyfin เวอร์ชั่นใหม่", - "NameSeasonUnknown": "ไม่ทราบปี", - "NameSeasonNumber": "ปี {0}", - "NameInstallFailed": "{0} ติดตั้งไม่สำเร็จ", - "MusicVideos": "MV", - "Music": "เพลง", - "Movies": "ภาพยนต์", - "MixedContent": "รายการแบบผสม", - "MessageServerConfigurationUpdated": "การตั้งค่า update แล้ว", - "MessageNamedServerConfigurationUpdatedWithValue": "รายการตั้งค่า {0} ได้รับการ update แล้ว", - "MessageApplicationUpdatedTo": "Jellyfin Server จะ update ไปที่ {0}", - "MessageApplicationUpdated": "Jellyfin Server update แล้ว", + "NotificationOptionApplicationUpdateInstalled": "ติดตั้งการอัปเดตแอพพลิเคชันแล้ว", + "NotificationOptionApplicationUpdateAvailable": "มีการอัปเดตแอพพลิเคชัน", + "NewVersionIsAvailable": "เวอร์ชันใหม่ของเซิร์ฟเวอร์ Jellyfin พร้อมให้ดาวน์โหลดแล้ว", + "NameSeasonUnknown": "ไม่ทราบซีซัน", + "NameSeasonNumber": "ซีซัน {0}", + "NameInstallFailed": "การติดตั้ง {0} ล้มเหลว", + "MusicVideos": "มิวสิควิดีโอ", + "Music": "ดนตรี", + "Movies": "ภาพยนตร์", + "MixedContent": "เนื้อหาผสม", + "MessageServerConfigurationUpdated": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์แล้ว", + "MessageNamedServerConfigurationUpdatedWithValue": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์ในส่วน {0} แล้ว", + "MessageApplicationUpdatedTo": "เซิร์ฟเวอร์ Jellyfin ได้รับการอัปเดตเป็น {0}", + "MessageApplicationUpdated": "อัพเดตเซิร์ฟเวอร์ Jellyfin แล้ว", "Latest": "ล่าสุด", - "LabelRunningTimeValue": "เวลาที่เล่น : {0}", - "LabelIpAddressValue": "IP address: {0}", - "ItemRemovedWithName": "{0} ถูกลบจากรายการ", - "ItemAddedWithName": "{0} ถูกเพิ่มในรายการ", - "Inherit": "การสืบทอด", - "HomeVideos": "วีดีโอส่วนตัว", - "HeaderRecordingGroups": "ค่ายบันทึก", + "LabelRunningTimeValue": "ผ่านไปแล้ว: {0}", + "LabelIpAddressValue": "ที่อยู่ IP: {0}", + "ItemRemovedWithName": "{0} ถูกลบออกจากไลบรารี", + "ItemAddedWithName": "{0} ถูกเพิ่มลงในไลบรารีแล้ว", + "Inherit": "สืบทอด", + "HomeVideos": "โฮมวิดีโอ", + "HeaderRecordingGroups": "กลุ่มการบันทึก", "HeaderNextUp": "ถัดไป", - "HeaderLiveTV": "รายการสด", - "HeaderFavoriteSongs": "เพลงโปรด", - "HeaderFavoriteShows": "รายการโชว์โปรด", - "HeaderFavoriteEpisodes": "ฉากโปรด", - "HeaderFavoriteArtists": "นักแสดงโปรด", - "HeaderFavoriteAlbums": "อัมบั้มโปรด", - "HeaderContinueWatching": "ชมต่อจากเดิม", - "HeaderCameraUploads": "Upload รูปภาพ", - "HeaderAlbumArtists": "อัลบั้มนักแสดง", + "HeaderLiveTV": "ทีวีสด", + "HeaderFavoriteSongs": "เพลงที่ชื่นชอบ", + "HeaderFavoriteShows": "รายการที่ชื่นชอบ", + "HeaderFavoriteEpisodes": "ตอนที่ชื่นชอบ", + "HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ", + "HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ", + "HeaderContinueWatching": "ดูต่อ", + "HeaderCameraUploads": "อัปโหลดรูปถ่าย", + "HeaderAlbumArtists": "อัลบั้มศิลปิน", "Genres": "ประเภท", "Folders": "โฟลเดอร์", "Favorites": "รายการโปรด", - "FailedLoginAttemptWithUserName": "การเชื่อมต่อล้มเหลวจาก {0}", - "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จ", - "DeviceOfflineWithName": "{0} ตัดการเชื่อมต่อ", - "Collections": "ชุด", - "ChapterNameValue": "บทที่ {0}", - "Channels": "ชาแนล", - "CameraImageUploadedFrom": "รูปภาพถูก upload จาก {0}", + "FailedLoginAttemptWithUserName": "ความพยายามในการเข้าสู่ระบบล้มเหลวจาก {0}", + "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จแล้ว", + "DeviceOfflineWithName": "{0} ยกเลิกการเชื่อมต่อแล้ว", + "Collections": "คอลเลกชัน", + "ChapterNameValue": "บท {0}", + "Channels": "ช่อง", + "CameraImageUploadedFrom": "ภาพถ่ายใหม่ได้ถูกอัปโหลดมาจาก {0}", "Books": "หนังสือ", - "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จ", - "Artists": "นักแสดง", - "Application": "แอปพลิเคชั่น", - "AppDeviceValues": "App: {0}, อุปกรณ์: {1}", + "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จแล้ว", + "Artists": "ศิลปิน", + "Application": "แอพพลิเคชัน", + "AppDeviceValues": "แอพ: {0}, อุปกรณ์: {1}", "Albums": "อัลบั้ม", "ScheduledTaskStartedWithName": "{0} เริ่มต้น", - "ScheduledTaskFailedWithName": "{0} ล้มเหลว" + "ScheduledTaskFailedWithName": "{0} ล้มเหลว", + "Songs": "เพลง", + "Shows": "รายการ", + "ServerNameNeedsToBeRestarted": "{0} ต้องการการรีสตาร์ท", + "TaskDownloadMissingSubtitlesDescription": "ค้นหาคำบรรยายที่หายไปในอินเทอร์เน็ตตามค่ากำหนดในข้อมูลเมตา", + "TaskDownloadMissingSubtitles": "ดาวน์โหลดคำบรรยายที่ขาดหายไป", + "TaskRefreshChannelsDescription": "รีเฟรชข้อมูลช่องอินเทอร์เน็ต", + "TaskRefreshChannels": "รีเฟรชช่อง", + "TaskCleanTranscodeDescription": "ลบไฟล์ทรานส์โค้ดที่มีอายุมากกว่าหนึ่งวัน", + "TaskCleanTranscode": "ล้างไดเรกทอรีทรานส์โค้ด", + "TaskUpdatePluginsDescription": "ดาวน์โหลดและติดตั้งโปรแกรมปรับปรุงให้กับปลั๊กอินที่กำหนดค่าให้อัปเดตโดยอัตโนมัติ", + "TaskUpdatePlugins": "อัปเดตปลั๊กอิน", + "TaskRefreshPeopleDescription": "อัปเดตข้อมูลเมตานักแสดงและผู้กำกับในไลบรารีสื่อ", + "TaskRefreshPeople": "รีเฟรชบุคคล", + "TaskCleanLogsDescription": "ลบไฟล์บันทึกที่เก่ากว่า {0} วัน", + "TaskCleanLogs": "ล้างไดเรกทอรีบันทึก", + "TaskRefreshLibraryDescription": "สแกนไลบรารีสื่อของคุณเพื่อหาไฟล์ใหม่และรีเฟรชข้อมูลเมตา", + "TaskRefreshLibrary": "สแกนไลบรารีสื่อ", + "TaskRefreshChapterImagesDescription": "สร้างภาพขนาดย่อสำหรับวิดีโอที่มีบท", + "TaskRefreshChapterImages": "แตกรูปภาพบท", + "TaskCleanCacheDescription": "ลบไฟล์แคชที่ระบบไม่ต้องการ", + "TaskCleanCache": "ล้างไดเรกทอรีแคช", + "TasksChannelsCategory": "ช่องอินเทอร์เน็ต", + "TasksApplicationCategory": "แอพพลิเคชัน", + "TasksLibraryCategory": "ไลบรารี", + "TasksMaintenanceCategory": "ปิดซ่อมบำรุง", + "VersionNumber": "เวอร์ชัน {0}", + "ValueSpecialEpisodeName": "พิเศษ - {0}", + "ValueHasBeenAddedToLibrary": "เพิ่ม {0} ลงในไลบรารีสื่อของคุณแล้ว", + "UserStoppedPlayingItemWithValues": "{0} เล่นเสร็จแล้ว {1} บน {2}", + "UserStartedPlayingItemWithValues": "{0} กำลังเล่น {1} บน {2}", + "UserPolicyUpdatedWithName": "มีการอัปเดตนโยบายผู้ใช้ของ {0}", + "UserPasswordChangedWithName": "มีการเปลี่ยนรหัสผ่านของผู้ใช้ {0}", + "UserOnlineFromDevice": "{0} ออนไลน์จาก {1}", + "UserOfflineFromDevice": "{0} ได้ยกเลิกการเชื่อมต่อจาก {1}", + "UserLockedOutWithName": "ผู้ใช้ {0} ถูกล็อก", + "UserDownloadingItemWithValues": "{0} กำลังดาวน์โหลด {1}", + "UserDeletedWithName": "ลบผู้ใช้ {0} แล้ว", + "UserCreatedWithName": "สร้างผู้ใช้ {0} แล้ว", + "User": "ผู้ใช้งาน", + "TvShows": "รายการทีวี", + "System": "ระบบ", + "Sync": "ซิงค์", + "SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้", + "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่" } diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 38ceadedb..d3b64fb31 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -152,10 +152,10 @@ namespace Emby.Server.Implementations.Playlists if (options.ItemIdList.Length > 0) { - AddToPlaylistInternal(playlist.Id.ToString("N", CultureInfo.InvariantCulture), options.ItemIdList, user, new DtoOptions(false) + await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false) { EnableImages = true - }); + }).ConfigureAwait(false); } return new PlaylistCreationResult(playlist.Id.ToString("N", CultureInfo.InvariantCulture)); @@ -184,17 +184,17 @@ namespace Emby.Server.Implementations.Playlists return Playlist.GetPlaylistItems(playlistMediaType, items, user, options); } - public void AddToPlaylist(string playlistId, ICollection itemIds, Guid userId) + public Task AddToPlaylistAsync(Guid playlistId, ICollection itemIds, Guid userId) { var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId); - AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false) + return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false) { EnableImages = true }); } - private void AddToPlaylistInternal(string playlistId, ICollection newItemIds, User user, DtoOptions options) + private async Task AddToPlaylistInternal(Guid playlistId, ICollection newItemIds, User user, DtoOptions options) { // Retrieve the existing playlist var playlist = _libraryManager.GetItemById(playlistId) as Playlist @@ -238,7 +238,7 @@ namespace Emby.Server.Implementations.Playlists // Update the playlist in the repository playlist.LinkedChildren = newLinkedChildren; - playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); // Update the playlist on disk if (playlist.IsFile) @@ -256,7 +256,7 @@ namespace Emby.Server.Implementations.Playlists RefreshPriority.High); } - public void RemoveFromPlaylist(string playlistId, IEnumerable entryIds) + public async Task RemoveFromPlaylistAsync(string playlistId, IEnumerable entryIds) { if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist)) { @@ -273,7 +273,7 @@ namespace Emby.Server.Implementations.Playlists .Select(i => i.Item1) .ToArray(); - playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); if (playlist.IsFile) { @@ -289,7 +289,7 @@ namespace Emby.Server.Implementations.Playlists RefreshPriority.High); } - public void MoveItem(string playlistId, string entryId, int newIndex) + public async Task MoveItemAsync(string playlistId, string entryId, int newIndex) { if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist)) { @@ -322,7 +322,7 @@ namespace Emby.Server.Implementations.Playlists playlist.LinkedChildren = newList.ToArray(); - playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); if (playlist.IsFile) { diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs new file mode 100644 index 000000000..140a67541 --- /dev/null +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs @@ -0,0 +1,285 @@ +using System; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Security.Cryptography; +using MediaBrowser.Common; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.QuickConnect; +using MediaBrowser.Controller.Security; +using MediaBrowser.Model.QuickConnect; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.QuickConnect +{ + /// + /// Quick connect implementation. + /// + public class QuickConnectManager : IQuickConnect, IDisposable + { + private readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider(); + private readonly ConcurrentDictionary _currentRequests = new ConcurrentDictionary(); + + private readonly IServerConfigurationManager _config; + private readonly ILogger _logger; + private readonly IAuthenticationRepository _authenticationRepository; + private readonly IAuthorizationContext _authContext; + private readonly IServerApplicationHost _appHost; + + /// + /// Initializes a new instance of the class. + /// Should only be called at server startup when a singleton is created. + /// + /// Configuration. + /// Logger. + /// Application host. + /// Authentication context. + /// Authentication repository. + public QuickConnectManager( + IServerConfigurationManager config, + ILogger logger, + IServerApplicationHost appHost, + IAuthorizationContext authContext, + IAuthenticationRepository authenticationRepository) + { + _config = config; + _logger = logger; + _appHost = appHost; + _authContext = authContext; + _authenticationRepository = authenticationRepository; + + ReloadConfiguration(); + } + + /// + public int CodeLength { get; set; } = 6; + + /// + public string TokenName { get; set; } = "QuickConnect"; + + /// + public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable; + + /// + public int Timeout { get; set; } = 5; + + private DateTime DateActivated { get; set; } + + /// + public void AssertActive() + { + if (State != QuickConnectState.Active) + { + throw new ArgumentException("Quick connect is not active on this server"); + } + } + + /// + public void Activate() + { + DateActivated = DateTime.UtcNow; + SetState(QuickConnectState.Active); + } + + /// + public void SetState(QuickConnectState newState) + { + _logger.LogDebug("Changed quick connect state from {State} to {newState}", State, newState); + + ExpireRequests(true); + + State = newState; + _config.Configuration.QuickConnectAvailable = newState == QuickConnectState.Available || newState == QuickConnectState.Active; + _config.SaveConfiguration(); + + _logger.LogDebug("Configuration saved"); + } + + /// + public QuickConnectResult TryConnect() + { + ExpireRequests(); + + if (State != QuickConnectState.Active) + { + _logger.LogDebug("Refusing quick connect initiation request, current state is {State}", State); + throw new AuthenticationException("Quick connect is not active on this server"); + } + + var code = GenerateCode(); + var result = new QuickConnectResult() + { + Secret = GenerateSecureRandom(), + DateAdded = DateTime.UtcNow, + Code = code + }; + + _currentRequests[code] = result; + return result; + } + + /// + public QuickConnectResult CheckRequestStatus(string secret) + { + ExpireRequests(); + AssertActive(); + + string code = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Code).DefaultIfEmpty(string.Empty).First(); + + if (!_currentRequests.TryGetValue(code, out QuickConnectResult result)) + { + throw new ResourceNotFoundException("Unable to find request with provided secret"); + } + + return result; + } + + /// + public string GenerateCode() + { + Span raw = stackalloc byte[4]; + + int min = (int)Math.Pow(10, CodeLength - 1); + int max = (int)Math.Pow(10, CodeLength); + + uint scale = uint.MaxValue; + while (scale == uint.MaxValue) + { + _rng.GetBytes(raw); + scale = BitConverter.ToUInt32(raw); + } + + int code = (int)(min + ((max - min) * (scale / (double)uint.MaxValue))); + return code.ToString(CultureInfo.InvariantCulture); + } + + /// + public bool AuthorizeRequest(Guid userId, string code) + { + ExpireRequests(); + AssertActive(); + + if (!_currentRequests.TryGetValue(code, out QuickConnectResult result)) + { + throw new ResourceNotFoundException("Unable to find request"); + } + + if (result.Authenticated) + { + throw new InvalidOperationException("Request is already authorized"); + } + + result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + + // Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated. + var added = result.DateAdded ?? DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(Timeout)); + result.DateAdded = added.Subtract(TimeSpan.FromMinutes(Timeout - 1)); + + _authenticationRepository.Create(new AuthenticationInfo + { + AppName = TokenName, + AccessToken = result.Authentication, + DateCreated = DateTime.UtcNow, + DeviceId = _appHost.SystemId, + DeviceName = _appHost.FriendlyName, + AppVersion = _appHost.ApplicationVersionString, + UserId = userId + }); + + _logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId); + + return true; + } + + /// + public int DeleteAllDevices(Guid user) + { + var raw = _authenticationRepository.Get(new AuthenticationInfoQuery() + { + DeviceId = _appHost.SystemId, + UserId = user + }); + + var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenName, StringComparison.Ordinal)); + + var removed = 0; + foreach (var token in tokens) + { + _authenticationRepository.Delete(token); + _logger.LogDebug("Deleted token {AccessToken}", token.AccessToken); + removed++; + } + + return removed; + } + + /// + /// Dispose. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Dispose. + /// + /// Dispose unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _rng?.Dispose(); + } + } + + private string GenerateSecureRandom(int length = 32) + { + Span bytes = stackalloc byte[length]; + _rng.GetBytes(bytes); + + return Hex.Encode(bytes); + } + + /// + public void ExpireRequests(bool expireAll = false) + { + // Check if quick connect should be deactivated + if (State == QuickConnectState.Active && DateTime.UtcNow > DateActivated.AddMinutes(Timeout) && !expireAll) + { + _logger.LogDebug("Quick connect time expired, deactivating"); + SetState(QuickConnectState.Available); + expireAll = true; + } + + // Expire stale connection requests + var code = string.Empty; + var values = _currentRequests.Values.ToList(); + + for (int i = 0; i < values.Count; i++) + { + var added = values[i].DateAdded ?? DateTime.UnixEpoch; + if (DateTime.UtcNow > added.AddMinutes(Timeout) || expireAll) + { + code = values[i].Code; + _logger.LogDebug("Removing expired request {code}", code); + + if (!_currentRequests.TryRemove(code, out _)) + { + _logger.LogWarning("Request {code} already expired", code); + } + } + } + } + + private void ReloadConfiguration() + { + State = _config.Configuration.QuickConnectAvailable ? QuickConnectState.Available : QuickConnectState.Unavailable; + } + } +} diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index 8a900f42c..bc01f9543 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -6,11 +6,10 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; @@ -22,37 +21,53 @@ namespace Emby.Server.Implementations.ScheduledTasks /// public class ScheduledTaskWorker : IScheduledTaskWorker { - public event EventHandler> TaskProgress; - - /// - /// Gets the scheduled task. - /// - /// The scheduled task. - public IScheduledTask ScheduledTask { get; private set; } - /// /// Gets or sets the json serializer. /// /// The json serializer. - private IJsonSerializer JsonSerializer { get; set; } + private readonly IJsonSerializer _jsonSerializer; /// /// Gets or sets the application paths. /// /// The application paths. - private IApplicationPaths ApplicationPaths { get; set; } + private readonly IApplicationPaths _applicationPaths; /// - /// Gets the logger. + /// Gets or sets the logger. /// /// The logger. - private ILogger Logger { get; set; } + private readonly ILogger _logger; /// - /// Gets the task manager. + /// Gets or sets the task manager. /// /// The task manager. - private ITaskManager TaskManager { get; set; } + private readonly ITaskManager _taskManager; + + /// + /// The _last execution result sync lock. + /// + private readonly object _lastExecutionResultSyncLock = new object(); + + private bool _readFromFile = false; + + /// + /// The _last execution result. + /// + private TaskResult _lastExecutionResult; + + private Task _currentTask; + + /// + /// The _triggers. + /// + private Tuple[] _triggers; + + /// + /// The _id. + /// + private string _id; /// /// Initializes a new instance of the class. @@ -71,7 +86,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// or /// jsonSerializer /// or - /// logger + /// logger. /// public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger) { @@ -101,23 +116,22 @@ namespace Emby.Server.Implementations.ScheduledTasks } ScheduledTask = scheduledTask; - ApplicationPaths = applicationPaths; - TaskManager = taskManager; - JsonSerializer = jsonSerializer; - Logger = logger; + _applicationPaths = applicationPaths; + _taskManager = taskManager; + _jsonSerializer = jsonSerializer; + _logger = logger; InitTriggerEvents(); } - private bool _readFromFile = false; + public event EventHandler> TaskProgress; + /// - /// The _last execution result. + /// Gets the scheduled task. /// - private TaskResult _lastExecutionResult; - /// - /// The _last execution result sync lock. - /// - private readonly object _lastExecutionResultSyncLock = new object(); + /// The scheduled task. + public IScheduledTask ScheduledTask { get; private set; } + /// /// Gets the last execution result. /// @@ -136,11 +150,11 @@ namespace Emby.Server.Implementations.ScheduledTasks { try { - _lastExecutionResult = JsonSerializer.DeserializeFromFile(path); + _lastExecutionResult = _jsonSerializer.DeserializeFromFile(path); } catch (Exception ex) { - Logger.LogError(ex, "Error deserializing {File}", path); + _logger.LogError(ex, "Error deserializing {File}", path); } } @@ -160,7 +174,7 @@ namespace Emby.Server.Implementations.ScheduledTasks lock (_lastExecutionResultSyncLock) { - JsonSerializer.SerializeToFile(value, path); + _jsonSerializer.SerializeToFile(value, path); } } } @@ -184,7 +198,7 @@ namespace Emby.Server.Implementations.ScheduledTasks public string Category => ScheduledTask.Category; /// - /// Gets the current cancellation token. + /// Gets or sets the current cancellation token. /// /// The current cancellation token source. private CancellationTokenSource CurrentCancellationTokenSource { get; set; } @@ -221,12 +235,7 @@ namespace Emby.Server.Implementations.ScheduledTasks public double? CurrentProgress { get; private set; } /// - /// The _triggers. - /// - private Tuple[] _triggers; - - /// - /// Gets the triggers that define when the task will run. + /// Gets or sets the triggers that define when the task will run. /// /// The triggers. private Tuple[] InternalTriggers @@ -255,7 +264,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// Gets the triggers that define when the task will run. /// /// The triggers. - /// value + /// value is null. public TaskTriggerInfo[] Triggers { get @@ -280,11 +289,6 @@ namespace Emby.Server.Implementations.ScheduledTasks } } - /// - /// The _id. - /// - private string _id; - /// /// Gets the unique id. /// @@ -325,9 +329,9 @@ namespace Emby.Server.Implementations.ScheduledTasks trigger.Stop(); - trigger.Triggered -= trigger_Triggered; - trigger.Triggered += trigger_Triggered; - trigger.Start(LastExecutionResult, Logger, Name, isApplicationStartup); + trigger.Triggered -= OnTriggerTriggered; + trigger.Triggered += OnTriggerTriggered; + trigger.Start(LastExecutionResult, _logger, Name, isApplicationStartup); } } @@ -336,7 +340,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// /// The source of the event. /// The instance containing the event data. - async void trigger_Triggered(object sender, EventArgs e) + private async void OnTriggerTriggered(object sender, EventArgs e) { var trigger = (ITaskTrigger)sender; @@ -347,19 +351,17 @@ namespace Emby.Server.Implementations.ScheduledTasks return; } - Logger.LogInformation("{0} fired for task: {1}", trigger.GetType().Name, Name); + _logger.LogInformation("{0} fired for task: {1}", trigger.GetType().Name, Name); trigger.Stop(); - TaskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions); + _taskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions); await Task.Delay(1000).ConfigureAwait(false); - trigger.Start(LastExecutionResult, Logger, Name, false); + trigger.Start(LastExecutionResult, _logger, Name, false); } - private Task _currentTask; - /// /// Executes the task. /// @@ -395,9 +397,9 @@ namespace Emby.Server.Implementations.ScheduledTasks CurrentCancellationTokenSource = new CancellationTokenSource(); - Logger.LogInformation("Executing {0}", Name); + _logger.LogInformation("Executing {0}", Name); - ((TaskManager)TaskManager).OnTaskExecuting(this); + ((TaskManager)_taskManager).OnTaskExecuting(this); progress.ProgressChanged += OnProgressChanged; @@ -423,7 +425,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } catch (Exception ex) { - Logger.LogError(ex, "Error"); + _logger.LogError(ex, "Error"); failureException = ex; @@ -476,7 +478,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { if (State == TaskState.Running) { - Logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name); + _logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name); CurrentCancellationTokenSource.Cancel(); } } @@ -487,7 +489,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// System.String. private string GetScheduledTasksConfigurationDirectory() { - return Path.Combine(ApplicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"); + return Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"); } /// @@ -496,7 +498,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// System.String. private string GetScheduledTasksDataDirectory() { - return Path.Combine(ApplicationPaths.DataPath, "ScheduledTasks"); + return Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"); } /// @@ -535,7 +537,7 @@ namespace Emby.Server.Implementations.ScheduledTasks TaskTriggerInfo[] list = null; if (File.Exists(path)) { - list = JsonSerializer.DeserializeFromFile(path); + list = _jsonSerializer.DeserializeFromFile(path); } // Return defaults if file doesn't exist. @@ -571,7 +573,7 @@ namespace Emby.Server.Implementations.ScheduledTasks Directory.CreateDirectory(Path.GetDirectoryName(path)); - JsonSerializer.SerializeToFile(triggers, path); + _jsonSerializer.SerializeToFile(triggers, path); } /// @@ -585,7 +587,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { var elapsedTime = endTime - startTime; - Logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds); + _logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds); var result = new TaskResult { @@ -606,7 +608,7 @@ namespace Emby.Server.Implementations.ScheduledTasks LastExecutionResult = result; - ((TaskManager)TaskManager).OnTaskCompleted(this, result); + ((TaskManager)_taskManager).OnTaskCompleted(this, result); } /// @@ -615,6 +617,7 @@ namespace Emby.Server.Implementations.ScheduledTasks public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } /// @@ -635,12 +638,12 @@ namespace Emby.Server.Implementations.ScheduledTasks { try { - Logger.LogInformation(Name + ": Cancelling"); + _logger.LogInformation(Name + ": Cancelling"); token.Cancel(); } catch (Exception ex) { - Logger.LogError(ex, "Error calling CancellationToken.Cancel();"); + _logger.LogError(ex, "Error calling CancellationToken.Cancel();"); } } @@ -649,21 +652,21 @@ namespace Emby.Server.Implementations.ScheduledTasks { try { - Logger.LogInformation(Name + ": Waiting on Task"); + _logger.LogInformation(Name + ": Waiting on Task"); var exited = Task.WaitAll(new[] { task }, 2000); if (exited) { - Logger.LogInformation(Name + ": Task exited"); + _logger.LogInformation(Name + ": Task exited"); } else { - Logger.LogInformation(Name + ": Timed out waiting for task to stop"); + _logger.LogInformation(Name + ": Timed out waiting for task to stop"); } } catch (Exception ex) { - Logger.LogError(ex, "Error calling Task.WaitAll();"); + _logger.LogError(ex, "Error calling Task.WaitAll();"); } } @@ -671,12 +674,12 @@ namespace Emby.Server.Implementations.ScheduledTasks { try { - Logger.LogDebug(Name + ": Disposing CancellationToken"); + _logger.LogDebug(Name + ": Disposing CancellationToken"); token.Dispose(); } catch (Exception ex) { - Logger.LogError(ex, "Error calling CancellationToken.Dispose();"); + _logger.LogError(ex, "Error calling CancellationToken.Dispose();"); } } @@ -692,8 +695,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// /// The info. /// BaseTaskTrigger. - /// - /// Invalid trigger type: + info.Type + /// Invalid trigger type: + info.Type. private ITaskTrigger GetTrigger(TaskTriggerInfo info) { var options = new TaskOptions @@ -765,7 +767,7 @@ namespace Emby.Server.Implementations.ScheduledTasks foreach (var triggerInfo in InternalTriggers) { var trigger = triggerInfo.Item2; - trigger.Triggered -= trigger_Triggered; + trigger.Triggered -= OnTriggerTriggered; trigger.Stop(); } } diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs index 81096026b..6f81bf49b 100644 --- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs @@ -5,8 +5,8 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; @@ -207,6 +207,7 @@ namespace Emby.Server.Implementations.ScheduledTasks public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } /// diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs index 402b39a26..54e18eaea 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; -using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks.Tasks { @@ -15,12 +16,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// public class DeleteLogFileTask : IScheduledTask, IConfigurableScheduledTask { - /// - /// Gets or sets the configuration manager. - /// - /// The configuration manager. - private IConfigurationManager ConfigurationManager { get; set; } - + private readonly IConfigurationManager _configurationManager; private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; @@ -32,18 +28,43 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// The localization manager. public DeleteLogFileTask(IConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization) { - ConfigurationManager = configurationManager; + _configurationManager = configurationManager; _fileSystem = fileSystem; _localization = localization; } + /// + public string Name => _localization.GetLocalizedString("TaskCleanLogs"); + + /// + public string Description => string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("TaskCleanLogsDescription"), + _configurationManager.CommonConfiguration.LogFileRetentionDays); + + /// + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + + /// + public string Key => "CleanLogFiles"; + + /// + public bool IsHidden => false; + + /// + public bool IsEnabled => true; + + /// + public bool IsLogged => true; + /// /// Creates the triggers that define when the task will run. /// /// IEnumerable{BaseTaskTrigger}. public IEnumerable GetDefaultTriggers() { - return new[] { + return new[] + { new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} }; } @@ -57,10 +78,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks public Task Execute(CancellationToken cancellationToken, IProgress progress) { // Delete log files more than n days old - var minDateModified = DateTime.UtcNow.AddDays(-ConfigurationManager.CommonConfiguration.LogFileRetentionDays); + var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays); // Only delete the .txt log files, the *.log files created by serilog get managed by itself - var filesToDelete = _fileSystem.GetFiles(ConfigurationManager.CommonApplicationPaths.LogDirectoryPath, new[] { ".txt" }, true, true) + var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, new[] { ".txt" }, true, true) .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified) .ToList(); @@ -83,26 +104,5 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks return Task.CompletedTask; } - - /// - public string Name => _localization.GetLocalizedString("TaskCleanLogs"); - - /// - public string Description => string.Format(_localization.GetLocalizedString("TaskCleanLogsDescription"), ConfigurationManager.CommonConfiguration.LogFileRetentionDays); - - /// - public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); - - /// - public string Key => "CleanLogFiles"; - - /// - public bool IsHidden => false; - - /// - public bool IsEnabled => true; - - /// - public bool IsLogged => true; } } diff --git a/Emby.Server.Implementations/Services/HttpResult.cs b/Emby.Server.Implementations/Services/HttpResult.cs deleted file mode 100644 index 8ba86f756..000000000 --- a/Emby.Server.Implementations/Services/HttpResult.cs +++ /dev/null @@ -1,64 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; - -namespace Emby.Server.Implementations.Services -{ - public class HttpResult - : IHttpResult, IAsyncStreamWriter - { - public HttpResult(object response, string contentType, HttpStatusCode statusCode) - { - this.Headers = new Dictionary(); - - this.Response = response; - this.ContentType = contentType; - this.StatusCode = statusCode; - } - - public object Response { get; set; } - - public string ContentType { get; set; } - - public IDictionary Headers { get; private set; } - - public int Status { get; set; } - - public HttpStatusCode StatusCode - { - get => (HttpStatusCode)Status; - set => Status = (int)value; - } - - public IRequest RequestContext { get; set; } - - public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) - { - var response = RequestContext?.Response; - - if (this.Response is byte[] bytesResponse) - { - var contentLength = bytesResponse.Length; - - if (response != null) - { - response.ContentLength = contentLength; - } - - if (contentLength > 0) - { - await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false); - } - - return; - } - - await ResponseHelper.WriteObject(this.RequestContext, this.Response, response).ConfigureAwait(false); - } - } -} diff --git a/Emby.Server.Implementations/Services/RequestHelper.cs b/Emby.Server.Implementations/Services/RequestHelper.cs deleted file mode 100644 index 1f9c7fc22..000000000 --- a/Emby.Server.Implementations/Services/RequestHelper.cs +++ /dev/null @@ -1,51 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.IO; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; - -namespace Emby.Server.Implementations.Services -{ - public class RequestHelper - { - public static Func> GetRequestReader(HttpListenerHost host, string contentType) - { - switch (GetContentTypeWithoutEncoding(contentType)) - { - case "application/xml": - case "text/xml": - case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml - return host.DeserializeXml; - - case "application/json": - case "text/json": - return host.DeserializeJson; - } - - return null; - } - - public static Action GetResponseWriter(HttpListenerHost host, string contentType) - { - switch (GetContentTypeWithoutEncoding(contentType)) - { - case "application/xml": - case "text/xml": - case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml - return host.SerializeToXml; - - case "application/json": - case "text/json": - return host.SerializeToJson; - } - - return null; - } - - private static string GetContentTypeWithoutEncoding(string contentType) - { - return contentType?.Split(';')[0].ToLowerInvariant().Trim(); - } - } -} diff --git a/Emby.Server.Implementations/Services/ResponseHelper.cs b/Emby.Server.Implementations/Services/ResponseHelper.cs deleted file mode 100644 index a329b531d..000000000 --- a/Emby.Server.Implementations/Services/ResponseHelper.cs +++ /dev/null @@ -1,141 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.IO; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; - -namespace Emby.Server.Implementations.Services -{ - public static class ResponseHelper - { - public static Task WriteToResponse(HttpResponse response, IRequest request, object result, CancellationToken cancellationToken) - { - if (result == null) - { - if (response.StatusCode == (int)HttpStatusCode.OK) - { - response.StatusCode = (int)HttpStatusCode.NoContent; - } - - response.ContentLength = 0; - return Task.CompletedTask; - } - - var httpResult = result as IHttpResult; - if (httpResult != null) - { - httpResult.RequestContext = request; - request.ResponseContentType = httpResult.ContentType ?? request.ResponseContentType; - } - - var defaultContentType = request.ResponseContentType; - - if (httpResult != null) - { - if (httpResult.RequestContext == null) - { - httpResult.RequestContext = request; - } - - response.StatusCode = httpResult.Status; - } - - if (result is IHasHeaders responseOptions) - { - foreach (var responseHeaders in responseOptions.Headers) - { - if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase)) - { - response.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture); - continue; - } - - response.Headers.Add(responseHeaders.Key, responseHeaders.Value); - } - } - - // ContentType='text/html' is the default for a HttpResponse - // Do not override if another has been set - if (response.ContentType == null || response.ContentType == "text/html") - { - response.ContentType = defaultContentType; - } - - if (response.ContentType == "application/json") - { - response.ContentType += "; charset=utf-8"; - } - - switch (result) - { - case IAsyncStreamWriter asyncStreamWriter: - return asyncStreamWriter.WriteToAsync(response.Body, cancellationToken); - case IStreamWriter streamWriter: - streamWriter.WriteTo(response.Body); - return Task.CompletedTask; - case FileWriter fileWriter: - return fileWriter.WriteToAsync(response, cancellationToken); - case Stream stream: - return CopyStream(stream, response.Body); - case byte[] bytes: - response.ContentType = "application/octet-stream"; - response.ContentLength = bytes.Length; - - if (bytes.Length > 0) - { - return response.Body.WriteAsync(bytes, 0, bytes.Length, cancellationToken); - } - - return Task.CompletedTask; - case string responseText: - var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText); - response.ContentLength = responseTextAsBytes.Length; - - if (responseTextAsBytes.Length > 0) - { - return response.Body.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken); - } - - return Task.CompletedTask; - } - - return WriteObject(request, result, response); - } - - private static async Task CopyStream(Stream src, Stream dest) - { - using (src) - { - await src.CopyToAsync(dest).ConfigureAwait(false); - } - } - - public static async Task WriteObject(IRequest request, object result, HttpResponse response) - { - var contentType = request.ResponseContentType; - var serializer = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType); - - using (var ms = new MemoryStream()) - { - serializer(result, ms); - - ms.Position = 0; - - var contentLength = ms.Length; - response.ContentLength = contentLength; - - if (contentLength > 0) - { - await ms.CopyToAsync(response.Body).ConfigureAwait(false); - } - } - } - } -} diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs deleted file mode 100644 index 47e7261e8..000000000 --- a/Emby.Server.Implementations/Services/ServiceController.cs +++ /dev/null @@ -1,202 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Services -{ - public delegate object ActionInvokerFn(object intance, object request); - - public delegate void VoidActionInvokerFn(object intance, object request); - - public class ServiceController - { - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - public ServiceController(ILogger logger) - { - _logger = logger; - } - - public void Init(HttpListenerHost appHost, IEnumerable serviceTypes) - { - foreach (var serviceType in serviceTypes) - { - RegisterService(appHost, serviceType); - } - } - - public void RegisterService(HttpListenerHost appHost, Type serviceType) - { - // Make sure the provided type implements IService - if (!typeof(IService).IsAssignableFrom(serviceType)) - { - _logger.LogWarning("Tried to register a service that does not implement IService: {ServiceType}", serviceType); - return; - } - - var processedReqs = new HashSet(); - - var actions = ServiceExecGeneral.Reset(serviceType); - - foreach (var mi in serviceType.GetActions()) - { - var requestType = mi.GetParameters()[0].ParameterType; - if (processedReqs.Contains(requestType)) - { - continue; - } - - processedReqs.Add(requestType); - - ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions); - - // var returnMarker = GetTypeWithGenericTypeDefinitionOf(requestType, typeof(IReturn<>)); - // var responseType = returnMarker != null ? - // GetGenericArguments(returnMarker)[0] - // : mi.ReturnType != typeof(object) && mi.ReturnType != typeof(void) ? - // mi.ReturnType - // : Type.GetType(requestType.FullName + "Response"); - - RegisterRestPaths(appHost, requestType, serviceType); - - appHost.AddServiceInfo(serviceType, requestType); - } - } - - public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap(); - - public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType) - { - var attrs = appHost.GetRouteAttributes(requestType); - foreach (var attr in attrs) - { - var restPath = new RestPath(appHost.CreateInstance, appHost.GetParseFn, requestType, serviceType, attr.Path, attr.Verbs, attr.IsHidden, attr.Summary, attr.Description); - - RegisterRestPath(restPath); - } - } - - private static readonly char[] InvalidRouteChars = new[] { '?', '&' }; - - public void RegisterRestPath(RestPath restPath) - { - if (restPath.Path[0] != '/') - { - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - "Route '{0}' on '{1}' must start with a '/'", - restPath.Path, - restPath.RequestType.GetMethodName())); - } - - if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1) - { - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - "Route '{0}' on '{1}' contains invalid chars. ", - restPath.Path, - restPath.RequestType.GetMethodName())); - } - - if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List pathsAtFirstMatch)) - { - pathsAtFirstMatch.Add(restPath); - } - else - { - RestPathMap[restPath.FirstMatchHashKey] = new List() { restPath }; - } - } - - public RestPath GetRestPathForRequest(string httpMethod, string pathInfo) - { - var matchUsingPathParts = RestPath.GetPathPartsForMatching(pathInfo); - - List firstMatches; - - var yieldedHashMatches = RestPath.GetFirstMatchHashKeys(matchUsingPathParts); - foreach (var potentialHashMatch in yieldedHashMatches) - { - if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) - { - continue; - } - - var bestScore = -1; - RestPath bestMatch = null; - foreach (var restPath in firstMatches) - { - var score = restPath.MatchScore(httpMethod, matchUsingPathParts); - if (score > bestScore) - { - bestScore = score; - bestMatch = restPath; - } - } - - if (bestScore > 0 && bestMatch != null) - { - return bestMatch; - } - } - - var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts); - foreach (var potentialHashMatch in yieldedWildcardMatches) - { - if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) - { - continue; - } - - var bestScore = -1; - RestPath bestMatch = null; - foreach (var restPath in firstMatches) - { - var score = restPath.MatchScore(httpMethod, matchUsingPathParts); - if (score > bestScore) - { - bestScore = score; - bestMatch = restPath; - } - } - - if (bestScore > 0 && bestMatch != null) - { - return bestMatch; - } - } - - return null; - } - - public Task Execute(HttpListenerHost httpHost, object requestDto, IRequest req) - { - var requestType = requestDto.GetType(); - req.OperationName = requestType.Name; - - var serviceType = httpHost.GetServiceTypeByRequest(requestType); - - var service = httpHost.CreateInstance(serviceType); - - if (service is IRequiresRequest serviceRequiresContext) - { - serviceRequiresContext.Request = req; - } - - // Executes the service and returns the result - return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName()); - } - } -} diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs deleted file mode 100644 index 7b970627e..000000000 --- a/Emby.Server.Implementations/Services/ServiceExec.cs +++ /dev/null @@ -1,230 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; - -namespace Emby.Server.Implementations.Services -{ - public static class ServiceExecExtensions - { - public static string[] AllVerbs = new[] { - "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616 - "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", // RFC 2518 - "VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT", - "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY", // RFC 3253 - "ORDERPATCH", // RFC 3648 - "ACL", // RFC 3744 - "PATCH", // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/ - "SEARCH", // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/ - "BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY", - "POLL", "SUBSCRIBE", "UNSUBSCRIBE" - }; - - public static List GetActions(this Type serviceType) - { - var list = new List(); - - foreach (var mi in serviceType.GetRuntimeMethods()) - { - if (!mi.IsPublic) - { - continue; - } - - if (mi.IsStatic) - { - continue; - } - - if (mi.GetParameters().Length != 1) - { - continue; - } - - var actionName = mi.Name; - if (!AllVerbs.Contains(actionName, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - - list.Add(mi); - } - - return list; - } - } - - internal static class ServiceExecGeneral - { - private static Dictionary execMap = new Dictionary(); - - public static void CreateServiceRunnersFor(Type requestType, List actions) - { - foreach (var actionCtx in actions) - { - if (execMap.ContainsKey(actionCtx.Id)) - { - continue; - } - - execMap[actionCtx.Id] = actionCtx; - } - } - - public static Task Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName) - { - var actionName = request.Verb ?? "POST"; - - if (execMap.TryGetValue(ServiceMethod.Key(serviceType, actionName, requestName), out ServiceMethod actionContext)) - { - if (actionContext.RequestFilters != null) - { - foreach (var requestFilter in actionContext.RequestFilters) - { - requestFilter.RequestFilter(request, request.Response, requestDto); - if (request.Response.HasStarted) - { - Task.FromResult(null); - } - } - } - - var response = actionContext.ServiceAction(instance, requestDto); - - if (response is Task taskResponse) - { - return GetTaskResult(taskResponse); - } - - return Task.FromResult(response); - } - - var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant(); - throw new NotImplementedException( - string.Format( - CultureInfo.InvariantCulture, - "Could not find method named {1}({0}) or Any({0}) on Service {2}", - requestDto.GetType().GetMethodName(), - expectedMethodName, - serviceType.GetMethodName())); - } - - private static async Task GetTaskResult(Task task) - { - try - { - if (task is Task taskObject) - { - return await taskObject.ConfigureAwait(false); - } - - await task.ConfigureAwait(false); - - var type = task.GetType().GetTypeInfo(); - if (!type.IsGenericType) - { - return null; - } - - var resultProperty = type.GetDeclaredProperty("Result"); - if (resultProperty == null) - { - return null; - } - - var result = resultProperty.GetValue(task); - - // hack alert - if (result.GetType().Name.IndexOf("voidtaskresult", StringComparison.OrdinalIgnoreCase) != -1) - { - return null; - } - - return result; - } - catch (TypeAccessException) - { - return null; // return null for void Task's - } - } - - public static List Reset(Type serviceType) - { - var actions = new List(); - - foreach (var mi in serviceType.GetActions()) - { - var actionName = mi.Name; - var args = mi.GetParameters(); - - var requestType = args[0].ParameterType; - var actionCtx = new ServiceMethod - { - Id = ServiceMethod.Key(serviceType, actionName, requestType.GetMethodName()) - }; - - actionCtx.ServiceAction = CreateExecFn(serviceType, requestType, mi); - - var reqFilters = new List(); - - foreach (var attr in mi.GetCustomAttributes(true)) - { - if (attr is IHasRequestFilter hasReqFilter) - { - reqFilters.Add(hasReqFilter); - } - } - - if (reqFilters.Count > 0) - { - actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray(); - } - - actions.Add(actionCtx); - } - - return actions; - } - - private static ActionInvokerFn CreateExecFn(Type serviceType, Type requestType, MethodInfo mi) - { - var serviceParam = Expression.Parameter(typeof(object), "serviceObj"); - var serviceStrong = Expression.Convert(serviceParam, serviceType); - - var requestDtoParam = Expression.Parameter(typeof(object), "requestDto"); - var requestDtoStrong = Expression.Convert(requestDtoParam, requestType); - - Expression callExecute = Expression.Call( - serviceStrong, mi, requestDtoStrong); - - if (mi.ReturnType != typeof(void)) - { - var executeFunc = Expression.Lambda( - callExecute, - serviceParam, - requestDtoParam).Compile(); - - return executeFunc; - } - else - { - var executeFunc = Expression.Lambda( - callExecute, - serviceParam, - requestDtoParam).Compile(); - - return (service, request) => - { - executeFunc(service, request); - return null; - }; - } - } - } -} diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs deleted file mode 100644 index b4166f771..000000000 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ /dev/null @@ -1,212 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Net.Mime; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Services -{ - public class ServiceHandler - { - private RestPath _restPath; - - private string _responseContentType; - - internal ServiceHandler(RestPath restPath, string responseContentType) - { - _restPath = restPath; - _responseContentType = responseContentType; - } - - protected static Task CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType) - { - if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0) - { - var deserializer = RequestHelper.GetRequestReader(host, contentType); - if (deserializer != null) - { - return deserializer.Invoke(requestType, httpReq.InputStream); - } - } - - return Task.FromResult(host.CreateInstance(requestType)); - } - - public static string GetSanitizedPathInfo(string pathInfo, out string contentType) - { - contentType = null; - var pos = pathInfo.LastIndexOf('.'); - if (pos != -1) - { - var format = pathInfo.AsSpan().Slice(pos + 1); - contentType = GetFormatContentType(format); - if (contentType != null) - { - pathInfo = pathInfo.Substring(0, pos); - } - } - - return pathInfo; - } - - private static string GetFormatContentType(ReadOnlySpan format) - { - if (format.Equals("json", StringComparison.Ordinal)) - { - return MediaTypeNames.Application.Json; - } - else if (format.Equals("xml", StringComparison.Ordinal)) - { - return MediaTypeNames.Application.Xml; - } - - return null; - } - - public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken) - { - httpReq.Items["__route"] = _restPath; - - if (_responseContentType != null) - { - httpReq.ResponseContentType = _responseContentType; - } - - var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false); - - httpHost.ApplyRequestFilters(httpReq, httpRes, request); - - httpRes.HttpContext.SetServiceStackRequest(httpReq); - var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false); - - // Apply response filters - foreach (var responseFilter in httpHost.ResponseFilters) - { - responseFilter(httpReq, httpRes, response); - } - - await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false); - } - - public static async Task CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath) - { - var requestType = restPath.RequestType; - - if (RequireqRequestStream(requestType)) - { - // Used by IRequiresRequestStream - var requestParams = GetRequestParams(httpReq.Response.HttpContext.Request); - var request = ServiceHandler.CreateRequest(httpReq, restPath, requestParams, host.CreateInstance(requestType)); - - var rawReq = (IRequiresRequestStream)request; - rawReq.RequestStream = httpReq.InputStream; - return rawReq; - } - else - { - var requestParams = GetFlattenedRequestParams(httpReq.Response.HttpContext.Request); - - var requestDto = await CreateContentTypeRequest(host, httpReq, restPath.RequestType, httpReq.ContentType).ConfigureAwait(false); - - return CreateRequest(httpReq, restPath, requestParams, requestDto); - } - } - - public static bool RequireqRequestStream(Type requestType) - { - var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo(); - - return requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo()); - } - - public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary requestParams, object requestDto) - { - var pathInfo = !restPath.IsWildCardPath - ? GetSanitizedPathInfo(httpReq.PathInfo, out _) - : httpReq.PathInfo; - - return restPath.CreateRequest(pathInfo, requestParams, requestDto); - } - - /// - /// Duplicate Params are given a unique key by appending a #1 suffix - /// - private static Dictionary GetRequestParams(HttpRequest request) - { - var map = new Dictionary(); - - foreach (var pair in request.Query) - { - var values = pair.Value; - if (values.Count == 1) - { - map[pair.Key] = values[0]; - } - else - { - for (var i = 0; i < values.Count; i++) - { - map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i]; - } - } - } - - if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT")) - && request.HasFormContentType) - { - foreach (var pair in request.Form) - { - var values = pair.Value; - if (values.Count == 1) - { - map[pair.Key] = values[0]; - } - else - { - for (var i = 0; i < values.Count; i++) - { - map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i]; - } - } - } - } - - return map; - } - - private static bool IsMethod(string method, string expected) - => string.Equals(method, expected, StringComparison.OrdinalIgnoreCase); - - /// - /// Duplicate params have their values joined together in a comma-delimited string. - /// - private static Dictionary GetFlattenedRequestParams(HttpRequest request) - { - var map = new Dictionary(); - - foreach (var pair in request.Query) - { - map[pair.Key] = pair.Value; - } - - if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT")) - && request.HasFormContentType) - { - foreach (var pair in request.Form) - { - map[pair.Key] = pair.Value; - } - } - - return map; - } - } -} diff --git a/Emby.Server.Implementations/Services/ServiceMethod.cs b/Emby.Server.Implementations/Services/ServiceMethod.cs deleted file mode 100644 index 5116cc04f..000000000 --- a/Emby.Server.Implementations/Services/ServiceMethod.cs +++ /dev/null @@ -1,20 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace Emby.Server.Implementations.Services -{ - public class ServiceMethod - { - public string Id { get; set; } - - public ActionInvokerFn ServiceAction { get; set; } - - public MediaBrowser.Model.Services.IHasRequestFilter[] RequestFilters { get; set; } - - public static string Key(Type serviceType, string method, string requestDtoName) - { - return serviceType.FullName + " " + method.ToUpperInvariant() + " " + requestDtoName; - } - } -} diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs deleted file mode 100644 index 442b2ab1c..000000000 --- a/Emby.Server.Implementations/Services/ServicePath.cs +++ /dev/null @@ -1,550 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.Services -{ - public class RestPath - { - private const string WildCard = "*"; - private const char WildCardChar = '*'; - private const string PathSeperator = "/"; - private const char PathSeperatorChar = '/'; - private const char ComponentSeperator = '.'; - private const string VariablePrefix = "{"; - - private readonly bool[] componentsWithSeparators; - - private readonly string restPath; - public bool IsWildCardPath { get; private set; } - - private readonly string[] literalsToMatch; - - private readonly string[] variablesNames; - - private readonly bool[] isWildcard; - private readonly int wildcardCount = 0; - - internal static string[] IgnoreAttributesNamed = new[] - { - nameof(JsonIgnoreAttribute) - }; - - private static Type _excludeType = typeof(Stream); - - public int VariableArgsCount { get; set; } - - /// - /// The number of segments separated by '/' determinable by path.Split('/').Length - /// e.g. /path/to/here.ext == 3 - /// - public int PathComponentsCount { get; set; } - - /// - /// Gets or sets the total number of segments after subparts have been exploded ('.') - /// e.g. /path/to/here.ext == 4. - /// - public int TotalComponentsCount { get; set; } - - public string[] Verbs { get; private set; } - - public Type RequestType { get; private set; } - - public Type ServiceType { get; private set; } - - public string Path => this.restPath; - - public string Summary { get; private set; } - - public string Description { get; private set; } - - public bool IsHidden { get; private set; } - - public static string[] GetPathPartsForMatching(string pathInfo) - { - return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries); - } - - public static List GetFirstMatchHashKeys(string[] pathPartsForMatching) - { - var hashPrefix = pathPartsForMatching.Length + PathSeperator; - return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching); - } - - public static List GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching) - { - const string hashPrefix = WildCard + PathSeperator; - return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching); - } - - private static List GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching) - { - var list = new List(); - - foreach (var part in pathPartsForMatching) - { - list.Add(hashPrefix + part); - - if (part.IndexOf(ComponentSeperator) == -1) - { - continue; - } - - var subParts = part.Split(ComponentSeperator); - foreach (var subPart in subParts) - { - list.Add(hashPrefix + subPart); - } - } - - return list; - } - - public RestPath(Func createInstanceFn, Func> getParseFn, Type requestType, Type serviceType, string path, string verbs, bool isHidden = false, string summary = null, string description = null) - { - this.RequestType = requestType; - this.ServiceType = serviceType; - this.Summary = summary; - this.IsHidden = isHidden; - this.Description = description; - this.restPath = path; - - this.Verbs = string.IsNullOrWhiteSpace(verbs) ? ServiceExecExtensions.AllVerbs : verbs.ToUpperInvariant().Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries); - - var componentsList = new List(); - - // We only split on '.' if the restPath has them. Allows for /{action}.{type} - var hasSeparators = new List(); - foreach (var component in this.restPath.Split(PathSeperatorChar)) - { - if (string.IsNullOrEmpty(component)) - { - continue; - } - - if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1 - && component.IndexOf(ComponentSeperator) != -1) - { - hasSeparators.Add(true); - componentsList.AddRange(component.Split(ComponentSeperator)); - } - else - { - hasSeparators.Add(false); - componentsList.Add(component); - } - } - - var components = componentsList.ToArray(); - this.TotalComponentsCount = components.Length; - - this.literalsToMatch = new string[this.TotalComponentsCount]; - this.variablesNames = new string[this.TotalComponentsCount]; - this.isWildcard = new bool[this.TotalComponentsCount]; - this.componentsWithSeparators = hasSeparators.ToArray(); - this.PathComponentsCount = this.componentsWithSeparators.Length; - string firstLiteralMatch = null; - - for (var i = 0; i < components.Length; i++) - { - var component = components[i]; - - if (component.StartsWith(VariablePrefix, StringComparison.Ordinal)) - { - var variableName = component.Substring(1, component.Length - 2); - if (variableName[variableName.Length - 1] == WildCardChar) - { - this.isWildcard[i] = true; - variableName = variableName.Substring(0, variableName.Length - 1); - } - - this.variablesNames[i] = variableName; - this.VariableArgsCount++; - } - else - { - this.literalsToMatch[i] = component.ToLowerInvariant(); - - if (firstLiteralMatch == null) - { - firstLiteralMatch = this.literalsToMatch[i]; - } - } - } - - for (var i = 0; i < components.Length - 1; i++) - { - if (!this.isWildcard[i]) - { - continue; - } - - if (this.literalsToMatch[i + 1] == null) - { - throw new ArgumentException( - "A wildcard path component must be at the end of the path or followed by a literal path component."); - } - } - - this.wildcardCount = this.isWildcard.Length; - this.IsWildCardPath = this.wildcardCount > 0; - - this.FirstMatchHashKey = !this.IsWildCardPath - ? this.PathComponentsCount + PathSeperator + firstLiteralMatch - : WildCardChar + PathSeperator + firstLiteralMatch; - - this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType); - - _propertyNamesMap = new HashSet( - GetSerializableProperties(RequestType).Select(x => x.Name), - StringComparer.OrdinalIgnoreCase); - } - - internal static IEnumerable GetSerializableProperties(Type type) - { - foreach (var prop in GetPublicProperties(type)) - { - if (prop.GetMethod == null - || _excludeType == prop.PropertyType) - { - continue; - } - - var ignored = false; - foreach (var attr in prop.GetCustomAttributes(true)) - { - if (IgnoreAttributesNamed.Contains(attr.GetType().Name)) - { - ignored = true; - break; - } - } - - if (!ignored) - { - yield return prop; - } - } - } - - private static IEnumerable GetPublicProperties(Type type) - { - if (type.IsInterface) - { - var propertyInfos = new List(); - var considered = new List() - { - type - }; - var queue = new Queue(); - queue.Enqueue(type); - - while (queue.Count > 0) - { - var subType = queue.Dequeue(); - foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces) - { - if (considered.Contains(subInterface)) - { - continue; - } - - considered.Add(subInterface); - queue.Enqueue(subInterface); - } - - var newPropertyInfos = GetTypesPublicProperties(subType) - .Where(x => !propertyInfos.Contains(x)); - - propertyInfos.InsertRange(0, newPropertyInfos); - } - - return propertyInfos; - } - - return GetTypesPublicProperties(type) - .Where(x => x.GetIndexParameters().Length == 0); - } - - private static IEnumerable GetTypesPublicProperties(Type subType) - { - foreach (var pi in subType.GetRuntimeProperties()) - { - var mi = pi.GetMethod ?? pi.SetMethod; - if (mi != null && mi.IsStatic) - { - continue; - } - - yield return pi; - } - } - - /// - /// Provide for quick lookups based on hashes that can be determined from a request url. - /// - public string FirstMatchHashKey { get; private set; } - - private readonly StringMapTypeDeserializer typeDeserializer; - - private readonly HashSet _propertyNamesMap; - - public int MatchScore(string httpMethod, string[] withPathInfoParts) - { - var isMatch = IsMatch(httpMethod, withPathInfoParts, out var wildcardMatchCount); - if (!isMatch) - { - return -1; - } - - // Routes with least wildcard matches get the highest score - var score = Math.Max(100 - wildcardMatchCount, 1) * 1000 - // Routes with less variable (and more literal) matches - + Math.Max(10 - VariableArgsCount, 1) * 100; - - // Exact verb match is better than ANY - if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase)) - { - score += 10; - } - else - { - score += 1; - } - - return score; - } - - /// - /// For performance withPathInfoParts should already be a lower case string - /// to minimize redundant matching operations. - /// - public bool IsMatch(string httpMethod, string[] withPathInfoParts, out int wildcardMatchCount) - { - wildcardMatchCount = 0; - - if (withPathInfoParts.Length != this.PathComponentsCount && !this.IsWildCardPath) - { - return false; - } - - if (!Verbs.Contains(httpMethod, StringComparer.OrdinalIgnoreCase)) - { - return false; - } - - if (!ExplodeComponents(ref withPathInfoParts)) - { - return false; - } - - if (this.TotalComponentsCount != withPathInfoParts.Length && !this.IsWildCardPath) - { - return false; - } - - int pathIx = 0; - for (var i = 0; i < this.TotalComponentsCount; i++) - { - if (this.isWildcard[i]) - { - if (i < this.TotalComponentsCount - 1) - { - // Continue to consume up until a match with the next literal - while (pathIx < withPathInfoParts.Length - && !string.Equals(withPathInfoParts[pathIx], this.literalsToMatch[i + 1], StringComparison.InvariantCultureIgnoreCase)) - { - pathIx++; - wildcardMatchCount++; - } - - // Ensure there are still enough parts left to match the remainder - if ((withPathInfoParts.Length - pathIx) < (this.TotalComponentsCount - i - 1)) - { - return false; - } - } - else - { - // A wildcard at the end matches the remainder of path - wildcardMatchCount += withPathInfoParts.Length - pathIx; - pathIx = withPathInfoParts.Length; - } - } - else - { - var literalToMatch = this.literalsToMatch[i]; - if (literalToMatch == null) - { - // Matching an ordinary (non-wildcard) variable consumes a single part - pathIx++; - continue; - } - - if (withPathInfoParts.Length <= pathIx - || !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } - - pathIx++; - } - } - - return pathIx == withPathInfoParts.Length; - } - - private bool ExplodeComponents(ref string[] withPathInfoParts) - { - var totalComponents = new List(); - for (var i = 0; i < withPathInfoParts.Length; i++) - { - var component = withPathInfoParts[i]; - if (string.IsNullOrEmpty(component)) - { - continue; - } - - if (this.PathComponentsCount != this.TotalComponentsCount - && this.componentsWithSeparators[i]) - { - var subComponents = component.Split(ComponentSeperator); - if (subComponents.Length < 2) - { - return false; - } - - totalComponents.AddRange(subComponents); - } - else - { - totalComponents.Add(component); - } - } - - withPathInfoParts = totalComponents.ToArray(); - return true; - } - - public object CreateRequest(string pathInfo, Dictionary queryStringAndFormData, object fromInstance) - { - var requestComponents = pathInfo.Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries); - - ExplodeComponents(ref requestComponents); - - if (requestComponents.Length != this.TotalComponentsCount) - { - var isValidWildCardPath = this.IsWildCardPath - && requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount; - - if (!isValidWildCardPath) - { - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - "Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'", - pathInfo, - this.restPath)); - } - } - - var requestKeyValuesMap = new Dictionary(); - var pathIx = 0; - for (var i = 0; i < this.TotalComponentsCount; i++) - { - var variableName = this.variablesNames[i]; - if (variableName == null) - { - pathIx++; - continue; - } - - if (!this._propertyNamesMap.Contains(variableName)) - { - if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase)) - { - pathIx++; - continue; - } - - throw new ArgumentException("Could not find property " - + variableName + " on " + RequestType.GetMethodName()); - } - - var value = requestComponents.Length > pathIx ? requestComponents[pathIx] : null; // wildcard has arg mismatch - if (value != null && this.isWildcard[i]) - { - if (i == this.TotalComponentsCount - 1) - { - // Wildcard at end of path definition consumes all the rest - var sb = new StringBuilder(); - sb.Append(value); - for (var j = pathIx + 1; j < requestComponents.Length; j++) - { - sb.Append(PathSeperatorChar) - .Append(requestComponents[j]); - } - - value = sb.ToString(); - } - else - { - // Wildcard in middle of path definition consumes up until it - // hits a match for the next element in the definition (which must be a literal) - // It may consume 0 or more path parts - var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1]; - if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) - { - var sb = new StringBuilder(value); - pathIx++; - while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) - { - sb.Append(PathSeperatorChar) - .Append(requestComponents[pathIx++]); - } - - value = sb.ToString(); - } - else - { - value = null; - } - } - } - else - { - // Variable consumes single path item - pathIx++; - } - - requestKeyValuesMap[variableName] = value; - } - - if (queryStringAndFormData != null) - { - // Query String and form data can override variable path matches - // path variables < query string < form data - foreach (var name in queryStringAndFormData) - { - requestKeyValuesMap[name.Key] = name.Value; - } - } - - return this.typeDeserializer.PopulateFromMap(fromInstance, requestKeyValuesMap); - } - - public class RestPathMap : SortedDictionary> - { - public RestPathMap() : base(StringComparer.OrdinalIgnoreCase) - { - } - } - } -} diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs deleted file mode 100644 index 165bb0fc4..000000000 --- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs +++ /dev/null @@ -1,118 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Reflection; -using MediaBrowser.Common.Extensions; - -namespace Emby.Server.Implementations.Services -{ - /// - /// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls) - /// - public class StringMapTypeDeserializer - { - internal class PropertySerializerEntry - { - public PropertySerializerEntry(Action propertySetFn, Func propertyParseStringFn, Type propertyType) - { - PropertySetFn = propertySetFn; - PropertyParseStringFn = propertyParseStringFn; - PropertyType = propertyType; - } - - public Action PropertySetFn { get; private set; } - - public Func PropertyParseStringFn { get; private set; } - - public Type PropertyType { get; private set; } - } - - private readonly Type type; - private readonly Dictionary propertySetterMap - = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public Func GetParseFn(Type propertyType) - { - if (propertyType == typeof(string)) - { - return s => s; - } - - return _GetParseFn(propertyType); - } - - private readonly Func _CreateInstanceFn; - private readonly Func> _GetParseFn; - - public StringMapTypeDeserializer(Func createInstanceFn, Func> getParseFn, Type type) - { - _CreateInstanceFn = createInstanceFn; - _GetParseFn = getParseFn; - this.type = type; - - foreach (var propertyInfo in RestPath.GetSerializableProperties(type)) - { - var propertySetFn = TypeAccessor.GetSetPropertyMethod(propertyInfo); - var propertyType = propertyInfo.PropertyType; - var propertyParseStringFn = GetParseFn(propertyType); - var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType); - - propertySetterMap[propertyInfo.Name] = propertySerializer; - } - } - - public object PopulateFromMap(object instance, IDictionary keyValuePairs) - { - PropertySerializerEntry propertySerializerEntry = null; - - if (instance == null) - { - instance = _CreateInstanceFn(type); - } - - foreach (var pair in keyValuePairs) - { - string propertyName = pair.Key; - string propertyTextValue = pair.Value; - - if (propertyTextValue == null - || !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry) - || propertySerializerEntry.PropertySetFn == null) - { - continue; - } - - if (propertySerializerEntry.PropertyType == typeof(bool)) - { - // InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value - propertyTextValue = StringExtensions.LeftPart(propertyTextValue, ',').ToString(); - } - - var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue); - if (value == null) - { - continue; - } - - propertySerializerEntry.PropertySetFn(instance, value); - } - - return instance; - } - } - - internal static class TypeAccessor - { - public static Action GetSetPropertyMethod(PropertyInfo propertyInfo) - { - if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0) - { - return null; - } - - var setMethodInfo = propertyInfo.SetMethod; - return (instance, value) => setMethodInfo.Invoke(instance, new[] { value }); - } - } -} diff --git a/Emby.Server.Implementations/Services/UrlExtensions.cs b/Emby.Server.Implementations/Services/UrlExtensions.cs deleted file mode 100644 index 92e36b60e..000000000 --- a/Emby.Server.Implementations/Services/UrlExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Common.Extensions; - -namespace Emby.Server.Implementations.Services -{ - /// - /// Donated by Ivan Korneliuk from his post: - /// http://korneliuk.blogspot.com/2012/08/servicestack-reusing-dtos.html - /// - /// Modified to only allow using routes matching the supplied HTTP Verb. - /// - public static class UrlExtensions - { - public static string GetMethodName(this Type type) - { - var typeName = type.FullName != null // can be null, e.g. generic types - ? StringExtensions.LeftPart(type.FullName, "[[", StringComparison.Ordinal).ToString() // Generic Fullname - .Replace(type.Namespace + ".", string.Empty, StringComparison.Ordinal) // Trim Namespaces - .Replace("+", ".", StringComparison.Ordinal) // Convert nested into normal type - : type.Name; - - return type.IsGenericParameter ? "'" + typeName : typeName; - } - } -} diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 862a7296c..ca8e0e29b 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Data.Events; using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; @@ -17,6 +18,8 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Session; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; @@ -24,7 +27,6 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Model.Devices; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; @@ -40,25 +42,16 @@ namespace Emby.Server.Implementations.Session /// public class SessionManager : ISessionManager, IDisposable { - /// - /// The user data repository. - /// private readonly IUserDataManager _userDataManager; - - /// - /// The logger. - /// private readonly ILogger _logger; - + private readonly IEventManager _eventManager; private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; private readonly IMusicManager _musicManager; private readonly IDtoService _dtoService; private readonly IImageProcessor _imageProcessor; private readonly IMediaSourceManager _mediaSourceManager; - private readonly IServerApplicationHost _appHost; - private readonly IAuthenticationRepository _authRepo; private readonly IDeviceManager _deviceManager; @@ -75,6 +68,7 @@ namespace Emby.Server.Implementations.Session public SessionManager( ILogger logger, + IEventManager eventManager, IUserDataManager userDataManager, ILibraryManager libraryManager, IUserManager userManager, @@ -87,6 +81,7 @@ namespace Emby.Server.Implementations.Session IMediaSourceManager mediaSourceManager) { _logger = logger; + _eventManager = eventManager; _userDataManager = userDataManager; _libraryManager = libraryManager; _userManager = userManager; @@ -209,6 +204,8 @@ namespace Emby.Server.Implementations.Session } } + _eventManager.Publish(new SessionStartedEventArgs(info)); + EventHelper.QueueEventIfNotNull( SessionStarted, this, @@ -230,6 +227,8 @@ namespace Emby.Server.Implementations.Session }, _logger); + _eventManager.Publish(new SessionEndedEventArgs(info)); + info.Dispose(); } @@ -667,22 +666,26 @@ namespace Emby.Server.Implementations.Session } } + var eventArgs = new PlaybackProgressEventArgs + { + Item = libraryItem, + Users = users, + MediaSourceId = info.MediaSourceId, + MediaInfo = info.Item, + DeviceName = session.DeviceName, + ClientName = session.Client, + DeviceId = session.DeviceId, + Session = session + }; + + await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false); + // Nothing to save here // Fire events to inform plugins EventHelper.QueueEventIfNotNull( PlaybackStart, this, - new PlaybackProgressEventArgs - { - Item = libraryItem, - Users = users, - MediaSourceId = info.MediaSourceId, - MediaInfo = info.Item, - DeviceName = session.DeviceName, - ClientName = session.Client, - DeviceId = session.DeviceId, - Session = session - }, + eventArgs, _logger); StartIdleCheckTimer(); @@ -750,23 +753,25 @@ namespace Emby.Server.Implementations.Session } } - PlaybackProgress?.Invoke( - this, - new PlaybackProgressEventArgs - { - Item = libraryItem, - Users = users, - PlaybackPositionTicks = session.PlayState.PositionTicks, - MediaSourceId = session.PlayState.MediaSourceId, - MediaInfo = info.Item, - DeviceName = session.DeviceName, - ClientName = session.Client, - DeviceId = session.DeviceId, - IsPaused = info.IsPaused, - PlaySessionId = info.PlaySessionId, - IsAutomated = isAutomated, - Session = session - }); + var eventArgs = new PlaybackProgressEventArgs + { + Item = libraryItem, + Users = users, + PlaybackPositionTicks = session.PlayState.PositionTicks, + MediaSourceId = session.PlayState.MediaSourceId, + MediaInfo = info.Item, + DeviceName = session.DeviceName, + ClientName = session.Client, + DeviceId = session.DeviceId, + IsPaused = info.IsPaused, + PlaySessionId = info.PlaySessionId, + IsAutomated = isAutomated, + Session = session + }; + + await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false); + + PlaybackProgress?.Invoke(this, eventArgs); if (!isAutomated) { @@ -943,23 +948,23 @@ namespace Emby.Server.Implementations.Session } } - EventHelper.QueueEventIfNotNull( - PlaybackStopped, - this, - new PlaybackStopEventArgs - { - Item = libraryItem, - Users = users, - PlaybackPositionTicks = info.PositionTicks, - PlayedToCompletion = playedToCompletion, - MediaSourceId = info.MediaSourceId, - MediaInfo = info.Item, - DeviceName = session.DeviceName, - ClientName = session.Client, - DeviceId = session.DeviceId, - Session = session - }, - _logger); + var eventArgs = new PlaybackStopEventArgs + { + Item = libraryItem, + Users = users, + PlaybackPositionTicks = info.PositionTicks, + PlayedToCompletion = playedToCompletion, + MediaSourceId = info.MediaSourceId, + MediaInfo = info.Item, + DeviceName = session.DeviceName, + ClientName = session.Client, + DeviceId = session.DeviceId, + Session = session + }; + + await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false); + + EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger); } private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed) @@ -1424,6 +1429,24 @@ namespace Emby.Server.Implementations.Session return AuthenticateNewSessionInternal(request, false); } + public Task AuthenticateQuickConnect(AuthenticationRequest request, string token) + { + var result = _authRepo.Get(new AuthenticationInfoQuery() + { + AccessToken = token, + DeviceId = _appHost.SystemId, + Limit = 1 + }); + + if (result.TotalRecordCount == 0) + { + throw new SecurityException("Unknown quick connect token"); + } + + request.UserId = result.Items[0].UserId; + return AuthenticateNewSessionInternal(request, false); + } + private async Task AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword) { CheckDisposed(); diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index 8bebd37dc..15c2af220 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -4,9 +4,9 @@ using System.Linq; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Session private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; - private readonly IHttpServer _httpServer; + private readonly IWebSocketManager _webSocketManager; /// /// The KeepAlive cancellation token. @@ -72,19 +72,19 @@ namespace Emby.Server.Implementations.Session /// The logger. /// The session manager. /// The logger factory. - /// The HTTP server. + /// The HTTP server. public SessionWebSocketListener( ILogger logger, ISessionManager sessionManager, ILoggerFactory loggerFactory, - IHttpServer httpServer) + IWebSocketManager webSocketManager) { _logger = logger; _sessionManager = sessionManager; _loggerFactory = loggerFactory; - _httpServer = httpServer; + _webSocketManager = webSocketManager; - httpServer.WebSocketConnected += OnServerManagerWebSocketConnected; + webSocketManager.WebSocketConnected += OnServerManagerWebSocketConnected; } private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs e) @@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Session /// public void Dispose() { - _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected; + _webSocketManager.WebSocketConnected -= OnServerManagerWebSocketConnected; StopKeepAlive(); } diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs deleted file mode 100644 index ae1a8d0b7..000000000 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ /dev/null @@ -1,248 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Mime; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; - -namespace Emby.Server.Implementations.SocketSharp -{ - public class WebSocketSharpRequest : IHttpRequest - { - private const string FormUrlEncoded = "application/x-www-form-urlencoded"; - private const string MultiPartFormData = "multipart/form-data"; - private const string Soap11 = "text/xml; charset=utf-8"; - - private string _remoteIp; - private Dictionary _items; - private string _responseContentType; - - public WebSocketSharpRequest(HttpRequest httpRequest, HttpResponse httpResponse, string operationName) - { - this.OperationName = operationName; - this.Request = httpRequest; - this.Response = httpResponse; - } - - public string Accept => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Accept]) ? null : Request.Headers[HeaderNames.Accept].ToString(); - - public string Authorization => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Authorization]) ? null : Request.Headers[HeaderNames.Authorization].ToString(); - - public HttpRequest Request { get; } - - public HttpResponse Response { get; } - - public string OperationName { get; set; } - - public string RawUrl => Request.GetEncodedPathAndQuery(); - - public string AbsoluteUri => Request.GetDisplayUrl().TrimEnd('/'); - - public string RemoteIp - { - get - { - if (_remoteIp != null) - { - return _remoteIp; - } - - IPAddress ip; - - // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip - // (if the server is behind a reverse proxy for example) - if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XForwardedFor), out ip)) - { - if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip)) - { - ip = Request.HttpContext.Connection.RemoteIpAddress; - - // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests) - ip ??= IPAddress.Loopback; - } - } - - return _remoteIp = NormalizeIp(ip).ToString(); - } - } - - public string[] AcceptTypes => Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); - - public Dictionary Items => _items ?? (_items = new Dictionary()); - - public string ResponseContentType - { - get => - _responseContentType - ?? (_responseContentType = GetResponseContentType(Request)); - set => _responseContentType = value; - } - - public string PathInfo => Request.Path.Value; - - public string UserAgent => Request.Headers[HeaderNames.UserAgent]; - - public IHeaderDictionary Headers => Request.Headers; - - public IQueryCollection QueryString => Request.Query; - - public bool IsLocal => - (Request.HttpContext.Connection.LocalIpAddress == null - && Request.HttpContext.Connection.RemoteIpAddress == null) - || Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress); - - public string HttpMethod => Request.Method; - - public string Verb => HttpMethod; - - public string ContentType => Request.ContentType; - - public Uri UrlReferrer => Request.GetTypedHeaders().Referer; - - public Stream InputStream => Request.Body; - - public long ContentLength => Request.ContentLength ?? 0; - - private string GetHeader(string name) => Request.Headers[name].ToString(); - - private static IPAddress NormalizeIp(IPAddress ip) - { - if (ip.IsIPv4MappedToIPv6) - { - return ip.MapToIPv4(); - } - - return ip; - } - - public static string GetResponseContentType(HttpRequest httpReq) - { - var specifiedContentType = GetQueryStringContentType(httpReq); - if (!string.IsNullOrEmpty(specifiedContentType)) - { - return specifiedContentType; - } - - const string ServerDefaultContentType = MediaTypeNames.Application.Json; - - var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept); - string defaultContentType = null; - if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData)) - { - defaultContentType = ServerDefaultContentType; - } - - var acceptsAnything = false; - var hasDefaultContentType = defaultContentType != null; - if (acceptContentTypes != null) - { - foreach (ReadOnlySpan acceptsType in acceptContentTypes) - { - ReadOnlySpan contentType = acceptsType; - var index = contentType.IndexOf(';'); - if (index != -1) - { - contentType = contentType.Slice(0, index); - } - - contentType = contentType.Trim(); - acceptsAnything = contentType.Equals("*/*", StringComparison.OrdinalIgnoreCase); - - if (acceptsAnything) - { - break; - } - } - - if (acceptsAnything) - { - if (hasDefaultContentType) - { - return defaultContentType; - } - else - { - return ServerDefaultContentType; - } - } - } - - if (acceptContentTypes == null && httpReq.ContentType == Soap11) - { - return Soap11; - } - - // We could also send a '406 Not Acceptable', but this is allowed also - return ServerDefaultContentType; - } - - public static bool HasAnyOfContentTypes(HttpRequest request, params string[] contentTypes) - { - if (contentTypes == null || request.ContentType == null) - { - return false; - } - - foreach (var contentType in contentTypes) - { - if (IsContentType(request, contentType)) - { - return true; - } - } - - return false; - } - - public static bool IsContentType(HttpRequest request, string contentType) - { - return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase); - } - - private static string GetQueryStringContentType(HttpRequest httpReq) - { - ReadOnlySpan format = httpReq.Query["format"].ToString(); - if (format == ReadOnlySpan.Empty) - { - const int FormatMaxLength = 4; - ReadOnlySpan pi = httpReq.Path.ToString(); - if (pi == null || pi.Length <= FormatMaxLength) - { - return null; - } - - if (pi[0] == '/') - { - pi = pi.Slice(1); - } - - format = pi.LeftPart('/'); - if (format.Length > FormatMaxLength) - { - return null; - } - } - - format = format.LeftPart('.'); - if (format.Contains("json", StringComparison.OrdinalIgnoreCase)) - { - return "application/json"; - } - else if (format.Contains("xml", StringComparison.OrdinalIgnoreCase)) - { - return "application/xml"; - } - - return null; - } - } -} diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index fb3f09ba6..802cd026e 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -1,93 +1,32 @@ using System; using System.Collections.Generic; -using System.Net.Http; -using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.StreamingDtos; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; namespace Jellyfin.Api.Controllers { /// /// The audio controller. /// - // TODO: In order to autheneticate this in the future, Dlna playback will require updating + // TODO: In order to authenticate this in the future, Dlna playback will require updating public class AudioController : BaseJellyfinApiController { - private readonly IDlnaManager _dlnaManager; - private readonly IAuthorizationContext _authContext; - private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly IFileSystem _fileSystem; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IConfiguration _configuration; - private readonly IDeviceManager _deviceManager; - private readonly TranscodingJobHelper _transcodingJobHelper; - private readonly IHttpClientFactory _httpClientFactory; + private readonly AudioHelper _audioHelper; private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive; /// /// Initializes a new instance of the class. /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// The singleton. - /// Instance of the interface. - public AudioController( - IDlnaManager dlnaManager, - IUserManager userManger, - IAuthorizationContext authorizationContext, - ILibraryManager libraryManager, - IMediaSourceManager mediaSourceManager, - IServerConfigurationManager serverConfigurationManager, - IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - ISubtitleEncoder subtitleEncoder, - IConfiguration configuration, - IDeviceManager deviceManager, - TranscodingJobHelper transcodingJobHelper, - IHttpClientFactory httpClientFactory) + /// Instance of . + public AudioController(AudioHelper audioHelper) { - _dlnaManager = dlnaManager; - _authContext = authorizationContext; - _userManager = userManger; - _libraryManager = libraryManager; - _mediaSourceManager = mediaSourceManager; - _serverConfigurationManager = serverConfigurationManager; - _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; - _subtitleEncoder = subtitleEncoder; - _configuration = configuration; - _deviceManager = deviceManager; - _transcodingJobHelper = transcodingJobHelper; - _httpClientFactory = httpClientFactory; + _audioHelper = audioHelper; } /// @@ -144,9 +83,9 @@ namespace Jellyfin.Api.Controllers /// Optional. The streaming options. /// Audio stream returned. /// A containing the audio file. - [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetAudioStreamByContainer")] + [HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")] [HttpGet("{itemId}/stream", Name = "GetAudioStream")] - [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadAudioStreamByContainer")] + [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")] [HttpHead("{itemId}/stream", Name = "HeadAudioStream")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetAudioStream( @@ -200,10 +139,6 @@ namespace Jellyfin.Api.Controllers [FromQuery] EncodingContext? context, [FromQuery] Dictionary? streamOptions) { - bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; - - var cancellationTokenSource = new CancellationTokenSource(); - StreamingRequestDto streamingRequest = new StreamingRequestDto { Id = itemId, @@ -257,97 +192,7 @@ namespace Jellyfin.Api.Controllers StreamOptions = streamOptions }; - using var state = await StreamingHelpers.GetStreamingState( - streamingRequest, - Request, - _authContext, - _mediaSourceManager, - _userManager, - _libraryManager, - _serverConfigurationManager, - _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, - _dlnaManager, - _deviceManager, - _transcodingJobHelper, - _transcodingJobType, - cancellationTokenSource.Token) - .ConfigureAwait(false); - - if (@static.HasValue && @static.Value && state.DirectStreamProvider != null) - { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); - - await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None) - { - AllowEndOfFile = false - }.WriteToAsync(Response.Body, CancellationToken.None) - .ConfigureAwait(false); - - // TODO (moved from MediaBrowser.Api): Don't hardcode contentType - return File(Response.Body, MimeTypes.GetMimeType("file.ts")!); - } - - // Static remote stream - if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http) - { - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); - - var httpClient = _httpClientFactory.CreateClient(); - return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false); - } - - if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File) - { - return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically"); - } - - var outputPath = state.OutputFilePath; - var outputPathExists = System.IO.File.Exists(outputPath); - - var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); - var isTranscodeCached = outputPathExists && transcodingJob != null; - - StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager); - - // Static stream - if (@static.HasValue && @static.Value) - { - var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath); - - if (state.MediaSource.IsInfiniteStream) - { - await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None) - { - AllowEndOfFile = false - }.WriteToAsync(Response.Body, CancellationToken.None) - .ConfigureAwait(false); - - return File(Response.Body, contentType); - } - - return FileStreamResponseHelpers.GetStaticFileResult( - state.MediaPath, - contentType, - isHeadRequest, - this); - } - - // Need to start ffmpeg (because media can't be returned directly) - var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); - var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration); - var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath); - return await FileStreamResponseHelpers.GetTranscodedFile( - state, - isHeadRequest, - this, - _transcodingJobHelper, - ffmpegCommandLineArguments, - Request, - _transcodingJobType, - cancellationTokenSource).ConfigureAwait(false); + return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false); } } } diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index 53821a188..c5910d6e8 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; @@ -51,7 +52,7 @@ namespace Jellyfin.Api.Controllers /// A with information about the new collection. [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult CreateCollection( + public async Task> CreateCollection( [FromQuery] string? name, [FromQuery] string? ids, [FromQuery] Guid? parentId, @@ -59,14 +60,14 @@ namespace Jellyfin.Api.Controllers { var userId = _authContext.GetAuthorizationInfo(Request).UserId; - var item = _collectionManager.CreateCollection(new CollectionCreationOptions + var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions { IsLocked = isLocked, Name = name, ParentId = parentId, ItemIdList = RequestHelpers.Split(ids, ',', true), UserIds = new[] { userId } - }); + }).ConfigureAwait(false); var dtoOptions = new DtoOptions().AddClientFields(Request); @@ -87,9 +88,9 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("{collectionId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds) + public async Task AddToCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds) { - _collectionManager.AddToCollection(collectionId, RequestHelpers.Split(itemIds, ',', true)); + await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(true); return NoContent(); } @@ -102,9 +103,9 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpDelete("{collectionId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds) + public async Task RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds) { - _collectionManager.RemoveFromCollection(collectionId, RequestHelpers.Split(itemIds, ',', true)); + await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(false); return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 019703dae..20fb0ec87 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -117,7 +117,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("MediaEncoder/Path")] [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdateMediaEncoderPath([FromForm, Required] MediaEncoderPathDto mediaEncoderPath) + public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath) { _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType); return NoContent(); diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 23d10e215..1aed20ade 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -48,7 +48,7 @@ namespace Jellyfin.Api.Controllers [HttpGet] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetDevices([FromQuery] bool? supportsSync, [FromQuery, Required] Guid? userId) + public ActionResult> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; return _deviceManager.GetDevices(deviceQuery); diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index 0100d642b..ee87d917b 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -221,16 +221,15 @@ namespace Jellyfin.Api.Controllers private Task ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service) { - return service.ProcessControlRequestAsync(new ControlRequest + return service.ProcessControlRequestAsync(new ControlRequest(Request.Headers) { - Headers = Request.Headers, InputXml = requestStream, TargetServerUuId = id, RequestedUrl = GetAbsoluteUri() }); } - private EventSubscriptionResponse ProcessEventRequest(IEventManager eventManager) + private EventSubscriptionResponse ProcessEventRequest(IDlnaEventManager dlnaEventManager) { var subscriptionId = Request.Headers["SID"]; if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase)) @@ -241,17 +240,17 @@ namespace Jellyfin.Api.Controllers if (string.IsNullOrEmpty(notificationType)) { - return eventManager.RenewEventSubscription( + return dlnaEventManager.RenewEventSubscription( subscriptionId, notificationType, timeoutString, callback); } - return eventManager.CreateEventSubscription(notificationType, timeoutString, callback); + return dlnaEventManager.CreateEventSubscription(notificationType, timeoutString, callback); } - return eventManager.CancelEventSubscription(subscriptionId); + return dlnaEventManager.CancelEventSubscription(subscriptionId); } } } diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index d581ab8cd..b115ac6cd 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -13,7 +13,6 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; @@ -22,7 +21,6 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Authorization; @@ -53,9 +51,9 @@ namespace Jellyfin.Api.Controllers private readonly IConfiguration _configuration; private readonly IDeviceManager _deviceManager; private readonly TranscodingJobHelper _transcodingJobHelper; - private readonly INetworkManager _networkManager; private readonly ILogger _logger; private readonly EncodingHelper _encodingHelper; + private readonly DynamicHlsHelper _dynamicHlsHelper; private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls; @@ -74,8 +72,8 @@ namespace Jellyfin.Api.Controllers /// Instance of the interface. /// Instance of the interface. /// Instance of the class. - /// Instance of the interface. /// Instance of the interface. + /// Instance of . public DynamicHlsController( ILibraryManager libraryManager, IUserManager userManager, @@ -89,8 +87,8 @@ namespace Jellyfin.Api.Controllers IConfiguration configuration, IDeviceManager deviceManager, TranscodingJobHelper transcodingJobHelper, - INetworkManager networkManager, - ILogger logger) + ILogger logger, + DynamicHlsHelper dynamicHlsHelper) { _libraryManager = libraryManager; _userManager = userManager; @@ -104,8 +102,8 @@ namespace Jellyfin.Api.Controllers _configuration = configuration; _deviceManager = deviceManager; _transcodingJobHelper = transcodingJobHelper; - _networkManager = networkManager; _logger = logger; + _dynamicHlsHelper = dynamicHlsHelper; _encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration); } @@ -220,8 +218,6 @@ namespace Jellyfin.Api.Controllers [FromQuery] Dictionary streamOptions, [FromQuery] bool enableAdaptiveBitrateStreaming = true) { - var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; - var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new HlsVideoRequestDto { Id = itemId, @@ -276,8 +272,7 @@ namespace Jellyfin.Api.Controllers EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming }; - return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource) - .ConfigureAwait(false); + return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); } /// @@ -390,8 +385,6 @@ namespace Jellyfin.Api.Controllers [FromQuery] Dictionary streamOptions, [FromQuery] bool enableAdaptiveBitrateStreaming = true) { - var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; - var cancellationTokenSource = new CancellationTokenSource(); var streamingRequest = new HlsAudioRequestDto { Id = itemId, @@ -446,8 +439,7 @@ namespace Jellyfin.Api.Controllers EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming }; - return await GetMasterPlaylistInternal(streamingRequest, isHeadRequest, enableAdaptiveBitrateStreaming, cancellationTokenSource) - .ConfigureAwait(false); + return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); } /// @@ -1118,106 +1110,6 @@ namespace Jellyfin.Api.Controllers .ConfigureAwait(false); } - private async Task GetMasterPlaylistInternal( - StreamingRequestDto streamingRequest, - bool isHeadRequest, - bool enableAdaptiveBitrateStreaming, - CancellationTokenSource cancellationTokenSource) - { - using var state = await StreamingHelpers.GetStreamingState( - streamingRequest, - Request, - _authContext, - _mediaSourceManager, - _userManager, - _libraryManager, - _serverConfigurationManager, - _mediaEncoder, - _fileSystem, - _subtitleEncoder, - _configuration, - _dlnaManager, - _deviceManager, - _transcodingJobHelper, - _transcodingJobType, - cancellationTokenSource.Token) - .ConfigureAwait(false); - - Response.Headers.Add(HeaderNames.Expires, "0"); - if (isHeadRequest) - { - return new FileContentResult(Array.Empty(), MimeTypes.GetMimeType("playlist.m3u8")); - } - - var totalBitrate = state.OutputAudioBitrate ?? 0 + state.OutputVideoBitrate ?? 0; - - var builder = new StringBuilder(); - - builder.AppendLine("#EXTM3U"); - - var isLiveStream = state.IsSegmentedLiveStream; - - var queryString = Request.QueryString.ToString(); - - // from universal audio service - if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)) - { - queryString += "&SegmentContainer=" + state.Request.SegmentContainer; - } - - // from universal audio service - if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1) - { - queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons; - } - - // Main stream - var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8"; - - playlistUrl += queryString; - - var subtitleStreams = state.MediaSource - .MediaStreams - .Where(i => i.IsTextSubtitleStream) - .ToList(); - - var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest) - ? "subs" - : null; - - // If we're burning in subtitles then don't add additional subs to the manifest - if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) - { - subtitleGroup = null; - } - - if (!string.IsNullOrWhiteSpace(subtitleGroup)) - { - AddSubtitles(state, subtitleStreams, builder); - } - - AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); - - if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming)) - { - var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0; - - // By default, vary by just 200k - var variation = GetBitrateVariation(totalBitrate); - - var newBitrate = totalBitrate - variation; - var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); - AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); - - variation *= 2; - newBitrate = totalBitrate - variation; - variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); - AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); - } - - return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); - } - private async Task GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, string name, CancellationTokenSource cancellationTokenSource) { using var state = await StreamingHelpers.GetStreamingState( @@ -1411,330 +1303,6 @@ namespace Jellyfin.Api.Controllers return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); } - private void AddSubtitles(StreamState state, IEnumerable subtitles, StringBuilder builder) - { - var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index; - const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\""; - - foreach (var stream in subtitles) - { - var name = stream.DisplayTitle; - - var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index; - var isForced = stream.IsForced; - - var url = string.Format( - CultureInfo.InvariantCulture, - "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}", - state.Request.MediaSourceId, - stream.Index.ToString(CultureInfo.InvariantCulture), - 30.ToString(CultureInfo.InvariantCulture), - ClaimHelpers.GetToken(Request.HttpContext.User)); - - var line = string.Format( - CultureInfo.InvariantCulture, - Format, - name, - isDefault ? "YES" : "NO", - isForced ? "YES" : "NO", - url, - stream.Language ?? "Unknown"); - - builder.AppendLine(line); - } - } - - private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup) - { - builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=") - .Append(bitrate.ToString(CultureInfo.InvariantCulture)) - .Append(",AVERAGE-BANDWIDTH=") - .Append(bitrate.ToString(CultureInfo.InvariantCulture)); - - AppendPlaylistCodecsField(builder, state); - - AppendPlaylistResolutionField(builder, state); - - AppendPlaylistFramerateField(builder, state); - - if (!string.IsNullOrWhiteSpace(subtitleGroup)) - { - builder.Append(",SUBTITLES=\"") - .Append(subtitleGroup) - .Append('"'); - } - - builder.Append(Environment.NewLine); - builder.AppendLine(url); - } - - /// - /// Appends a CODECS field containing formatted strings of - /// the active streams output video and audio codecs. - /// - /// - /// - /// - /// StringBuilder to append the field to. - /// StreamState of the current stream. - private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state) - { - // Video - string videoCodecs = string.Empty; - int? videoCodecLevel = GetOutputVideoCodecLevel(state); - if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue) - { - videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value); - } - - // Audio - string audioCodecs = string.Empty; - if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec)) - { - audioCodecs = GetPlaylistAudioCodecs(state); - } - - StringBuilder codecs = new StringBuilder(); - - codecs.Append(videoCodecs); - - if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs)) - { - codecs.Append(','); - } - - codecs.Append(audioCodecs); - - if (codecs.Length > 1) - { - builder.Append(",CODECS=\"") - .Append(codecs) - .Append('"'); - } - } - - /// - /// Appends a RESOLUTION field containing the resolution of the output stream. - /// - /// - /// StringBuilder to append the field to. - /// StreamState of the current stream. - private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state) - { - if (state.OutputWidth.HasValue && state.OutputHeight.HasValue) - { - builder.Append(",RESOLUTION=") - .Append(state.OutputWidth.GetValueOrDefault()) - .Append('x') - .Append(state.OutputHeight.GetValueOrDefault()); - } - } - - /// - /// Appends a FRAME-RATE field containing the framerate of the output stream. - /// - /// - /// StringBuilder to append the field to. - /// StreamState of the current stream. - private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state) - { - double? framerate = null; - if (state.TargetFramerate.HasValue) - { - framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3); - } - else if (state.VideoStream?.RealFrameRate != null) - { - framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3); - } - - if (framerate.HasValue) - { - builder.Append(",FRAME-RATE=") - .Append(framerate.Value); - } - } - - private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming) - { - // Within the local network this will likely do more harm than good. - var ip = RequestHelpers.NormalizeIp(Request.HttpContext.Connection.RemoteIpAddress).ToString(); - if (_networkManager.IsInLocalNetwork(ip)) - { - return false; - } - - if (!enableAdaptiveBitrateStreaming) - { - return false; - } - - if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath)) - { - // Opening live streams is so slow it's not even worth it - return false; - } - - if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) - { - return false; - } - - if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec)) - { - return false; - } - - if (!state.IsOutputVideo) - { - return false; - } - - // Having problems in android - return false; - // return state.VideoRequest.VideoBitRate.HasValue; - } - - /// - /// Get the H.26X level of the output video stream. - /// - /// StreamState of the current stream. - /// H.26X level of the output video stream. - private int? GetOutputVideoCodecLevel(StreamState state) - { - string? levelString; - if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) - && state.VideoStream.Level.HasValue) - { - levelString = state.VideoStream?.Level.ToString(); - } - else - { - levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec); - } - - if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel)) - { - return parsedLevel; - } - - return null; - } - - /// - /// Gets a formatted string of the output audio codec, for use in the CODECS field. - /// - /// - /// - /// StreamState of the current stream. - /// Formatted audio codec string. - private string GetPlaylistAudioCodecs(StreamState state) - { - if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase)) - { - string? profile = state.GetRequestedProfiles("aac").FirstOrDefault(); - return HlsCodecStringHelpers.GetAACString(profile); - } - - if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) - { - return HlsCodecStringHelpers.GetMP3String(); - } - - if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) - { - return HlsCodecStringHelpers.GetAC3String(); - } - - if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase)) - { - return HlsCodecStringHelpers.GetEAC3String(); - } - - return string.Empty; - } - - /// - /// Gets a formatted string of the output video codec, for use in the CODECS field. - /// - /// - /// - /// StreamState of the current stream. - /// Video codec. - /// Video level. - /// Formatted video codec string. - private string GetPlaylistVideoCodecs(StreamState state, string codec, int level) - { - if (level == 0) - { - // This is 0 when there's no requested H.26X level in the device profile - // and the source is not encoded in H.26X - _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist"); - return string.Empty; - } - - if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)) - { - string profile = state.GetRequestedProfiles("h264").FirstOrDefault(); - return HlsCodecStringHelpers.GetH264String(profile, level); - } - - if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) - { - string profile = state.GetRequestedProfiles("h265").FirstOrDefault(); - - return HlsCodecStringHelpers.GetH265String(profile, level); - } - - return string.Empty; - } - - private int GetBitrateVariation(int bitrate) - { - // By default, vary by just 50k - var variation = 50000; - - if (bitrate >= 10000000) - { - variation = 2000000; - } - else if (bitrate >= 5000000) - { - variation = 1500000; - } - else if (bitrate >= 3000000) - { - variation = 1000000; - } - else if (bitrate >= 2000000) - { - variation = 500000; - } - else if (bitrate >= 1000000) - { - variation = 300000; - } - else if (bitrate >= 600000) - { - variation = 200000; - } - else if (bitrate >= 400000) - { - variation = 100000; - } - - return variation; - } - - private string ReplaceBitrate(string url, int oldValue, int newValue) - { - return url.Replace( - "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture), - "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture), - StringComparison.OrdinalIgnoreCase); - } - private double[] GetSegmentLengths(StreamState state) { var result = new List(); @@ -1788,7 +1356,7 @@ namespace Jellyfin.Api.Controllers return string.Format( CultureInfo.InvariantCulture, - "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -f hls -max_delay 5000000 -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"", + "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size 2048 -f hls -max_delay 5000000 -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"", inputModifier, _encodingHelper.GetInputArgument(state, encodingOptions), threads, @@ -2089,7 +1657,7 @@ namespace Jellyfin.Api.Controllers return Task.CompletedTask; }); - return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, this); + return FileStreamResponseHelpers.GetStaticFileResult(segmentPath, MimeTypes.GetMimeType(segmentPath)!, false, HttpContext); } private long GetEndPositionTicks(StreamState state, int requestedIndex) diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index e4a6842bc..816252f80 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -61,7 +61,7 @@ namespace Jellyfin.Api.Controllers var file = segmentId + Path.GetExtension(Request.Path); file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file); - return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, this); + return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext); } /// @@ -148,7 +148,7 @@ namespace Jellyfin.Api.Controllers return Task.CompletedTask; }); - return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, this); + return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, HttpContext); } } } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 8f5c6beb3..a204fe35c 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -113,7 +113,7 @@ namespace Jellyfin.Api.Controllers user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType))); await _providerManager - .SaveImage(user, memoryStream, mimeType, user.ProfileImage.Path) + .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) .ConfigureAwait(false); await _userManager.UpdateUserAsync(user).ConfigureAwait(false); @@ -174,7 +174,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult DeleteItemImage( + public async Task DeleteItemImage( [FromRoute] Guid itemId, [FromRoute] ImageType imageType, [FromRoute] int? imageIndex = null) @@ -185,7 +185,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - item.DeleteImage(imageType, imageIndex ?? 0); + await item.DeleteImageAsync(imageType, imageIndex ?? 0).ConfigureAwait(false); return NoContent(); } @@ -218,7 +218,7 @@ namespace Jellyfin.Api.Controllers // Handle image/png; charset=utf-8 var mimeType = Request.ContentType.Split(';').FirstOrDefault(); await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); - item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -237,7 +237,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateItemImageIndex( + public async Task UpdateItemImageIndex( [FromRoute] Guid itemId, [FromRoute] ImageType imageType, [FromRoute] int imageIndex, @@ -249,7 +249,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - item.SwapImages(imageType, imageIndex, newIndex); + await item.SwapImagesAsync(imageType, imageIndex, newIndex).ConfigureAwait(false); return NoContent(); } @@ -264,7 +264,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult> GetItemImageInfos([FromRoute] Guid itemId) + public async Task>> GetItemImageInfos([FromRoute] Guid itemId) { var item = _libraryManager.GetItemById(itemId); if (item == null) @@ -281,7 +281,7 @@ namespace Jellyfin.Api.Controllers return list; } - _libraryManager.UpdateImages(item); // this makes sure dimensions and hashes are correct + await _libraryManager.UpdateImagesAsync(item).ConfigureAwait(false); // this makes sure dimensions and hashes are correct foreach (var image in itemImages) { @@ -1281,9 +1281,9 @@ namespace Jellyfin.Api.Controllers Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false))); // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified - if (!(dateImageModified > ifModifiedSinceHeader)) + if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue) { - if (ifModifiedSinceHeader.Add(cacheDuration!.Value) < DateTime.UtcNow) + if (ifModifiedSinceHeader.Add(cacheDuration.Value) < DateTime.UtcNow) { Response.StatusCode = StatusCodes.Status304NotModified; return new ContentResult(); diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 4b40c6ada..ec52f4996 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Jellyfin.Api.Constants; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -67,7 +68,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Items/{itemId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, Required] BaseItemDto request) + public async Task UpdateItem([FromRoute] Guid itemId, [FromBody, Required] BaseItemDto request) { var item = _libraryManager.GetItemById(itemId); if (item == null) @@ -101,7 +102,7 @@ namespace Jellyfin.Api.Controllers item.OnMetadataChanged(); - item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); if (isLockedChanged && item.IsFolder) { @@ -110,7 +111,7 @@ namespace Jellyfin.Api.Controllers foreach (var child in folder.GetRecursiveChildren()) { child.IsLocked = newLockData; - child.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + await child.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); } } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 1b8b68313..f9273bad6 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -4,10 +4,10 @@ using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -266,7 +266,9 @@ namespace Jellyfin.Api.Controllers bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id) // Assume all folders inside an EnabledChannel are enabled - || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id); + || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id) + // Assume all items inside an EnabledChannel are enabled + || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.ChannelId); var collectionFolders = _libraryManager.GetCollectionFolders(item); foreach (var collectionFolder in collectionFolders) diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 4548e202a..a30873e9e 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -19,6 +19,7 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Net; @@ -35,8 +36,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Book = MediaBrowser.Controller.Entities.Book; -using Movie = Jellyfin.Data.Entities.Movie; -using MusicAlbum = Jellyfin.Data.Entities.MusicAlbum; namespace Jellyfin.Api.Controllers { @@ -619,7 +618,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.Download)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDownload([FromRoute] Guid itemId) + public async Task GetDownload([FromRoute] Guid itemId) { var item = _libraryManager.GetItemById(itemId); if (item == null) @@ -648,7 +647,7 @@ namespace Jellyfin.Api.Controllers if (user != null) { - LogDownload(item, user, auth); + await LogDownloadAsync(item, user, auth).ConfigureAwait(false); } var path = item.Path; @@ -861,17 +860,17 @@ namespace Jellyfin.Api.Controllers : item; } - private void LogDownload(BaseItem item, User user, AuthorizationInfo auth) + private async Task LogDownloadAsync(BaseItem item, User user, AuthorizationInfo auth) { try { - _activityManager.Create(new ActivityLog( + await _activityManager.CreateAsync(new ActivityLog( string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name), "UserDownloadingContent", auth.UserId) { ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device), - }); + }).ConfigureAwait(false); } catch { diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index cdab4f356..d290e3c5b 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -76,7 +76,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? name, [FromQuery] string? collectionType, [FromQuery] string[] paths, - [FromBody] LibraryOptionsDto? libraryOptionsDto, + [FromBody] AddVirtualFolderDto? libraryOptionsDto, [FromQuery] bool refreshLibrary = false) { var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions(); @@ -312,19 +312,17 @@ namespace Jellyfin.Api.Controllers /// /// Update library options. /// - /// The library name. - /// The library options. + /// The library name and options. /// Library updated. /// A . [HttpPost("LibraryOptions")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdateLibraryOptions( - [FromQuery] string? id, - [FromBody] LibraryOptions? libraryOptions) + [FromBody] UpdateLibraryOptionsDto request) { - var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id); + var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id); - collectionFolder.UpdateLibraryOptions(libraryOptions); + collectionFolder.UpdateLibraryOptions(request.LibraryOptions); return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 97a88bc14..3ccfc826d 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -592,11 +592,11 @@ namespace Jellyfin.Api.Controllers GenreIds = RequestHelpers.GetGuids(genreIds) }; - if (!librarySeriesId.Equals(Guid.Empty)) + if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty)) { query.IsSeries = true; - if (_libraryManager.GetItemById(librarySeriesId ?? Guid.Empty) is Series series) + if (_libraryManager.GetItemById(librarySeriesId.Value) is Series series) { query.Name = series.Name; } @@ -1004,7 +1004,7 @@ namespace Jellyfin.Api.Controllers /// Validate login. /// Created listings provider returned. /// A containing the created listings provider. - [HttpGet("ListingProviders")] + [HttpPost("ListingProviders")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")] diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index 517113074..1e154a039 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -1,30 +1,18 @@ using System; using System.Buffers; using System.ComponentModel.DataAnnotations; -using System.Globalization; using System.Linq; using System.Net.Mime; -using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.MediaInfoDtos; using Jellyfin.Api.Models.VideoDtos; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -42,12 +30,9 @@ namespace Jellyfin.Api.Controllers private readonly IMediaSourceManager _mediaSourceManager; private readonly IDeviceManager _deviceManager; private readonly ILibraryManager _libraryManager; - private readonly INetworkManager _networkManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly IUserManager _userManager; private readonly IAuthorizationContext _authContext; private readonly ILogger _logger; - private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly MediaInfoHelper _mediaInfoHelper; /// /// Initializes a new instance of the class. @@ -55,32 +40,23 @@ namespace Jellyfin.Api.Controllers /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. + /// Instance of the . public MediaInfoController( IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager, - INetworkManager networkManager, - IMediaEncoder mediaEncoder, - IUserManager userManager, IAuthorizationContext authContext, ILogger logger, - IServerConfigurationManager serverConfigurationManager) + MediaInfoHelper mediaInfoHelper) { _mediaSourceManager = mediaSourceManager; _deviceManager = deviceManager; _libraryManager = libraryManager; - _networkManager = networkManager; - _mediaEncoder = mediaEncoder; - _userManager = userManager; _authContext = authContext; _logger = logger; - _serverConfigurationManager = serverConfigurationManager; + _mediaInfoHelper = mediaInfoHelper; } /// @@ -94,7 +70,10 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery, Required] Guid? userId) { - return await GetPlaybackInfoInternal(itemId, userId).ConfigureAwait(false); + return await _mediaInfoHelper.GetPlaybackInfo( + itemId, + userId) + .ConfigureAwait(false); } /// @@ -153,7 +132,12 @@ namespace Jellyfin.Api.Controllers } } - var info = await GetPlaybackInfoInternal(itemId, userId, mediaSourceId, liveStreamId).ConfigureAwait(false); + var info = await _mediaInfoHelper.GetPlaybackInfo( + itemId, + userId, + mediaSourceId, + liveStreamId) + .ConfigureAwait(false); if (profile != null) { @@ -162,7 +146,7 @@ namespace Jellyfin.Api.Controllers foreach (var mediaSource in info.MediaSources) { - SetDeviceSpecificData( + _mediaInfoHelper.SetDeviceSpecificData( item, mediaSource, profile, @@ -179,10 +163,11 @@ namespace Jellyfin.Api.Controllers enableDirectStream, enableTranscoding, allowVideoStreamCopy, - allowAudioStreamCopy); + allowAudioStreamCopy, + Request.HttpContext.Connection.RemoteIpAddress.ToString()); } - SortMediaSources(info, maxStreamingBitrate); + _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate); } if (autoOpenLiveStream) @@ -191,21 +176,23 @@ namespace Jellyfin.Api.Controllers if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId)) { - var openStreamResult = await OpenMediaSource(new LiveStreamRequest - { - AudioStreamIndex = audioStreamIndex, - DeviceProfile = deviceProfile?.DeviceProfile, - EnableDirectPlay = enableDirectPlay, - EnableDirectStream = enableDirectStream, - ItemId = itemId, - MaxAudioChannels = maxAudioChannels, - MaxStreamingBitrate = maxStreamingBitrate, - PlaySessionId = info.PlaySessionId, - StartTimeTicks = startTimeTicks, - SubtitleStreamIndex = subtitleStreamIndex, - UserId = userId ?? Guid.Empty, - OpenToken = mediaSource.OpenToken - }).ConfigureAwait(false); + var openStreamResult = await _mediaInfoHelper.OpenMediaSource( + Request, + new LiveStreamRequest + { + AudioStreamIndex = audioStreamIndex, + DeviceProfile = deviceProfile?.DeviceProfile, + EnableDirectPlay = enableDirectPlay, + EnableDirectStream = enableDirectStream, + ItemId = itemId, + MaxAudioChannels = maxAudioChannels, + MaxStreamingBitrate = maxStreamingBitrate, + PlaySessionId = info.PlaySessionId, + StartTimeTicks = startTimeTicks, + SubtitleStreamIndex = subtitleStreamIndex, + UserId = userId ?? Guid.Empty, + OpenToken = mediaSource.OpenToken + }).ConfigureAwait(false); info.MediaSources = new[] { openStreamResult.MediaSource }; } @@ -215,7 +202,7 @@ namespace Jellyfin.Api.Controllers { foreach (var mediaSource in info.MediaSources) { - NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video); + _mediaInfoHelper.NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video); } } @@ -271,7 +258,7 @@ namespace Jellyfin.Api.Controllers EnableDirectStream = enableDirectStream, DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http } }; - return await OpenMediaSource(request).ConfigureAwait(false); + return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false); } /// @@ -282,9 +269,9 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("LiveStreams/Close")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CloseLiveStream([FromQuery, Required] string? liveStreamId) + public async Task CloseLiveStream([FromQuery, Required] string? liveStreamId) { - _mediaSourceManager.CloseLiveStream(liveStreamId).GetAwaiter().GetResult(); + await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false); return NoContent(); } @@ -324,454 +311,5 @@ namespace Jellyfin.Api.Controllers ArrayPool.Shared.Return(buffer); } } - - private async Task GetPlaybackInfoInternal( - Guid id, - Guid? userId, - string? mediaSourceId = null, - string? liveStreamId = null) - { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? _userManager.GetUserById(userId.Value) - : null; - var item = _libraryManager.GetItemById(id); - var result = new PlaybackInfoResponse(); - - MediaSourceInfo[] mediaSources; - if (string.IsNullOrWhiteSpace(liveStreamId)) - { - // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes? - var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false); - - if (string.IsNullOrWhiteSpace(mediaSourceId)) - { - mediaSources = mediaSourcesList.ToArray(); - } - else - { - mediaSources = mediaSourcesList - .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)) - .ToArray(); - } - } - else - { - var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false); - - mediaSources = new[] { mediaSource }; - } - - if (mediaSources.Length == 0) - { - result.MediaSources = Array.Empty(); - - result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream; - } - else - { - // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it - // Should we move this directly into MediaSourceManager? - result.MediaSources = JsonSerializer.Deserialize(JsonSerializer.SerializeToUtf8Bytes(mediaSources)); - - result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - } - - return result; - } - - private void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type) - { - mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type); - } - - private void SetDeviceSpecificData( - BaseItem item, - MediaSourceInfo mediaSource, - DeviceProfile profile, - AuthorizationInfo auth, - long? maxBitrate, - long startTimeTicks, - string mediaSourceId, - int? audioStreamIndex, - int? subtitleStreamIndex, - int? maxAudioChannels, - string playSessionId, - Guid userId, - bool enableDirectPlay, - bool enableDirectStream, - bool enableTranscoding, - bool allowVideoStreamCopy, - bool allowAudioStreamCopy) - { - var streamBuilder = new StreamBuilder(_mediaEncoder, _logger); - - var options = new VideoOptions - { - MediaSources = new[] { mediaSource }, - Context = EncodingContext.Streaming, - DeviceId = auth.DeviceId, - ItemId = item.Id, - Profile = profile, - MaxAudioChannels = maxAudioChannels - }; - - if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase)) - { - options.MediaSourceId = mediaSourceId; - options.AudioStreamIndex = audioStreamIndex; - options.SubtitleStreamIndex = subtitleStreamIndex; - } - - var user = _userManager.GetUserById(userId); - - if (!enableDirectPlay) - { - mediaSource.SupportsDirectPlay = false; - } - - if (!enableDirectStream) - { - mediaSource.SupportsDirectStream = false; - } - - if (!enableTranscoding) - { - mediaSource.SupportsTranscoding = false; - } - - if (item is Audio) - { - _logger.LogInformation( - "User policy for {0}. EnableAudioPlaybackTranscoding: {1}", - user.Username, - user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); - } - else - { - _logger.LogInformation( - "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}", - user.Username, - user.HasPermission(PermissionKind.EnablePlaybackRemuxing), - user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding), - user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); - } - - // Beginning of Playback Determination: Attempt DirectPlay first - if (mediaSource.SupportsDirectPlay) - { - if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) - { - mediaSource.SupportsDirectPlay = false; - } - else - { - var supportsDirectStream = mediaSource.SupportsDirectStream; - - // Dummy this up to fool StreamBuilder - mediaSource.SupportsDirectStream = true; - options.MaxBitrate = maxBitrate; - - if (item is Audio) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) - { - options.ForceDirectPlay = true; - } - } - else if (item is Video) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) - { - options.ForceDirectPlay = true; - } - } - - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); - - if (streamInfo == null || !streamInfo.IsDirectStream) - { - mediaSource.SupportsDirectPlay = false; - } - - // Set this back to what it was - mediaSource.SupportsDirectStream = supportsDirectStream; - - if (streamInfo != null) - { - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - } - } - } - - if (mediaSource.SupportsDirectStream) - { - if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) - { - mediaSource.SupportsDirectStream = false; - } - else - { - options.MaxBitrate = GetMaxBitrate(maxBitrate, user); - - if (item is Audio) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) - { - options.ForceDirectStream = true; - } - } - else if (item is Video) - { - if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) - && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) - { - options.ForceDirectStream = true; - } - } - - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); - - if (streamInfo == null || !streamInfo.IsDirectStream) - { - mediaSource.SupportsDirectStream = false; - } - - if (streamInfo != null) - { - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - } - } - } - - if (mediaSource.SupportsTranscoding) - { - options.MaxBitrate = GetMaxBitrate(maxBitrate, user); - - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); - - if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) - { - if (streamInfo != null) - { - streamInfo.PlaySessionId = playSessionId; - streamInfo.StartPositionTicks = startTimeTicks; - mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); - mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; - mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - - // Do this after the above so that StartPositionTicks is set - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - } - } - else - { - if (streamInfo != null) - { - streamInfo.PlaySessionId = playSessionId; - - if (streamInfo.PlayMethod == PlayMethod.Transcode) - { - streamInfo.StartPositionTicks = startTimeTicks; - mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); - - if (!allowVideoStreamCopy) - { - mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; - } - - if (!allowAudioStreamCopy) - { - mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; - } - - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - } - - if (!allowAudioStreamCopy) - { - mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; - } - - mediaSource.TranscodingContainer = streamInfo.Container; - mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - - // Do this after the above so that StartPositionTicks is set - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - } - } - } - - foreach (var attachment in mediaSource.MediaAttachments) - { - attachment.DeliveryUrl = string.Format( - CultureInfo.InvariantCulture, - "/Videos/{0}/{1}/Attachments/{2}", - item.Id, - mediaSource.Id, - attachment.Index); - } - } - - private async Task OpenMediaSource(LiveStreamRequest request) - { - var authInfo = _authContext.GetAuthorizationInfo(Request); - - var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false); - - var profile = request.DeviceProfile; - if (profile == null) - { - var caps = _deviceManager.GetCapabilities(authInfo.DeviceId); - if (caps != null) - { - profile = caps.DeviceProfile; - } - } - - if (profile != null) - { - var item = _libraryManager.GetItemById(request.ItemId); - - SetDeviceSpecificData( - item, - result.MediaSource, - profile, - authInfo, - request.MaxStreamingBitrate, - request.StartTimeTicks ?? 0, - result.MediaSource.Id, - request.AudioStreamIndex, - request.SubtitleStreamIndex, - request.MaxAudioChannels, - request.PlaySessionId, - request.UserId, - request.EnableDirectPlay, - request.EnableDirectStream, - true, - true, - true); - } - else - { - if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl)) - { - result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId; - } - } - - // here was a check if (result.MediaSource != null) but Rider said it will never be null - NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video); - - return result; - } - - private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken) - { - var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken); - mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex; - - mediaSource.TranscodeReasons = info.TranscodeReasons; - - foreach (var profile in profiles) - { - foreach (var stream in mediaSource.MediaStreams) - { - if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index) - { - stream.DeliveryMethod = profile.DeliveryMethod; - - if (profile.DeliveryMethod == SubtitleDeliveryMethod.External) - { - stream.DeliveryUrl = profile.Url.TrimStart('-'); - stream.IsExternalUrl = profile.IsExternalUrl; - } - } - } - } - } - - private long? GetMaxBitrate(long? clientMaxBitrate, User user) - { - var maxBitrate = clientMaxBitrate; - var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0; - - if (remoteClientMaxBitrate <= 0) - { - remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit; - } - - if (remoteClientMaxBitrate > 0) - { - var isInLocalNetwork = _networkManager.IsInLocalNetwork(Request.HttpContext.Connection.RemoteIpAddress.ToString()); - - _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, Request.HttpContext.Connection.RemoteIpAddress.ToString(), isInLocalNetwork); - if (!isInLocalNetwork) - { - maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate); - } - } - - return maxBitrate; - } - - private void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate) - { - var originalList = result.MediaSources.ToList(); - - result.MediaSources = result.MediaSources.OrderBy(i => - { - // Nothing beats direct playing a file - if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File) - { - return 0; - } - - return 1; - }) - .ThenBy(i => - { - // Let's assume direct streaming a file is just as desirable as direct playing a remote url - if (i.SupportsDirectPlay || i.SupportsDirectStream) - { - return 0; - } - - return 1; - }) - .ThenBy(i => - { - return i.Protocol switch - { - MediaProtocol.File => 0, - _ => 1, - }; - }) - .ThenBy(i => - { - if (maxBitrate.HasValue && i.Bitrate.HasValue) - { - return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2; - } - - return 1; - }) - .ThenBy(originalList.IndexOf) - .ToArray(); - } } } diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 148d8a18e..7fcfc749d 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -10,6 +10,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Dto; @@ -181,7 +182,7 @@ namespace Jellyfin.Api.Controllers DtoOptions dtoOptions, RecommendationType type) { - var itemTypes = new List { nameof(MediaBrowser.Controller.Entities.Movies.Movie) }; + var itemTypes = new List { nameof(Movie) }; if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) { itemTypes.Add(nameof(Trailer)); diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index cf3e780b4..0ceda6815 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using Jellyfin.Api.Constants; diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 12c87d7c3..f4c6a9253 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -83,12 +83,12 @@ namespace Jellyfin.Api.Controllers /// An on success. [HttpPost("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult AddToPlaylist( - [FromRoute] string? playlistId, + public async Task AddToPlaylist( + [FromRoute] Guid playlistId, [FromQuery] string? ids, [FromQuery] Guid? userId) { - _playlistManager.AddToPlaylist(playlistId, RequestHelpers.GetGuids(ids), userId ?? Guid.Empty); + await _playlistManager.AddToPlaylistAsync(playlistId, RequestHelpers.GetGuids(ids), userId ?? Guid.Empty).ConfigureAwait(false); return NoContent(); } @@ -102,12 +102,12 @@ namespace Jellyfin.Api.Controllers /// An on success. [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult MoveItem( + public async Task MoveItem( [FromRoute] string? playlistId, [FromRoute] string? itemId, [FromRoute] int newIndex) { - _playlistManager.MoveItem(playlistId, itemId, newIndex); + await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false); return NoContent(); } @@ -120,9 +120,9 @@ namespace Jellyfin.Api.Controllers /// An on success. [HttpDelete("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult RemoveFromPlaylist([FromRoute] string? playlistId, [FromQuery] string? entryIds) + public async Task RemoveFromPlaylist([FromRoute] string? playlistId, [FromQuery] string? entryIds) { - _playlistManager.RemoveFromPlaylist(playlistId, RequestHelpers.Split(entryIds, ',', true)); + await _playlistManager.RemoveFromPlaylistAsync(playlistId, RequestHelpers.Split(entryIds, ',', true)).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index 0422bfe72..22f2ca5c3 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -94,7 +94,7 @@ namespace Jellyfin.Api.Controllers /// Item id. /// Item marked as unplayed. /// A containing the . - [HttpDelete("Users/{userId}/PlayedItem/{itemId}")] + [HttpDelete("Users/{userId}/PlayedItems/{itemId}")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult MarkUnplayedItem([FromRoute] Guid userId, [FromRoute] Guid itemId) { diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index b2f34680b..a82f2621a 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -120,10 +120,14 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - var configuration = (BasePluginConfiguration)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions) + var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions) .ConfigureAwait(false); - plugin.UpdateConfiguration(configuration); + if (configuration != null) + { + plugin.UpdateConfiguration(configuration); + } + return NoContent(); } diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs new file mode 100644 index 000000000..73da2f906 --- /dev/null +++ b/Jellyfin.Api/Controllers/QuickConnectController.cs @@ -0,0 +1,154 @@ +using System.ComponentModel.DataAnnotations; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.QuickConnect; +using MediaBrowser.Model.QuickConnect; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Quick connect controller. + /// + public class QuickConnectController : BaseJellyfinApiController + { + private readonly IQuickConnect _quickConnect; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public QuickConnectController(IQuickConnect quickConnect) + { + _quickConnect = quickConnect; + } + + /// + /// Gets the current quick connect state. + /// + /// Quick connect state returned. + /// The current . + [HttpGet("Status")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetStatus() + { + _quickConnect.ExpireRequests(); + return _quickConnect.State; + } + + /// + /// Initiate a new quick connect request. + /// + /// Quick connect request successfully created. + /// Quick connect is not active on this server. + /// A with a secret and code for future use or an error message. + [HttpGet("Initiate")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult Initiate() + { + return _quickConnect.TryConnect(); + } + + /// + /// Attempts to retrieve authentication information. + /// + /// Secret previously returned from the Initiate endpoint. + /// Quick connect result returned. + /// Unknown quick connect secret. + /// An updated . + [HttpGet("Connect")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult Connect([FromQuery, Required] string secret) + { + try + { + return _quickConnect.CheckRequestStatus(secret); + } + catch (ResourceNotFoundException) + { + return NotFound("Unknown secret"); + } + } + + /// + /// Temporarily activates quick connect for five minutes. + /// + /// Quick connect has been temporarily activated. + /// Quick connect is unavailable on this server. + /// An on success. + [HttpPost("Activate")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public ActionResult Activate() + { + if (_quickConnect.State == QuickConnectState.Unavailable) + { + return Forbid("Quick connect is unavailable"); + } + + _quickConnect.Activate(); + return NoContent(); + } + + /// + /// Enables or disables quick connect. + /// + /// New . + /// Quick connect state set successfully. + /// An on success. + [HttpPost("Available")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult Available([FromQuery] QuickConnectState status = QuickConnectState.Available) + { + _quickConnect.SetState(status); + return NoContent(); + } + + /// + /// Authorizes a pending quick connect request. + /// + /// Quick connect code to authorize. + /// Quick connect result authorized successfully. + /// Unknown user id. + /// Boolean indicating if the authorization was successful. + [HttpPost("Authorize")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public ActionResult Authorize([FromQuery, Required] string code) + { + var userId = ClaimHelpers.GetUserId(Request.HttpContext.User); + if (!userId.HasValue) + { + return Forbid("Unknown user id"); + } + + return _quickConnect.AuthorizeRequest(userId.Value, code); + } + + /// + /// Deauthorize all quick connect devices for the current user. + /// + /// All quick connect devices were deleted. + /// The number of devices that were deleted. + [HttpPost("Deauthorize")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult Deauthorize() + { + var userId = ClaimHelpers.GetUserId(Request.HttpContext.User); + if (!userId.HasValue) + { + return 0; + } + + return _quickConnect.DeleteAllDevices(userId.Value); + } + } +} diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index a203c50b9..30a4f73fc 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -221,7 +221,7 @@ namespace Jellyfin.Api.Controllers await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None) .ConfigureAwait(false); - item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 48b57bdb7..ba8d51598 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -153,7 +153,6 @@ namespace Jellyfin.Api.Controllers /// The ids of the items to play, comma delimited. /// The starting position of the first item. /// The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now. - /// The . /// Instruction sent to session. /// A . [HttpPost("Sessions/{sessionId}/Playing")] @@ -163,17 +162,14 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] string? sessionId, [FromQuery] Guid[] itemIds, [FromQuery] long? startPositionTicks, - [FromQuery] PlayCommand playCommand, - [FromBody, Required] PlayRequest playRequest) + [FromQuery] PlayCommand playCommand) { - if (playRequest == null) + var playRequest = new PlayRequest { - throw new ArgumentException("Request Body may not be null"); - } - - playRequest.ItemIds = itemIds; - playRequest.StartPositionTicks = startPositionTicks; - playRequest.PlayCommand = playCommand; + ItemIds = itemIds, + StartPositionTicks = startPositionTicks, + PlayCommand = playCommand + }; _sessionManager.SendPlayCommand( RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, @@ -413,7 +409,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostFullCapabilities( - [FromQuery, Required] string? id, + [FromQuery] string? id, [FromBody, Required] ClientCapabilities capabilities) { if (string.IsNullOrWhiteSpace(id)) @@ -480,7 +476,7 @@ namespace Jellyfin.Api.Controllers /// /// Password reset providers retrieved. /// An with the password reset providers. - [HttpGet("Auto/PasswordResetProviders")] + [HttpGet("Auth/PasswordResetProviders")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.RequiresElevation)] public ActionResult> GetPasswordResetProviders() diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index c8e3cc4f5..9c259cc19 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; @@ -64,21 +65,16 @@ namespace Jellyfin.Api.Controllers /// /// Sets the initial startup wizard configuration. /// - /// The UI language culture. - /// The metadata country code. - /// The preferred language for metadata. + /// The updated startup configuration. /// Configuration saved. /// A indicating success. [HttpPost("Configuration")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdateInitialConfiguration( - [FromForm] string? uiCulture, - [FromForm] string? metadataCountryCode, - [FromForm] string? preferredMetadataLanguage) + public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration) { - _config.Configuration.UICulture = uiCulture; - _config.Configuration.MetadataCountryCode = metadataCountryCode; - _config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage; + _config.Configuration.UICulture = startupConfiguration.UICulture; + _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode; + _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage; _config.SaveConfiguration(); return NoContent(); } @@ -86,16 +82,15 @@ namespace Jellyfin.Api.Controllers /// /// Sets remote access and UPnP. /// - /// Enable remote access. - /// Enable UPnP. + /// The startup remote access dto. /// Configuration saved. /// A indicating success. [HttpPost("RemoteAccess")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) + public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto) { - _config.Configuration.EnableRemoteAccess = enableRemoteAccess; - _config.Configuration.EnableUPnP = enableAutomaticPortMapping; + _config.Configuration.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess; + _config.Configuration.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping; _config.SaveConfiguration(); return NoContent(); } @@ -131,7 +126,7 @@ namespace Jellyfin.Api.Controllers /// [HttpPost("User")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task UpdateStartupUser([FromForm] StartupUserDto startupUserDto) + public async Task UpdateStartupUser([FromBody] StartupUserDto startupUserDto) { var user = _userManager.Users.First(); diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 75df16aa7..b13cf9fa5 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -2,17 +2,20 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Net.Http; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; -using Jellyfin.Api.Models.VideoDtos; +using Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.MediaInfo; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers { @@ -23,27 +26,39 @@ namespace Jellyfin.Api.Controllers public class UniversalAudioController : BaseJellyfinApiController { private readonly IAuthorizationContext _authorizationContext; - private readonly MediaInfoController _mediaInfoController; - private readonly DynamicHlsController _dynamicHlsController; - private readonly AudioController _audioController; + private readonly IDeviceManager _deviceManager; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly MediaInfoHelper _mediaInfoHelper; + private readonly AudioHelper _audioHelper; + private readonly DynamicHlsHelper _dynamicHlsHelper; /// /// Initializes a new instance of the class. /// /// Instance of the interface. - /// Instance of the . - /// Instance of the . - /// Instance of the . + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of . + /// Instance of . + /// Instance of . public UniversalAudioController( IAuthorizationContext authorizationContext, - MediaInfoController mediaInfoController, - DynamicHlsController dynamicHlsController, - AudioController audioController) + IDeviceManager deviceManager, + ILibraryManager libraryManager, + ILogger logger, + MediaInfoHelper mediaInfoHelper, + AudioHelper audioHelper, + DynamicHlsHelper dynamicHlsHelper) { _authorizationContext = authorizationContext; - _mediaInfoController = mediaInfoController; - _dynamicHlsController = dynamicHlsController; - _audioController = audioController; + _deviceManager = deviceManager; + _libraryManager = libraryManager; + _logger = logger; + _mediaInfoHelper = mediaInfoHelper; + _audioHelper = audioHelper; + _dynamicHlsHelper = dynamicHlsHelper; } /// @@ -70,9 +85,9 @@ namespace Jellyfin.Api.Controllers /// Redirected to remote audio stream. /// A containing the audio file. [HttpGet("Audio/{itemId}/universal")] - [HttpGet("Audio/{itemId}/{universal=universal}.{container?}", Name = "GetUniversalAudioStream_2")] + [HttpGet("Audio/{itemId}/universal.{container}", Name = "GetUniversalAudioStream_2")] [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")] - [HttpHead("Audio/{itemId}/{universal=universal}.{container?}", Name = "HeadUniversalAudioStream_2")] + [HttpHead("Audio/{itemId}/universal.{container}", Name = "HeadUniversalAudioStream_2")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status302Found)] @@ -95,24 +110,68 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool breakOnNonKeyFrames, [FromQuery] bool enableRedirection = true) { - bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head; var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); _authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId; - var playbackInfoResult = await _mediaInfoController.GetPostedPlaybackInfo( - itemId, - userId, - maxStreamingBitrate, - startTimeTicks, - null, - null, - maxAudioChannels, - mediaSourceId, - null, - new DeviceProfileDto { DeviceProfile = deviceProfile }) - .ConfigureAwait(false); - var mediaSource = playbackInfoResult.Value.MediaSources[0]; + var authInfo = _authorizationContext.GetAuthorizationInfo(Request); + _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile); + + if (deviceProfile == null) + { + var clientCapabilities = _deviceManager.GetCapabilities(authInfo.DeviceId); + if (clientCapabilities != null) + { + deviceProfile = clientCapabilities.DeviceProfile; + } + } + + var info = await _mediaInfoHelper.GetPlaybackInfo( + itemId, + userId, + mediaSourceId) + .ConfigureAwait(false); + + if (deviceProfile != null) + { + // set device specific data + var item = _libraryManager.GetItemById(itemId); + + foreach (var sourceInfo in info.MediaSources) + { + _mediaInfoHelper.SetDeviceSpecificData( + item, + sourceInfo, + deviceProfile, + authInfo, + maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate, + startTimeTicks ?? 0, + mediaSourceId ?? string.Empty, + null, + null, + maxAudioChannels, + info!.PlaySessionId!, + userId ?? Guid.Empty, + true, + true, + true, + true, + true, + Request.HttpContext.Connection.RemoteIpAddress.ToString()); + } + + _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate); + } + + if (info.MediaSources != null) + { + foreach (var source in info.MediaSources) + { + _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile!, DlnaProfileType.Video); + } + } + + var mediaSource = info.MediaSources![0]; if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http) { if (enableRedirection) @@ -127,129 +186,71 @@ namespace Jellyfin.Api.Controllers var isStatic = mediaSource.SupportsDirectStream; if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) { - var transcodingProfile = deviceProfile.TranscodingProfiles[0]; - // hls segment container can only be mpegts or fmp4 per ffmpeg documentation // TODO: remove this when we switch back to the segment muxer var supportedHlsContainers = new[] { "mpegts", "fmp4" }; - if (isHeadRequest) + var dynamicHlsRequestDto = new HlsAudioRequestDto { - _dynamicHlsController.Request.Method = HttpMethod.Head.Method; - } - - return await _dynamicHlsController.GetMasterHlsAudioPlaylist( - itemId, - ".m3u8", - isStatic, - null, - null, - null, - playbackInfoResult.Value.PlaySessionId, + Id = itemId, + Container = ".m3u8", + Static = isStatic, + PlaySessionId = info.PlaySessionId, // fallback to mpegts if device reports some weird value unsupported by hls - Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts", - null, - null, - mediaSource.Id, - deviceId, - transcodingProfile.AudioCodec, - null, - null, - null, - transcodingProfile.BreakOnNonKeyFrames, - maxAudioSampleRate, - maxAudioBitDepth, - null, - isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)), - maxAudioChannels, - null, - null, - null, - null, - null, - startTimeTicks, - null, - null, - null, - null, - SubtitleDeliveryMethod.Hls, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), - null, - null, - EncodingContext.Static, - new Dictionary()) - .ConfigureAwait(false); - } - else - { - if (isHeadRequest) - { - _audioController.Request.Method = HttpMethod.Head.Method; - } + SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts", + MediaSourceId = mediaSourceId, + DeviceId = deviceId, + AudioCodec = audioCodec, + EnableAutoStreamCopy = true, + AllowAudioStreamCopy = true, + AllowVideoStreamCopy = true, + BreakOnNonKeyFrames = breakOnNonKeyFrames, + AudioSampleRate = maxAudioSampleRate, + MaxAudioChannels = maxAudioChannels, + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)), + StartTimeTicks = startTimeTicks, + SubtitleMethod = SubtitleDeliveryMethod.Hls, + RequireAvc = true, + DeInterlace = true, + RequireNonAnamorphic = true, + EnableMpegtsM2TsMode = true, + TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), + Context = EncodingContext.Static, + StreamOptions = new Dictionary(), + EnableAdaptiveBitrateStreaming = true + }; - return await _audioController.GetAudioStream( - itemId, - isStatic ? null : ("." + mediaSource.TranscodingContainer), - isStatic, - null, - null, - null, - playbackInfoResult.Value.PlaySessionId, - null, - null, - null, - mediaSource.Id, - deviceId, - audioCodec, - null, - null, - null, - breakOnNonKeyFrames, - maxAudioSampleRate, - maxAudioBitDepth, - isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)), - null, - maxAudioChannels, - null, - null, - null, - null, - null, - startTimeTicks, - null, - null, - null, - null, - SubtitleDeliveryMethod.Embed, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), - null, - null, - null, - null) + return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true) .ConfigureAwait(false); } + + var audioStreamingDto = new StreamingRequestDto + { + Id = itemId, + Container = isStatic ? null : ("." + mediaSource.TranscodingContainer), + Static = isStatic, + PlaySessionId = info.PlaySessionId, + MediaSourceId = mediaSourceId, + DeviceId = deviceId, + AudioCodec = audioCodec, + EnableAutoStreamCopy = true, + AllowAudioStreamCopy = true, + AllowVideoStreamCopy = true, + BreakOnNonKeyFrames = breakOnNonKeyFrames, + AudioSampleRate = maxAudioSampleRate, + MaxAudioChannels = maxAudioChannels, + AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)), + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = maxAudioChannels, + CopyTimestamps = true, + StartTimeTicks = startTimeTicks, + SubtitleMethod = SubtitleDeliveryMethod.Embed, + TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()), + Context = EncodingContext.Static + }; + + return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false); } private DeviceProfile GetDeviceProfile( diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 272312522..d67f82219 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -216,6 +216,40 @@ namespace Jellyfin.Api.Controllers } } + /// + /// Authenticates a user with quick connect. + /// + /// The request. + /// User authenticated. + /// Missing token. + /// A containing an with information about the new session. + [HttpPost("AuthenticateWithQuickConnect")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request) + { + var auth = _authContext.GetAuthorizationInfo(Request); + + try + { + var authRequest = new AuthenticationRequest + { + App = auth.Client, + AppVersion = auth.Version, + DeviceId = auth.DeviceId, + DeviceName = auth.Device, + }; + + return await _sessionManager.AuthenticateQuickConnect( + authRequest, + request.Token).ConfigureAwait(false); + } + catch (SecurityException e) + { + // rethrow adding IP address to message + throw new SecurityException($"[{HttpContext.Connection.RemoteIpAddress}] {e.Message}", e); + } + } + /// /// Updates a user's password. /// diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index ae32dbf03..0978d44e9 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -161,7 +161,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult DeleteAlternateSources([FromRoute] Guid itemId) + public async Task DeleteAlternateSources([FromRoute] Guid itemId) { var video = (Video)_libraryManager.GetItemById(itemId); @@ -180,12 +180,12 @@ namespace Jellyfin.Api.Controllers link.SetPrimaryVersionId(null); link.LinkedAlternateVersions = Array.Empty(); - link.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + await link.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); } video.LinkedAlternateVersions = Array.Empty(); video.SetPrimaryVersionId(null); - video.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + await video.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -201,7 +201,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public ActionResult MergeVersions([FromQuery, Required] string? itemIds) + public async Task MergeVersions([FromQuery, Required] string? itemIds) { var items = RequestHelpers.Split(itemIds, ',', true) .Select(i => _libraryManager.GetItemById(i)) @@ -233,37 +233,40 @@ namespace Jellyfin.Api.Controllers .First(); } - var list = primaryVersion.LinkedAlternateVersions.ToList(); + var alternateVersionsOfPrimary = primaryVersion.LinkedAlternateVersions.ToList(); foreach (var item in items.Where(i => i.Id != primaryVersion.Id)) { item.SetPrimaryVersionId(primaryVersion.Id.ToString("N", CultureInfo.InvariantCulture)); - item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - list.Add(new LinkedChild + if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase))) { - Path = item.Path, - ItemId = item.Id - }); + alternateVersionsOfPrimary.Add(new LinkedChild + { + Path = item.Path, + ItemId = item.Id + }); + } foreach (var linkedItem in item.LinkedAlternateVersions) { - if (!list.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase))) + if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase))) { - list.Add(linkedItem); + alternateVersionsOfPrimary.Add(linkedItem); } } if (item.LinkedAlternateVersions.Length > 0) { item.LinkedAlternateVersions = Array.Empty(); - item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); } } - primaryVersion.LinkedAlternateVersions = list.ToArray(); - primaryVersion.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + primaryVersion.LinkedAlternateVersions = alternateVersionsOfPrimary.ToArray(); + await primaryVersion.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); return NoContent(); } @@ -471,7 +474,7 @@ namespace Jellyfin.Api.Controllers StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager); var httpClient = _httpClientFactory.CreateClient(); - return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false); + return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, HttpContext).ConfigureAwait(false); } if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File) @@ -507,7 +510,7 @@ namespace Jellyfin.Api.Controllers state.MediaPath, contentType, isHeadRequest, - this); + HttpContext); } // Need to start ffmpeg (because media can't be returned directly) @@ -517,10 +520,9 @@ namespace Jellyfin.Api.Controllers return await FileStreamResponseHelpers.GetTranscodedFile( state, isHeadRequest, - this, + HttpContext, _transcodingJobHelper, ffmpegCommandLineArguments, - Request, _transcodingJobType, cancellationTokenSource).ConfigureAwait(false); } diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs new file mode 100644 index 000000000..eae2be05b --- /dev/null +++ b/Jellyfin.Api/Helpers/AudioHelper.cs @@ -0,0 +1,195 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; + +namespace Jellyfin.Api.Helpers +{ + /// + /// Audio helper. + /// + public class AudioHelper + { + private readonly IDlnaManager _dlnaManager; + private readonly IAuthorizationContext _authContext; + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IFileSystem _fileSystem; + private readonly ISubtitleEncoder _subtitleEncoder; + private readonly IConfiguration _configuration; + private readonly IDeviceManager _deviceManager; + private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IHttpContextAccessor _httpContextAccessor; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of . + /// Instance of the interface. + /// Instance of the interface. + public AudioHelper( + IDlnaManager dlnaManager, + IAuthorizationContext authContext, + IUserManager userManager, + ILibraryManager libraryManager, + IMediaSourceManager mediaSourceManager, + IServerConfigurationManager serverConfigurationManager, + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + ISubtitleEncoder subtitleEncoder, + IConfiguration configuration, + IDeviceManager deviceManager, + TranscodingJobHelper transcodingJobHelper, + IHttpClientFactory httpClientFactory, + IHttpContextAccessor httpContextAccessor) + { + _dlnaManager = dlnaManager; + _authContext = authContext; + _userManager = userManager; + _libraryManager = libraryManager; + _mediaSourceManager = mediaSourceManager; + _serverConfigurationManager = serverConfigurationManager; + _mediaEncoder = mediaEncoder; + _fileSystem = fileSystem; + _subtitleEncoder = subtitleEncoder; + _configuration = configuration; + _deviceManager = deviceManager; + _transcodingJobHelper = transcodingJobHelper; + _httpClientFactory = httpClientFactory; + _httpContextAccessor = httpContextAccessor; + } + + /// + /// Get audio stream. + /// + /// Transcoding job type. + /// Streaming controller.Request dto. + /// A containing the resulting . + public async Task GetAudioStream( + TranscodingJobType transcodingJobType, + StreamingRequestDto streamingRequest) + { + bool isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == System.Net.WebRequestMethods.Http.Head; + var cancellationTokenSource = new CancellationTokenSource(); + + using var state = await StreamingHelpers.GetStreamingState( + streamingRequest, + _httpContextAccessor.HttpContext.Request, + _authContext, + _mediaSourceManager, + _userManager, + _libraryManager, + _serverConfigurationManager, + _mediaEncoder, + _fileSystem, + _subtitleEncoder, + _configuration, + _dlnaManager, + _deviceManager, + _transcodingJobHelper, + transcodingJobType, + cancellationTokenSource.Token) + .ConfigureAwait(false); + + if (streamingRequest.Static && state.DirectStreamProvider != null) + { + StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); + + await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None) + { + AllowEndOfFile = false + }.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None) + .ConfigureAwait(false); + + // TODO (moved from MediaBrowser.Api): Don't hardcode contentType + return new FileStreamResult(_httpContextAccessor.HttpContext.Response.Body, MimeTypes.GetMimeType("file.ts")!); + } + + // Static remote stream + if (streamingRequest.Static && state.InputProtocol == MediaProtocol.Http) + { + StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, true, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); + + var httpClient = _httpClientFactory.CreateClient(); + return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, httpClient, _httpContextAccessor.HttpContext).ConfigureAwait(false); + } + + if (streamingRequest.Static && state.InputProtocol != MediaProtocol.File) + { + return new BadRequestObjectResult($"Input protocol {state.InputProtocol} cannot be streamed statically"); + } + + var outputPath = state.OutputFilePath; + var outputPathExists = System.IO.File.Exists(outputPath); + + var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); + var isTranscodeCached = outputPathExists && transcodingJob != null; + + StreamingHelpers.AddDlnaHeaders(state, _httpContextAccessor.HttpContext.Response.Headers, streamingRequest.Static || isTranscodeCached, streamingRequest.StartTimeTicks, _httpContextAccessor.HttpContext.Request, _dlnaManager); + + // Static stream + if (streamingRequest.Static) + { + var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath); + + if (state.MediaSource.IsInfiniteStream) + { + await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None) + { + AllowEndOfFile = false + }.WriteToAsync(_httpContextAccessor.HttpContext.Response.Body, CancellationToken.None) + .ConfigureAwait(false); + + return new FileStreamResult(_httpContextAccessor.HttpContext.Response.Body, contentType); + } + + return FileStreamResponseHelpers.GetStaticFileResult( + state.MediaPath, + contentType, + isHeadRequest, + _httpContextAccessor.HttpContext); + } + + // Need to start ffmpeg (because media can't be returned directly) + var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); + var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration); + var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath); + return await FileStreamResponseHelpers.GetTranscodedFile( + state, + isHeadRequest, + _httpContextAccessor.HttpContext, + _transcodingJobHelper, + ffmpegCommandLineArguments, + transcodingJobType, + cancellationTokenSource).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs new file mode 100644 index 000000000..6a8829d46 --- /dev/null +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -0,0 +1,550 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Security.Claims; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; + +namespace Jellyfin.Api.Helpers +{ + /// + /// Dynamic hls helper. + /// + public class DynamicHlsHelper + { + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IDlnaManager _dlnaManager; + private readonly IAuthorizationContext _authContext; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IFileSystem _fileSystem; + private readonly ISubtitleEncoder _subtitleEncoder; + private readonly IConfiguration _configuration; + private readonly IDeviceManager _deviceManager; + private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly INetworkManager _networkManager; + private readonly ILogger _logger; + private readonly IHttpContextAccessor _httpContextAccessor; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of . + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public DynamicHlsHelper( + ILibraryManager libraryManager, + IUserManager userManager, + IDlnaManager dlnaManager, + IAuthorizationContext authContext, + IMediaSourceManager mediaSourceManager, + IServerConfigurationManager serverConfigurationManager, + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + ISubtitleEncoder subtitleEncoder, + IConfiguration configuration, + IDeviceManager deviceManager, + TranscodingJobHelper transcodingJobHelper, + INetworkManager networkManager, + ILogger logger, + IHttpContextAccessor httpContextAccessor) + { + _libraryManager = libraryManager; + _userManager = userManager; + _dlnaManager = dlnaManager; + _authContext = authContext; + _mediaSourceManager = mediaSourceManager; + _serverConfigurationManager = serverConfigurationManager; + _mediaEncoder = mediaEncoder; + _fileSystem = fileSystem; + _subtitleEncoder = subtitleEncoder; + _configuration = configuration; + _deviceManager = deviceManager; + _transcodingJobHelper = transcodingJobHelper; + _networkManager = networkManager; + _logger = logger; + _httpContextAccessor = httpContextAccessor; + } + + /// + /// Get master hls playlist. + /// + /// Transcoding job type. + /// Streaming request dto. + /// Enable adaptive bitrate streaming. + /// A containing the resulting . + public async Task GetMasterHlsPlaylist( + TranscodingJobType transcodingJobType, + StreamingRequestDto streamingRequest, + bool enableAdaptiveBitrateStreaming) + { + var isHeadRequest = _httpContextAccessor.HttpContext.Request.Method == WebRequestMethods.Http.Head; + var cancellationTokenSource = new CancellationTokenSource(); + return await GetMasterPlaylistInternal( + streamingRequest, + isHeadRequest, + enableAdaptiveBitrateStreaming, + transcodingJobType, + cancellationTokenSource).ConfigureAwait(false); + } + + private async Task GetMasterPlaylistInternal( + StreamingRequestDto streamingRequest, + bool isHeadRequest, + bool enableAdaptiveBitrateStreaming, + TranscodingJobType transcodingJobType, + CancellationTokenSource cancellationTokenSource) + { + using var state = await StreamingHelpers.GetStreamingState( + streamingRequest, + _httpContextAccessor.HttpContext.Request, + _authContext, + _mediaSourceManager, + _userManager, + _libraryManager, + _serverConfigurationManager, + _mediaEncoder, + _fileSystem, + _subtitleEncoder, + _configuration, + _dlnaManager, + _deviceManager, + _transcodingJobHelper, + transcodingJobType, + cancellationTokenSource.Token) + .ConfigureAwait(false); + + _httpContextAccessor.HttpContext.Response.Headers.Add(HeaderNames.Expires, "0"); + if (isHeadRequest) + { + return new FileContentResult(Array.Empty(), MimeTypes.GetMimeType("playlist.m3u8")); + } + + var totalBitrate = state.OutputAudioBitrate ?? 0 + state.OutputVideoBitrate ?? 0; + + var builder = new StringBuilder(); + + builder.AppendLine("#EXTM3U"); + + var isLiveStream = state.IsSegmentedLiveStream; + + var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString(); + + // from universal audio service + if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)) + { + queryString += "&SegmentContainer=" + state.Request.SegmentContainer; + } + + // from universal audio service + if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1) + { + queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons; + } + + // Main stream + var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8"; + + playlistUrl += queryString; + + var subtitleStreams = state.MediaSource + .MediaStreams + .Where(i => i.IsTextSubtitleStream) + .ToList(); + + var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest) + ? "subs" + : null; + + // If we're burning in subtitles then don't add additional subs to the manifest + if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + { + subtitleGroup = null; + } + + if (!string.IsNullOrWhiteSpace(subtitleGroup)) + { + AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.Request.HttpContext.User); + } + + AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); + + if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.Request.HttpContext.Connection.RemoteIpAddress)) + { + var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0; + + // By default, vary by just 200k + var variation = GetBitrateVariation(totalBitrate); + + var newBitrate = totalBitrate - variation; + var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); + AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); + + variation *= 2; + newBitrate = totalBitrate - variation; + variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); + AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); + } + + return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); + } + + private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup) + { + builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=") + .Append(bitrate.ToString(CultureInfo.InvariantCulture)) + .Append(",AVERAGE-BANDWIDTH=") + .Append(bitrate.ToString(CultureInfo.InvariantCulture)); + + AppendPlaylistCodecsField(builder, state); + + AppendPlaylistResolutionField(builder, state); + + AppendPlaylistFramerateField(builder, state); + + if (!string.IsNullOrWhiteSpace(subtitleGroup)) + { + builder.Append(",SUBTITLES=\"") + .Append(subtitleGroup) + .Append('"'); + } + + builder.Append(Environment.NewLine); + builder.AppendLine(url); + } + + /// + /// Appends a CODECS field containing formatted strings of + /// the active streams output video and audio codecs. + /// + /// + /// + /// + /// StringBuilder to append the field to. + /// StreamState of the current stream. + private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state) + { + // Video + string videoCodecs = string.Empty; + int? videoCodecLevel = GetOutputVideoCodecLevel(state); + if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue) + { + videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value); + } + + // Audio + string audioCodecs = string.Empty; + if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec)) + { + audioCodecs = GetPlaylistAudioCodecs(state); + } + + StringBuilder codecs = new StringBuilder(); + + codecs.Append(videoCodecs); + + if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs)) + { + codecs.Append(','); + } + + codecs.Append(audioCodecs); + + if (codecs.Length > 1) + { + builder.Append(",CODECS=\"") + .Append(codecs) + .Append('"'); + } + } + + /// + /// Appends a RESOLUTION field containing the resolution of the output stream. + /// + /// + /// StringBuilder to append the field to. + /// StreamState of the current stream. + private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state) + { + if (state.OutputWidth.HasValue && state.OutputHeight.HasValue) + { + builder.Append(",RESOLUTION=") + .Append(state.OutputWidth.GetValueOrDefault()) + .Append('x') + .Append(state.OutputHeight.GetValueOrDefault()); + } + } + + /// + /// Appends a FRAME-RATE field containing the framerate of the output stream. + /// + /// + /// StringBuilder to append the field to. + /// StreamState of the current stream. + private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state) + { + double? framerate = null; + if (state.TargetFramerate.HasValue) + { + framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3); + } + else if (state.VideoStream?.RealFrameRate != null) + { + framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3); + } + + if (framerate.HasValue) + { + builder.Append(",FRAME-RATE=") + .Append(framerate.Value); + } + } + + private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress) + { + // Within the local network this will likely do more harm than good. + var ip = RequestHelpers.NormalizeIp(ipAddress).ToString(); + if (_networkManager.IsInLocalNetwork(ip)) + { + return false; + } + + if (!enableAdaptiveBitrateStreaming) + { + return false; + } + + if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath)) + { + // Opening live streams is so slow it's not even worth it + return false; + } + + if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) + { + return false; + } + + if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec)) + { + return false; + } + + if (!state.IsOutputVideo) + { + return false; + } + + // Having problems in android + return false; + // return state.VideoRequest.VideoBitRate.HasValue; + } + + private void AddSubtitles(StreamState state, IEnumerable subtitles, StringBuilder builder, ClaimsPrincipal user) + { + var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index; + const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\""; + + foreach (var stream in subtitles) + { + var name = stream.DisplayTitle; + + var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index; + var isForced = stream.IsForced; + + var url = string.Format( + CultureInfo.InvariantCulture, + "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}", + state.Request.MediaSourceId, + stream.Index.ToString(CultureInfo.InvariantCulture), + 30.ToString(CultureInfo.InvariantCulture), + ClaimHelpers.GetToken(user)); + + var line = string.Format( + CultureInfo.InvariantCulture, + Format, + name, + isDefault ? "YES" : "NO", + isForced ? "YES" : "NO", + url, + stream.Language ?? "Unknown"); + + builder.AppendLine(line); + } + } + + /// + /// Get the H.26X level of the output video stream. + /// + /// StreamState of the current stream. + /// H.26X level of the output video stream. + private int? GetOutputVideoCodecLevel(StreamState state) + { + string? levelString; + if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && state.VideoStream.Level.HasValue) + { + levelString = state.VideoStream?.Level.ToString(); + } + else + { + levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec); + } + + if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel)) + { + return parsedLevel; + } + + return null; + } + + /// + /// Gets a formatted string of the output audio codec, for use in the CODECS field. + /// + /// + /// + /// StreamState of the current stream. + /// Formatted audio codec string. + private string GetPlaylistAudioCodecs(StreamState state) + { + if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase)) + { + string? profile = state.GetRequestedProfiles("aac").FirstOrDefault(); + return HlsCodecStringHelpers.GetAACString(profile); + } + + if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringHelpers.GetMP3String(); + } + + if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringHelpers.GetAC3String(); + } + + if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringHelpers.GetEAC3String(); + } + + return string.Empty; + } + + /// + /// Gets a formatted string of the output video codec, for use in the CODECS field. + /// + /// + /// + /// StreamState of the current stream. + /// Video codec. + /// Video level. + /// Formatted video codec string. + private string GetPlaylistVideoCodecs(StreamState state, string codec, int level) + { + if (level == 0) + { + // This is 0 when there's no requested H.26X level in the device profile + // and the source is not encoded in H.26X + _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist"); + return string.Empty; + } + + if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)) + { + string profile = state.GetRequestedProfiles("h264").FirstOrDefault(); + return HlsCodecStringHelpers.GetH264String(profile, level); + } + + if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) + { + string profile = state.GetRequestedProfiles("h265").FirstOrDefault(); + + return HlsCodecStringHelpers.GetH265String(profile, level); + } + + return string.Empty; + } + + private int GetBitrateVariation(int bitrate) + { + // By default, vary by just 50k + var variation = 50000; + + if (bitrate >= 10000000) + { + variation = 2000000; + } + else if (bitrate >= 5000000) + { + variation = 1500000; + } + else if (bitrate >= 3000000) + { + variation = 1000000; + } + else if (bitrate >= 2000000) + { + variation = 500000; + } + else if (bitrate >= 1000000) + { + variation = 300000; + } + else if (bitrate >= 600000) + { + variation = 200000; + } + else if (bitrate >= 400000) + { + variation = 100000; + } + + return variation; + } + + private string ReplaceBitrate(string url, int oldValue, int newValue) + { + return url.Replace( + "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture), + "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture), + StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index 5a59313c9..deb54dbe6 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -22,14 +22,14 @@ namespace Jellyfin.Api.Helpers /// /// The current . /// Whether the current request is a HTTP HEAD request so only the headers get returned. - /// The managing the response. /// The making the remote request. + /// The current http context. /// A containing the API response. public static async Task GetStaticRemoteStreamResult( StreamState state, bool isHeadRequest, - ControllerBase controller, - HttpClient httpClient) + HttpClient httpClient, + HttpContext httpContext) { if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent)) { @@ -40,14 +40,14 @@ namespace Jellyfin.Api.Helpers var response = await httpClient.GetAsync(state.MediaPath).ConfigureAwait(false); var contentType = response.Content.Headers.ContentType.ToString(); - controller.Response.Headers[HeaderNames.AcceptRanges] = "none"; + httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none"; if (isHeadRequest) { - return controller.File(Array.Empty(), contentType); + return new FileContentResult(Array.Empty(), contentType); } - return controller.File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType); + return new FileStreamResult(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), contentType); } /// @@ -56,23 +56,23 @@ namespace Jellyfin.Api.Helpers /// The path to the file. /// The content type of the file. /// Whether the current request is a HTTP HEAD request so only the headers get returned. - /// The managing the response. + /// The current http context. /// An the file. public static ActionResult GetStaticFileResult( string path, string contentType, bool isHeadRequest, - ControllerBase controller) + HttpContext httpContext) { - controller.Response.ContentType = contentType; + httpContext.Response.ContentType = contentType; // if the request is a head request, return a NoContent result with the same headers as it would with a GET request if (isHeadRequest) { - return controller.NoContent(); + return new NoContentResult(); } - return controller.PhysicalFile(path, contentType); + return new PhysicalFileResult(path, contentType); } /// @@ -80,34 +80,32 @@ namespace Jellyfin.Api.Helpers /// /// The current . /// Whether the current request is a HTTP HEAD request so only the headers get returned. - /// The managing the response. + /// The current http context. /// The singleton. /// The command line arguments to start ffmpeg. - /// The starting the transcoding. /// The . /// The . /// A containing the transcoded file. public static async Task GetTranscodedFile( StreamState state, bool isHeadRequest, - ControllerBase controller, + HttpContext httpContext, TranscodingJobHelper transcodingJobHelper, string ffmpegCommandLineArguments, - HttpRequest request, TranscodingJobType transcodingJobType, CancellationTokenSource cancellationTokenSource) { // Use the command line args with a dummy playlist path var outputPath = state.OutputFilePath; - controller.Response.Headers[HeaderNames.AcceptRanges] = "none"; + httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none"; var contentType = state.GetMimeType(outputPath); // Headers only if (isHeadRequest) { - return controller.File(Array.Empty(), contentType); + return new FileContentResult(Array.Empty(), contentType); } var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath); @@ -117,7 +115,7 @@ namespace Jellyfin.Api.Helpers TranscodingJobDto? job; if (!File.Exists(outputPath)) { - job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false); + job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, httpContext.Request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false); } else { @@ -128,7 +126,7 @@ namespace Jellyfin.Api.Helpers var memoryStream = new MemoryStream(); await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None).WriteToAsync(memoryStream, CancellationToken.None).ConfigureAwait(false); memoryStream.Position = 0; - return controller.File(memoryStream, contentType); + return new FileStreamResult(memoryStream, contentType); } finally { diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs new file mode 100644 index 000000000..3a736d1e8 --- /dev/null +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -0,0 +1,577 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Session; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Helpers +{ + /// + /// Media info helper. + /// + public class MediaInfoHelper + { + private readonly IUserManager _userManager; + private readonly ILibraryManager _libraryManager; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly ILogger _logger; + private readonly INetworkManager _networkManager; + private readonly IDeviceManager _deviceManager; + private readonly IAuthorizationContext _authContext; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public MediaInfoHelper( + IUserManager userManager, + ILibraryManager libraryManager, + IMediaSourceManager mediaSourceManager, + IMediaEncoder mediaEncoder, + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + INetworkManager networkManager, + IDeviceManager deviceManager, + IAuthorizationContext authContext) + { + _userManager = userManager; + _libraryManager = libraryManager; + _mediaSourceManager = mediaSourceManager; + _mediaEncoder = mediaEncoder; + _serverConfigurationManager = serverConfigurationManager; + _logger = logger; + _networkManager = networkManager; + _deviceManager = deviceManager; + _authContext = authContext; + } + + /// + /// Get playback info. + /// + /// Item id. + /// User Id. + /// Media source id. + /// Live stream id. + /// A containing the . + public async Task GetPlaybackInfo( + Guid id, + Guid? userId, + string? mediaSourceId = null, + string? liveStreamId = null) + { + var user = userId.HasValue && !userId.Equals(Guid.Empty) + ? _userManager.GetUserById(userId.Value) + : null; + var item = _libraryManager.GetItemById(id); + var result = new PlaybackInfoResponse(); + + MediaSourceInfo[] mediaSources; + if (string.IsNullOrWhiteSpace(liveStreamId)) + { + // TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes? + var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(mediaSourceId)) + { + mediaSources = mediaSourcesList.ToArray(); + } + else + { + mediaSources = mediaSourcesList + .Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + } + } + else + { + var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false); + + mediaSources = new[] { mediaSource }; + } + + if (mediaSources.Length == 0) + { + result.MediaSources = Array.Empty(); + + result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream; + } + else + { + // Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it + // Should we move this directly into MediaSourceManager? + var mediaSourcesClone = JsonSerializer.Deserialize(JsonSerializer.SerializeToUtf8Bytes(mediaSources)); + if (mediaSourcesClone != null) + { + result.MediaSources = mediaSourcesClone; + } + + result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + } + + return result; + } + + /// + /// SetDeviceSpecificData. + /// + /// Item to set data for. + /// Media source info. + /// Device profile. + /// Authorization info. + /// Max bitrate. + /// Start time ticks. + /// Media source id. + /// Audio stream index. + /// Subtitle stream index. + /// Max audio channels. + /// Play session id. + /// User id. + /// Enable direct play. + /// Enable direct stream. + /// Enable transcoding. + /// Allow video stream copy. + /// Allow audio stream copy. + /// Requesting IP address. + public void SetDeviceSpecificData( + BaseItem item, + MediaSourceInfo mediaSource, + DeviceProfile profile, + AuthorizationInfo auth, + long? maxBitrate, + long startTimeTicks, + string mediaSourceId, + int? audioStreamIndex, + int? subtitleStreamIndex, + int? maxAudioChannels, + string playSessionId, + Guid userId, + bool enableDirectPlay, + bool enableDirectStream, + bool enableTranscoding, + bool allowVideoStreamCopy, + bool allowAudioStreamCopy, + string ipAddress) + { + var streamBuilder = new StreamBuilder(_mediaEncoder, _logger); + + var options = new VideoOptions + { + MediaSources = new[] { mediaSource }, + Context = EncodingContext.Streaming, + DeviceId = auth.DeviceId, + ItemId = item.Id, + Profile = profile, + MaxAudioChannels = maxAudioChannels + }; + + if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase)) + { + options.MediaSourceId = mediaSourceId; + options.AudioStreamIndex = audioStreamIndex; + options.SubtitleStreamIndex = subtitleStreamIndex; + } + + var user = _userManager.GetUserById(userId); + + if (!enableDirectPlay) + { + mediaSource.SupportsDirectPlay = false; + } + + if (!enableDirectStream) + { + mediaSource.SupportsDirectStream = false; + } + + if (!enableTranscoding) + { + mediaSource.SupportsTranscoding = false; + } + + if (item is Audio) + { + _logger.LogInformation( + "User policy for {0}. EnableAudioPlaybackTranscoding: {1}", + user.Username, + user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); + } + else + { + _logger.LogInformation( + "User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}", + user.Username, + user.HasPermission(PermissionKind.EnablePlaybackRemuxing), + user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding), + user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)); + } + + // Beginning of Playback Determination: Attempt DirectPlay first + if (mediaSource.SupportsDirectPlay) + { + if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) + { + mediaSource.SupportsDirectPlay = false; + } + else + { + var supportsDirectStream = mediaSource.SupportsDirectStream; + + // Dummy this up to fool StreamBuilder + mediaSource.SupportsDirectStream = true; + options.MaxBitrate = maxBitrate; + + if (item is Audio) + { + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) + { + options.ForceDirectPlay = true; + } + } + else if (item is Video) + { + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) + { + options.ForceDirectPlay = true; + } + } + + // The MediaSource supports direct stream, now test to see if the client supports it + var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) + ? streamBuilder.BuildAudioItem(options) + : streamBuilder.BuildVideoItem(options); + + if (streamInfo == null || !streamInfo.IsDirectStream) + { + mediaSource.SupportsDirectPlay = false; + } + + // Set this back to what it was + mediaSource.SupportsDirectStream = supportsDirectStream; + + if (streamInfo != null) + { + SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + } + } + } + + if (mediaSource.SupportsDirectStream) + { + if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) + { + mediaSource.SupportsDirectStream = false; + } + else + { + options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); + + if (item is Audio) + { + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)) + { + options.ForceDirectStream = true; + } + } + else if (item is Video) + { + if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding) + && !user.HasPermission(PermissionKind.EnablePlaybackRemuxing)) + { + options.ForceDirectStream = true; + } + } + + // The MediaSource supports direct stream, now test to see if the client supports it + var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) + ? streamBuilder.BuildAudioItem(options) + : streamBuilder.BuildVideoItem(options); + + if (streamInfo == null || !streamInfo.IsDirectStream) + { + mediaSource.SupportsDirectStream = false; + } + + if (streamInfo != null) + { + SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + } + } + } + + if (mediaSource.SupportsTranscoding) + { + options.MaxBitrate = GetMaxBitrate(maxBitrate, user, ipAddress); + + // The MediaSource supports direct stream, now test to see if the client supports it + var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) + ? streamBuilder.BuildAudioItem(options) + : streamBuilder.BuildVideoItem(options); + + if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding)) + { + if (streamInfo != null) + { + streamInfo.PlaySessionId = playSessionId; + streamInfo.StartPositionTicks = startTimeTicks; + mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); + mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; + mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; + mediaSource.TranscodingContainer = streamInfo.Container; + mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; + + // Do this after the above so that StartPositionTicks is set + SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + } + } + else + { + if (streamInfo != null) + { + streamInfo.PlaySessionId = playSessionId; + + if (streamInfo.PlayMethod == PlayMethod.Transcode) + { + streamInfo.StartPositionTicks = startTimeTicks; + mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); + + if (!allowVideoStreamCopy) + { + mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; + } + + if (!allowAudioStreamCopy) + { + mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; + } + + mediaSource.TranscodingContainer = streamInfo.Container; + mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; + } + + if (!allowAudioStreamCopy) + { + mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; + } + + mediaSource.TranscodingContainer = streamInfo.Container; + mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; + + // Do this after the above so that StartPositionTicks is set + SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + } + } + } + + foreach (var attachment in mediaSource.MediaAttachments) + { + attachment.DeliveryUrl = string.Format( + CultureInfo.InvariantCulture, + "/Videos/{0}/{1}/Attachments/{2}", + item.Id, + mediaSource.Id, + attachment.Index); + } + } + + /// + /// Sort media source. + /// + /// Playback info response. + /// Max bitrate. + public void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate) + { + var originalList = result.MediaSources.ToList(); + + result.MediaSources = result.MediaSources.OrderBy(i => + { + // Nothing beats direct playing a file + if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File) + { + return 0; + } + + return 1; + }) + .ThenBy(i => + { + // Let's assume direct streaming a file is just as desirable as direct playing a remote url + if (i.SupportsDirectPlay || i.SupportsDirectStream) + { + return 0; + } + + return 1; + }) + .ThenBy(i => + { + return i.Protocol switch + { + MediaProtocol.File => 0, + _ => 1, + }; + }) + .ThenBy(i => + { + if (maxBitrate.HasValue && i.Bitrate.HasValue) + { + return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2; + } + + return 1; + }) + .ThenBy(originalList.IndexOf) + .ToArray(); + } + + /// + /// Open media source. + /// + /// Http Request. + /// Live stream request. + /// A containing the . + public async Task OpenMediaSource(HttpRequest httpRequest, LiveStreamRequest request) + { + var authInfo = _authContext.GetAuthorizationInfo(httpRequest); + + var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false); + + var profile = request.DeviceProfile; + if (profile == null) + { + var clientCapabilities = _deviceManager.GetCapabilities(authInfo.DeviceId); + if (clientCapabilities != null) + { + profile = clientCapabilities.DeviceProfile; + } + } + + if (profile != null) + { + var item = _libraryManager.GetItemById(request.ItemId); + + SetDeviceSpecificData( + item, + result.MediaSource, + profile, + authInfo, + request.MaxStreamingBitrate, + request.StartTimeTicks ?? 0, + result.MediaSource.Id, + request.AudioStreamIndex, + request.SubtitleStreamIndex, + request.MaxAudioChannels, + request.PlaySessionId, + request.UserId, + request.EnableDirectPlay, + request.EnableDirectStream, + true, + true, + true, + httpRequest.HttpContext.Connection.RemoteIpAddress.ToString()); + } + else + { + if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl)) + { + result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId; + } + } + + // here was a check if (result.MediaSource != null) but Rider said it will never be null + NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video); + + return result; + } + + /// + /// Normalize media source container. + /// + /// Media source. + /// Device profile. + /// Dlna profile type. + public void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type) + { + mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type); + } + + private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken) + { + var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken); + mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex; + + mediaSource.TranscodeReasons = info.TranscodeReasons; + + foreach (var profile in profiles) + { + foreach (var stream in mediaSource.MediaStreams) + { + if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index) + { + stream.DeliveryMethod = profile.DeliveryMethod; + + if (profile.DeliveryMethod == SubtitleDeliveryMethod.External) + { + stream.DeliveryUrl = profile.Url.TrimStart('-'); + stream.IsExternalUrl = profile.IsExternalUrl; + } + } + } + } + } + + private long? GetMaxBitrate(long? clientMaxBitrate, User user, string ipAddress) + { + var maxBitrate = clientMaxBitrate; + var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0; + + if (remoteClientMaxBitrate <= 0) + { + remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit; + } + + if (remoteClientMaxBitrate > 0) + { + var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress); + + _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork); + if (!isInLocalNetwork) + { + maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate); + } + } + + return maxBitrate; + } + } +} diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs index 432df9708..e00ed3304 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs +++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs @@ -130,34 +130,10 @@ namespace Jellyfin.Api.Helpers private async Task CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken) { var array = ArrayPool.Shared.Rent(IODefaults.CopyToBufferSize); - int bytesRead; - int totalBytesRead = 0; - - if (readAsync) + try { - bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false); - } - else - { - bytesRead = source.Read(array, 0, array.Length); - } - - while (bytesRead != 0) - { - var bytesToWrite = bytesRead; - - if (bytesToWrite > 0) - { - await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); - - _bytesWritten += bytesRead; - totalBytesRead += bytesRead; - - if (_job != null) - { - _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten); - } - } + int bytesRead; + int totalBytesRead = 0; if (readAsync) { @@ -167,9 +143,40 @@ namespace Jellyfin.Api.Helpers { bytesRead = source.Read(array, 0, array.Length); } - } - return totalBytesRead; + while (bytesRead != 0) + { + var bytesToWrite = bytesRead; + + if (bytesToWrite > 0) + { + await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); + + _bytesWritten += bytesRead; + totalBytesRead += bytesRead; + + if (_job != null) + { + _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten); + } + } + + if (readAsync) + { + bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false); + } + else + { + bytesRead = source.Read(array, 0, array.Length); + } + } + + return totalBytesRead; + } + finally + { + ArrayPool.Shared.Return(array); + } } } } diff --git a/Jellyfin.Api/Models/LibraryStructureDto/LibraryOptionsDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/AddVirtualFolderDto.cs similarity index 80% rename from Jellyfin.Api/Models/LibraryStructureDto/LibraryOptionsDto.cs rename to Jellyfin.Api/Models/LibraryStructureDto/AddVirtualFolderDto.cs index a13cb90db..ab68d5223 100644 --- a/Jellyfin.Api/Models/LibraryStructureDto/LibraryOptionsDto.cs +++ b/Jellyfin.Api/Models/LibraryStructureDto/AddVirtualFolderDto.cs @@ -3,13 +3,13 @@ namespace Jellyfin.Api.Models.LibraryStructureDto { /// - /// Library options dto. + /// Add virtual folder dto. /// - public class LibraryOptionsDto + public class AddVirtualFolderDto { /// /// Gets or sets library options. /// public LibraryOptions? LibraryOptions { get; set; } } -} \ No newline at end of file +} diff --git a/Jellyfin.Api/Models/LibraryStructureDto/UpdateLibraryOptionsDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/UpdateLibraryOptionsDto.cs new file mode 100644 index 000000000..c78ed51f7 --- /dev/null +++ b/Jellyfin.Api/Models/LibraryStructureDto/UpdateLibraryOptionsDto.cs @@ -0,0 +1,21 @@ +using System; +using MediaBrowser.Model.Configuration; + +namespace Jellyfin.Api.Models.LibraryStructureDto +{ + /// + /// Update library options dto. + /// + public class UpdateLibraryOptionsDto + { + /// + /// Gets or sets the library item id. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets library options. + /// + public LibraryOptions? LibraryOptions { get; set; } + } +} diff --git a/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs new file mode 100644 index 000000000..4027ba41a --- /dev/null +++ b/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Api.Models.StartupDtos +{ + /// + /// Startup remote access dto. + /// + public class StartupRemoteAccessDto + { + /// + /// Gets or sets a value indicating whether enable remote access. + /// + [Required] + public bool EnableRemoteAccess { get; set; } + + /// + /// Gets or sets a value indicating whether enable automatic port mapping. + /// + [Required] + public bool EnableAutomaticPortMapping { get; set; } + } +} diff --git a/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs b/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs new file mode 100644 index 000000000..c3a2d5cec --- /dev/null +++ b/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Api.Models.UserDtos +{ + /// + /// The quick connect request body. + /// + public class QuickConnectDto + { + /// + /// Gets or sets the quick connect token. + /// + [Required] + public string? Token { get; set; } + } +} diff --git a/Jellyfin.Api/TypeConverters/DateTimeTypeConverter.cs b/Jellyfin.Api/TypeConverters/DateTimeTypeConverter.cs new file mode 100644 index 000000000..315b47329 --- /dev/null +++ b/Jellyfin.Api/TypeConverters/DateTimeTypeConverter.cs @@ -0,0 +1,44 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace Jellyfin.Api.TypeConverters +{ + /// + /// Custom datetime parser. + /// + public class DateTimeTypeConverter : TypeConverter + { + /// + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + if (sourceType == typeof(string)) + { + return true; + } + + return base.CanConvertFrom(context, sourceType); + } + + /// + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value is string dateString) + { + // Mark Played Item. + if (DateTime.TryParseExact(dateString, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dateTime)) + { + return dateTime; + } + + // Get Activity Logs. + if (DateTime.TryParse(dateString, null, DateTimeStyles.RoundtripKind, out dateTime)) + { + return dateTime; + } + } + + return base.ConvertFrom(context, culture, value); + } + } +} diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs index 6395b8d62..849b3b709 100644 --- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs @@ -1,8 +1,8 @@ using System; using System.Threading.Tasks; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Activity; -using MediaBrowser.Model.Events; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.WebSocketListeners diff --git a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs index 12f815ff7..8a966c137 100644 --- a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; diff --git a/Jellyfin.Data/DayOfWeekHelper.cs b/Jellyfin.Data/DayOfWeekHelper.cs index 32a41368d..4e75f4cfd 100644 --- a/Jellyfin.Data/DayOfWeekHelper.cs +++ b/Jellyfin.Data/DayOfWeekHelper.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using Jellyfin.Data.Enums; diff --git a/Jellyfin.Data/Entities/ActivityLog.cs b/Jellyfin.Data/Entities/ActivityLog.cs index 522c20664..3858916d1 100644 --- a/Jellyfin.Data/Entities/ActivityLog.cs +++ b/Jellyfin.Data/Entities/ActivityLog.cs @@ -1,6 +1,9 @@ +#pragma warning disable CS1591 + using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; using Microsoft.Extensions.Logging; namespace Jellyfin.Data.Entities @@ -8,7 +11,7 @@ namespace Jellyfin.Data.Entities /// /// An entity referencing an activity log entry. /// - public partial class ActivityLog : ISavingChanges + public partial class ActivityLog : IHasConcurrencyToken { /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Data/Entities/Artwork.cs b/Jellyfin.Data/Entities/Artwork.cs deleted file mode 100644 index 6ed32eac3..000000000 --- a/Jellyfin.Data/Entities/Artwork.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; - -namespace Jellyfin.Data.Entities -{ - public partial class Artwork - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected Artwork() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Artwork CreateArtworkUnsafe() - { - return new Artwork(); - } - - /// - /// Public constructor with required data. - /// - /// - /// - /// - /// - public Artwork(string path, Enums.ArtKind kind, Metadata _metadata0, PersonRole _personrole1) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - this.Path = path; - - this.Kind = kind; - - if (_metadata0 == null) - { - throw new ArgumentNullException(nameof(_metadata0)); - } - - _metadata0.Artwork.Add(this); - - if (_personrole1 == null) - { - throw new ArgumentNullException(nameof(_personrole1)); - } - - _personrole1.Artwork = this; - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// - /// - /// - /// - public static Artwork Create(string path, Enums.ArtKind kind, Metadata _metadata0, PersonRole _personrole1) - { - return new Artwork(path, kind, _metadata0, _personrole1); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Backing field for Path. - /// - protected string _Path; - /// - /// When provided in a partial class, allows value of Path to be changed before setting. - /// - partial void SetPath(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Path to be changed before returning. - /// - partial void GetPath(ref string result); - - /// - /// Required, Max length = 65535 - /// - [Required] - [MaxLength(65535)] - [StringLength(65535)] - public string Path - { - get - { - string value = _Path; - GetPath(ref value); - return _Path = value; - } - - set - { - string oldValue = _Path; - SetPath(oldValue, ref value); - if (oldValue != value) - { - _Path = value; - } - } - } - - /// - /// Backing field for Kind. - /// - internal Enums.ArtKind _Kind; - /// - /// When provided in a partial class, allows value of Kind to be changed before setting. - /// - partial void SetKind(Enums.ArtKind oldValue, ref Enums.ArtKind newValue); - /// - /// When provided in a partial class, allows value of Kind to be changed before returning. - /// - partial void GetKind(ref Enums.ArtKind result); - - /// - /// Indexed, Required. - /// - [Required] - public Enums.ArtKind Kind - { - get - { - Enums.ArtKind value = _Kind; - GetKind(ref value); - return _Kind = value; - } - - set - { - Enums.ArtKind oldValue = _Kind; - SetKind(oldValue, ref value); - if (oldValue != value) - { - _Kind = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/Book.cs b/Jellyfin.Data/Entities/Book.cs deleted file mode 100644 index c4d12496e..000000000 --- a/Jellyfin.Data/Entities/Book.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Book : LibraryItem - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected Book() - { - BookMetadata = new HashSet(); - Releases = new HashSet(); - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Book CreateBookUnsafe() - { - return new Book(); - } - - /// - /// Public constructor with required data. - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - public Book(Guid urlid, DateTime dateadded) - { - this.UrlId = urlid; - - this.BookMetadata = new HashSet(); - this.Releases = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - public static Book Create(Guid urlid, DateTime dateadded) - { - return new Book(urlid, dateadded); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - [ForeignKey("BookMetadata_BookMetadata_Id")] - public virtual ICollection BookMetadata { get; protected set; } - - [ForeignKey("Release_Releases_Id")] - public virtual ICollection Releases { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/BookMetadata.cs b/Jellyfin.Data/Entities/BookMetadata.cs deleted file mode 100644 index df43090d3..000000000 --- a/Jellyfin.Data/Entities/BookMetadata.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class BookMetadata : Metadata - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected BookMetadata() - { - Publishers = new HashSet(); - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static BookMetadata CreateBookMetadataUnsafe() - { - return new BookMetadata(); - } - - /// - /// Public constructor with required data. - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public BookMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Book _book0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_book0 == null) - { - throw new ArgumentNullException(nameof(_book0)); - } - - _book0.BookMetadata.Add(this); - - this.Publishers = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public static BookMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Book _book0) - { - return new BookMetadata(title, language, dateadded, datemodified, _book0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for ISBN. - /// - protected long? _ISBN; - /// - /// When provided in a partial class, allows value of ISBN to be changed before setting. - /// - partial void SetISBN(long? oldValue, ref long? newValue); - /// - /// When provided in a partial class, allows value of ISBN to be changed before returning. - /// - partial void GetISBN(ref long? result); - - public long? ISBN - { - get - { - long? value = _ISBN; - GetISBN(ref value); - return _ISBN = value; - } - - set - { - long? oldValue = _ISBN; - SetISBN(oldValue, ref value); - if (oldValue != value) - { - _ISBN = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - [ForeignKey("Company_Publishers_Id")] - public virtual ICollection Publishers { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/Chapter.cs b/Jellyfin.Data/Entities/Chapter.cs deleted file mode 100644 index 4575cdb4d..000000000 --- a/Jellyfin.Data/Entities/Chapter.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Chapter - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected Chapter() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Chapter CreateChapterUnsafe() - { - return new Chapter(); - } - - /// - /// Public constructor with required data. - /// - /// ISO-639-3 3-character language codes. - /// - /// - public Chapter(string language, long timestart, Release _release0) - { - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - this.TimeStart = timestart; - - if (_release0 == null) - { - throw new ArgumentNullException(nameof(_release0)); - } - - _release0.Chapters.Add(this); - - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// ISO-639-3 3-character language codes. - /// - /// - public static Chapter Create(string language, long timestart, Release _release0) - { - return new Chapter(language, timestart, _release0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Backing field for Name. - /// - protected string _Name; - /// - /// When provided in a partial class, allows value of Name to be changed before setting. - /// - partial void SetName(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Name to be changed before returning. - /// - partial void GetName(ref string result); - - /// - /// Max length = 1024 - /// - [MaxLength(1024)] - [StringLength(1024)] - public string Name - { - get - { - string value = _Name; - GetName(ref value); - return _Name = value; - } - - set - { - string oldValue = _Name; - SetName(oldValue, ref value); - if (oldValue != value) - { - _Name = value; - } - } - } - - /// - /// Backing field for Language. - /// - protected string _Language; - /// - /// When provided in a partial class, allows value of Language to be changed before setting. - /// - partial void SetLanguage(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Language to be changed before returning. - /// - partial void GetLanguage(ref string result); - - /// - /// Required, Min length = 3, Max length = 3 - /// ISO-639-3 3-character language codes. - /// - [Required] - [MinLength(3)] - [MaxLength(3)] - [StringLength(3)] - public string Language - { - get - { - string value = _Language; - GetLanguage(ref value); - return _Language = value; - } - - set - { - string oldValue = _Language; - SetLanguage(oldValue, ref value); - if (oldValue != value) - { - _Language = value; - } - } - } - - /// - /// Backing field for TimeStart. - /// - protected long _TimeStart; - /// - /// When provided in a partial class, allows value of TimeStart to be changed before setting. - /// - partial void SetTimeStart(long oldValue, ref long newValue); - /// - /// When provided in a partial class, allows value of TimeStart to be changed before returning. - /// - partial void GetTimeStart(ref long result); - - /// - /// Required. - /// - [Required] - public long TimeStart - { - get - { - long value = _TimeStart; - GetTimeStart(ref value); - return _TimeStart = value; - } - - set - { - long oldValue = _TimeStart; - SetTimeStart(oldValue, ref value); - if (oldValue != value) - { - _TimeStart = value; - } - } - } - - /// - /// Backing field for TimeEnd. - /// - protected long? _TimeEnd; - /// - /// When provided in a partial class, allows value of TimeEnd to be changed before setting. - /// - partial void SetTimeEnd(long? oldValue, ref long? newValue); - /// - /// When provided in a partial class, allows value of TimeEnd to be changed before returning. - /// - partial void GetTimeEnd(ref long? result); - - public long? TimeEnd - { - get - { - long? value = _TimeEnd; - GetTimeEnd(ref value); - return _TimeEnd = value; - } - - set - { - long? oldValue = _TimeEnd; - SetTimeEnd(oldValue, ref value); - if (oldValue != value) - { - _TimeEnd = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/Collection.cs b/Jellyfin.Data/Entities/Collection.cs deleted file mode 100644 index 01836d893..000000000 --- a/Jellyfin.Data/Entities/Collection.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Collection - { - partial void Init(); - - /// - /// Default constructor. - /// - public Collection() - { - CollectionItem = new LinkedList(); - - Init(); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Backing field for Name. - /// - protected string _Name; - /// - /// When provided in a partial class, allows value of Name to be changed before setting. - /// - partial void SetName(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Name to be changed before returning. - /// - partial void GetName(ref string result); - - /// - /// Max length = 1024 - /// - [MaxLength(1024)] - [StringLength(1024)] - public string Name - { - get - { - string value = _Name; - GetName(ref value); - return _Name = value; - } - - set - { - string oldValue = _Name; - SetName(oldValue, ref value); - if (oldValue != value) - { - _Name = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("CollectionItem_CollectionItem_Id")] - public virtual ICollection CollectionItem { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/CollectionItem.cs b/Jellyfin.Data/Entities/CollectionItem.cs deleted file mode 100644 index d879806ee..000000000 --- a/Jellyfin.Data/Entities/CollectionItem.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class CollectionItem - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected CollectionItem() - { - // NOTE: This class has one-to-one associations with CollectionItem. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static CollectionItem CreateCollectionItemUnsafe() - { - return new CollectionItem(); - } - - /// - /// Public constructor with required data. - /// - /// - /// - /// - public CollectionItem(Collection _collection0, CollectionItem _collectionitem1, CollectionItem _collectionitem2) - { - // NOTE: This class has one-to-one associations with CollectionItem. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - if (_collection0 == null) - { - throw new ArgumentNullException(nameof(_collection0)); - } - - _collection0.CollectionItem.Add(this); - - if (_collectionitem1 == null) - { - throw new ArgumentNullException(nameof(_collectionitem1)); - } - - _collectionitem1.Next = this; - - if (_collectionitem2 == null) - { - throw new ArgumentNullException(nameof(_collectionitem2)); - } - - _collectionitem2.Previous = this; - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// - /// - /// - public static CollectionItem Create(Collection _collection0, CollectionItem _collectionitem1, CollectionItem _collectionitem2) - { - return new CollectionItem(_collection0, _collectionitem1, _collectionitem2); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - /// - /// Required. - /// - [ForeignKey("LibraryItem_Id")] - public virtual LibraryItem LibraryItem { get; set; } - - /// - /// TODO check if this properly updated dependant and has the proper principal relationship - /// - [ForeignKey("CollectionItem_Next_Id")] - public virtual CollectionItem Next { get; set; } - - /// - /// TODO check if this properly updated dependant and has the proper principal relationship - /// - [ForeignKey("CollectionItem_Previous_Id")] - public virtual CollectionItem Previous { get; set; } - } -} - diff --git a/Jellyfin.Data/Entities/Company.cs b/Jellyfin.Data/Entities/Company.cs deleted file mode 100644 index e905a17da..000000000 --- a/Jellyfin.Data/Entities/Company.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Company - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected Company() - { - CompanyMetadata = new HashSet(); - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Company CreateCompanyUnsafe() - { - return new Company(); - } - - /// - /// Public constructor with required data. - /// - /// - /// - /// - /// - /// - public Company(MovieMetadata _moviemetadata0, SeriesMetadata _seriesmetadata1, MusicAlbumMetadata _musicalbummetadata2, BookMetadata _bookmetadata3, Company _company4) - { - if (_moviemetadata0 == null) - { - throw new ArgumentNullException(nameof(_moviemetadata0)); - } - - _moviemetadata0.Studios.Add(this); - - if (_seriesmetadata1 == null) - { - throw new ArgumentNullException(nameof(_seriesmetadata1)); - } - - _seriesmetadata1.Networks.Add(this); - - if (_musicalbummetadata2 == null) - { - throw new ArgumentNullException(nameof(_musicalbummetadata2)); - } - - _musicalbummetadata2.Labels.Add(this); - - if (_bookmetadata3 == null) - { - throw new ArgumentNullException(nameof(_bookmetadata3)); - } - - _bookmetadata3.Publishers.Add(this); - - if (_company4 == null) - { - throw new ArgumentNullException(nameof(_company4)); - } - - _company4.Parent = this; - - this.CompanyMetadata = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// - /// - /// - /// - /// - public static Company Create(MovieMetadata _moviemetadata0, SeriesMetadata _seriesmetadata1, MusicAlbumMetadata _musicalbummetadata2, BookMetadata _bookmetadata3, Company _company4) - { - return new Company(_moviemetadata0, _seriesmetadata1, _musicalbummetadata2, _bookmetadata3, _company4); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("CompanyMetadata_CompanyMetadata_Id")] - public virtual ICollection CompanyMetadata { get; protected set; } - [ForeignKey("Company_Parent_Id")] - public virtual Company Parent { get; set; } - } -} - diff --git a/Jellyfin.Data/Entities/CompanyMetadata.cs b/Jellyfin.Data/Entities/CompanyMetadata.cs deleted file mode 100644 index e75349cf2..000000000 --- a/Jellyfin.Data/Entities/CompanyMetadata.cs +++ /dev/null @@ -1,230 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; - -namespace Jellyfin.Data.Entities -{ - public partial class CompanyMetadata : Metadata - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected CompanyMetadata() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static CompanyMetadata CreateCompanyMetadataUnsafe() - { - return new CompanyMetadata(); - } - - /// - /// Public constructor with required data. - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public CompanyMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Company _company0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_company0 == null) - { - throw new ArgumentNullException(nameof(_company0)); - } - - _company0.CompanyMetadata.Add(this); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public static CompanyMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Company _company0) - { - return new CompanyMetadata(title, language, dateadded, datemodified, _company0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Description. - /// - protected string _Description; - /// - /// When provided in a partial class, allows value of Description to be changed before setting. - /// - partial void SetDescription(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Description to be changed before returning. - /// - partial void GetDescription(ref string result); - - /// - /// Max length = 65535 - /// - [MaxLength(65535)] - [StringLength(65535)] - public string Description - { - get - { - string value = _Description; - GetDescription(ref value); - return _Description = value; - } - - set - { - string oldValue = _Description; - SetDescription(oldValue, ref value); - if (oldValue != value) - { - _Description = value; - } - } - } - - /// - /// Backing field for Headquarters. - /// - protected string _Headquarters; - /// - /// When provided in a partial class, allows value of Headquarters to be changed before setting. - /// - partial void SetHeadquarters(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Headquarters to be changed before returning. - /// - partial void GetHeadquarters(ref string result); - - /// - /// Max length = 255 - /// - [MaxLength(255)] - [StringLength(255)] - public string Headquarters - { - get - { - string value = _Headquarters; - GetHeadquarters(ref value); - return _Headquarters = value; - } - - set - { - string oldValue = _Headquarters; - SetHeadquarters(oldValue, ref value); - if (oldValue != value) - { - _Headquarters = value; - } - } - } - - /// - /// Backing field for Country. - /// - protected string _Country; - /// - /// When provided in a partial class, allows value of Country to be changed before setting. - /// - partial void SetCountry(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Country to be changed before returning. - /// - partial void GetCountry(ref string result); - - /// - /// Max length = 2 - /// - [MaxLength(2)] - [StringLength(2)] - public string Country - { - get - { - string value = _Country; - GetCountry(ref value); - return _Country = value; - } - - set - { - string oldValue = _Country; - SetCountry(oldValue, ref value); - if (oldValue != value) - { - _Country = value; - } - } - } - - /// - /// Backing field for Homepage. - /// - protected string _Homepage; - /// - /// When provided in a partial class, allows value of Homepage to be changed before setting. - /// - partial void SetHomepage(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Homepage to be changed before returning. - /// - partial void GetHomepage(ref string result); - - /// - /// Max length = 1024 - /// - [MaxLength(1024)] - [StringLength(1024)] - public string Homepage - { - get - { - string value = _Homepage; - GetHomepage(ref value); - return _Homepage = value; - } - - set - { - string oldValue = _Homepage; - SetHomepage(oldValue, ref value); - if (oldValue != value) - { - _Homepage = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/CustomItem.cs b/Jellyfin.Data/Entities/CustomItem.cs deleted file mode 100644 index 446391591..000000000 --- a/Jellyfin.Data/Entities/CustomItem.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class CustomItem : LibraryItem - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected CustomItem() - { - CustomItemMetadata = new HashSet(); - Releases = new HashSet(); - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static CustomItem CreateCustomItemUnsafe() - { - return new CustomItem(); - } - - /// - /// Public constructor with required data. - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - public CustomItem(Guid urlid, DateTime dateadded) - { - this.UrlId = urlid; - - this.CustomItemMetadata = new HashSet(); - this.Releases = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - public static CustomItem Create(Guid urlid, DateTime dateadded) - { - return new CustomItem(urlid, dateadded); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("CustomItemMetadata_CustomItemMetadata_Id")] - public virtual ICollection CustomItemMetadata { get; protected set; } - - [ForeignKey("Release_Releases_Id")] - public virtual ICollection Releases { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/CustomItemMetadata.cs b/Jellyfin.Data/Entities/CustomItemMetadata.cs deleted file mode 100644 index 965ed731f..000000000 --- a/Jellyfin.Data/Entities/CustomItemMetadata.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; - -namespace Jellyfin.Data.Entities -{ - public partial class CustomItemMetadata : Metadata - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected CustomItemMetadata() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static CustomItemMetadata CreateCustomItemMetadataUnsafe() - { - return new CustomItemMetadata(); - } - - /// - /// Public constructor with required data. - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public CustomItemMetadata(string title, string language, DateTime dateadded, DateTime datemodified, CustomItem _customitem0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_customitem0 == null) - { - throw new ArgumentNullException(nameof(_customitem0)); - } - - _customitem0.CustomItemMetadata.Add(this); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public static CustomItemMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, CustomItem _customitem0) - { - return new CustomItemMetadata(title, language, dateadded, datemodified, _customitem0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/Episode.cs b/Jellyfin.Data/Entities/Episode.cs deleted file mode 100644 index 57fbf894b..000000000 --- a/Jellyfin.Data/Entities/Episode.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Episode : LibraryItem - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected Episode() - { - // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - Releases = new HashSet(); - EpisodeMetadata = new HashSet(); - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Episode CreateEpisodeUnsafe() - { - return new Episode(); - } - - /// - /// Public constructor with required data. - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - /// - public Episode(Guid urlid, DateTime dateadded, Season _season0) - { - // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - this.UrlId = urlid; - - if (_season0 == null) - { - throw new ArgumentNullException(nameof(_season0)); - } - - _season0.Episodes.Add(this); - - this.Releases = new HashSet(); - this.EpisodeMetadata = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - /// - public static Episode Create(Guid urlid, DateTime dateadded, Season _season0) - { - return new Episode(urlid, dateadded, _season0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for EpisodeNumber. - /// - protected int? _EpisodeNumber; - /// - /// When provided in a partial class, allows value of EpisodeNumber to be changed before setting. - /// - partial void SetEpisodeNumber(int? oldValue, ref int? newValue); - /// - /// When provided in a partial class, allows value of EpisodeNumber to be changed before returning. - /// - partial void GetEpisodeNumber(ref int? result); - - public int? EpisodeNumber - { - get - { - int? value = _EpisodeNumber; - GetEpisodeNumber(ref value); - return _EpisodeNumber = value; - } - - set - { - int? oldValue = _EpisodeNumber; - SetEpisodeNumber(oldValue, ref value); - if (oldValue != value) - { - _EpisodeNumber = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("Release_Releases_Id")] - public virtual ICollection Releases { get; protected set; } - [ForeignKey("EpisodeMetadata_EpisodeMetadata_Id")] - public virtual ICollection EpisodeMetadata { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/EpisodeMetadata.cs b/Jellyfin.Data/Entities/EpisodeMetadata.cs deleted file mode 100644 index 9a21fd50f..000000000 --- a/Jellyfin.Data/Entities/EpisodeMetadata.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; - -namespace Jellyfin.Data.Entities -{ - public partial class EpisodeMetadata : Metadata - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected EpisodeMetadata() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static EpisodeMetadata CreateEpisodeMetadataUnsafe() - { - return new EpisodeMetadata(); - } - - /// - /// Public constructor with required data. - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public EpisodeMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Episode _episode0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_episode0 == null) - { - throw new ArgumentNullException(nameof(_episode0)); - } - - _episode0.EpisodeMetadata.Add(this); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public static EpisodeMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Episode _episode0) - { - return new EpisodeMetadata(title, language, dateadded, datemodified, _episode0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Outline. - /// - protected string _Outline; - /// - /// When provided in a partial class, allows value of Outline to be changed before setting. - /// - partial void SetOutline(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Outline to be changed before returning. - /// - partial void GetOutline(ref string result); - - /// - /// Max length = 1024 - /// - [MaxLength(1024)] - [StringLength(1024)] - public string Outline - { - get - { - string value = _Outline; - GetOutline(ref value); - return _Outline = value; - } - - set - { - string oldValue = _Outline; - SetOutline(oldValue, ref value); - if (oldValue != value) - { - _Outline = value; - } - } - } - - /// - /// Backing field for Plot. - /// - protected string _Plot; - /// - /// When provided in a partial class, allows value of Plot to be changed before setting. - /// - partial void SetPlot(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Plot to be changed before returning. - /// - partial void GetPlot(ref string result); - - /// - /// Max length = 65535 - /// - [MaxLength(65535)] - [StringLength(65535)] - public string Plot - { - get - { - string value = _Plot; - GetPlot(ref value); - return _Plot = value; - } - - set - { - string oldValue = _Plot; - SetPlot(oldValue, ref value); - if (oldValue != value) - { - _Plot = value; - } - } - } - - /// - /// Backing field for Tagline. - /// - protected string _Tagline; - /// - /// When provided in a partial class, allows value of Tagline to be changed before setting. - /// - partial void SetTagline(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Tagline to be changed before returning. - /// - partial void GetTagline(ref string result); - - /// - /// Max length = 1024 - /// - [MaxLength(1024)] - [StringLength(1024)] - public string Tagline - { - get - { - string value = _Tagline; - GetTagline(ref value); - return _Tagline = value; - } - - set - { - string oldValue = _Tagline; - SetTagline(oldValue, ref value); - if (oldValue != value) - { - _Tagline = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/Genre.cs b/Jellyfin.Data/Entities/Genre.cs deleted file mode 100644 index 24e6815d8..000000000 --- a/Jellyfin.Data/Entities/Genre.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Genre - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected Genre() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Genre CreateGenreUnsafe() - { - return new Genre(); - } - - /// - /// Public constructor with required data. - /// - /// - /// - public Genre(string name, Metadata _metadata0) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - this.Name = name; - - if (_metadata0 == null) - { - throw new ArgumentNullException(nameof(_metadata0)); - } - - _metadata0.Genres.Add(this); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// - /// - public static Genre Create(string name, Metadata _metadata0) - { - return new Genre(name, _metadata0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Backing field for Name. - /// - internal string _Name; - /// - /// When provided in a partial class, allows value of Name to be changed before setting. - /// - partial void SetName(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Name to be changed before returning. - /// - partial void GetName(ref string result); - - /// - /// Indexed, Required, Max length = 255 - /// - [Required] - [MaxLength(255)] - [StringLength(255)] - public string Name - { - get - { - string value = _Name; - GetName(ref value); - return _Name = value; - } - - set - { - string oldValue = _Name; - SetName(oldValue, ref value); - if (oldValue != value) - { - _Name = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/Group.cs b/Jellyfin.Data/Entities/Group.cs index 47833378e..ca12ba421 100644 --- a/Jellyfin.Data/Entities/Group.cs +++ b/Jellyfin.Data/Entities/Group.cs @@ -1,16 +1,19 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Jellyfin.Data.Enums; +using Jellyfin.Data.Interfaces; namespace Jellyfin.Data.Entities { /// /// An entity representing a group. /// - public partial class Group : IHasPermissions, ISavingChanges + public partial class Group : IHasPermissions, IHasConcurrencyToken { /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Data/Entities/ImageInfo.cs b/Jellyfin.Data/Entities/ImageInfo.cs index 64e36a791..cf0895ad4 100644 --- a/Jellyfin.Data/Entities/ImageInfo.cs +++ b/Jellyfin.Data/Entities/ImageInfo.cs @@ -1,4 +1,6 @@ -using System; +#pragma warning disable CS1591 + +using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; diff --git a/Jellyfin.Data/Entities/ItemDisplayPreferences.cs b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs index 95c08e6c6..023cdc740 100644 --- a/Jellyfin.Data/Entities/ItemDisplayPreferences.cs +++ b/Jellyfin.Data/Entities/ItemDisplayPreferences.cs @@ -1,4 +1,6 @@ -using System; +#pragma warning disable CS1591 + +using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Enums; diff --git a/Jellyfin.Data/Entities/Libraries/Artwork.cs b/Jellyfin.Data/Entities/Libraries/Artwork.cs new file mode 100644 index 000000000..7be22af25 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Artwork.cs @@ -0,0 +1,81 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Enums; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing artwork. + /// + public class Artwork : IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + /// The path. + /// The kind of art. + /// The owner. + public Artwork(string path, ArtKind kind, IHasArtwork owner) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(nameof(path)); + } + + Path = path; + Kind = kind; + + owner?.Artwork.Add(this); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected Artwork() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the path. + /// + /// + /// Required, Max length = 65535. + /// + [Required] + [MaxLength(65535)] + [StringLength(65535)] + public string Path { get; set; } + + /// + /// Gets or sets the kind of artwork. + /// + /// + /// Required. + /// + public ArtKind Kind { get; set; } + + /// + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Book.cs b/Jellyfin.Data/Entities/Libraries/Book.cs new file mode 100644 index 000000000..8337788dd --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Book.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a book. + /// + public class Book : LibraryItem, IHasReleases + { + /// + /// Initializes a new instance of the class. + /// + public Book() + { + BookMetadata = new HashSet(); + Releases = new HashSet(); + } + + /// + /// Gets or sets a collection containing the metadata for this book. + /// + public virtual ICollection BookMetadata { get; protected set; } + + /// + public virtual ICollection Releases { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/BookMetadata.cs b/Jellyfin.Data/Entities/Libraries/BookMetadata.cs new file mode 100644 index 000000000..bd716712b --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/BookMetadata.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity containing metadata for a book. + /// + public class BookMetadata : Metadata, IHasCompanies + { + /// + /// Initializes a new instance of the class. + /// + /// The title or name of the object. + /// ISO-639-3 3-character language codes. + /// The book. + public BookMetadata(string title, string language, Book book) : base(title, language) + { + if (book == null) + { + throw new ArgumentNullException(nameof(book)); + } + + book.BookMetadata.Add(this); + + Publishers = new HashSet(); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected BookMetadata() + { + } + + /// + /// Gets or sets the ISBN. + /// + public long? Isbn { get; set; } + + /// + /// Gets or sets a collection of the publishers for this book. + /// + public virtual ICollection Publishers { get; protected set; } + + /// + [NotMapped] + public ICollection Companies => Publishers; + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Chapter.cs b/Jellyfin.Data/Entities/Libraries/Chapter.cs new file mode 100644 index 000000000..d9293c3cc --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Chapter.cs @@ -0,0 +1,102 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a chapter. + /// + public class Chapter : IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + /// ISO-639-3 3-character language codes. + /// The start time for this chapter. + /// The release. + public Chapter(string language, long startTime, Release release) + { + if (string.IsNullOrEmpty(language)) + { + throw new ArgumentNullException(nameof(language)); + } + + Language = language; + StartTime = startTime; + + if (release == null) + { + throw new ArgumentNullException(nameof(release)); + } + + release.Chapters.Add(this); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected Chapter() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the name. + /// + /// + /// Max length = 1024. + /// + [MaxLength(1024)] + [StringLength(1024)] + public string Name { get; set; } + + /// + /// Gets or sets the language. + /// + /// + /// Required, Min length = 3, Max length = 3 + /// ISO-639-3 3-character language codes. + /// + [Required] + [MinLength(3)] + [MaxLength(3)] + [StringLength(3)] + public string Language { get; set; } + + /// + /// Gets or sets the start time. + /// + /// + /// Required. + /// + public long StartTime { get; set; } + + /// + /// Gets or sets the end time. + /// + public long? EndTime { get; set; } + + /// + [ConcurrencyCheck] + public uint RowVersion { get; protected set; } + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Collection.cs b/Jellyfin.Data/Entities/Libraries/Collection.cs new file mode 100644 index 000000000..2e1bbcfb6 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Collection.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a collection. + /// + public class Collection : IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + public Collection() + { + Items = new HashSet(); + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the name. + /// + /// + /// Max length = 1024. + /// + [MaxLength(1024)] + [StringLength(1024)] + public string Name { get; set; } + + /// + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// + /// Gets or sets a collection containing this collection's items. + /// + public virtual ICollection Items { get; protected set; } + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/CollectionItem.cs b/Jellyfin.Data/Entities/Libraries/CollectionItem.cs new file mode 100644 index 000000000..c9306f630 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/CollectionItem.cs @@ -0,0 +1,94 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a collection item. + /// + public class CollectionItem : IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + /// The collection. + /// The previous item. + /// The next item. + public CollectionItem(Collection collection, CollectionItem previous, CollectionItem next) + { + if (collection == null) + { + throw new ArgumentNullException(nameof(collection)); + } + + collection.Items.Add(this); + + if (next != null) + { + Next = next; + next.Previous = this; + } + + if (previous != null) + { + Previous = previous; + previous.Next = this; + } + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected CollectionItem() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + /// + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// + /// Gets or sets the library item. + /// + /// + /// Required. + /// + public virtual LibraryItem LibraryItem { get; set; } + + /// + /// Gets or sets the next item in the collection. + /// + /// + /// TODO check if this properly updated dependant and has the proper principal relationship + /// + public virtual CollectionItem Next { get; set; } + + /// + /// Gets or sets the previous item in the collection. + /// + /// + /// TODO check if this properly updated dependant and has the proper principal relationship + /// + public virtual CollectionItem Previous { get; set; } + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Company.cs b/Jellyfin.Data/Entities/Libraries/Company.cs new file mode 100644 index 000000000..02da26bc2 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Company.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a company. + /// + public class Company : IHasCompanies, IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + /// The owner of this company. + public Company(IHasCompanies owner) + { + owner?.Companies.Add(this); + + CompanyMetadata = new HashSet(); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected Company() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// + /// Gets or sets a collection containing the metadata. + /// + public virtual ICollection CompanyMetadata { get; protected set; } + + /// + /// Gets or sets a collection containing this company's child companies. + /// + public virtual ICollection ChildCompanies { get; protected set; } + + /// + [NotMapped] + public ICollection Companies => ChildCompanies; + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs b/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs new file mode 100644 index 000000000..60cc96a34 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs @@ -0,0 +1,74 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity holding metadata for a . + /// + public class CompanyMetadata : Metadata + { + /// + /// Initializes a new instance of the class. + /// + /// The title or name of the object. + /// ISO-639-3 3-character language codes. + /// The company. + public CompanyMetadata(string title, string language, Company company) : base(title, language) + { + if (company == null) + { + throw new ArgumentNullException(nameof(company)); + } + + company.CompanyMetadata.Add(this); + } + + /// + /// Initializes a new instance of the class. + /// + protected CompanyMetadata() + { + } + + /// + /// Gets or sets the description. + /// + /// + /// Max length = 65535. + /// + [MaxLength(65535)] + [StringLength(65535)] + public string Description { get; set; } + + /// + /// Gets or sets the headquarters. + /// + /// + /// Max length = 255. + /// + [MaxLength(255)] + [StringLength(255)] + public string Headquarters { get; set; } + + /// + /// Gets or sets the country code. + /// + /// + /// Max length = 2. + /// + [MaxLength(2)] + [StringLength(2)] + public string Country { get; set; } + + /// + /// Gets or sets the homepage. + /// + /// + /// Max length = 1024. + /// + [MaxLength(1024)] + [StringLength(1024)] + public string Homepage { get; set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/CustomItem.cs b/Jellyfin.Data/Entities/Libraries/CustomItem.cs new file mode 100644 index 000000000..6a4f0a537 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/CustomItem.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a custom item. + /// + public class CustomItem : LibraryItem, IHasReleases + { + /// + /// Initializes a new instance of the class. + /// + public CustomItem() + { + CustomItemMetadata = new HashSet(); + Releases = new HashSet(); + } + + /// + /// Gets or sets a collection containing the metadata for this item. + /// + public virtual ICollection CustomItemMetadata { get; protected set; } + + /// + public virtual ICollection Releases { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs b/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs new file mode 100644 index 000000000..bc1835528 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs @@ -0,0 +1,36 @@ +using System; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity containing metadata for a custom item. + /// + public class CustomItemMetadata : Metadata + { + /// + /// Initializes a new instance of the class. + /// + /// The title or name of the object. + /// ISO-639-3 3-character language codes. + /// The item. + public CustomItemMetadata(string title, string language, CustomItem item) : base(title, language) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + item.CustomItemMetadata.Add(this); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected CustomItemMetadata() + { + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Episode.cs b/Jellyfin.Data/Entities/Libraries/Episode.cs new file mode 100644 index 000000000..430a11e3c --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Episode.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing an episode. + /// + public class Episode : LibraryItem, IHasReleases + { + /// + /// Initializes a new instance of the class. + /// + /// The season. + public Episode(Season season) + { + if (season == null) + { + throw new ArgumentNullException(nameof(season)); + } + + season.Episodes.Add(this); + + Releases = new HashSet(); + EpisodeMetadata = new HashSet(); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected Episode() + { + } + + /// + /// Gets or sets the episode number. + /// + public int? EpisodeNumber { get; set; } + + /// + public virtual ICollection Releases { get; protected set; } + + /// + /// Gets or sets a collection containing the metadata for this episode. + /// + public virtual ICollection EpisodeMetadata { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs b/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs new file mode 100644 index 000000000..348100cb4 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs @@ -0,0 +1,67 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity containing metadata for an . + /// + public class EpisodeMetadata : Metadata + { + /// + /// Initializes a new instance of the class. + /// + /// The title or name of the object. + /// ISO-639-3 3-character language codes. + /// The episode. + public EpisodeMetadata(string title, string language, Episode episode) : base(title, language) + { + if (episode == null) + { + throw new ArgumentNullException(nameof(episode)); + } + + episode.EpisodeMetadata.Add(this); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected EpisodeMetadata() + { + } + + /// + /// Gets or sets the outline. + /// + /// + /// Max length = 1024. + /// + [MaxLength(1024)] + [StringLength(1024)] + public string Outline { get; set; } + + /// + /// Gets or sets the plot. + /// + /// + /// Max length = 65535. + /// + [MaxLength(65535)] + [StringLength(65535)] + public string Plot { get; set; } + + /// + /// Gets or sets the tagline. + /// + /// + /// Max length = 1024. + /// + [MaxLength(1024)] + [StringLength(1024)] + public string Tagline { get; set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Genre.cs b/Jellyfin.Data/Entities/Libraries/Genre.cs new file mode 100644 index 000000000..aeedd7bfd --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Genre.cs @@ -0,0 +1,75 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a genre. + /// + public class Genre : IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + /// The name. + /// The metadata. + public Genre(string name, Metadata metadata) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + Name = name; + + if (metadata == null) + { + throw new ArgumentNullException(nameof(metadata)); + } + + metadata.Genres.Add(this); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected Genre() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the name. + /// + /// + /// Indexed, Required, Max length = 255. + /// + [Required] + [MaxLength(255)] + [StringLength(255)] + public string Name { get; set; } + + /// + [ConcurrencyCheck] + public uint RowVersion { get; protected set; } + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Library.cs b/Jellyfin.Data/Entities/Libraries/Library.cs new file mode 100644 index 000000000..4f82a2e2a --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Library.cs @@ -0,0 +1,76 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a library. + /// + public class Library : IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the library. + public Library(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + Name = name; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected Library() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the name. + /// + /// + /// Required, Max length = 128. + /// + [Required] + [MaxLength(128)] + [StringLength(128)] + public string Name { get; set; } + + /// + /// Gets or sets the root path of the library. + /// + /// + /// Required. + /// + [Required] + public string Path { get; set; } + + /// + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/LibraryItem.cs b/Jellyfin.Data/Entities/Libraries/LibraryItem.cs new file mode 100644 index 000000000..a9167aa7f --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/LibraryItem.cs @@ -0,0 +1,63 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a library item. + /// + public abstract class LibraryItem : IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + /// The library of this item. + protected LibraryItem(Library library) + { + DateAdded = DateTime.UtcNow; + Library = library; + } + + /// + /// Initializes a new instance of the class. + /// + protected LibraryItem() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the date this library item was added. + /// + public DateTime DateAdded { get; protected set; } + + /// + [ConcurrencyCheck] + public uint RowVersion { get; protected set; } + + /// + /// Gets or sets the library of this item. + /// + /// + /// Required. + /// + [Required] + public virtual Library Library { get; set; } + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/MediaFile.cs b/Jellyfin.Data/Entities/Libraries/MediaFile.cs new file mode 100644 index 000000000..8bc649c98 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/MediaFile.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Enums; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a file on disk. + /// + public class MediaFile : IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + /// The path relative to the LibraryRoot. + /// The file kind. + /// The release. + public MediaFile(string path, MediaFileKind kind, Release release) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(nameof(path)); + } + + Path = path; + Kind = kind; + + if (release == null) + { + throw new ArgumentNullException(nameof(release)); + } + + release.MediaFiles.Add(this); + + MediaFileStreams = new HashSet(); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected MediaFile() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the path relative to the library root. + /// + /// + /// Required, Max length = 65535. + /// + [Required] + [MaxLength(65535)] + [StringLength(65535)] + public string Path { get; set; } + + /// + /// Gets or sets the kind of media file. + /// + /// + /// Required. + /// + public MediaFileKind Kind { get; set; } + + /// + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// + /// Gets or sets a collection containing the streams in this file. + /// + public virtual ICollection MediaFileStreams { get; protected set; } + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/MediaFileStream.cs b/Jellyfin.Data/Entities/Libraries/MediaFileStream.cs new file mode 100644 index 000000000..5b03e260e --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/MediaFileStream.cs @@ -0,0 +1,67 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a stream in a media file. + /// + public class MediaFileStream : IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + /// The number of this stream. + /// The media file. + public MediaFileStream(int streamNumber, MediaFile mediaFile) + { + StreamNumber = streamNumber; + + if (mediaFile == null) + { + throw new ArgumentNullException(nameof(mediaFile)); + } + + mediaFile.MediaFileStreams.Add(this); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected MediaFileStream() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the stream number. + /// + /// + /// Required. + /// + public int StreamNumber { get; set; } + + /// + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Metadata.cs b/Jellyfin.Data/Entities/Libraries/Metadata.cs new file mode 100644 index 000000000..877bb5fbd --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Metadata.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An abstract class that holds metadata. + /// + public abstract class Metadata : IHasArtwork, IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + /// The title or name of the object. + /// ISO-639-3 3-character language codes. + protected Metadata(string title, string language) + { + if (string.IsNullOrEmpty(title)) + { + throw new ArgumentNullException(nameof(title)); + } + + if (string.IsNullOrEmpty(language)) + { + throw new ArgumentNullException(nameof(language)); + } + + Title = title; + Language = language; + DateAdded = DateTime.UtcNow; + DateModified = DateAdded; + + PersonRoles = new HashSet(); + Genres = new HashSet(); + Artwork = new HashSet(); + Ratings = new HashSet(); + Sources = new HashSet(); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to being abstract. + /// + protected Metadata() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the title. + /// + /// + /// Required, Max length = 1024. + /// + [Required] + [MaxLength(1024)] + [StringLength(1024)] + public string Title { get; set; } + + /// + /// Gets or sets the original title. + /// + /// + /// Max length = 1024. + /// + [MaxLength(1024)] + [StringLength(1024)] + public string OriginalTitle { get; set; } + + /// + /// Gets or sets the sort title. + /// + /// + /// Max length = 1024. + /// + [MaxLength(1024)] + [StringLength(1024)] + public string SortTitle { get; set; } + + /// + /// Gets or sets the language. + /// + /// + /// Required, Min length = 3, Max length = 3. + /// ISO-639-3 3-character language codes. + /// + [Required] + [MinLength(3)] + [MaxLength(3)] + [StringLength(3)] + public string Language { get; set; } + + /// + /// Gets or sets the release date. + /// + public DateTimeOffset? ReleaseDate { get; set; } + + /// + /// Gets or sets the date added. + /// + /// + /// Required. + /// + public DateTime DateAdded { get; protected set; } + + /// + /// Gets or sets the date modified. + /// + /// + /// Required. + /// + public DateTime DateModified { get; set; } + + /// + /// Gets or sets the row version. + /// + /// + /// Required, ConcurrencyToken. + /// + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// + /// Gets or sets a collection containing the person roles for this item. + /// + public virtual ICollection PersonRoles { get; protected set; } + + /// + /// Gets or sets a collection containing the generes for this item. + /// + public virtual ICollection Genres { get; protected set; } + + /// + public virtual ICollection Artwork { get; protected set; } + + /// + /// Gets or sets a collection containing the ratings for this item. + /// + public virtual ICollection Ratings { get; protected set; } + + /// + /// Gets or sets a collection containing the metadata sources for this item. + /// + public virtual ICollection Sources { get; protected set; } + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs b/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs new file mode 100644 index 000000000..a18a612bc --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs @@ -0,0 +1,67 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a metadata provider. + /// + public class MetadataProvider : IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the metadata provider. + public MetadataProvider(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + Name = name; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected MetadataProvider() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the name. + /// + /// + /// Required, Max length = 1024. + /// + [Required] + [MaxLength(1024)] + [StringLength(1024)] + public string Name { get; set; } + + /// + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs b/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs new file mode 100644 index 000000000..6e6de598e --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs @@ -0,0 +1,83 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a unique identifier for a metadata provider. + /// + public class MetadataProviderId : IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + /// The provider id. + /// The metadata entity. + public MetadataProviderId(string providerId, Metadata metadata) + { + if (string.IsNullOrEmpty(providerId)) + { + throw new ArgumentNullException(nameof(providerId)); + } + + ProviderId = providerId; + + if (metadata == null) + { + throw new ArgumentNullException(nameof(metadata)); + } + + metadata.Sources.Add(this); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected MetadataProviderId() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the provider id. + /// + /// + /// Required, Max length = 255. + /// + [Required] + [MaxLength(255)] + [StringLength(255)] + public string ProviderId { get; set; } + + /// + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// + /// Gets or sets the metadata provider. + /// + /// + /// Required. + /// + public virtual MetadataProvider MetadataProvider { get; set; } + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Movie.cs b/Jellyfin.Data/Entities/Libraries/Movie.cs new file mode 100644 index 000000000..0a8cc83dd --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Movie.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a movie. + /// + public class Movie : LibraryItem, IHasReleases + { + /// + /// Initializes a new instance of the class. + /// + public Movie() + { + Releases = new HashSet(); + MovieMetadata = new HashSet(); + } + + /// + public virtual ICollection Releases { get; protected set; } + + /// + /// Gets or sets a collection containing the metadata for this movie. + /// + public virtual ICollection MovieMetadata { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs b/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs new file mode 100644 index 000000000..31102bf13 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity holding the metadata for a movie. + /// + public class MovieMetadata : Metadata, IHasCompanies + { + /// + /// Initializes a new instance of the class. + /// + /// The title or name of the movie. + /// ISO-639-3 3-character language codes. + /// The movie. + public MovieMetadata(string title, string language, Movie movie) : base(title, language) + { + Studios = new HashSet(); + + movie.MovieMetadata.Add(this); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected MovieMetadata() + { + } + + /// + /// Gets or sets the outline. + /// + /// + /// Max length = 1024. + /// + [MaxLength(1024)] + [StringLength(1024)] + public string Outline { get; set; } + + /// + /// Gets or sets the tagline. + /// + /// + /// Max length = 1024. + /// + [MaxLength(1024)] + [StringLength(1024)] + public string Tagline { get; set; } + + /// + /// Gets or sets the plot. + /// + /// + /// Max length = 65535. + /// + [MaxLength(65535)] + [StringLength(65535)] + public string Plot { get; set; } + + /// + /// Gets or sets the country code. + /// + /// + /// Max length = 2. + /// + [MaxLength(2)] + [StringLength(2)] + public string Country { get; set; } + + /// + /// Gets or sets the studios that produced this movie. + /// + public virtual ICollection Studios { get; protected set; } + + /// + [NotMapped] + public ICollection Companies => Studios; + } +} diff --git a/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs b/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs new file mode 100644 index 000000000..2ed1f78c5 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a music album. + /// + public class MusicAlbum : LibraryItem + { + /// + /// Initializes a new instance of the class. + /// + public MusicAlbum() + { + MusicAlbumMetadata = new HashSet(); + Tracks = new HashSet(); + } + + /// + /// Gets or sets a collection containing the album metadata. + /// + public virtual ICollection MusicAlbumMetadata { get; protected set; } + + /// + /// Gets or sets a collection containing the tracks. + /// + public virtual ICollection Tracks { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs b/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs new file mode 100644 index 000000000..cc5919bfe --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity holding the metadata for a music album. + /// + public class MusicAlbumMetadata : Metadata + { + /// + /// Initializes a new instance of the class. + /// + /// The title or name of the album. + /// ISO-639-3 3-character language codes. + /// The music album. + public MusicAlbumMetadata(string title, string language, MusicAlbum album) : base(title, language) + { + Labels = new HashSet(); + + album.MusicAlbumMetadata.Add(this); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected MusicAlbumMetadata() + { + } + + /// + /// Gets or sets the barcode. + /// + /// + /// Max length = 255. + /// + [MaxLength(255)] + [StringLength(255)] + public string Barcode { get; set; } + + /// + /// Gets or sets the label number. + /// + /// + /// Max length = 255. + /// + [MaxLength(255)] + [StringLength(255)] + public string LabelNumber { get; set; } + + /// + /// Gets or sets the country code. + /// + /// + /// Max length = 2. + /// + [MaxLength(2)] + [StringLength(2)] + public string Country { get; set; } + + /// + /// Gets or sets a collection containing the labels. + /// + public virtual ICollection Labels { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Person.cs b/Jellyfin.Data/Entities/Libraries/Person.cs new file mode 100644 index 000000000..8beb3dd08 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Person.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a person. + /// + public class Person : IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the person. + public Person(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + Name = name; + DateAdded = DateTime.UtcNow; + DateModified = DateAdded; + + Sources = new HashSet(); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected Person() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the name. + /// + /// + /// Required, Max length = 1024. + /// + [Required] + [MaxLength(1024)] + [StringLength(1024)] + public string Name { get; set; } + + /// + /// Gets or sets the source id. + /// + /// + /// Max length = 255. + /// + [MaxLength(256)] + [StringLength(256)] + public string SourceId { get; set; } + + /// + /// Gets or sets the date added. + /// + /// + /// Required. + /// + public DateTime DateAdded { get; protected set; } + + /// + /// Gets or sets the date modified. + /// + /// + /// Required. + /// + public DateTime DateModified { get; set; } + + /// + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// + /// Gets or sets a list of metadata sources for this person. + /// + public virtual ICollection Sources { get; protected set; } + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/PersonRole.cs b/Jellyfin.Data/Entities/Libraries/PersonRole.cs new file mode 100644 index 000000000..5290228d6 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/PersonRole.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Enums; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a person's role in media. + /// + public class PersonRole : IHasArtwork, IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + /// The role type. + /// The metadata. + public PersonRole(PersonRoleType type, Metadata metadata) + { + Type = type; + + if (metadata == null) + { + throw new ArgumentNullException(nameof(metadata)); + } + + metadata.PersonRoles.Add(this); + + Sources = new HashSet(); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected PersonRole() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the name of the person's role. + /// + /// + /// Max length = 1024. + /// + [MaxLength(1024)] + [StringLength(1024)] + public string Role { get; set; } + + /// + /// Gets or sets the person's role type. + /// + /// + /// Required. + /// + public PersonRoleType Type { get; set; } + + /// + [ConcurrencyCheck] + public uint RowVersion { get; protected set; } + + /// + /// Gets or sets the person. + /// + /// + /// Required. + /// + [Required] + public virtual Person Person { get; set; } + + /// + public virtual ICollection Artwork { get; protected set; } + + /// + /// Gets or sets a collection containing the metadata sources for this person role. + /// + public virtual ICollection Sources { get; protected set; } + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Photo.cs b/Jellyfin.Data/Entities/Libraries/Photo.cs new file mode 100644 index 000000000..44338a4ce --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Photo.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a photo. + /// + public class Photo : LibraryItem, IHasReleases + { + /// + /// Initializes a new instance of the class. + /// + public Photo() + { + PhotoMetadata = new HashSet(); + Releases = new HashSet(); + } + + /// + /// Gets or sets a collection containing the photo metadata. + /// + public virtual ICollection PhotoMetadata { get; protected set; } + + /// + public virtual ICollection Releases { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs b/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs new file mode 100644 index 000000000..1ef9dd5f9 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs @@ -0,0 +1,36 @@ +using System; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity that holds metadata for a photo. + /// + public class PhotoMetadata : Metadata + { + /// + /// Initializes a new instance of the class. + /// + /// The title or name of the photo. + /// ISO-639-3 3-character language codes. + /// The photo. + public PhotoMetadata(string title, string language, Photo photo) : base(title, language) + { + if (photo == null) + { + throw new ArgumentNullException(nameof(photo)); + } + + photo.PhotoMetadata.Add(this); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected PhotoMetadata() + { + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Rating.cs b/Jellyfin.Data/Entities/Libraries/Rating.cs new file mode 100644 index 000000000..ba054a39e --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Rating.cs @@ -0,0 +1,78 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a rating for an entity. + /// + public class Rating : IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + /// The value. + /// The metadata. + public Rating(double value, Metadata metadata) + { + Value = value; + + if (metadata == null) + { + throw new ArgumentNullException(nameof(metadata)); + } + + metadata.Ratings.Add(this); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected Rating() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the value. + /// + /// + /// Required. + /// + public double Value { get; set; } + + /// + /// Gets or sets the number of votes. + /// + public int? Votes { get; set; } + + /// + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// + /// Gets or sets the rating type. + /// If this is null it's the internal user rating. + /// + public virtual RatingSource RatingType { get; set; } + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/RatingSource.cs b/Jellyfin.Data/Entities/Libraries/RatingSource.cs new file mode 100644 index 000000000..549f41804 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/RatingSource.cs @@ -0,0 +1,92 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// This is the entity to store review ratings, not age ratings. + /// + public class RatingSource : IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + /// The minimum value. + /// The maximum value. + /// The rating. + public RatingSource(double minimumValue, double maximumValue, Rating rating) + { + MinimumValue = minimumValue; + MaximumValue = maximumValue; + + if (rating == null) + { + throw new ArgumentNullException(nameof(rating)); + } + + rating.RatingType = this; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected RatingSource() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the name. + /// + /// + /// Max length = 1024. + /// + [MaxLength(1024)] + [StringLength(1024)] + public string Name { get; set; } + + /// + /// Gets or sets the minimum value. + /// + /// + /// Required. + /// + public double MinimumValue { get; set; } + + /// + /// Gets or sets the maximum value. + /// + /// + /// Required. + /// + public double MaximumValue { get; set; } + + /// + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// + /// Gets or sets the metadata source. + /// + public virtual MetadataProviderId Source { get; set; } + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Release.cs b/Jellyfin.Data/Entities/Libraries/Release.cs new file mode 100644 index 000000000..43c7080d7 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Release.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a release for a library item, eg. Director's cut vs. standard. + /// + public class Release : IHasConcurrencyToken + { + /// + /// Initializes a new instance of the class. + /// + /// The name of this release. + /// The owner of this release. + public Release(string name, IHasReleases owner) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + Name = name; + + owner?.Releases.Add(this); + + MediaFiles = new HashSet(); + Chapters = new HashSet(); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected Release() + { + } + + /// + /// Gets or sets the id. + /// + /// + /// Identity, Indexed, Required. + /// + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the name. + /// + /// + /// Required, Max length = 1024. + /// + [Required] + [MaxLength(1024)] + [StringLength(1024)] + public string Name { get; set; } + + /// + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// + /// Gets or sets a collection containing the media files for this release. + /// + public virtual ICollection MediaFiles { get; protected set; } + + /// + /// Gets or sets a collection containing the chapters for this release. + /// + public virtual ICollection Chapters { get; protected set; } + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Season.cs b/Jellyfin.Data/Entities/Libraries/Season.cs new file mode 100644 index 000000000..eef788bad --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Season.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a season. + /// + public class Season : LibraryItem + { + /// + /// Initializes a new instance of the class. + /// + /// The series. + public Season(Series series) + { + if (series == null) + { + throw new ArgumentNullException(nameof(series)); + } + + series.Seasons.Add(this); + + Episodes = new HashSet(); + SeasonMetadata = new HashSet(); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected Season() + { + } + + /// + /// Gets or sets the season number. + /// + public int? SeasonNumber { get; set; } + + /// + /// Gets or sets the season metadata. + /// + public virtual ICollection SeasonMetadata { get; protected set; } + + /// + /// Gets or sets a collection containing the number of episodes. + /// + public virtual ICollection Episodes { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs b/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs new file mode 100644 index 000000000..eedeb089e --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs @@ -0,0 +1,47 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity that holds metadata for seasons. + /// + public class SeasonMetadata : Metadata + { + /// + /// Initializes a new instance of the class. + /// + /// The title or name of the object. + /// ISO-639-3 3-character language codes. + /// The season. + public SeasonMetadata(string title, string language, Season season) : base(title, language) + { + if (season == null) + { + throw new ArgumentNullException(nameof(season)); + } + + season.SeasonMetadata.Add(this); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected SeasonMetadata() + { + } + + /// + /// Gets or sets the outline. + /// + /// + /// Max length = 1024. + /// + [MaxLength(1024)] + [StringLength(1024)] + public string Outline { get; set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Series.cs b/Jellyfin.Data/Entities/Libraries/Series.cs new file mode 100644 index 000000000..e959c1fe0 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Series.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a a series. + /// + public class Series : LibraryItem + { + /// + /// Initializes a new instance of the class. + /// + public Series() + { + DateAdded = DateTime.UtcNow; + Seasons = new HashSet(); + SeriesMetadata = new HashSet(); + } + + /// + /// Gets or sets the days of week. + /// + public DayOfWeek? AirsDayOfWeek { get; set; } + + /// + /// Gets or sets the time the show airs, ignore the date portion. + /// + public DateTimeOffset? AirsTime { get; set; } + + /// + /// Gets or sets the date the series first aired. + /// + public DateTime? FirstAired { get; set; } + + /// + /// Gets or sets a collection containing the series metadata. + /// + public virtual ICollection SeriesMetadata { get; protected set; } + + /// + /// Gets or sets a collection containing the seasons. + /// + public virtual ICollection Seasons { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs b/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs new file mode 100644 index 000000000..898f3006d --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing series metadata. + /// + public class SeriesMetadata : Metadata, IHasCompanies + { + /// + /// Initializes a new instance of the class. + /// + /// The title or name of the object. + /// ISO-639-3 3-character language codes. + /// The series. + public SeriesMetadata(string title, string language, Series series) : base(title, language) + { + if (series == null) + { + throw new ArgumentNullException(nameof(series)); + } + + series.SeriesMetadata.Add(this); + + Networks = new HashSet(); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected SeriesMetadata() + { + } + + /// + /// Gets or sets the outline. + /// + /// + /// Max length = 1024. + /// + [MaxLength(1024)] + [StringLength(1024)] + public string Outline { get; set; } + + /// + /// Gets or sets the plot. + /// + /// + /// Max length = 65535. + /// + [MaxLength(65535)] + [StringLength(65535)] + public string Plot { get; set; } + + /// + /// Gets or sets the tagline. + /// + /// + /// Max length = 1024. + /// + [MaxLength(1024)] + [StringLength(1024)] + public string Tagline { get; set; } + + /// + /// Gets or sets the country code. + /// + /// + /// Max length = 2. + /// + [MaxLength(2)] + [StringLength(2)] + public string Country { get; set; } + + /// + /// Gets or sets a collection containing the networks. + /// + public virtual ICollection Networks { get; protected set; } + + /// + [NotMapped] + public ICollection Companies => Networks; + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Track.cs b/Jellyfin.Data/Entities/Libraries/Track.cs new file mode 100644 index 000000000..09ce82a9b --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Track.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity representing a track. + /// + public class Track : LibraryItem, IHasReleases + { + /// + /// Initializes a new instance of the class. + /// + /// The album. + public Track(MusicAlbum album) + { + if (album == null) + { + throw new ArgumentNullException(nameof(album)); + } + + album.Tracks.Add(this); + + Releases = new HashSet(); + TrackMetadata = new HashSet(); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected Track() + { + } + + /// + /// Gets or sets the track number. + /// + public int? TrackNumber { get; set; } + + /// + public virtual ICollection Releases { get; protected set; } + + /// + /// Gets or sets a collection containing the track metadata. + /// + public virtual ICollection TrackMetadata { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs b/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs new file mode 100644 index 000000000..048068a1a --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs @@ -0,0 +1,36 @@ +using System; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// + /// An entity holding metadata for a track. + /// + public class TrackMetadata : Metadata + { + /// + /// Initializes a new instance of the class. + /// + /// The title or name of the object. + /// ISO-639-3 3-character language codes. + /// The track. + public TrackMetadata(string title, string language, Track track) : base(title, language) + { + if (track == null) + { + throw new ArgumentNullException(nameof(track)); + } + + track.TrackMetadata.Add(this); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected TrackMetadata() + { + } + } +} diff --git a/Jellyfin.Data/Entities/Library.cs b/Jellyfin.Data/Entities/Library.cs deleted file mode 100644 index d935e43b1..000000000 --- a/Jellyfin.Data/Entities/Library.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Library - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected Library() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Library CreateLibraryUnsafe() - { - return new Library(); - } - - /// - /// Public constructor with required data. - /// - /// - public Library(string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - this.Name = name; - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// - public static Library Create(string name) - { - return new Library(name); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Backing field for Name. - /// - protected string _Name; - /// - /// When provided in a partial class, allows value of Name to be changed before setting. - /// - partial void SetName(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Name to be changed before returning. - /// - partial void GetName(ref string result); - - /// - /// Required, Max length = 1024 - /// - [Required] - [MaxLength(1024)] - [StringLength(1024)] - public string Name - { - get - { - string value = _Name; - GetName(ref value); - return _Name = value; - } - - set - { - string oldValue = _Name; - SetName(oldValue, ref value); - if (oldValue != value) - { - _Name = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/LibraryItem.cs b/Jellyfin.Data/Entities/LibraryItem.cs deleted file mode 100644 index f41753560..000000000 --- a/Jellyfin.Data/Entities/LibraryItem.cs +++ /dev/null @@ -1,172 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public abstract partial class LibraryItem - { - partial void Init(); - - /// - /// Default constructor. Protected due to being abstract. - /// - protected LibraryItem() - { - Init(); - } - - /// - /// Public constructor with required data. - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - protected LibraryItem(Guid urlid, DateTime dateadded) - { - this.UrlId = urlid; - - - Init(); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Backing field for UrlId. - /// - internal Guid _UrlId; - /// - /// When provided in a partial class, allows value of UrlId to be changed before setting. - /// - partial void SetUrlId(Guid oldValue, ref Guid newValue); - /// - /// When provided in a partial class, allows value of UrlId to be changed before returning. - /// - partial void GetUrlId(ref Guid result); - - /// - /// Indexed, Required - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - /// - [Required] - public Guid UrlId - { - get - { - Guid value = _UrlId; - GetUrlId(ref value); - return _UrlId = value; - } - - set - { - Guid oldValue = _UrlId; - SetUrlId(oldValue, ref value); - if (oldValue != value) - { - _UrlId = value; - } - } - } - - /// - /// Backing field for DateAdded. - /// - protected DateTime _DateAdded; - /// - /// When provided in a partial class, allows value of DateAdded to be changed before setting. - /// - partial void SetDateAdded(DateTime oldValue, ref DateTime newValue); - /// - /// When provided in a partial class, allows value of DateAdded to be changed before returning. - /// - partial void GetDateAdded(ref DateTime result); - - /// - /// Required. - /// - [Required] - public DateTime DateAdded - { - get - { - DateTime value = _DateAdded; - GetDateAdded(ref value); - return _DateAdded = value; - } - - internal set - { - DateTime oldValue = _DateAdded; - SetDateAdded(oldValue, ref value); - if (oldValue != value) - { - _DateAdded = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - /// - /// Required. - /// - [ForeignKey("LibraryRoot_Id")] - public virtual LibraryRoot LibraryRoot { get; set; } - } -} - diff --git a/Jellyfin.Data/Entities/LibraryRoot.cs b/Jellyfin.Data/Entities/LibraryRoot.cs deleted file mode 100644 index 9695ed638..000000000 --- a/Jellyfin.Data/Entities/LibraryRoot.cs +++ /dev/null @@ -1,197 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class LibraryRoot - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected LibraryRoot() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static LibraryRoot CreateLibraryRootUnsafe() - { - return new LibraryRoot(); - } - - /// - /// Public constructor with required data. - /// - /// Absolute Path. - public LibraryRoot(string path) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - this.Path = path; - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// Absolute Path. - public static LibraryRoot Create(string path) - { - return new LibraryRoot(path); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Backing field for Path. - /// - protected string _Path; - /// - /// When provided in a partial class, allows value of Path to be changed before setting. - /// - partial void SetPath(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Path to be changed before returning. - /// - partial void GetPath(ref string result); - - /// - /// Required, Max length = 65535 - /// Absolute Path. - /// - [Required] - [MaxLength(65535)] - [StringLength(65535)] - public string Path - { - get - { - string value = _Path; - GetPath(ref value); - return _Path = value; - } - - set - { - string oldValue = _Path; - SetPath(oldValue, ref value); - if (oldValue != value) - { - _Path = value; - } - } - } - - /// - /// Backing field for NetworkPath. - /// - protected string _NetworkPath; - /// - /// When provided in a partial class, allows value of NetworkPath to be changed before setting. - /// - partial void SetNetworkPath(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of NetworkPath to be changed before returning. - /// - partial void GetNetworkPath(ref string result); - - /// - /// Max length = 65535 - /// Absolute network path, for example for transcoding sattelites. - /// - [MaxLength(65535)] - [StringLength(65535)] - public string NetworkPath - { - get - { - string value = _NetworkPath; - GetNetworkPath(ref value); - return _NetworkPath = value; - } - - set - { - string oldValue = _NetworkPath; - SetNetworkPath(oldValue, ref value); - if (oldValue != value) - { - _NetworkPath = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - /// - /// Required. - /// - [ForeignKey("Library_Id")] - public virtual Library Library { get; set; } - } -} - diff --git a/Jellyfin.Data/Entities/MediaFile.cs b/Jellyfin.Data/Entities/MediaFile.cs deleted file mode 100644 index 7382cda95..000000000 --- a/Jellyfin.Data/Entities/MediaFile.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class MediaFile - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected MediaFile() - { - MediaFileStreams = new HashSet(); - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static MediaFile CreateMediaFileUnsafe() - { - return new MediaFile(); - } - - /// - /// Public constructor with required data. - /// - /// Relative to the LibraryRoot. - /// - /// - public MediaFile(string path, Enums.MediaFileKind kind, Release _release0) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - this.Path = path; - - this.Kind = kind; - - if (_release0 == null) - { - throw new ArgumentNullException(nameof(_release0)); - } - - _release0.MediaFiles.Add(this); - - this.MediaFileStreams = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// Relative to the LibraryRoot. - /// - /// - public static MediaFile Create(string path, Enums.MediaFileKind kind, Release _release0) - { - return new MediaFile(path, kind, _release0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Backing field for Path. - /// - protected string _Path; - /// - /// When provided in a partial class, allows value of Path to be changed before setting. - /// - partial void SetPath(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Path to be changed before returning. - /// - partial void GetPath(ref string result); - - /// - /// Required, Max length = 65535 - /// Relative to the LibraryRoot. - /// - [Required] - [MaxLength(65535)] - [StringLength(65535)] - public string Path - { - get - { - string value = _Path; - GetPath(ref value); - return _Path = value; - } - - set - { - string oldValue = _Path; - SetPath(oldValue, ref value); - if (oldValue != value) - { - _Path = value; - } - } - } - - /// - /// Backing field for Kind. - /// - protected Enums.MediaFileKind _Kind; - /// - /// When provided in a partial class, allows value of Kind to be changed before setting. - /// - partial void SetKind(Enums.MediaFileKind oldValue, ref Enums.MediaFileKind newValue); - /// - /// When provided in a partial class, allows value of Kind to be changed before returning. - /// - partial void GetKind(ref Enums.MediaFileKind result); - - /// - /// Required. - /// - [Required] - public Enums.MediaFileKind Kind - { - get - { - Enums.MediaFileKind value = _Kind; - GetKind(ref value); - return _Kind = value; - } - - set - { - Enums.MediaFileKind oldValue = _Kind; - SetKind(oldValue, ref value); - if (oldValue != value) - { - _Kind = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - [ForeignKey("MediaFileStream_MediaFileStreams_Id")] - public virtual ICollection MediaFileStreams { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/MediaFileStream.cs b/Jellyfin.Data/Entities/MediaFileStream.cs deleted file mode 100644 index 977fd54e1..000000000 --- a/Jellyfin.Data/Entities/MediaFileStream.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class MediaFileStream - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected MediaFileStream() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static MediaFileStream CreateMediaFileStreamUnsafe() - { - return new MediaFileStream(); - } - - /// - /// Public constructor with required data. - /// - /// - /// - public MediaFileStream(int streamnumber, MediaFile _mediafile0) - { - this.StreamNumber = streamnumber; - - if (_mediafile0 == null) - { - throw new ArgumentNullException(nameof(_mediafile0)); - } - - _mediafile0.MediaFileStreams.Add(this); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// - /// - public static MediaFileStream Create(int streamnumber, MediaFile _mediafile0) - { - return new MediaFileStream(streamnumber, _mediafile0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Backing field for StreamNumber. - /// - protected int _StreamNumber; - /// - /// When provided in a partial class, allows value of StreamNumber to be changed before setting. - /// - partial void SetStreamNumber(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of StreamNumber to be changed before returning. - /// - partial void GetStreamNumber(ref int result); - - /// - /// Required. - /// - [Required] - public int StreamNumber - { - get - { - int value = _StreamNumber; - GetStreamNumber(ref value); - return _StreamNumber = value; - } - - set - { - int oldValue = _StreamNumber; - SetStreamNumber(oldValue, ref value); - if (oldValue != value) - { - _StreamNumber = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/Metadata.cs b/Jellyfin.Data/Entities/Metadata.cs deleted file mode 100644 index a4ac6dc54..000000000 --- a/Jellyfin.Data/Entities/Metadata.cs +++ /dev/null @@ -1,395 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public abstract partial class Metadata - { - partial void Init(); - - /// - /// Default constructor. Protected due to being abstract. - /// - protected Metadata() - { - PersonRoles = new HashSet(); - Genres = new HashSet(); - Artwork = new HashSet(); - Ratings = new HashSet(); - Sources = new HashSet(); - - Init(); - } - - /// - /// Public constructor with required data. - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - protected Metadata(string title, string language, DateTime dateadded, DateTime datemodified) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - this.PersonRoles = new HashSet(); - this.Genres = new HashSet(); - this.Artwork = new HashSet(); - this.Ratings = new HashSet(); - this.Sources = new HashSet(); - - Init(); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Backing field for Title. - /// - protected string _Title; - /// - /// When provided in a partial class, allows value of Title to be changed before setting. - /// - partial void SetTitle(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Title to be changed before returning. - /// - partial void GetTitle(ref string result); - - /// - /// Required, Max length = 1024 - /// The title or name of the object. - /// - [Required] - [MaxLength(1024)] - [StringLength(1024)] - public string Title - { - get - { - string value = _Title; - GetTitle(ref value); - return _Title = value; - } - - set - { - string oldValue = _Title; - SetTitle(oldValue, ref value); - if (oldValue != value) - { - _Title = value; - } - } - } - - /// - /// Backing field for OriginalTitle. - /// - protected string _OriginalTitle; - /// - /// When provided in a partial class, allows value of OriginalTitle to be changed before setting. - /// - partial void SetOriginalTitle(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of OriginalTitle to be changed before returning. - /// - partial void GetOriginalTitle(ref string result); - - /// - /// Max length = 1024 - /// - [MaxLength(1024)] - [StringLength(1024)] - public string OriginalTitle - { - get - { - string value = _OriginalTitle; - GetOriginalTitle(ref value); - return _OriginalTitle = value; - } - - set - { - string oldValue = _OriginalTitle; - SetOriginalTitle(oldValue, ref value); - if (oldValue != value) - { - _OriginalTitle = value; - } - } - } - - /// - /// Backing field for SortTitle. - /// - protected string _SortTitle; - /// - /// When provided in a partial class, allows value of SortTitle to be changed before setting. - /// - partial void SetSortTitle(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of SortTitle to be changed before returning. - /// - partial void GetSortTitle(ref string result); - - /// - /// Max length = 1024 - /// - [MaxLength(1024)] - [StringLength(1024)] - public string SortTitle - { - get - { - string value = _SortTitle; - GetSortTitle(ref value); - return _SortTitle = value; - } - - set - { - string oldValue = _SortTitle; - SetSortTitle(oldValue, ref value); - if (oldValue != value) - { - _SortTitle = value; - } - } - } - - /// - /// Backing field for Language. - /// - protected string _Language; - /// - /// When provided in a partial class, allows value of Language to be changed before setting. - /// - partial void SetLanguage(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Language to be changed before returning. - /// - partial void GetLanguage(ref string result); - - /// - /// Required, Min length = 3, Max length = 3 - /// ISO-639-3 3-character language codes. - /// - [Required] - [MinLength(3)] - [MaxLength(3)] - [StringLength(3)] - public string Language - { - get - { - string value = _Language; - GetLanguage(ref value); - return _Language = value; - } - - set - { - string oldValue = _Language; - SetLanguage(oldValue, ref value); - if (oldValue != value) - { - _Language = value; - } - } - } - - /// - /// Backing field for ReleaseDate. - /// - protected DateTimeOffset? _ReleaseDate; - /// - /// When provided in a partial class, allows value of ReleaseDate to be changed before setting. - /// - partial void SetReleaseDate(DateTimeOffset? oldValue, ref DateTimeOffset? newValue); - /// - /// When provided in a partial class, allows value of ReleaseDate to be changed before returning. - /// - partial void GetReleaseDate(ref DateTimeOffset? result); - - public DateTimeOffset? ReleaseDate - { - get - { - DateTimeOffset? value = _ReleaseDate; - GetReleaseDate(ref value); - return _ReleaseDate = value; - } - - set - { - DateTimeOffset? oldValue = _ReleaseDate; - SetReleaseDate(oldValue, ref value); - if (oldValue != value) - { - _ReleaseDate = value; - } - } - } - - /// - /// Backing field for DateAdded. - /// - protected DateTime _DateAdded; - /// - /// When provided in a partial class, allows value of DateAdded to be changed before setting. - /// - partial void SetDateAdded(DateTime oldValue, ref DateTime newValue); - /// - /// When provided in a partial class, allows value of DateAdded to be changed before returning. - /// - partial void GetDateAdded(ref DateTime result); - - /// - /// Required. - /// - [Required] - public DateTime DateAdded - { - get - { - DateTime value = _DateAdded; - GetDateAdded(ref value); - return _DateAdded = value; - } - - internal set - { - DateTime oldValue = _DateAdded; - SetDateAdded(oldValue, ref value); - if (oldValue != value) - { - _DateAdded = value; - } - } - } - - /// - /// Backing field for DateModified. - /// - protected DateTime _DateModified; - /// - /// When provided in a partial class, allows value of DateModified to be changed before setting. - /// - partial void SetDateModified(DateTime oldValue, ref DateTime newValue); - /// - /// When provided in a partial class, allows value of DateModified to be changed before returning. - /// - partial void GetDateModified(ref DateTime result); - - /// - /// Required. - /// - [Required] - public DateTime DateModified - { - get - { - DateTime value = _DateModified; - GetDateModified(ref value); - return _DateModified = value; - } - - internal set - { - DateTime oldValue = _DateModified; - SetDateModified(oldValue, ref value); - if (oldValue != value) - { - _DateModified = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - [ForeignKey("PersonRole_PersonRoles_Id")] - public virtual ICollection PersonRoles { get; protected set; } - - [ForeignKey("PersonRole_PersonRoles_Id")] - public virtual ICollection Genres { get; protected set; } - - [ForeignKey("PersonRole_PersonRoles_Id")] - public virtual ICollection Artwork { get; protected set; } - - [ForeignKey("PersonRole_PersonRoles_Id")] - public virtual ICollection Ratings { get; protected set; } - - [ForeignKey("PersonRole_PersonRoles_Id")] - public virtual ICollection Sources { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/MetadataProvider.cs b/Jellyfin.Data/Entities/MetadataProvider.cs deleted file mode 100644 index e93ea97d6..000000000 --- a/Jellyfin.Data/Entities/MetadataProvider.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class MetadataProvider - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected MetadataProvider() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static MetadataProvider CreateMetadataProviderUnsafe() - { - return new MetadataProvider(); - } - - /// - /// Public constructor with required data. - /// - /// - public MetadataProvider(string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - this.Name = name; - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// - public static MetadataProvider Create(string name) - { - return new MetadataProvider(name); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Backing field for Name. - /// - protected string _Name; - /// - /// When provided in a partial class, allows value of Name to be changed before setting. - /// - partial void SetName(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Name to be changed before returning. - /// - partial void GetName(ref string result); - - /// - /// Required, Max length = 1024 - /// - [Required] - [MaxLength(1024)] - [StringLength(1024)] - public string Name - { - get - { - string value = _Name; - GetName(ref value); - return _Name = value; - } - - set - { - string oldValue = _Name; - SetName(oldValue, ref value); - if (oldValue != value) - { - _Name = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/MetadataProviderId.cs b/Jellyfin.Data/Entities/MetadataProviderId.cs deleted file mode 100644 index 68f139436..000000000 --- a/Jellyfin.Data/Entities/MetadataProviderId.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class MetadataProviderId - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected MetadataProviderId() - { - // NOTE: This class has one-to-one associations with MetadataProviderId. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static MetadataProviderId CreateMetadataProviderIdUnsafe() - { - return new MetadataProviderId(); - } - - /// - /// Public constructor with required data. - /// - /// - /// - /// - /// - /// - public MetadataProviderId(string providerid, Metadata _metadata0, Person _person1, PersonRole _personrole2, RatingSource _ratingsource3) - { - // NOTE: This class has one-to-one associations with MetadataProviderId. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - if (string.IsNullOrEmpty(providerid)) - { - throw new ArgumentNullException(nameof(providerid)); - } - - this.ProviderId = providerid; - - if (_metadata0 == null) - { - throw new ArgumentNullException(nameof(_metadata0)); - } - - _metadata0.Sources.Add(this); - - if (_person1 == null) - { - throw new ArgumentNullException(nameof(_person1)); - } - - _person1.Sources.Add(this); - - if (_personrole2 == null) - { - throw new ArgumentNullException(nameof(_personrole2)); - } - - _personrole2.Sources.Add(this); - - if (_ratingsource3 == null) - { - throw new ArgumentNullException(nameof(_ratingsource3)); - } - - _ratingsource3.Source = this; - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// - /// - /// - /// - /// - public static MetadataProviderId Create(string providerid, Metadata _metadata0, Person _person1, PersonRole _personrole2, RatingSource _ratingsource3) - { - return new MetadataProviderId(providerid, _metadata0, _person1, _personrole2, _ratingsource3); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Backing field for ProviderId. - /// - protected string _ProviderId; - /// - /// When provided in a partial class, allows value of ProviderId to be changed before setting. - /// - partial void SetProviderId(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of ProviderId to be changed before returning. - /// - partial void GetProviderId(ref string result); - - /// - /// Required, Max length = 255 - /// - [Required] - [MaxLength(255)] - [StringLength(255)] - public string ProviderId - { - get - { - string value = _ProviderId; - GetProviderId(ref value); - return _ProviderId = value; - } - - set - { - string oldValue = _ProviderId; - SetProviderId(oldValue, ref value); - if (oldValue != value) - { - _ProviderId = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - /// - /// Required. - /// - [ForeignKey("MetadataProvider_Id")] - public virtual MetadataProvider MetadataProvider { get; set; } - } -} - diff --git a/Jellyfin.Data/Entities/Movie.cs b/Jellyfin.Data/Entities/Movie.cs deleted file mode 100644 index 64326ca3a..000000000 --- a/Jellyfin.Data/Entities/Movie.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Movie : LibraryItem - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected Movie() - { - Releases = new HashSet(); - MovieMetadata = new HashSet(); - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Movie CreateMovieUnsafe() - { - return new Movie(); - } - - /// - /// Public constructor with required data. - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - public Movie(Guid urlid, DateTime dateadded) - { - this.UrlId = urlid; - - this.Releases = new HashSet(); - this.MovieMetadata = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - public static Movie Create(Guid urlid, DateTime dateadded) - { - return new Movie(urlid, dateadded); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - [ForeignKey("Release_Releases_Id")] - public virtual ICollection Releases { get; protected set; } - - [ForeignKey("MovieMetadata_MovieMetadata_Id")] - public virtual ICollection MovieMetadata { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/MovieMetadata.cs b/Jellyfin.Data/Entities/MovieMetadata.cs deleted file mode 100644 index cbcb78e37..000000000 --- a/Jellyfin.Data/Entities/MovieMetadata.cs +++ /dev/null @@ -1,238 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class MovieMetadata : Metadata - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected MovieMetadata() - { - Studios = new HashSet(); - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static MovieMetadata CreateMovieMetadataUnsafe() - { - return new MovieMetadata(); - } - - /// - /// Public constructor with required data. - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public MovieMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Movie _movie0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_movie0 == null) - { - throw new ArgumentNullException(nameof(_movie0)); - } - - _movie0.MovieMetadata.Add(this); - - this.Studios = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public static MovieMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Movie _movie0) - { - return new MovieMetadata(title, language, dateadded, datemodified, _movie0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Outline. - /// - protected string _Outline; - /// - /// When provided in a partial class, allows value of Outline to be changed before setting. - /// - partial void SetOutline(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Outline to be changed before returning. - /// - partial void GetOutline(ref string result); - - /// - /// Max length = 1024 - /// - [MaxLength(1024)] - [StringLength(1024)] - public string Outline - { - get - { - string value = _Outline; - GetOutline(ref value); - return _Outline = value; - } - - set - { - string oldValue = _Outline; - SetOutline(oldValue, ref value); - if (oldValue != value) - { - _Outline = value; - } - } - } - - /// - /// Backing field for Plot. - /// - protected string _Plot; - /// - /// When provided in a partial class, allows value of Plot to be changed before setting. - /// - partial void SetPlot(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Plot to be changed before returning. - /// - partial void GetPlot(ref string result); - - /// - /// Max length = 65535 - /// - [MaxLength(65535)] - [StringLength(65535)] - public string Plot - { - get - { - string value = _Plot; - GetPlot(ref value); - return _Plot = value; - } - - set - { - string oldValue = _Plot; - SetPlot(oldValue, ref value); - if (oldValue != value) - { - _Plot = value; - } - } - } - - /// - /// Backing field for Tagline. - /// - protected string _Tagline; - /// - /// When provided in a partial class, allows value of Tagline to be changed before setting. - /// - partial void SetTagline(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Tagline to be changed before returning. - /// - partial void GetTagline(ref string result); - - /// - /// Max length = 1024 - /// - [MaxLength(1024)] - [StringLength(1024)] - public string Tagline - { - get - { - string value = _Tagline; - GetTagline(ref value); - return _Tagline = value; - } - - set - { - string oldValue = _Tagline; - SetTagline(oldValue, ref value); - if (oldValue != value) - { - _Tagline = value; - } - } - } - - /// - /// Backing field for Country. - /// - protected string _Country; - /// - /// When provided in a partial class, allows value of Country to be changed before setting. - /// - partial void SetCountry(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Country to be changed before returning. - /// - partial void GetCountry(ref string result); - - /// - /// Max length = 2 - /// - [MaxLength(2)] - [StringLength(2)] - public string Country - { - get - { - string value = _Country; - GetCountry(ref value); - return _Country = value; - } - - set - { - string oldValue = _Country; - SetCountry(oldValue, ref value); - if (oldValue != value) - { - _Country = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("Company_Studios_Id")] - public virtual ICollection Studios { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/MusicAlbum.cs b/Jellyfin.Data/Entities/MusicAlbum.cs deleted file mode 100644 index 9afea1fb6..000000000 --- a/Jellyfin.Data/Entities/MusicAlbum.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class MusicAlbum : LibraryItem - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected MusicAlbum() - { - MusicAlbumMetadata = new HashSet(); - Tracks = new HashSet(); - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static MusicAlbum CreateMusicAlbumUnsafe() - { - return new MusicAlbum(); - } - - /// - /// Public constructor with required data. - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - public MusicAlbum(Guid urlid, DateTime dateadded) - { - this.UrlId = urlid; - - this.MusicAlbumMetadata = new HashSet(); - this.Tracks = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - public static MusicAlbum Create(Guid urlid, DateTime dateadded) - { - return new MusicAlbum(urlid, dateadded); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("MusicAlbumMetadata_MusicAlbumMetadata_Id")] - public virtual ICollection MusicAlbumMetadata { get; protected set; } - - [ForeignKey("Track_Tracks_Id")] - public virtual ICollection Tracks { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/MusicAlbumMetadata.cs b/Jellyfin.Data/Entities/MusicAlbumMetadata.cs deleted file mode 100644 index bfcbebbe8..000000000 --- a/Jellyfin.Data/Entities/MusicAlbumMetadata.cs +++ /dev/null @@ -1,201 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class MusicAlbumMetadata : Metadata - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected MusicAlbumMetadata() - { - Labels = new HashSet(); - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static MusicAlbumMetadata CreateMusicAlbumMetadataUnsafe() - { - return new MusicAlbumMetadata(); - } - - /// - /// Public constructor with required data. - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public MusicAlbumMetadata(string title, string language, DateTime dateadded, DateTime datemodified, MusicAlbum _musicalbum0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_musicalbum0 == null) - { - throw new ArgumentNullException(nameof(_musicalbum0)); - } - - _musicalbum0.MusicAlbumMetadata.Add(this); - - this.Labels = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public static MusicAlbumMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, MusicAlbum _musicalbum0) - { - return new MusicAlbumMetadata(title, language, dateadded, datemodified, _musicalbum0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Barcode. - /// - protected string _Barcode; - /// - /// When provided in a partial class, allows value of Barcode to be changed before setting. - /// - partial void SetBarcode(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Barcode to be changed before returning. - /// - partial void GetBarcode(ref string result); - - /// - /// Max length = 255 - /// - [MaxLength(255)] - [StringLength(255)] - public string Barcode - { - get - { - string value = _Barcode; - GetBarcode(ref value); - return _Barcode = value; - } - - set - { - string oldValue = _Barcode; - SetBarcode(oldValue, ref value); - if (oldValue != value) - { - _Barcode = value; - } - } - } - - /// - /// Backing field for LabelNumber. - /// - protected string _LabelNumber; - /// - /// When provided in a partial class, allows value of LabelNumber to be changed before setting. - /// - partial void SetLabelNumber(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of LabelNumber to be changed before returning. - /// - partial void GetLabelNumber(ref string result); - - /// - /// Max length = 255 - /// - [MaxLength(255)] - [StringLength(255)] - public string LabelNumber - { - get - { - string value = _LabelNumber; - GetLabelNumber(ref value); - return _LabelNumber = value; - } - - set - { - string oldValue = _LabelNumber; - SetLabelNumber(oldValue, ref value); - if (oldValue != value) - { - _LabelNumber = value; - } - } - } - - /// - /// Backing field for Country. - /// - protected string _Country; - /// - /// When provided in a partial class, allows value of Country to be changed before setting. - /// - partial void SetCountry(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Country to be changed before returning. - /// - partial void GetCountry(ref string result); - - /// - /// Max length = 2 - /// - [MaxLength(2)] - [StringLength(2)] - public string Country - { - get - { - string value = _Country; - GetCountry(ref value); - return _Country = value; - } - - set - { - string oldValue = _Country; - SetCountry(oldValue, ref value); - if (oldValue != value) - { - _Country = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - [ForeignKey("Company_Labels_Id")] - public virtual ICollection Labels { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/Permission.cs b/Jellyfin.Data/Entities/Permission.cs index b675e911d..c0f67f836 100644 --- a/Jellyfin.Data/Entities/Permission.cs +++ b/Jellyfin.Data/Entities/Permission.cs @@ -1,13 +1,16 @@ +#pragma warning disable CS1591 + using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Enums; +using Jellyfin.Data.Interfaces; namespace Jellyfin.Data.Entities { /// /// An entity representing whether the associated user has a specific permission. /// - public partial class Permission : ISavingChanges + public partial class Permission : IHasConcurrencyToken { /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Data/Entities/Person.cs b/Jellyfin.Data/Entities/Person.cs deleted file mode 100644 index b6d91ea86..000000000 --- a/Jellyfin.Data/Entities/Person.cs +++ /dev/null @@ -1,311 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Person - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected Person() - { - Sources = new HashSet(); - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Person CreatePersonUnsafe() - { - return new Person(); - } - - /// - /// Public constructor with required data. - /// - /// - /// - public Person(Guid urlid, string name, DateTime dateadded, DateTime datemodified) - { - this.UrlId = urlid; - - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - this.Name = name; - - this.Sources = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// - /// - public static Person Create(Guid urlid, string name, DateTime dateadded, DateTime datemodified) - { - return new Person(urlid, name, dateadded, datemodified); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Backing field for UrlId. - /// - protected Guid _UrlId; - /// - /// When provided in a partial class, allows value of UrlId to be changed before setting. - /// - partial void SetUrlId(Guid oldValue, ref Guid newValue); - /// - /// When provided in a partial class, allows value of UrlId to be changed before returning. - /// - partial void GetUrlId(ref Guid result); - - /// - /// Required. - /// - [Required] - public Guid UrlId - { - get - { - Guid value = _UrlId; - GetUrlId(ref value); - return _UrlId = value; - } - - set - { - Guid oldValue = _UrlId; - SetUrlId(oldValue, ref value); - if (oldValue != value) - { - _UrlId = value; - } - } - } - - /// - /// Backing field for Name. - /// - protected string _Name; - /// - /// When provided in a partial class, allows value of Name to be changed before setting. - /// - partial void SetName(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Name to be changed before returning. - /// - partial void GetName(ref string result); - - /// - /// Required, Max length = 1024 - /// - [Required] - [MaxLength(1024)] - [StringLength(1024)] - public string Name - { - get - { - string value = _Name; - GetName(ref value); - return _Name = value; - } - - set - { - string oldValue = _Name; - SetName(oldValue, ref value); - if (oldValue != value) - { - _Name = value; - } - } - } - - /// - /// Backing field for SourceId. - /// - protected string _SourceId; - /// - /// When provided in a partial class, allows value of SourceId to be changed before setting. - /// - partial void SetSourceId(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of SourceId to be changed before returning. - /// - partial void GetSourceId(ref string result); - - /// - /// Max length = 255 - /// - [MaxLength(255)] - [StringLength(255)] - public string SourceId - { - get - { - string value = _SourceId; - GetSourceId(ref value); - return _SourceId = value; - } - - set - { - string oldValue = _SourceId; - SetSourceId(oldValue, ref value); - if (oldValue != value) - { - _SourceId = value; - } - } - } - - /// - /// Backing field for DateAdded. - /// - protected DateTime _DateAdded; - /// - /// When provided in a partial class, allows value of DateAdded to be changed before setting. - /// - partial void SetDateAdded(DateTime oldValue, ref DateTime newValue); - /// - /// When provided in a partial class, allows value of DateAdded to be changed before returning. - /// - partial void GetDateAdded(ref DateTime result); - - /// - /// Required. - /// - [Required] - public DateTime DateAdded - { - get - { - DateTime value = _DateAdded; - GetDateAdded(ref value); - return _DateAdded = value; - } - - internal set - { - DateTime oldValue = _DateAdded; - SetDateAdded(oldValue, ref value); - if (oldValue != value) - { - _DateAdded = value; - } - } - } - - /// - /// Backing field for DateModified. - /// - protected DateTime _DateModified; - /// - /// When provided in a partial class, allows value of DateModified to be changed before setting. - /// - partial void SetDateModified(DateTime oldValue, ref DateTime newValue); - /// - /// When provided in a partial class, allows value of DateModified to be changed before returning. - /// - partial void GetDateModified(ref DateTime result); - - /// - /// Required. - /// - [Required] - public DateTime DateModified - { - get - { - DateTime value = _DateModified; - GetDateModified(ref value); - return _DateModified = value; - } - - internal set - { - DateTime oldValue = _DateModified; - SetDateModified(oldValue, ref value); - if (oldValue != value) - { - _DateModified = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("MetadataProviderId_Sources_Id")] - public virtual ICollection Sources { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/PersonRole.cs b/Jellyfin.Data/Entities/PersonRole.cs deleted file mode 100644 index 2dd5f116f..000000000 --- a/Jellyfin.Data/Entities/PersonRole.cs +++ /dev/null @@ -1,215 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class PersonRole - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected PersonRole() - { - // NOTE: This class has one-to-one associations with PersonRole. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - Sources = new HashSet(); - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static PersonRole CreatePersonRoleUnsafe() - { - return new PersonRole(); - } - - /// - /// Public constructor with required data. - /// - /// - /// - public PersonRole(Enums.PersonRoleType type, Metadata _metadata0) - { - // NOTE: This class has one-to-one associations with PersonRole. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - this.Type = type; - - if (_metadata0 == null) - { - throw new ArgumentNullException(nameof(_metadata0)); - } - - _metadata0.PersonRoles.Add(this); - - this.Sources = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// - /// - public static PersonRole Create(Enums.PersonRoleType type, Metadata _metadata0) - { - return new PersonRole(type, _metadata0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Backing field for Role. - /// - protected string _Role; - /// - /// When provided in a partial class, allows value of Role to be changed before setting. - /// - partial void SetRole(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Role to be changed before returning. - /// - partial void GetRole(ref string result); - - /// - /// Max length = 1024 - /// - [MaxLength(1024)] - [StringLength(1024)] - public string Role - { - get - { - string value = _Role; - GetRole(ref value); - return _Role = value; - } - - set - { - string oldValue = _Role; - SetRole(oldValue, ref value); - if (oldValue != value) - { - _Role = value; - } - } - } - - /// - /// Backing field for Type. - /// - protected Enums.PersonRoleType _Type; - /// - /// When provided in a partial class, allows value of Type to be changed before setting. - /// - partial void SetType(Enums.PersonRoleType oldValue, ref Enums.PersonRoleType newValue); - /// - /// When provided in a partial class, allows value of Type to be changed before returning. - /// - partial void GetType(ref Enums.PersonRoleType result); - - /// - /// Required. - /// - [Required] - public Enums.PersonRoleType Type - { - get - { - Enums.PersonRoleType value = _Type; - GetType(ref value); - return _Type = value; - } - - set - { - Enums.PersonRoleType oldValue = _Type; - SetType(oldValue, ref value); - if (oldValue != value) - { - _Type = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - /// - /// Required. - /// - [ForeignKey("Person_Id")] - - public virtual Person Person { get; set; } - - [ForeignKey("Artwork_Artwork_Id")] - public virtual Artwork Artwork { get; set; } - - [ForeignKey("MetadataProviderId_Sources_Id")] - public virtual ICollection Sources { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/Photo.cs b/Jellyfin.Data/Entities/Photo.cs deleted file mode 100644 index 9da55fe43..000000000 --- a/Jellyfin.Data/Entities/Photo.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Photo : LibraryItem - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected Photo() - { - PhotoMetadata = new HashSet(); - Releases = new HashSet(); - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Photo CreatePhotoUnsafe() - { - return new Photo(); - } - - /// - /// Public constructor with required data. - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - public Photo(Guid urlid, DateTime dateadded) - { - this.UrlId = urlid; - - this.PhotoMetadata = new HashSet(); - this.Releases = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - public static Photo Create(Guid urlid, DateTime dateadded) - { - return new Photo(urlid, dateadded); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("PhotoMetadata_PhotoMetadata_Id")] - public virtual ICollection PhotoMetadata { get; protected set; } - - [ForeignKey("Release_Releases_Id")] - public virtual ICollection Releases { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/PhotoMetadata.cs b/Jellyfin.Data/Entities/PhotoMetadata.cs deleted file mode 100644 index b5aec7229..000000000 --- a/Jellyfin.Data/Entities/PhotoMetadata.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class PhotoMetadata : Metadata - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected PhotoMetadata() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static PhotoMetadata CreatePhotoMetadataUnsafe() - { - return new PhotoMetadata(); - } - - /// - /// Public constructor with required data. - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public PhotoMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Photo _photo0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_photo0 == null) - { - throw new ArgumentNullException(nameof(_photo0)); - } - - _photo0.PhotoMetadata.Add(this); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public static PhotoMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Photo _photo0) - { - return new PhotoMetadata(title, language, dateadded, datemodified, _photo0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/Preference.cs b/Jellyfin.Data/Entities/Preference.cs index 0ca9d7eff..1797f0a40 100644 --- a/Jellyfin.Data/Entities/Preference.cs +++ b/Jellyfin.Data/Entities/Preference.cs @@ -2,13 +2,14 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Enums; +using Jellyfin.Data.Interfaces; namespace Jellyfin.Data.Entities { /// /// An entity representing a preference attached to a user or group. /// - public class Preference : ISavingChanges + public class Preference : IHasConcurrencyToken { /// /// Initializes a new instance of the class. diff --git a/Jellyfin.Data/Entities/ProviderMapping.cs b/Jellyfin.Data/Entities/ProviderMapping.cs index c53e3bf40..44ebfba76 100644 --- a/Jellyfin.Data/Entities/ProviderMapping.cs +++ b/Jellyfin.Data/Entities/ProviderMapping.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; diff --git a/Jellyfin.Data/Entities/Rating.cs b/Jellyfin.Data/Entities/Rating.cs deleted file mode 100644 index 49a0d502d..000000000 --- a/Jellyfin.Data/Entities/Rating.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Rating - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected Rating() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Rating CreateRatingUnsafe() - { - return new Rating(); - } - - /// - /// Public constructor with required data. - /// - /// - /// - public Rating(double value, Metadata _metadata0) - { - this.Value = value; - - if (_metadata0 == null) - { - throw new ArgumentNullException(nameof(_metadata0)); - } - - _metadata0.Ratings.Add(this); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// - /// - public static Rating Create(double value, Metadata _metadata0) - { - return new Rating(value, _metadata0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Backing field for Value. - /// - protected double _Value; - /// - /// When provided in a partial class, allows value of Value to be changed before setting. - /// - partial void SetValue(double oldValue, ref double newValue); - /// - /// When provided in a partial class, allows value of Value to be changed before returning. - /// - partial void GetValue(ref double result); - - /// - /// Required. - /// - [Required] - public double Value - { - get - { - double value = _Value; - GetValue(ref value); - return _Value = value; - } - - set - { - double oldValue = _Value; - SetValue(oldValue, ref value); - if (oldValue != value) - { - _Value = value; - } - } - } - - /// - /// Backing field for Votes. - /// - protected int? _Votes; - /// - /// When provided in a partial class, allows value of Votes to be changed before setting. - /// - partial void SetVotes(int? oldValue, ref int? newValue); - /// - /// When provided in a partial class, allows value of Votes to be changed before returning. - /// - partial void GetVotes(ref int? result); - - public int? Votes - { - get - { - int? value = _Votes; - GetVotes(ref value); - return _Votes = value; - } - - set - { - int? oldValue = _Votes; - SetVotes(oldValue, ref value); - if (oldValue != value) - { - _Votes = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - /// - /// If this is NULL it's the internal user rating. - /// - [ForeignKey("RatingSource_RatingType_Id")] - public virtual RatingSource RatingType { get; set; } - } -} - diff --git a/Jellyfin.Data/Entities/RatingSource.cs b/Jellyfin.Data/Entities/RatingSource.cs deleted file mode 100644 index b62d8b444..000000000 --- a/Jellyfin.Data/Entities/RatingSource.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - /// - /// This is the entity to store review ratings, not age ratings. - /// - public partial class RatingSource - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected RatingSource() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static RatingSource CreateRatingSourceUnsafe() - { - return new RatingSource(); - } - - /// - /// Public constructor with required data. - /// - /// - /// - /// - public RatingSource(double maximumvalue, double minimumvalue, Rating _rating0) - { - this.MaximumValue = maximumvalue; - - this.MinimumValue = minimumvalue; - - if (_rating0 == null) - { - throw new ArgumentNullException(nameof(_rating0)); - } - - _rating0.RatingType = this; - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// - /// - /// - public static RatingSource Create(double maximumvalue, double minimumvalue, Rating _rating0) - { - return new RatingSource(maximumvalue, minimumvalue, _rating0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Backing field for Name. - /// - protected string _Name; - /// - /// When provided in a partial class, allows value of Name to be changed before setting. - /// - partial void SetName(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Name to be changed before returning. - /// - partial void GetName(ref string result); - - /// - /// Max length = 1024 - /// - [MaxLength(1024)] - [StringLength(1024)] - public string Name - { - get - { - string value = _Name; - GetName(ref value); - return _Name = value; - } - - set - { - string oldValue = _Name; - SetName(oldValue, ref value); - if (oldValue != value) - { - _Name = value; - } - } - } - - /// - /// Backing field for MaximumValue. - /// - protected double _MaximumValue; - /// - /// When provided in a partial class, allows value of MaximumValue to be changed before setting. - /// - partial void SetMaximumValue(double oldValue, ref double newValue); - /// - /// When provided in a partial class, allows value of MaximumValue to be changed before returning. - /// - partial void GetMaximumValue(ref double result); - - /// - /// Required. - /// - [Required] - public double MaximumValue - { - get - { - double value = _MaximumValue; - GetMaximumValue(ref value); - return _MaximumValue = value; - } - - set - { - double oldValue = _MaximumValue; - SetMaximumValue(oldValue, ref value); - if (oldValue != value) - { - _MaximumValue = value; - } - } - } - - /// - /// Backing field for MinimumValue. - /// - protected double _MinimumValue; - /// - /// When provided in a partial class, allows value of MinimumValue to be changed before setting. - /// - partial void SetMinimumValue(double oldValue, ref double newValue); - /// - /// When provided in a partial class, allows value of MinimumValue to be changed before returning. - /// - partial void GetMinimumValue(ref double result); - - /// - /// Required. - /// - [Required] - public double MinimumValue - { - get - { - double value = _MinimumValue; - GetMinimumValue(ref value); - return _MinimumValue = value; - } - - set - { - double oldValue = _MinimumValue; - SetMinimumValue(oldValue, ref value); - if (oldValue != value) - { - _MinimumValue = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("MetadataProviderId_Source_Id")] - public virtual MetadataProviderId Source { get; set; } - } -} - diff --git a/Jellyfin.Data/Entities/Release.cs b/Jellyfin.Data/Entities/Release.cs deleted file mode 100644 index 1e9faa5a1..000000000 --- a/Jellyfin.Data/Entities/Release.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Release - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected Release() - { - MediaFiles = new HashSet(); - Chapters = new HashSet(); - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Release CreateReleaseUnsafe() - { - return new Release(); - } - - /// - /// Public constructor with required data. - /// - /// - /// - /// - /// - /// - /// - /// - public Release(string name, Movie _movie0, Episode _episode1, Track _track2, CustomItem _customitem3, Book _book4, Photo _photo5) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - this.Name = name; - - if (_movie0 == null) - { - throw new ArgumentNullException(nameof(_movie0)); - } - - _movie0.Releases.Add(this); - - if (_episode1 == null) - { - throw new ArgumentNullException(nameof(_episode1)); - } - - _episode1.Releases.Add(this); - - if (_track2 == null) - { - throw new ArgumentNullException(nameof(_track2)); - } - - _track2.Releases.Add(this); - - if (_customitem3 == null) - { - throw new ArgumentNullException(nameof(_customitem3)); - } - - _customitem3.Releases.Add(this); - - if (_book4 == null) - { - throw new ArgumentNullException(nameof(_book4)); - } - - _book4.Releases.Add(this); - - if (_photo5 == null) - { - throw new ArgumentNullException(nameof(_photo5)); - } - - _photo5.Releases.Add(this); - - this.MediaFiles = new HashSet(); - this.Chapters = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// - /// - /// - /// - /// - /// - /// - public static Release Create(string name, Movie _movie0, Episode _episode1, Track _track2, CustomItem _customitem3, Book _book4, Photo _photo5) - { - return new Release(name, _movie0, _episode1, _track2, _customitem3, _book4, _photo5); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Id. - /// - internal int _Id; - /// - /// When provided in a partial class, allows value of Id to be changed before setting. - /// - partial void SetId(int oldValue, ref int newValue); - /// - /// When provided in a partial class, allows value of Id to be changed before returning. - /// - partial void GetId(ref int result); - - /// - /// Identity, Indexed, Required. - /// - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// - /// Backing field for Name. - /// - protected string _Name; - /// - /// When provided in a partial class, allows value of Name to be changed before setting. - /// - partial void SetName(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Name to be changed before returning. - /// - partial void GetName(ref string result); - - /// - /// Required, Max length = 1024 - /// - [Required] - [MaxLength(1024)] - [StringLength(1024)] - public string Name - { - get - { - string value = _Name; - GetName(ref value); - return _Name = value; - } - - set - { - string oldValue = _Name; - SetName(oldValue, ref value); - if (oldValue != value) - { - _Name = value; - } - } - } - - /// - /// Required, ConcurrenyToken. - /// - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("MediaFile_MediaFiles_Id")] - public virtual ICollection MediaFiles { get; protected set; } - - [ForeignKey("Chapter_Chapters_Id")] - public virtual ICollection Chapters { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/Season.cs b/Jellyfin.Data/Entities/Season.cs deleted file mode 100644 index 4b1e78575..000000000 --- a/Jellyfin.Data/Entities/Season.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Season : LibraryItem - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected Season() - { - // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - SeasonMetadata = new HashSet(); - Episodes = new HashSet(); - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Season CreateSeasonUnsafe() - { - return new Season(); - } - - /// - /// Public constructor with required data. - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - /// - public Season(Guid urlid, DateTime dateadded, Series _series0) - { - // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - this.UrlId = urlid; - - if (_series0 == null) - { - throw new ArgumentNullException(nameof(_series0)); - } - - _series0.Seasons.Add(this); - - this.SeasonMetadata = new HashSet(); - this.Episodes = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - /// - public static Season Create(Guid urlid, DateTime dateadded, Series _series0) - { - return new Season(urlid, dateadded, _series0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for SeasonNumber. - /// - protected int? _SeasonNumber; - /// - /// When provided in a partial class, allows value of SeasonNumber to be changed before setting. - /// - partial void SetSeasonNumber(int? oldValue, ref int? newValue); - /// - /// When provided in a partial class, allows value of SeasonNumber to be changed before returning. - /// - partial void GetSeasonNumber(ref int? result); - - public int? SeasonNumber - { - get - { - int? value = _SeasonNumber; - GetSeasonNumber(ref value); - return _SeasonNumber = value; - } - - set - { - int? oldValue = _SeasonNumber; - SetSeasonNumber(oldValue, ref value); - if (oldValue != value) - { - _SeasonNumber = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("SeasonMetadata_SeasonMetadata_Id")] - public virtual ICollection SeasonMetadata { get; protected set; } - - [ForeignKey("Episode_Episodes_Id")] - public virtual ICollection Episodes { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/SeasonMetadata.cs b/Jellyfin.Data/Entities/SeasonMetadata.cs deleted file mode 100644 index 10d19875e..000000000 --- a/Jellyfin.Data/Entities/SeasonMetadata.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class SeasonMetadata : Metadata - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected SeasonMetadata() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static SeasonMetadata CreateSeasonMetadataUnsafe() - { - return new SeasonMetadata(); - } - - /// - /// Public constructor with required data. - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public SeasonMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Season _season0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_season0 == null) - { - throw new ArgumentNullException(nameof(_season0)); - } - - _season0.SeasonMetadata.Add(this); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public static SeasonMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Season _season0) - { - return new SeasonMetadata(title, language, dateadded, datemodified, _season0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Outline. - /// - protected string _Outline; - /// - /// When provided in a partial class, allows value of Outline to be changed before setting. - /// - partial void SetOutline(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Outline to be changed before returning. - /// - partial void GetOutline(ref string result); - - /// - /// Max length = 1024 - /// - [MaxLength(1024)] - [StringLength(1024)] - public string Outline - { - get - { - string value = _Outline; - GetOutline(ref value); - return _Outline = value; - } - - set - { - string oldValue = _Outline; - SetOutline(oldValue, ref value); - if (oldValue != value) - { - _Outline = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/Series.cs b/Jellyfin.Data/Entities/Series.cs deleted file mode 100644 index bede14acf..000000000 --- a/Jellyfin.Data/Entities/Series.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Series : LibraryItem - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected Series() - { - SeriesMetadata = new HashSet(); - Seasons = new HashSet(); - - Init(); - } - - /// - /// Public constructor with required data. - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - public Series(Guid urlid, DateTime dateadded) - { - this.UrlId = urlid; - - this.SeriesMetadata = new HashSet(); - this.Seasons = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - public static Series Create(Guid urlid, DateTime dateadded) - { - return new Series(urlid, dateadded); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for AirsDayOfWeek. - /// - protected DayOfWeek? _AirsDayOfWeek; - /// - /// When provided in a partial class, allows value of AirsDayOfWeek to be changed before setting. - /// - partial void SetAirsDayOfWeek(DayOfWeek? oldValue, ref DayOfWeek? newValue); - /// - /// When provided in a partial class, allows value of AirsDayOfWeek to be changed before returning. - /// - partial void GetAirsDayOfWeek(ref DayOfWeek? result); - - public DayOfWeek? AirsDayOfWeek - { - get - { - DayOfWeek? value = _AirsDayOfWeek; - GetAirsDayOfWeek(ref value); - return _AirsDayOfWeek = value; - } - - set - { - DayOfWeek? oldValue = _AirsDayOfWeek; - SetAirsDayOfWeek(oldValue, ref value); - if (oldValue != value) - { - _AirsDayOfWeek = value; - } - } - } - - /// - /// Backing field for AirsTime. - /// - protected DateTimeOffset? _AirsTime; - /// - /// When provided in a partial class, allows value of AirsTime to be changed before setting. - /// - partial void SetAirsTime(DateTimeOffset? oldValue, ref DateTimeOffset? newValue); - /// - /// When provided in a partial class, allows value of AirsTime to be changed before returning. - /// - partial void GetAirsTime(ref DateTimeOffset? result); - - /// - /// The time the show airs, ignore the date portion. - /// - public DateTimeOffset? AirsTime - { - get - { - DateTimeOffset? value = _AirsTime; - GetAirsTime(ref value); - return _AirsTime = value; - } - - set - { - DateTimeOffset? oldValue = _AirsTime; - SetAirsTime(oldValue, ref value); - if (oldValue != value) - { - _AirsTime = value; - } - } - } - - /// - /// Backing field for FirstAired. - /// - protected DateTimeOffset? _FirstAired; - /// - /// When provided in a partial class, allows value of FirstAired to be changed before setting. - /// - partial void SetFirstAired(DateTimeOffset? oldValue, ref DateTimeOffset? newValue); - /// - /// When provided in a partial class, allows value of FirstAired to be changed before returning. - /// - partial void GetFirstAired(ref DateTimeOffset? result); - - public DateTimeOffset? FirstAired - { - get - { - DateTimeOffset? value = _FirstAired; - GetFirstAired(ref value); - return _FirstAired = value; - } - - set - { - DateTimeOffset? oldValue = _FirstAired; - SetFirstAired(oldValue, ref value); - if (oldValue != value) - { - _FirstAired = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("SeriesMetadata_SeriesMetadata_Id")] - public virtual ICollection SeriesMetadata { get; protected set; } - - [ForeignKey("Season_Seasons_Id")] - public virtual ICollection Seasons { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/SeriesMetadata.cs b/Jellyfin.Data/Entities/SeriesMetadata.cs deleted file mode 100644 index 16eb59315..000000000 --- a/Jellyfin.Data/Entities/SeriesMetadata.cs +++ /dev/null @@ -1,238 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class SeriesMetadata : Metadata - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected SeriesMetadata() - { - Networks = new HashSet(); - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static SeriesMetadata CreateSeriesMetadataUnsafe() - { - return new SeriesMetadata(); - } - - /// - /// Public constructor with required data. - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public SeriesMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Series _series0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_series0 == null) - { - throw new ArgumentNullException(nameof(_series0)); - } - - _series0.SeriesMetadata.Add(this); - - this.Networks = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public static SeriesMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Series _series0) - { - return new SeriesMetadata(title, language, dateadded, datemodified, _series0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for Outline. - /// - protected string _Outline; - /// - /// When provided in a partial class, allows value of Outline to be changed before setting. - /// - partial void SetOutline(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Outline to be changed before returning. - /// - partial void GetOutline(ref string result); - - /// - /// Max length = 1024 - /// - [MaxLength(1024)] - [StringLength(1024)] - public string Outline - { - get - { - string value = _Outline; - GetOutline(ref value); - return _Outline = value; - } - - set - { - string oldValue = _Outline; - SetOutline(oldValue, ref value); - if (oldValue != value) - { - _Outline = value; - } - } - } - - /// - /// Backing field for Plot. - /// - protected string _Plot; - /// - /// When provided in a partial class, allows value of Plot to be changed before setting. - /// - partial void SetPlot(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Plot to be changed before returning. - /// - partial void GetPlot(ref string result); - - /// - /// Max length = 65535 - /// - [MaxLength(65535)] - [StringLength(65535)] - public string Plot - { - get - { - string value = _Plot; - GetPlot(ref value); - return _Plot = value; - } - - set - { - string oldValue = _Plot; - SetPlot(oldValue, ref value); - if (oldValue != value) - { - _Plot = value; - } - } - } - - /// - /// Backing field for Tagline. - /// - protected string _Tagline; - /// - /// When provided in a partial class, allows value of Tagline to be changed before setting. - /// - partial void SetTagline(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Tagline to be changed before returning. - /// - partial void GetTagline(ref string result); - - /// - /// Max length = 1024 - /// - [MaxLength(1024)] - [StringLength(1024)] - public string Tagline - { - get - { - string value = _Tagline; - GetTagline(ref value); - return _Tagline = value; - } - - set - { - string oldValue = _Tagline; - SetTagline(oldValue, ref value); - if (oldValue != value) - { - _Tagline = value; - } - } - } - - /// - /// Backing field for Country. - /// - protected string _Country; - /// - /// When provided in a partial class, allows value of Country to be changed before setting. - /// - partial void SetCountry(string oldValue, ref string newValue); - /// - /// When provided in a partial class, allows value of Country to be changed before returning. - /// - partial void GetCountry(ref string result); - - /// - /// Max length = 2 - /// - [MaxLength(2)] - [StringLength(2)] - public string Country - { - get - { - string value = _Country; - GetCountry(ref value); - return _Country = value; - } - - set - { - string oldValue = _Country; - SetCountry(oldValue, ref value); - if (oldValue != value) - { - _Country = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("Company_Networks_Id")] - public virtual ICollection Networks { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/Track.cs b/Jellyfin.Data/Entities/Track.cs deleted file mode 100644 index b7d7b5873..000000000 --- a/Jellyfin.Data/Entities/Track.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Track : LibraryItem - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected Track() - { - // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - Releases = new HashSet(); - TrackMetadata = new HashSet(); - - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static Track CreateTrackUnsafe() - { - return new Track(); - } - - /// - /// Public constructor with required data. - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - /// - public Track(Guid urlid, DateTime dateadded, MusicAlbum _musicalbum0) - { - // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - this.UrlId = urlid; - - if (_musicalbum0 == null) - { - throw new ArgumentNullException(nameof(_musicalbum0)); - } - - _musicalbum0.Tracks.Add(this); - - this.Releases = new HashSet(); - this.TrackMetadata = new HashSet(); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - /// - public static Track Create(Guid urlid, DateTime dateadded, MusicAlbum _musicalbum0) - { - return new Track(urlid, dateadded, _musicalbum0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// - /// Backing field for TrackNumber. - /// - protected int? _TrackNumber; - /// - /// When provided in a partial class, allows value of TrackNumber to be changed before setting. - /// - partial void SetTrackNumber(int? oldValue, ref int? newValue); - /// - /// When provided in a partial class, allows value of TrackNumber to be changed before returning. - /// - partial void GetTrackNumber(ref int? result); - - public int? TrackNumber - { - get - { - int? value = _TrackNumber; - GetTrackNumber(ref value); - return _TrackNumber = value; - } - - set - { - int? oldValue = _TrackNumber; - SetTrackNumber(oldValue, ref value); - if (oldValue != value) - { - _TrackNumber = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - [ForeignKey("Release_Releases_Id")] - public virtual ICollection Releases { get; protected set; } - - [ForeignKey("TrackMetadata_TrackMetadata_Id")] - public virtual ICollection TrackMetadata { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/TrackMetadata.cs b/Jellyfin.Data/Entities/TrackMetadata.cs deleted file mode 100644 index 23e1219aa..000000000 --- a/Jellyfin.Data/Entities/TrackMetadata.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class TrackMetadata : Metadata - { - partial void Init(); - - /// - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// - protected TrackMetadata() - { - Init(); - } - - /// - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// - public static TrackMetadata CreateTrackMetadataUnsafe() - { - return new TrackMetadata(); - } - - /// - /// Public constructor with required data. - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public TrackMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Track _track0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_track0 == null) - { - throw new ArgumentNullException(nameof(_track0)); - } - - _track0.TrackMetadata.Add(this); - - Init(); - } - - /// - /// Static create function (for use in LINQ queries, etc.) - /// - /// The title or name of the object. - /// ISO-639-3 3-character language codes. - /// - public static TrackMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Track _track0) - { - return new TrackMetadata(title, language, dateadded, datemodified, _track0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index 50810561f..7ea1f4498 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -6,13 +8,14 @@ using System.Globalization; using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; +using Jellyfin.Data.Interfaces; namespace Jellyfin.Data.Entities { /// /// An entity representing a user. /// - public partial class User : IHasPermissions, ISavingChanges + public partial class User : IHasPermissions, IHasConcurrencyToken { /// /// The values being delimited here are Guids, so commas work as they do not appear in Guids. diff --git a/Jellyfin.Data/Enums/ArtKind.cs b/Jellyfin.Data/Enums/ArtKind.cs index 6b69d68b2..71b4db6f2 100644 --- a/Jellyfin.Data/Enums/ArtKind.cs +++ b/Jellyfin.Data/Enums/ArtKind.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace Jellyfin.Data.Enums { public enum ArtKind diff --git a/Jellyfin.Data/Enums/IndexingKind.cs b/Jellyfin.Data/Enums/IndexingKind.cs index 9badc6573..fafe47e0c 100644 --- a/Jellyfin.Data/Enums/IndexingKind.cs +++ b/Jellyfin.Data/Enums/IndexingKind.cs @@ -1,4 +1,6 @@ -namespace Jellyfin.Data.Enums +#pragma warning disable CS1591 + +namespace Jellyfin.Data.Enums { public enum IndexingKind { diff --git a/Jellyfin.Data/Enums/MediaFileKind.cs b/Jellyfin.Data/Enums/MediaFileKind.cs index 12f48c558..b03591fb8 100644 --- a/Jellyfin.Data/Enums/MediaFileKind.cs +++ b/Jellyfin.Data/Enums/MediaFileKind.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace Jellyfin.Data.Enums { public enum MediaFileKind diff --git a/Jellyfin.Data/Enums/PersonRoleType.cs b/Jellyfin.Data/Enums/PersonRoleType.cs index 6e52f2c85..2d80eaa4c 100644 --- a/Jellyfin.Data/Enums/PersonRoleType.cs +++ b/Jellyfin.Data/Enums/PersonRoleType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace Jellyfin.Data.Enums { public enum PersonRoleType diff --git a/MediaBrowser.Model/Events/GenericEventArgs.cs b/Jellyfin.Data/Events/GenericEventArgs.cs similarity index 77% rename from MediaBrowser.Model/Events/GenericEventArgs.cs rename to Jellyfin.Data/Events/GenericEventArgs.cs index 44f60f811..7b9a5111e 100644 --- a/MediaBrowser.Model/Events/GenericEventArgs.cs +++ b/Jellyfin.Data/Events/GenericEventArgs.cs @@ -1,19 +1,13 @@ using System; -namespace MediaBrowser.Model.Events +namespace Jellyfin.Data.Events { /// /// Provides a generic EventArgs subclass that can hold any kind of object. /// - /// + /// The type of this event. public class GenericEventArgs : EventArgs { - /// - /// Gets or sets the argument. - /// - /// The argument. - public T Argument { get; set; } - /// /// Initializes a new instance of the class. /// @@ -22,5 +16,11 @@ namespace MediaBrowser.Model.Events { Argument = arg; } + + /// + /// Gets the argument. + /// + /// The argument. + public T Argument { get; } } } diff --git a/Jellyfin.Data/Events/System/PendingRestartEventArgs.cs b/Jellyfin.Data/Events/System/PendingRestartEventArgs.cs new file mode 100644 index 000000000..2aa40a946 --- /dev/null +++ b/Jellyfin.Data/Events/System/PendingRestartEventArgs.cs @@ -0,0 +1,11 @@ +using System; + +namespace Jellyfin.Data.Events.System +{ + /// + /// An event that occurs when there is a pending restart. + /// + public class PendingRestartEventArgs : EventArgs + { + } +} diff --git a/Jellyfin.Data/Events/Users/UserCreatedEventArgs.cs b/Jellyfin.Data/Events/Users/UserCreatedEventArgs.cs new file mode 100644 index 000000000..66f7c8d4f --- /dev/null +++ b/Jellyfin.Data/Events/Users/UserCreatedEventArgs.cs @@ -0,0 +1,18 @@ +using Jellyfin.Data.Entities; + +namespace Jellyfin.Data.Events.Users +{ + /// + /// An event that occurs when a user is created. + /// + public class UserCreatedEventArgs : GenericEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The user. + public UserCreatedEventArgs(User arg) : base(arg) + { + } + } +} diff --git a/Jellyfin.Data/Events/Users/UserDeletedEventArgs.cs b/Jellyfin.Data/Events/Users/UserDeletedEventArgs.cs new file mode 100644 index 000000000..0b9493375 --- /dev/null +++ b/Jellyfin.Data/Events/Users/UserDeletedEventArgs.cs @@ -0,0 +1,18 @@ +using Jellyfin.Data.Entities; + +namespace Jellyfin.Data.Events.Users +{ + /// + /// An event that occurs when a user is deleted. + /// + public class UserDeletedEventArgs : GenericEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The user. + public UserDeletedEventArgs(User arg) : base(arg) + { + } + } +} diff --git a/Jellyfin.Data/Events/Users/UserLockedOutEventArgs.cs b/Jellyfin.Data/Events/Users/UserLockedOutEventArgs.cs new file mode 100644 index 000000000..cca3726dc --- /dev/null +++ b/Jellyfin.Data/Events/Users/UserLockedOutEventArgs.cs @@ -0,0 +1,18 @@ +using Jellyfin.Data.Entities; + +namespace Jellyfin.Data.Events.Users +{ + /// + /// An event that occurs when a user is locked out. + /// + public class UserLockedOutEventArgs : GenericEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The user. + public UserLockedOutEventArgs(User arg) : base(arg) + { + } + } +} diff --git a/Jellyfin.Data/Events/Users/UserPasswordChangedEventArgs.cs b/Jellyfin.Data/Events/Users/UserPasswordChangedEventArgs.cs new file mode 100644 index 000000000..087ec9ab6 --- /dev/null +++ b/Jellyfin.Data/Events/Users/UserPasswordChangedEventArgs.cs @@ -0,0 +1,18 @@ +using Jellyfin.Data.Entities; + +namespace Jellyfin.Data.Events.Users +{ + /// + /// An event that occurs when a user's password has changed. + /// + public class UserPasswordChangedEventArgs : GenericEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The user. + public UserPasswordChangedEventArgs(User arg) : base(arg) + { + } + } +} diff --git a/Jellyfin.Data/Events/Users/UserUpdatedEventArgs.cs b/Jellyfin.Data/Events/Users/UserUpdatedEventArgs.cs new file mode 100644 index 000000000..2b4e49cdf --- /dev/null +++ b/Jellyfin.Data/Events/Users/UserUpdatedEventArgs.cs @@ -0,0 +1,18 @@ +using Jellyfin.Data.Entities; + +namespace Jellyfin.Data.Events.Users +{ + /// + /// An event that occurs when a user is updated. + /// + public class UserUpdatedEventArgs : GenericEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The user. + public UserUpdatedEventArgs(User arg) : base(arg) + { + } + } +} diff --git a/Jellyfin.Data/ISavingChanges.cs b/Jellyfin.Data/ISavingChanges.cs deleted file mode 100644 index f392dae6a..000000000 --- a/Jellyfin.Data/ISavingChanges.cs +++ /dev/null @@ -1,9 +0,0 @@ -#pragma warning disable CS1591 - -namespace Jellyfin.Data -{ - public interface ISavingChanges - { - void OnSavingChanges(); - } -} diff --git a/Jellyfin.Data/Interfaces/IHasArtwork.cs b/Jellyfin.Data/Interfaces/IHasArtwork.cs new file mode 100644 index 000000000..a4d9c54af --- /dev/null +++ b/Jellyfin.Data/Interfaces/IHasArtwork.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Jellyfin.Data.Entities.Libraries; + +namespace Jellyfin.Data.Interfaces +{ + /// + /// An interface abstracting an entity that has artwork. + /// + public interface IHasArtwork + { + /// + /// Gets a collection containing this entity's artwork. + /// + ICollection Artwork { get; } + } +} diff --git a/Jellyfin.Data/Interfaces/IHasCompanies.cs b/Jellyfin.Data/Interfaces/IHasCompanies.cs new file mode 100644 index 000000000..8f19ce04f --- /dev/null +++ b/Jellyfin.Data/Interfaces/IHasCompanies.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Jellyfin.Data.Entities.Libraries; + +namespace Jellyfin.Data.Interfaces +{ + /// + /// An abstraction representing an entity that has companies. + /// + public interface IHasCompanies + { + /// + /// Gets a collection containing this entity's companies. + /// + ICollection Companies { get; } + } +} diff --git a/Jellyfin.Data/Interfaces/IHasConcurrencyToken.cs b/Jellyfin.Data/Interfaces/IHasConcurrencyToken.cs new file mode 100644 index 000000000..2c4091493 --- /dev/null +++ b/Jellyfin.Data/Interfaces/IHasConcurrencyToken.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Data.Interfaces +{ + /// + /// An interface abstracting an entity that has a concurrency token. + /// + public interface IHasConcurrencyToken + { + /// + /// Gets the version of this row. Acts as a concurrency token. + /// + uint RowVersion { get; } + + /// + /// Called when saving changes to this entity. + /// + void OnSavingChanges(); + } +} diff --git a/Jellyfin.Data/IHasPermissions.cs b/Jellyfin.Data/Interfaces/IHasPermissions.cs similarity index 100% rename from Jellyfin.Data/IHasPermissions.cs rename to Jellyfin.Data/Interfaces/IHasPermissions.cs diff --git a/Jellyfin.Data/Interfaces/IHasReleases.cs b/Jellyfin.Data/Interfaces/IHasReleases.cs new file mode 100644 index 000000000..3b615893e --- /dev/null +++ b/Jellyfin.Data/Interfaces/IHasReleases.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Jellyfin.Data.Entities.Libraries; + +namespace Jellyfin.Data.Interfaces +{ + /// + /// An abstraction representing an entity that has releases. + /// + public interface IHasReleases + { + /// + /// Gets a collection containing this entity's releases. + /// + ICollection Releases { get; } + } +} diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 8ce0f3848..1e01521ad 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -4,12 +4,34 @@ netstandard2.0;netstandard2.1 false true + true + true + true + true + snupkg + + + + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + Jellyfin Contributors + Jellyfin.Data + 10.7.0 + https://github.com/jellyfin/jellyfin + GPL-3.0-only ../jellyfin.ruleset + + + + diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index 2deefbe81..abdd290d4 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -2,8 +2,8 @@ using System; using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Events; using MediaBrowser.Model.Activity; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Querying; namespace Jellyfin.Server.Implementations.Activity @@ -27,16 +27,6 @@ namespace Jellyfin.Server.Implementations.Activity /// public event EventHandler> EntryCreated; - /// - public void Create(ActivityLog entry) - { - using var dbContext = _provider.CreateContext(); - dbContext.ActivityLogs.Add(entry); - dbContext.SaveChanges(); - - EntryCreated?.Invoke(this, new GenericEventArgs(ConvertToOldModel(entry))); - } - /// public async Task CreateAsync(ActivityLog entry) { diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs new file mode 100644 index 000000000..449f27be2 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs @@ -0,0 +1,102 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Subtitles; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Globalization; +using Episode = MediaBrowser.Controller.Entities.TV.Episode; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Library +{ + /// + /// Creates an entry in the activity log whenever a subtitle download fails. + /// + public class SubtitleDownloadFailureLogger : IEventConsumer + { + private readonly ILocalizationManager _localizationManager; + private readonly IActivityManager _activityManager; + + /// + /// Initializes a new instance of the class. + /// + /// The localization manager. + /// The activity manager. + public SubtitleDownloadFailureLogger(ILocalizationManager localizationManager, IActivityManager activityManager) + { + _localizationManager = localizationManager; + _activityManager = activityManager; + } + + /// + public async Task OnEvent(SubtitleDownloadFailureEventArgs eventArgs) + { + await _activityManager.CreateAsync(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("SubtitleDownloadFailureFromForItem"), + eventArgs.Provider, + GetItemName(eventArgs.Item)), + "SubtitleDownloadFailure", + Guid.Empty) + { + ItemId = eventArgs.Item.Id.ToString("N", CultureInfo.InvariantCulture), + ShortOverview = eventArgs.Exception.Message + }).ConfigureAwait(false); + } + + private static string GetItemName(BaseItem item) + { + var name = item.Name; + if (item is Episode episode) + { + if (episode.IndexNumber.HasValue) + { + name = string.Format( + CultureInfo.InvariantCulture, + "Ep{0} - {1}", + episode.IndexNumber.Value, + name); + } + + if (episode.ParentIndexNumber.HasValue) + { + name = string.Format( + CultureInfo.InvariantCulture, + "S{0}, {1}", + episode.ParentIndexNumber.Value, + name); + } + } + + if (item is IHasSeries hasSeries) + { + name = hasSeries.SeriesName + " - " + name; + } + + if (item is IHasAlbumArtist hasAlbumArtist) + { + var artists = hasAlbumArtist.AlbumArtists; + + if (artists.Count > 0) + { + name = artists[0] + " - " + name; + } + } + else if (item is IHasArtist hasArtist) + { + var artists = hasArtist.Artists; + + if (artists.Count > 0) + { + name = artists[0] + " - " + name; + } + } + + return name; + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs new file mode 100644 index 000000000..f899b4497 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs @@ -0,0 +1,52 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Events; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Globalization; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Security +{ + /// + /// Creates an entry in the activity log when there is a failed login attempt. + /// + public class AuthenticationFailedLogger : IEventConsumer> + { + private readonly ILocalizationManager _localizationManager; + private readonly IActivityManager _activityManager; + + /// + /// Initializes a new instance of the class. + /// + /// The localization manager. + /// The activity manager. + public AuthenticationFailedLogger(ILocalizationManager localizationManager, IActivityManager activityManager) + { + _localizationManager = localizationManager; + _activityManager = activityManager; + } + + /// + public async Task OnEvent(GenericEventArgs eventArgs) + { + await _activityManager.CreateAsync(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("FailedLoginAttemptWithUserName"), + eventArgs.Argument.Username), + "AuthenticationFailed", + Guid.Empty) + { + LogSeverity = LogLevel.Error, + ShortOverview = string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("LabelIpAddressValue"), + eventArgs.Argument.RemoteEndPoint), + }).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs new file mode 100644 index 000000000..2f9f44ed6 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs @@ -0,0 +1,49 @@ +using System.Globalization; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Events; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Events; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Globalization; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Security +{ + /// + /// Creates an entry in the activity log when there is a successful login attempt. + /// + public class AuthenticationSucceededLogger : IEventConsumer> + { + private readonly ILocalizationManager _localizationManager; + private readonly IActivityManager _activityManager; + + /// + /// Initializes a new instance of the class. + /// + /// The localization manager. + /// The activity manager. + public AuthenticationSucceededLogger(ILocalizationManager localizationManager, IActivityManager activityManager) + { + _localizationManager = localizationManager; + _activityManager = activityManager; + } + + /// + public async Task OnEvent(GenericEventArgs e) + { + await _activityManager.CreateAsync(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("AuthenticationSucceededWithUserName"), + e.Argument.User.Name), + "AuthenticationSucceeded", + e.Argument.User.Id) + { + ShortOverview = string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("LabelIpAddressValue"), + e.Argument.SessionInfo.RemoteEndPoint), + }).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs new file mode 100644 index 000000000..ec4a76e7f --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs @@ -0,0 +1,104 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Notifications; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Session +{ + /// + /// Creates an entry in the activity log whenever a user starts playback. + /// + public class PlaybackStartLogger : IEventConsumer + { + private readonly ILogger _logger; + private readonly ILocalizationManager _localizationManager; + private readonly IActivityManager _activityManager; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The localization manager. + /// The activity manager. + public PlaybackStartLogger(ILogger logger, ILocalizationManager localizationManager, IActivityManager activityManager) + { + _logger = logger; + _localizationManager = localizationManager; + _activityManager = activityManager; + } + + /// + public async Task OnEvent(PlaybackStartEventArgs eventArgs) + { + if (eventArgs.MediaInfo == null) + { + _logger.LogWarning("PlaybackStart reported with null media info."); + return; + } + + if (eventArgs.Item != null && eventArgs.Item.IsThemeMedia) + { + // Don't report theme song or local trailer playback + return; + } + + if (eventArgs.Users.Count == 0) + { + return; + } + + var user = eventArgs.Users[0]; + + await _activityManager.CreateAsync(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("UserStartedPlayingItemWithValues"), + user.Username, + GetItemName(eventArgs.MediaInfo), + eventArgs.DeviceName), + GetPlaybackNotificationType(eventArgs.MediaInfo.MediaType), + user.Id)) + .ConfigureAwait(false); + } + + private static string GetItemName(BaseItemDto item) + { + var name = item.Name; + + if (!string.IsNullOrEmpty(item.SeriesName)) + { + name = item.SeriesName + " - " + name; + } + + if (item.Artists != null && item.Artists.Count > 0) + { + name = item.Artists[0] + " - " + name; + } + + return name; + } + + private static string GetPlaybackNotificationType(string mediaType) + { + if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + { + return NotificationType.AudioPlayback.ToString(); + } + + if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + { + return NotificationType.VideoPlayback.ToString(); + } + + return null; + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs new file mode 100644 index 000000000..51a882c14 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs @@ -0,0 +1,106 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Notifications; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Session +{ + /// + /// Creates an activity log entry whenever a user stops playback. + /// + public class PlaybackStopLogger : IEventConsumer + { + private readonly ILogger _logger; + private readonly ILocalizationManager _localizationManager; + private readonly IActivityManager _activityManager; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The localization manager. + /// The activity manager. + public PlaybackStopLogger(ILogger logger, ILocalizationManager localizationManager, IActivityManager activityManager) + { + _logger = logger; + _localizationManager = localizationManager; + _activityManager = activityManager; + } + + /// + public async Task OnEvent(PlaybackStopEventArgs eventArgs) + { + var item = eventArgs.MediaInfo; + + if (item == null) + { + _logger.LogWarning("PlaybackStopped reported with null media info."); + return; + } + + if (eventArgs.Item != null && eventArgs.Item.IsThemeMedia) + { + // Don't report theme song or local trailer playback + return; + } + + if (eventArgs.Users.Count == 0) + { + return; + } + + var user = eventArgs.Users[0]; + + await _activityManager.CreateAsync(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("UserStoppedPlayingItemWithValues"), + user.Username, + GetItemName(item), + eventArgs.DeviceName), + GetPlaybackStoppedNotificationType(item.MediaType), + user.Id)) + .ConfigureAwait(false); + } + + private static string GetItemName(BaseItemDto item) + { + var name = item.Name; + + if (!string.IsNullOrEmpty(item.SeriesName)) + { + name = item.SeriesName + " - " + name; + } + + if (item.Artists != null && item.Artists.Count > 0) + { + name = item.Artists[0] + " - " + name; + } + + return name; + } + + private static string GetPlaybackStoppedNotificationType(string mediaType) + { + if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + { + return NotificationType.AudioPlaybackStopped.ToString(); + } + + if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + { + return NotificationType.VideoPlaybackStopped.ToString(); + } + + return null; + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs new file mode 100644 index 000000000..cf20946ec --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs @@ -0,0 +1,54 @@ +using System.Globalization; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Session; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Globalization; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Session +{ + /// + /// Creates an entry in the activity log whenever a session ends. + /// + public class SessionEndedLogger : IEventConsumer + { + private readonly ILocalizationManager _localizationManager; + private readonly IActivityManager _activityManager; + + /// + /// Initializes a new instance of the class. + /// + /// The localization manager. + /// The activity manager. + public SessionEndedLogger(ILocalizationManager localizationManager, IActivityManager activityManager) + { + _localizationManager = localizationManager; + _activityManager = activityManager; + } + + /// + public async Task OnEvent(SessionEndedEventArgs eventArgs) + { + if (string.IsNullOrEmpty(eventArgs.Argument.UserName)) + { + return; + } + + await _activityManager.CreateAsync(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("UserOfflineFromDevice"), + eventArgs.Argument.UserName, + eventArgs.Argument.DeviceName), + "SessionEnded", + eventArgs.Argument.UserId) + { + ShortOverview = string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("LabelIpAddressValue"), + eventArgs.Argument.RemoteEndPoint), + }).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs new file mode 100644 index 000000000..6a0f29b09 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs @@ -0,0 +1,54 @@ +using System.Globalization; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Session; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Globalization; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Session +{ + /// + /// Creates an entry in the activity log when a session is started. + /// + public class SessionStartedLogger : IEventConsumer + { + private readonly ILocalizationManager _localizationManager; + private readonly IActivityManager _activityManager; + + /// + /// Initializes a new instance of the class. + /// + /// The localization manager. + /// The activity manager. + public SessionStartedLogger(ILocalizationManager localizationManager, IActivityManager activityManager) + { + _localizationManager = localizationManager; + _activityManager = activityManager; + } + + /// + public async Task OnEvent(SessionStartedEventArgs eventArgs) + { + if (string.IsNullOrEmpty(eventArgs.Argument.UserName)) + { + return; + } + + await _activityManager.CreateAsync(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("UserOnlineFromDevice"), + eventArgs.Argument.UserName, + eventArgs.Argument.DeviceName), + "SessionStarted", + eventArgs.Argument.UserId) + { + ShortOverview = string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("LabelIpAddressValue"), + eventArgs.Argument.RemoteEndPoint) + }).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/System/PendingRestartNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/System/PendingRestartNotifier.cs new file mode 100644 index 000000000..2fa38dd71 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/System/PendingRestartNotifier.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Events.System; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Session; + +namespace Jellyfin.Server.Implementations.Events.Consumers.System +{ + /// + /// Notifies users when there is a pending restart. + /// + public class PendingRestartNotifier : IEventConsumer + { + private readonly ISessionManager _sessionManager; + + /// + /// Initializes a new instance of the class. + /// + /// The session manager. + public PendingRestartNotifier(ISessionManager sessionManager) + { + _sessionManager = sessionManager; + } + + /// + public async Task OnEvent(PendingRestartEventArgs eventArgs) + { + await _sessionManager.SendRestartRequiredNotification(CancellationToken.None).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs new file mode 100644 index 000000000..05201a346 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Events; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Notifications; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Implementations.Events.Consumers.System +{ + /// + /// Creates an activity log entry whenever a task is completed. + /// + public class TaskCompletedLogger : IEventConsumer + { + private readonly ILocalizationManager _localizationManager; + private readonly IActivityManager _activityManager; + + /// + /// Initializes a new instance of the class. + /// + /// The localization manager. + /// The activity manager. + public TaskCompletedLogger(ILocalizationManager localizationManager, IActivityManager activityManager) + { + _localizationManager = localizationManager; + _activityManager = activityManager; + } + + /// + public async Task OnEvent(TaskCompletionEventArgs e) + { + var result = e.Result; + var task = e.Task; + + if (task.ScheduledTask is IConfigurableScheduledTask activityTask + && !activityTask.IsLogged) + { + return; + } + + var time = result.EndTimeUtc - result.StartTimeUtc; + var runningTime = string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("LabelRunningTimeValue"), + ToUserFriendlyString(time)); + + if (result.Status == TaskCompletionStatus.Failed) + { + var vals = new List(); + + if (!string.IsNullOrEmpty(e.Result.ErrorMessage)) + { + vals.Add(e.Result.ErrorMessage); + } + + if (!string.IsNullOrEmpty(e.Result.LongErrorMessage)) + { + vals.Add(e.Result.LongErrorMessage); + } + + await _activityManager.CreateAsync(new ActivityLog( + string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name), + NotificationType.TaskFailed.ToString(), + Guid.Empty) + { + LogSeverity = LogLevel.Error, + Overview = string.Join(Environment.NewLine, vals), + ShortOverview = runningTime + }).ConfigureAwait(false); + } + } + + private static string ToUserFriendlyString(TimeSpan span) + { + const int DaysInYear = 365; + const int DaysInMonth = 30; + + // Get each non-zero value from TimeSpan component + var values = new List(); + + // Number of years + int days = span.Days; + if (days >= DaysInYear) + { + int years = days / DaysInYear; + values.Add(CreateValueString(years, "year")); + days %= DaysInYear; + } + + // Number of months + if (days >= DaysInMonth) + { + int months = days / DaysInMonth; + values.Add(CreateValueString(months, "month")); + days = days % DaysInMonth; + } + + // Number of days + if (days >= 1) + { + values.Add(CreateValueString(days, "day")); + } + + // Number of hours + if (span.Hours >= 1) + { + values.Add(CreateValueString(span.Hours, "hour")); + } + + // Number of minutes + if (span.Minutes >= 1) + { + values.Add(CreateValueString(span.Minutes, "minute")); + } + + // Number of seconds (include when 0 if no other components included) + if (span.Seconds >= 1 || values.Count == 0) + { + values.Add(CreateValueString(span.Seconds, "second")); + } + + // Combine values into string + var builder = new StringBuilder(); + for (int i = 0; i < values.Count; i++) + { + if (builder.Length > 0) + { + builder.Append(i == values.Count - 1 ? " and " : ", "); + } + + builder.Append(values[i]); + } + + // Return result + return builder.ToString(); + } + + /// + /// Constructs a string description of a time-span value. + /// + /// The value of this item. + /// The name of this item (singular form). + private static string CreateValueString(int value, string description) + { + return string.Format( + CultureInfo.InvariantCulture, + "{0:#,##0} {1}", + value, + value == 1 ? description : string.Format(CultureInfo.InvariantCulture, "{0}s", description)); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs new file mode 100644 index 000000000..80ed56cd8 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Tasks; + +namespace Jellyfin.Server.Implementations.Events.Consumers.System +{ + /// + /// Notifies admin users when a task is completed. + /// + public class TaskCompletedNotifier : IEventConsumer + { + private readonly ISessionManager _sessionManager; + + /// + /// Initializes a new instance of the class. + /// + /// The session manager. + public TaskCompletedNotifier(ISessionManager sessionManager) + { + _sessionManager = sessionManager; + } + + /// + public async Task OnEvent(TaskCompletionEventArgs eventArgs) + { + await _sessionManager.SendMessageToAdminSessions("ScheduledTaskEnded", eventArgs.Result, CancellationToken.None).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs new file mode 100644 index 000000000..1c600683a --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Updates; +using MediaBrowser.Controller.Session; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Updates +{ + /// + /// Notifies admin users when a plugin installation is cancelled. + /// + public class PluginInstallationCancelledNotifier : IEventConsumer + { + private readonly ISessionManager _sessionManager; + + /// + /// Initializes a new instance of the class. + /// + /// The session manager. + public PluginInstallationCancelledNotifier(ISessionManager sessionManager) + { + _sessionManager = sessionManager; + } + + /// + public async Task OnEvent(PluginInstallationCancelledEventArgs eventArgs) + { + await _sessionManager.SendMessageToAdminSessions("PackageInstallationCancelled", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs new file mode 100644 index 000000000..d71c298c5 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs @@ -0,0 +1,51 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Common.Updates; +using MediaBrowser.Controller.Events; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Notifications; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Updates +{ + /// + /// Creates an entry in the activity log when a package installation fails. + /// + public class PluginInstallationFailedLogger : IEventConsumer + { + private readonly ILocalizationManager _localizationManager; + private readonly IActivityManager _activityManager; + + /// + /// Initializes a new instance of the class. + /// + /// The localization manager. + /// The activity manager. + public PluginInstallationFailedLogger(ILocalizationManager localizationManager, IActivityManager activityManager) + { + _localizationManager = localizationManager; + _activityManager = activityManager; + } + + /// + public async Task OnEvent(InstallationFailedEventArgs eventArgs) + { + await _activityManager.CreateAsync(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("NameInstallFailed"), + eventArgs.InstallationInfo.Name), + NotificationType.InstallationFailed.ToString(), + Guid.Empty) + { + ShortOverview = string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("VersionNumber"), + eventArgs.InstallationInfo.Version), + Overview = eventArgs.Exception.Message + }).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs new file mode 100644 index 000000000..ea0c878d4 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Updates; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Session; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Updates +{ + /// + /// Notifies admin users when a plugin installation fails. + /// + public class PluginInstallationFailedNotifier : IEventConsumer + { + private readonly ISessionManager _sessionManager; + + /// + /// Initializes a new instance of the class. + /// + /// The session manager. + public PluginInstallationFailedNotifier(ISessionManager sessionManager) + { + _sessionManager = sessionManager; + } + + /// + public async Task OnEvent(InstallationFailedEventArgs eventArgs) + { + await _sessionManager.SendMessageToAdminSessions("PackageInstallationFailed", eventArgs.InstallationInfo, CancellationToken.None).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs new file mode 100644 index 000000000..8837172db --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs @@ -0,0 +1,50 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Updates; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Notifications; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Updates +{ + /// + /// Creates an entry in the activity log when a plugin is installed. + /// + public class PluginInstalledLogger : IEventConsumer + { + private readonly ILocalizationManager _localizationManager; + private readonly IActivityManager _activityManager; + + /// + /// Initializes a new instance of the class. + /// + /// The localization manager. + /// The activity manager. + public PluginInstalledLogger(ILocalizationManager localizationManager, IActivityManager activityManager) + { + _localizationManager = localizationManager; + _activityManager = activityManager; + } + + /// + public async Task OnEvent(PluginInstalledEventArgs eventArgs) + { + await _activityManager.CreateAsync(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("PluginInstalledWithName"), + eventArgs.Argument.Name), + NotificationType.PluginInstalled.ToString(), + Guid.Empty) + { + ShortOverview = string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("VersionNumber"), + eventArgs.Argument.Version) + }).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs new file mode 100644 index 000000000..3dda5a04c --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Updates; +using MediaBrowser.Controller.Session; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Updates +{ + /// + /// Notifies admin users when a plugin is installed. + /// + public class PluginInstalledNotifier : IEventConsumer + { + private readonly ISessionManager _sessionManager; + + /// + /// Initializes a new instance of the class. + /// + /// The session manager. + public PluginInstalledNotifier(ISessionManager sessionManager) + { + _sessionManager = sessionManager; + } + + /// + public async Task OnEvent(PluginInstalledEventArgs eventArgs) + { + await _sessionManager.SendMessageToAdminSessions("PackageInstallationCompleted", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs new file mode 100644 index 000000000..f691d11a7 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Updates; +using MediaBrowser.Controller.Session; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Updates +{ + /// + /// Notifies admin users when a plugin is being installed. + /// + public class PluginInstallingNotifier : IEventConsumer + { + private readonly ISessionManager _sessionManager; + + /// + /// Initializes a new instance of the class. + /// + /// The session manager. + public PluginInstallingNotifier(ISessionManager sessionManager) + { + _sessionManager = sessionManager; + } + + /// + public async Task OnEvent(PluginInstallingEventArgs eventArgs) + { + await _sessionManager.SendMessageToAdminSessions("PackageInstalling", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs new file mode 100644 index 000000000..91a30069e --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs @@ -0,0 +1,45 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Updates; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Notifications; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Updates +{ + /// + /// Creates an entry in the activity log when a plugin is uninstalled. + /// + public class PluginUninstalledLogger : IEventConsumer + { + private readonly ILocalizationManager _localizationManager; + private readonly IActivityManager _activityManager; + + /// + /// Initializes a new instance of the class. + /// + /// The localization manager. + /// The activity manager. + public PluginUninstalledLogger(ILocalizationManager localizationManager, IActivityManager activityManager) + { + _localizationManager = localizationManager; + _activityManager = activityManager; + } + + /// + public async Task OnEvent(PluginUninstalledEventArgs e) + { + await _activityManager.CreateAsync(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("PluginUninstalledWithName"), + e.Argument.Name), + NotificationType.PluginUninstalled.ToString(), + Guid.Empty)) + .ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs new file mode 100644 index 000000000..709692f6b --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Updates; +using MediaBrowser.Controller.Session; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Updates +{ + /// + /// Notifies admin users when a plugin is uninstalled. + /// + public class PluginUninstalledNotifier : IEventConsumer + { + private readonly ISessionManager _sessionManager; + + /// + /// Initializes a new instance of the class. + /// + /// The session manager. + public PluginUninstalledNotifier(ISessionManager sessionManager) + { + _sessionManager = sessionManager; + } + + /// + public async Task OnEvent(PluginUninstalledEventArgs eventArgs) + { + await _sessionManager.SendMessageToAdminSessions("PluginUninstalled", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs new file mode 100644 index 000000000..9ce16f774 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs @@ -0,0 +1,51 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Updates; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Notifications; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Updates +{ + /// + /// Creates an entry in the activity log when a plugin is updated. + /// + public class PluginUpdatedLogger : IEventConsumer + { + private readonly ILocalizationManager _localizationManager; + private readonly IActivityManager _activityManager; + + /// + /// Initializes a new instance of the class. + /// + /// The localization manager. + /// The activity manager. + public PluginUpdatedLogger(ILocalizationManager localizationManager, IActivityManager activityManager) + { + _localizationManager = localizationManager; + _activityManager = activityManager; + } + + /// + public async Task OnEvent(PluginUpdatedEventArgs eventArgs) + { + await _activityManager.CreateAsync(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("PluginUpdatedWithName"), + eventArgs.Argument.Name), + NotificationType.PluginUpdateInstalled.ToString(), + Guid.Empty) + { + ShortOverview = string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("VersionNumber"), + eventArgs.Argument.Version), + Overview = eventArgs.Argument.Changelog + }).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs new file mode 100644 index 000000000..dc855cc36 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs @@ -0,0 +1,43 @@ +using System.Globalization; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Events.Users; +using MediaBrowser.Controller.Events; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Globalization; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Users +{ + /// + /// Creates an entry in the activity log when a user is created. + /// + public class UserCreatedLogger : IEventConsumer + { + private readonly ILocalizationManager _localizationManager; + private readonly IActivityManager _activityManager; + + /// + /// Initializes a new instance of the class. + /// + /// The localization manager. + /// The activity manager. + public UserCreatedLogger(ILocalizationManager localizationManager, IActivityManager activityManager) + { + _localizationManager = localizationManager; + _activityManager = activityManager; + } + + /// + public async Task OnEvent(UserCreatedEventArgs eventArgs) + { + await _activityManager.CreateAsync(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("UserCreatedWithName"), + eventArgs.Argument.Username), + "UserCreated", + eventArgs.Argument.Id)) + .ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs new file mode 100644 index 000000000..c68a62c81 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs @@ -0,0 +1,44 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Events.Users; +using MediaBrowser.Controller.Events; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Globalization; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Users +{ + /// + /// Adds an entry to the activity log when a user is deleted. + /// + public class UserDeletedLogger : IEventConsumer + { + private readonly ILocalizationManager _localizationManager; + private readonly IActivityManager _activityManager; + + /// + /// Initializes a new instance of the class. + /// + /// The localization manager. + /// The activity manager. + public UserDeletedLogger(ILocalizationManager localizationManager, IActivityManager activityManager) + { + _localizationManager = localizationManager; + _activityManager = activityManager; + } + + /// + public async Task OnEvent(UserDeletedEventArgs eventArgs) + { + await _activityManager.CreateAsync(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("UserDeletedWithName"), + eventArgs.Argument.Username), + "UserDeleted", + Guid.Empty)) + .ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs new file mode 100644 index 000000000..10367a939 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Events.Users; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Session; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Users +{ + /// + /// Notifies the user's sessions when a user is deleted. + /// + public class UserDeletedNotifier : IEventConsumer + { + private readonly ISessionManager _sessionManager; + + /// + /// Initializes a new instance of the class. + /// + /// The session manager. + public UserDeletedNotifier(ISessionManager sessionManager) + { + _sessionManager = sessionManager; + } + + /// + public async Task OnEvent(UserDeletedEventArgs eventArgs) + { + await _sessionManager.SendMessageToUserSessions( + new List { eventArgs.Argument.Id }, + "UserDeleted", + eventArgs.Argument.Id.ToString("N", CultureInfo.InvariantCulture), + CancellationToken.None).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs new file mode 100644 index 000000000..a31f222ee --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs @@ -0,0 +1,47 @@ +using System.Globalization; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Events.Users; +using MediaBrowser.Controller.Events; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Notifications; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Users +{ + /// + /// Creates an entry in the activity log when a user is locked out. + /// + public class UserLockedOutLogger : IEventConsumer + { + private readonly ILocalizationManager _localizationManager; + private readonly IActivityManager _activityManager; + + /// + /// Initializes a new instance of the class. + /// + /// The localization manager. + /// The activity manager. + public UserLockedOutLogger(ILocalizationManager localizationManager, IActivityManager activityManager) + { + _localizationManager = localizationManager; + _activityManager = activityManager; + } + + /// + public async Task OnEvent(UserLockedOutEventArgs eventArgs) + { + await _activityManager.CreateAsync(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("UserLockedOutWithName"), + eventArgs.Argument.Username), + NotificationType.UserLockedOut.ToString(), + eventArgs.Argument.Id) + { + LogSeverity = LogLevel.Error + }).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs new file mode 100644 index 000000000..dc8ecbf48 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs @@ -0,0 +1,43 @@ +using System.Globalization; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Events.Users; +using MediaBrowser.Controller.Events; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Globalization; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Users +{ + /// + /// Creates an entry in the activity log when a user's password is changed. + /// + public class UserPasswordChangedLogger : IEventConsumer + { + private readonly ILocalizationManager _localizationManager; + private readonly IActivityManager _activityManager; + + /// + /// Initializes a new instance of the class. + /// + /// The localization manager. + /// The activity manager. + public UserPasswordChangedLogger(ILocalizationManager localizationManager, IActivityManager activityManager) + { + _localizationManager = localizationManager; + _activityManager = activityManager; + } + + /// + public async Task OnEvent(UserPasswordChangedEventArgs eventArgs) + { + await _activityManager.CreateAsync(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localizationManager.GetLocalizedString("UserPasswordChangedWithName"), + eventArgs.Argument.Username), + "UserPasswordChanged", + eventArgs.Argument.Id)) + .ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs new file mode 100644 index 000000000..6081dd044 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Events.Users; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Session; + +namespace Jellyfin.Server.Implementations.Events.Consumers.Users +{ + /// + /// Notifies a user when their account has been updated. + /// + public class UserUpdatedNotifier : IEventConsumer + { + private readonly IUserManager _userManager; + private readonly ISessionManager _sessionManager; + + /// + /// Initializes a new instance of the class. + /// + /// The user manager. + /// The session manager. + public UserUpdatedNotifier(IUserManager userManager, ISessionManager sessionManager) + { + _userManager = userManager; + _sessionManager = sessionManager; + } + + /// + public async Task OnEvent(UserUpdatedEventArgs e) + { + await _sessionManager.SendMessageToUserSessions( + new List { e.Argument.Id }, + "UserUpdated", + _userManager.GetUserDto(e.Argument), + CancellationToken.None).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/EventManager.cs b/Jellyfin.Server.Implementations/Events/EventManager.cs new file mode 100644 index 000000000..707002442 --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/EventManager.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading.Tasks; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Events; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Implementations.Events +{ + /// + /// Handles the firing of events. + /// + public class EventManager : IEventManager + { + private readonly ILogger _logger; + private readonly IServerApplicationHost _appHost; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The application host. + public EventManager(ILogger logger, IServerApplicationHost appHost) + { + _logger = logger; + _appHost = appHost; + } + + /// + public void Publish(T eventArgs) + where T : EventArgs + { + Task.WaitAll(PublishInternal(eventArgs)); + } + + /// + public async Task PublishAsync(T eventArgs) + where T : EventArgs + { + await PublishInternal(eventArgs).ConfigureAwait(false); + } + + private async Task PublishInternal(T eventArgs) + where T : EventArgs + { + using var scope = _appHost.ServiceProvider.CreateScope(); + foreach (var service in scope.ServiceProvider.GetServices>()) + { + try + { + await service.OnEvent(eventArgs).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.LogError(e, "Uncaught exception in EventConsumer {type}: ", service.GetType()); + } + } + } + } +} diff --git a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs new file mode 100644 index 000000000..5d558189b --- /dev/null +++ b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs @@ -0,0 +1,72 @@ +using Jellyfin.Data.Events; +using Jellyfin.Data.Events.System; +using Jellyfin.Data.Events.Users; +using Jellyfin.Server.Implementations.Events.Consumers.Library; +using Jellyfin.Server.Implementations.Events.Consumers.Security; +using Jellyfin.Server.Implementations.Events.Consumers.Session; +using Jellyfin.Server.Implementations.Events.Consumers.System; +using Jellyfin.Server.Implementations.Events.Consumers.Updates; +using Jellyfin.Server.Implementations.Events.Consumers.Users; +using MediaBrowser.Common.Updates; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Session; +using MediaBrowser.Controller.Events.Updates; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.Subtitles; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Jellyfin.Server.Implementations.Events +{ + /// + /// A class containing extensions to for eventing. + /// + public static class EventingServiceCollectionExtensions + { + /// + /// Adds the event services to the service collection. + /// + /// The service collection. + public static void AddEventServices(this IServiceCollection collection) + { + // Library consumers + collection.AddScoped, SubtitleDownloadFailureLogger>(); + + // Security consumers + collection.AddScoped>, AuthenticationFailedLogger>(); + collection.AddScoped>, AuthenticationSucceededLogger>(); + + // Session consumers + collection.AddScoped, PlaybackStartLogger>(); + collection.AddScoped, PlaybackStopLogger>(); + collection.AddScoped, SessionEndedLogger>(); + collection.AddScoped, SessionStartedLogger>(); + + // System consumers + collection.AddScoped, PendingRestartNotifier>(); + collection.AddScoped, TaskCompletedLogger>(); + collection.AddScoped, TaskCompletedNotifier>(); + + // Update consumers + collection.AddScoped, PluginInstallationCancelledNotifier>(); + collection.AddScoped, PluginInstallationFailedLogger>(); + collection.AddScoped, PluginInstallationFailedNotifier>(); + collection.AddScoped, PluginInstalledLogger>(); + collection.AddScoped, PluginInstalledNotifier>(); + collection.AddScoped, PluginInstallingNotifier>(); + collection.AddScoped, PluginUninstalledLogger>(); + collection.AddScoped, PluginUninstalledNotifier>(); + collection.AddScoped, PluginUpdatedLogger>(); + + // User consumers + collection.AddScoped, UserCreatedLogger>(); + collection.AddScoped, UserDeletedLogger>(); + collection.AddScoped, UserDeletedNotifier>(); + collection.AddScoped, UserLockedOutLogger>(); + collection.AddScoped, UserPasswordChangedLogger>(); + collection.AddScoped, UserUpdatedNotifier>(); + } + } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 7d864ebc6..08e4db388 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -2,8 +2,8 @@ using System; using System.Linq; -using Jellyfin.Data; using Jellyfin.Data.Entities; +using Jellyfin.Data.Interfaces; using Microsoft.EntityFrameworkCore; namespace Jellyfin.Server.Implementations @@ -130,7 +130,7 @@ namespace Jellyfin.Server.Implementations foreach (var saveEntity in ChangeTracker.Entries() .Where(e => e.State == EntityState.Modified) .Select(entry => entry.Entity) - .OfType()) + .OfType()) { saveEntity.OnSavingChanges(); } diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs index 140853e52..1fb89c4a6 100644 --- a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs +++ b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs @@ -4,12 +4,12 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Events; namespace Jellyfin.Server.Implementations.Users { diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 11402ee05..8f04baa08 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -10,18 +10,20 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Data.Events; +using Jellyfin.Data.Events.Users; using MediaBrowser.Common; using MediaBrowser.Common.Cryptography; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Users; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -34,6 +36,7 @@ namespace Jellyfin.Server.Implementations.Users public class UserManager : IUserManager { private readonly JellyfinDbProvider _dbProvider; + private readonly IEventManager _eventManager; private readonly ICryptoProvider _cryptoProvider; private readonly INetworkManager _networkManager; private readonly IApplicationHost _appHost; @@ -49,6 +52,7 @@ namespace Jellyfin.Server.Implementations.Users /// Initializes a new instance of the class. /// /// The database provider. + /// The event manager. /// The cryptography provider. /// The network manager. /// The application host. @@ -56,6 +60,7 @@ namespace Jellyfin.Server.Implementations.Users /// The logger. public UserManager( JellyfinDbProvider dbProvider, + IEventManager eventManager, ICryptoProvider cryptoProvider, INetworkManager networkManager, IApplicationHost appHost, @@ -63,6 +68,7 @@ namespace Jellyfin.Server.Implementations.Users ILogger logger) { _dbProvider = dbProvider; + _eventManager = eventManager; _cryptoProvider = cryptoProvider; _networkManager = networkManager; _appHost = appHost; @@ -77,21 +83,9 @@ namespace Jellyfin.Server.Implementations.Users _defaultPasswordResetProvider = _passwordResetProviders.OfType().First(); } - /// - public event EventHandler>? OnUserPasswordChanged; - /// public event EventHandler>? OnUserUpdated; - /// - public event EventHandler>? OnUserCreated; - - /// - public event EventHandler>? OnUserDeleted; - - /// - public event EventHandler>? OnUserLockedOut; - /// public IEnumerable Users { @@ -234,7 +228,7 @@ namespace Jellyfin.Server.Implementations.Users dbContext.Users.Add(newUser); await dbContext.SaveChangesAsync().ConfigureAwait(false); - OnUserCreated?.Invoke(this, new GenericEventArgs(newUser)); + await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false); return newUser; } @@ -293,7 +287,8 @@ namespace Jellyfin.Server.Implementations.Users dbContext.RemoveRange(user.AccessSchedules); dbContext.Users.Remove(user); dbContext.SaveChanges(); - OnUserDeleted?.Invoke(this, new GenericEventArgs(user)); + + _eventManager.Publish(new UserDeletedEventArgs(user)); } /// @@ -319,7 +314,7 @@ namespace Jellyfin.Server.Implementations.Users await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false); await UpdateUserAsync(user).ConfigureAwait(false); - OnUserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); + await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false); } /// @@ -338,7 +333,7 @@ namespace Jellyfin.Server.Implementations.Users user.EasyPassword = newPasswordSha1; UpdateUser(user); - OnUserPasswordChanged?.Invoke(this, new GenericEventArgs(user)); + _eventManager.Publish(new UserPasswordChangedEventArgs(user)); } /// @@ -407,13 +402,13 @@ namespace Jellyfin.Server.Implementations.Users EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing), AccessSchedules = user.AccessSchedules.ToArray(), BlockedTags = user.GetPreference(PreferenceKind.BlockedTags), - EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels), + EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels)?.Select(Guid.Parse).ToArray(), EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices), - EnabledFolders = user.GetPreference(PreferenceKind.EnabledFolders), + EnabledFolders = user.GetPreference(PreferenceKind.EnabledFolders)?.Select(Guid.Parse).ToArray(), EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders), SyncPlayAccess = user.SyncPlayAccess, - BlockedChannels = user.GetPreference(PreferenceKind.BlockedChannels), - BlockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders), + BlockedChannels = user.GetPreference(PreferenceKind.BlockedChannels)?.Select(Guid.Parse).ToArray(), + BlockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders)?.Select(Guid.Parse).ToArray(), BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems).Select(Enum.Parse).ToArray() } }; @@ -740,9 +735,9 @@ namespace Jellyfin.Server.Implementations.Users PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems?.Select(i => i.ToString()).ToArray() ?? Array.Empty()); user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); - user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels); + user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray()); user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); - user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders); + user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray()); user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); dbContext.Update(user); @@ -901,7 +896,7 @@ namespace Jellyfin.Server.Implementations.Users if (maxInvalidLogins.HasValue && user.InvalidLoginAttemptCount >= maxInvalidLogins) { user.SetPermission(PermissionKind.IsDisabled, true); - OnUserLockedOut?.Invoke(this, new GenericEventArgs(user)); + await _eventManager.PublishAsync(new UserLockedOutEventArgs(user)).ConfigureAwait(false); _logger.LogWarning( "Disabling user {Username} due to {Attempts} unsuccessful login attempts.", user.Username, diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 29a59e1c8..755844dd9 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -1,20 +1,20 @@ using System; using System.Collections.Generic; -using System.IO; using System.Reflection; using Emby.Drawing; using Emby.Server.Implementations; using Jellyfin.Drawing.Skia; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; +using Jellyfin.Server.Implementations.Events; using Jellyfin.Server.Implementations.Users; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Activity; using MediaBrowser.Model.IO; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -33,30 +33,33 @@ namespace Jellyfin.Server /// The to be used by the . /// The to be used by the . /// The to be used by the . + /// The to be used by the . public CoreAppHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, - INetworkManager networkManager) + INetworkManager networkManager, + IServiceCollection collection) : base( applicationPaths, loggerFactory, options, fileSystem, - networkManager) + networkManager, + collection) { } /// - protected override void RegisterServices(IServiceCollection serviceCollection) + protected override void RegisterServices() { // Register an image encoder bool useSkiaEncoder = SkiaEncoder.IsNativeLibAvailable(); Type imageEncoderType = useSkiaEncoder ? typeof(SkiaEncoder) : typeof(NullImageEncoder); - serviceCollection.AddSingleton(typeof(IImageEncoder), imageEncoderType); + ServiceCollection.AddSingleton(typeof(IImageEncoder), imageEncoderType); // Log a warning if the Skia encoder could not be used if (!useSkiaEncoder) @@ -71,13 +74,15 @@ namespace Jellyfin.Server // .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"), // ServiceLifetime.Transient); - serviceCollection.AddSingleton(); + ServiceCollection.AddEventServices(); + ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); + ServiceCollection.AddSingleton(); - base.RegisterServices(serviceCollection); + base.RegisterServices(); } /// diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index c9539681b..2b002c6f3 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,3 +1,4 @@ +using Jellyfin.Server.Middleware; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; @@ -48,5 +49,55 @@ namespace Jellyfin.Server.Extensions c.InjectStylesheet($"/{baseUrl}api-docs/redoc/custom.css"); }); } + + /// + /// Adds IP based access validation to the application pipeline. + /// + /// The application builder. + /// The updated application builder. + public static IApplicationBuilder UseIpBasedAccessValidation(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware(); + } + + /// + /// Adds LAN based access filtering to the application pipeline. + /// + /// The application builder. + /// The updated application builder. + public static IApplicationBuilder UseLanFiltering(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware(); + } + + /// + /// Adds base url redirection to the application pipeline. + /// + /// The application builder. + /// The updated application builder. + public static IApplicationBuilder UseBaseUrlRedirection(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware(); + } + + /// + /// Adds a custom message during server startup to the application pipeline. + /// + /// The application builder. + /// The updated application builder. + public static IApplicationBuilder UseServerStartupMessage(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware(); + } + + /// + /// Adds a WebSocket request handler to the application pipeline. + /// + /// The application builder. + /// The updated application builder. + public static IApplicationBuilder UseWebSocketHandler(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware(); + } } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 2e2bfea68..0160a05f9 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -18,6 +18,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Server.Formatters; using Jellyfin.Server.Models; +using MediaBrowser.Common; using MediaBrowser.Common.Json; using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authentication; @@ -135,10 +136,11 @@ namespace Jellyfin.Server.Extensions /// /// The service collection. /// The base url for the API. + /// An IEnumberable containing all plugin assemblies with API controllers. /// The MVC builder. - public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl) + public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl, IEnumerable pluginAssemblies) { - return serviceCollection + IMvcBuilder mvcBuilder = serviceCollection .AddCors(options => { options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy); @@ -168,6 +170,9 @@ namespace Jellyfin.Server.Extensions // From JsonDefaults options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling; options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented; + options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition; + options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling; + options.JsonSerializerOptions.Converters.Clear(); foreach (var converter in jsonOptions.Converters) { @@ -176,8 +181,14 @@ namespace Jellyfin.Server.Extensions // From JsonDefaults.PascalCase options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy; - }) - .AddControllersAsServices(); + }); + + foreach (Assembly pluginAssembly in pluginAssemblies) + { + mvcBuilder.AddApplicationPart(pluginAssembly); + } + + return mvcBuilder.AddControllersAsServices(); } /// @@ -194,7 +205,7 @@ namespace Jellyfin.Server.Extensions { Type = SecuritySchemeType.ApiKey, In = ParameterLocation.Header, - Name = "X-Emby-Token", + Name = "X-Emby-Authorization", Description = "API key header parameter" }); diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs new file mode 100644 index 000000000..9316737bd --- /dev/null +++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions; + +namespace Jellyfin.Server.Middleware +{ + /// + /// Redirect requests without baseurl prefix to the baseurl prefixed URL. + /// + public class BaseUrlRedirectionMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The next delegate in the pipeline. + /// The logger. + /// The application configuration. + public BaseUrlRedirectionMiddleware( + RequestDelegate next, + ILogger logger, + IConfiguration configuration) + { + _next = next; + _logger = logger; + _configuration = configuration; + } + + /// + /// Executes the middleware action. + /// + /// The current HTTP context. + /// The server configuration manager. + /// The async task. + public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager) + { + var localPath = httpContext.Request.Path.ToString(); + var baseUrlPrefix = serverConfigurationManager.Configuration.BaseUrl; + + if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) + || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase) + || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) + || string.IsNullOrEmpty(localPath) + || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase)) + { + // Always redirect back to the default path if the base prefix is invalid or missing + _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath); + httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]); + return; + } + + await _next(httpContext).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs new file mode 100644 index 000000000..59b5fb1ed --- /dev/null +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -0,0 +1,76 @@ +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Server.Middleware +{ + /// + /// Validates the IP of requests coming from local networks wrt. remote access. + /// + public class IpBasedAccessValidationMiddleware + { + private readonly RequestDelegate _next; + + /// + /// Initializes a new instance of the class. + /// + /// The next delegate in the pipeline. + public IpBasedAccessValidationMiddleware(RequestDelegate next) + { + _next = next; + } + + /// + /// Executes the middleware action. + /// + /// The current HTTP context. + /// The network manager. + /// The server configuration manager. + /// The async task. + public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) + { + if (httpContext.Request.IsLocal()) + { + await _next(httpContext).ConfigureAwait(false); + return; + } + + var remoteIp = httpContext.Request.RemoteIp(); + + if (serverConfigurationManager.Configuration.EnableRemoteAccess) + { + var addressFilter = serverConfigurationManager.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + + if (addressFilter.Length > 0 && !networkManager.IsInLocalNetwork(remoteIp)) + { + if (serverConfigurationManager.Configuration.IsRemoteIPFilterBlacklist) + { + if (networkManager.IsAddressInSubnets(remoteIp, addressFilter)) + { + return; + } + } + else + { + if (!networkManager.IsAddressInSubnets(remoteIp, addressFilter)) + { + return; + } + } + } + } + else + { + if (!networkManager.IsInLocalNetwork(remoteIp)) + { + return; + } + } + + await _next(httpContext).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs new file mode 100644 index 000000000..9d795145a --- /dev/null +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Server.Middleware +{ + /// + /// Validates the LAN host IP based on application configuration. + /// + public class LanFilteringMiddleware + { + private readonly RequestDelegate _next; + + /// + /// Initializes a new instance of the class. + /// + /// The next delegate in the pipeline. + public LanFilteringMiddleware(RequestDelegate next) + { + _next = next; + } + + /// + /// Executes the middleware action. + /// + /// The current HTTP context. + /// The network manager. + /// The server configuration manager. + /// The async task. + public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) + { + var currentHost = httpContext.Request.Host.ToString(); + var hosts = serverConfigurationManager + .Configuration + .LocalNetworkAddresses + .Select(NormalizeConfiguredLocalAddress) + .ToList(); + + if (hosts.Count == 0) + { + await _next(httpContext).ConfigureAwait(false); + return; + } + + currentHost ??= string.Empty; + + if (networkManager.IsInPrivateAddressSpace(currentHost)) + { + hosts.Add("localhost"); + hosts.Add("127.0.0.1"); + + if (hosts.All(i => currentHost.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1)) + { + return; + } + } + + await _next(httpContext).ConfigureAwait(false); + } + + private static string NormalizeConfiguredLocalAddress(string address) + { + var add = address.AsSpan().Trim('/'); + int index = add.IndexOf('/'); + if (index != -1) + { + add = add.Slice(index + 1); + } + + return add.TrimStart('/').ToString(); + } + } +} diff --git a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs new file mode 100644 index 000000000..ea81c03a2 --- /dev/null +++ b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs @@ -0,0 +1,49 @@ +using System.Net.Mime; +using System.Threading.Tasks; +using MediaBrowser.Controller; +using MediaBrowser.Model.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Server.Middleware +{ + /// + /// Shows a custom message during server startup. + /// + public class ServerStartupMessageMiddleware + { + private readonly RequestDelegate _next; + + /// + /// Initializes a new instance of the class. + /// + /// The next delegate in the pipeline. + public ServerStartupMessageMiddleware(RequestDelegate next) + { + _next = next; + } + + /// + /// Executes the middleware action. + /// + /// The current HTTP context. + /// The server application host. + /// The localization manager. + /// The async task. + public async Task Invoke( + HttpContext httpContext, + IServerApplicationHost serverApplicationHost, + ILocalizationManager localizationManager) + { + if (serverApplicationHost.CoreStartupHasCompleted) + { + await _next(httpContext).ConfigureAwait(false); + return; + } + + var message = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); + httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; + httpContext.Response.ContentType = MediaTypeNames.Text.Html; + await httpContext.Response.WriteAsync(message, httpContext.RequestAborted).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs b/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs new file mode 100644 index 000000000..b7a5d2b34 --- /dev/null +++ b/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; +using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Server.Middleware +{ + /// + /// Handles WebSocket requests. + /// + public class WebSocketHandlerMiddleware + { + private readonly RequestDelegate _next; + + /// + /// Initializes a new instance of the class. + /// + /// The next delegate in the pipeline. + public WebSocketHandlerMiddleware(RequestDelegate next) + { + _next = next; + } + + /// + /// Executes the middleware action. + /// + /// The current HTTP context. + /// The WebSocket connection manager. + /// The async task. + public async Task Invoke(HttpContext httpContext, IWebSocketManager webSocketManager) + { + if (!httpContext.WebSockets.IsWebSocketRequest) + { + await _next(httpContext).ConfigureAwait(false); + return; + } + + await webSocketManager.WebSocketRequestHandler(httpContext).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index b15ccf01e..7f57358ec 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -81,6 +81,11 @@ namespace Jellyfin.Server.Migrations.Routines foreach (var result in results) { var dto = JsonSerializer.Deserialize(result[3].ToString(), _jsonOptions); + if (dto == null) + { + continue; + } + var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version) ? chromecastDict[version] : ChromecastVersion.Stable; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index 274e6ab73..74c550331 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -1,5 +1,7 @@ using System; +using System.Globalization; using System.IO; +using System.Linq; using Emby.Server.Implementations.Data; using Emby.Server.Implementations.Serialization; using Jellyfin.Data.Entities; @@ -74,7 +76,12 @@ namespace Jellyfin.Server.Migrations.Routines foreach (var entry in queryResult) { - UserMockup mockup = JsonSerializer.Deserialize(entry[2].ToBlob(), JsonDefaults.GetOptions()); + UserMockup? mockup = JsonSerializer.Deserialize(entry[2].ToBlob(), JsonDefaults.GetOptions()); + if (mockup == null) + { + continue; + } + var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name); var config = File.Exists(Path.Combine(userDataDir, "config.xml")) @@ -161,9 +168,9 @@ namespace Jellyfin.Server.Migrations.Routines } user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); - user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels); + user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray()); user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); - user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders); + user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray()); user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews); user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index f6ac4e2a3..b9a90f9db 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -11,7 +11,6 @@ using System.Threading; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; -using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Networking; using Jellyfin.Api.Controllers; @@ -28,6 +27,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Serilog; using Serilog.Extensions.Logging; using SQLitePCL; +using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Jellyfin.Server @@ -154,13 +154,15 @@ namespace Jellyfin.Server ApplicationHost.LogEnvironmentInfo(_logger, appPaths); PerformStaticInitialization(); + var serviceCollection = new ServiceCollection(); var appHost = new CoreAppHost( appPaths, _loggerFactory, options, new ManagedFileSystem(_loggerFactory.CreateLogger(), appPaths), - new NetworkManager(_loggerFactory.CreateLogger())); + new NetworkManager(_loggerFactory.CreateLogger()), + serviceCollection); try { @@ -178,8 +180,7 @@ namespace Jellyfin.Server } } - ServiceCollection serviceCollection = new ServiceCollection(); - appHost.Init(serviceCollection); + appHost.Init(); var webHost = new WebHostBuilder().ConfigureWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build(); @@ -593,7 +594,7 @@ namespace Jellyfin.Server var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; if (startupConfig != null && !startupConfig.HostWebClient()) { - inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/swagger"; + inMemoryDefaultConfig[ConfigurationExtensions.DefaultRedirectKey] = "api-docs/swagger"; } return config diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index c0ddd9af3..6ec052e25 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -1,7 +1,12 @@ -using System.Net.Http; +using System; +using System.ComponentModel; +using System.Net.Http.Headers; +using Jellyfin.Api.TypeConverters; using Jellyfin.Server.Extensions; using Jellyfin.Server.Middleware; using Jellyfin.Server.Models; +using MediaBrowser.Common; +using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; @@ -18,14 +23,19 @@ namespace Jellyfin.Server public class Startup { private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IServerApplicationHost _serverApplicationHost; /// /// Initializes a new instance of the class. /// /// The server configuration manager. - public Startup(IServerConfigurationManager serverConfigurationManager) + /// The server application host. + public Startup( + IServerConfigurationManager serverConfigurationManager, + IServerApplicationHost serverApplicationHost) { _serverConfigurationManager = serverConfigurationManager; + _serverApplicationHost = serverApplicationHost; } /// @@ -36,7 +46,13 @@ namespace Jellyfin.Server { services.AddResponseCompression(); services.AddHttpContextAccessor(); - services.AddJellyfinApi(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/')); + services.AddHttpsRedirection(options => + { + options.HttpsPort = _serverApplicationHost.HttpsPort; + }); + services.AddJellyfinApi( + _serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'), + _serverApplicationHost.GetApiPluginAssemblies()); services.AddJellyfinApiSwagger(); @@ -44,7 +60,23 @@ namespace Jellyfin.Server services.AddCustomAuthentication(); services.AddJellyfinApiAuthorization(); - services.AddHttpClient(); + + var productHeader = new ProductInfoHeaderValue( + _serverApplicationHost.Name.Replace(' ', '-'), + _serverApplicationHost.ApplicationVersionString); + services + .AddHttpClient(NamedClient.Default, c => + { + c.DefaultRequestHeaders.UserAgent.Add(productHeader); + }) + .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler()); + + services.AddHttpClient(NamedClient.MusicBrainz, c => + { + c.DefaultRequestHeaders.UserAgent.Add(productHeader); + c.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"({_serverApplicationHost.ApplicationUserAgentAddress})")); + }) + .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler()); } /// @@ -52,11 +84,9 @@ namespace Jellyfin.Server /// /// The application builder. /// The webhost environment. - /// The server application host. public void Configure( IApplicationBuilder app, - IWebHostEnvironment env, - IServerApplicationHost serverApplicationHost) + IWebHostEnvironment env) { if (env.IsDevelopment()) { @@ -71,13 +101,18 @@ namespace Jellyfin.Server app.UseResponseCompression(); - // TODO app.UseMiddleware(); + app.UseCors(ServerCorsPolicy.DefaultPolicyName); + + if (_serverConfigurationManager.Configuration.RequireHttps + && _serverApplicationHost.ListenWithHttps) + { + app.UseHttpsRedirection(); + } app.UseStaticFiles(); app.UseAuthentication(); app.UseJellyfinApiSwagger(_serverConfigurationManager); app.UseRouting(); - app.UseCors(ServerCorsPolicy.DefaultPolicyName); app.UseAuthorization(); if (_serverConfigurationManager.Configuration.EnableMetrics) { @@ -85,6 +120,12 @@ namespace Jellyfin.Server app.UseHttpMetrics(); } + app.UseLanFiltering(); + app.UseIpBasedAccessValidation(); + app.UseBaseUrlRedirection(); + app.UseWebSocketHandler(); + app.UseServerStartupMessage(); + app.UseEndpoints(endpoints => { endpoints.MapControllers(); @@ -94,7 +135,8 @@ namespace Jellyfin.Server } }); - app.Use(serverApplicationHost.ExecuteHttpHandlerAsync); + // Add type descriptor for legacy datetime parsing. + TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter))); } } } diff --git a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs index d746207c7..e0cf3f9ac 100644 --- a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs +++ b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Model.Services; +using System.Net; +using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Common.Extensions @@ -8,26 +9,55 @@ namespace MediaBrowser.Common.Extensions /// public static class HttpContextExtensions { - private const string ServiceStackRequest = "ServiceStackRequest"; - /// - /// Set the ServiceStack request. + /// Checks the origin of the HTTP request. /// - /// The HttpContext instance. - /// The service stack request instance. - public static void SetServiceStackRequest(this HttpContext httpContext, IRequest request) + /// The incoming HTTP request. + /// true if the request is coming from LAN, false otherwise. + public static bool IsLocal(this HttpRequest request) { - httpContext.Items[ServiceStackRequest] = request; + return (request.HttpContext.Connection.LocalIpAddress == null + && request.HttpContext.Connection.RemoteIpAddress == null) + || request.HttpContext.Connection.LocalIpAddress.Equals(request.HttpContext.Connection.RemoteIpAddress); } /// - /// Get the ServiceStack request. + /// Extracts the remote IP address of the caller of the HTTP request. /// - /// The HttpContext instance. - /// The service stack request instance. - public static IRequest GetServiceStackRequest(this HttpContext httpContext) + /// The HTTP request. + /// The remote caller IP address. + public static string RemoteIp(this HttpRequest request) { - return (IRequest)httpContext.Items[ServiceStackRequest]; + var cachedRemoteIp = request.HttpContext.Items["RemoteIp"]?.ToString(); + if (!string.IsNullOrEmpty(cachedRemoteIp)) + { + return cachedRemoteIp; + } + + IPAddress ip; + + // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip + // (if the server is behind a reverse proxy for example) + if (!IPAddress.TryParse(request.Headers[CustomHeaderNames.XForwardedFor].ToString(), out ip)) + { + if (!IPAddress.TryParse(request.Headers[CustomHeaderNames.XRealIP].ToString(), out ip)) + { + ip = request.HttpContext.Connection.RemoteIpAddress; + + // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests) + ip ??= IPAddress.Loopback; + } + } + + if (ip.IsIPv4MappedToIPv6) + { + ip = ip.MapToIPv4(); + } + + var normalizedIp = ip.ToString(); + + request.HttpContext.Items["RemoteIp"] = normalizedIp; + return normalizedIp; } } } diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index e8d9282e4..849037ac4 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Reflection; using System.Threading.Tasks; using MediaBrowser.Common.Plugins; using Microsoft.Extensions.DependencyInjection; @@ -76,6 +77,12 @@ namespace MediaBrowser.Common /// The plugins. IReadOnlyList Plugins { get; } + /// + /// Gets all plugin assemblies which implement a custom rest api. + /// + /// An containing the plugin assemblies. + IEnumerable GetApiPluginAssemblies(); + /// /// Notifies the pending restart. /// @@ -116,8 +123,7 @@ namespace MediaBrowser.Common /// /// Initializes this instance. /// - /// The service collection. - void Init(IServiceCollection serviceCollection); + void Init(); /// /// Creates the instance. diff --git a/MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs b/MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs deleted file mode 100644 index 56c0ecbe9..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonDoubleConverter.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Buffers; -using System.Buffers.Text; -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Double to String JSON converter. - /// Web client send quoted doubles. - /// - public class JsonDoubleConverter : JsonConverter - { - /// - /// Read JSON string as double. - /// - /// . - /// Type. - /// Options. - /// Parsed value. - public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.String) - { - // try to parse number directly from bytes - var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; - if (Utf8Parser.TryParse(span, out double number, out var bytesConsumed) && span.Length == bytesConsumed) - { - return number; - } - - // try to parse from a string if the above failed, this covers cases with other escaped/UTF characters - if (double.TryParse(reader.GetString(), out number)) - { - return number; - } - } - - // fallback to default handling - return reader.GetDouble(); - } - - /// - /// Write double to JSON string. - /// - /// . - /// Value to write. - /// Options. - public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) - { - writer.WriteNumberValue(value); - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs b/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs deleted file mode 100644 index 70c375b8c..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Buffers; -using System.Buffers.Text; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Converts a GUID object or value to/from JSON. - /// - public class JsonInt32Converter : JsonConverter - { - /// - public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.String) - { - ReadOnlySpan span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; - if (Utf8Parser.TryParse(span, out int number, out int bytesConsumed) && span.Length == bytesConsumed) - { - return number; - } - - if (int.TryParse(reader.GetString(), out number)) - { - return number; - } - } - - return reader.GetInt32(); - } - - /// - public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) - { - writer.WriteNumberValue(value); - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs b/MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs deleted file mode 100644 index 427f1fa7e..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonInt64Converter.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Buffers; -using System.Buffers.Text; -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Parse JSON string as long. - /// Javascript does not support 64-bit integers. - /// - public class JsonInt64Converter : JsonConverter - { - /// - /// Read JSON string as int64. - /// - /// . - /// Type. - /// Options. - /// Parsed value. - public override long Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.String) - { - // try to parse number directly from bytes - var span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan; - if (Utf8Parser.TryParse(span, out long number, out var bytesConsumed) && span.Length == bytesConsumed) - { - return number; - } - - // try to parse from a string if the above failed, this covers cases with other escaped/UTF characters - if (long.TryParse(reader.GetString(), out number)) - { - return number; - } - } - - // fallback to default handling - return reader.GetInt64(); - } - - /// - /// Write long to JSON long. - /// - /// . - /// Value to write. - /// Options. - public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options) - { - writer.WriteNumberValue(value); - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs deleted file mode 100644 index 8053461f0..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs +++ /dev/null @@ -1,82 +0,0 @@ -#nullable enable - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// Converter for Dictionaries without string key. - /// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys. - /// - /// Type of key. - /// Type of value. - internal sealed class JsonNonStringKeyDictionaryConverter : JsonConverter> - { - /// - /// Read JSON. - /// - /// The Utf8JsonReader. - /// The type to convert. - /// The json serializer options. - /// Typed dictionary. - /// Dictionary key type not supported. - public override IDictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var convertedType = typeof(Dictionary<,>).MakeGenericType(typeof(string), typeToConvert.GenericTypeArguments[1]); - var value = JsonSerializer.Deserialize(ref reader, convertedType, options); - var instance = (Dictionary)Activator.CreateInstance( - typeToConvert, - BindingFlags.Instance | BindingFlags.Public, - null, - null, - CultureInfo.CurrentCulture); - var enumerator = (IEnumerator)convertedType.GetMethod("GetEnumerator")!.Invoke(value, null); - var parse = typeof(TKey).GetMethod( - "Parse", - 0, - BindingFlags.Public | BindingFlags.Static, - null, - CallingConventions.Any, - new[] { typeof(string) }, - null); - if (parse == null) - { - throw new NotSupportedException($"{typeof(TKey)} as TKey in IDictionary is not supported."); - } - - while (enumerator.MoveNext()) - { - var element = (KeyValuePair)enumerator.Current; - instance.Add((TKey)parse.Invoke(null, new[] { (object?)element.Key }), element.Value); - } - - return instance; - } - - /// - /// Write dictionary as Json. - /// - /// The Utf8JsonWriter. - /// The dictionary value. - /// The Json serializer options. - public override void Write(Utf8JsonWriter writer, IDictionary value, JsonSerializerOptions options) - { - var convertedDictionary = new Dictionary(value.Count); - foreach (var (k, v) in value) - { - if (k != null) - { - convertedDictionary[k.ToString()] = v; - } - } - - JsonSerializer.Serialize(writer, convertedDictionary, options); - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs deleted file mode 100644 index 52f360740..000000000 --- a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs +++ /dev/null @@ -1,59 +0,0 @@ -#nullable enable - -using System; -using System.Collections; -using System.Globalization; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Common.Json.Converters -{ - /// - /// https://github.com/dotnet/runtime/issues/30524#issuecomment-524619972. - /// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys. - /// - internal sealed class JsonNonStringKeyDictionaryConverterFactory : JsonConverterFactory - { - /// - /// Only convert objects that implement IDictionary and do not have string keys. - /// - /// Type convert. - /// Conversion ability. - public override bool CanConvert(Type typeToConvert) - { - if (!typeToConvert.IsGenericType) - { - return false; - } - - // Let built in converter handle string keys - if (typeToConvert.GenericTypeArguments[0] == typeof(string)) - { - return false; - } - - // Only support objects that implement IDictionary - return typeToConvert.GetInterface(nameof(IDictionary)) != null; - } - - /// - /// Create converter for generic dictionary type. - /// - /// Type to convert. - /// Json serializer options. - /// JsonConverter for given type. - public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) - { - var converterType = typeof(JsonNonStringKeyDictionaryConverter<,>) - .MakeGenericType(typeToConvert.GenericTypeArguments[0], typeToConvert.GenericTypeArguments[1]); - var converter = (JsonConverter)Activator.CreateInstance( - converterType, - BindingFlags.Instance | BindingFlags.Public, - null, - null, - CultureInfo.CurrentCulture); - return converter; - } - } -} diff --git a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs new file mode 100644 index 000000000..cffc41ba3 --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs @@ -0,0 +1,44 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Common.Json.Converters +{ + /// + /// Converts a nullable struct or value to/from JSON. + /// Required - some clients send an empty string. + /// + /// The struct type. + public class JsonNullableStructConverter : JsonConverter + where T : struct + { + private readonly JsonConverter _baseJsonConverter; + + /// + /// Initializes a new instance of the class. + /// + /// The base json converter. + public JsonNullableStructConverter(JsonConverter baseJsonConverter) + { + _baseJsonConverter = baseJsonConverter; + } + + /// + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Handle empty string. + if (reader.TokenType == JsonTokenType.String && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty) || reader.ValueSpan.IsEmpty)) + { + return null; + } + + return _baseJsonConverter.Read(ref reader, typeToConvert, options); + } + + /// + public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) + { + _baseJsonConverter.Write(writer, value, options); + } + } +} diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index 36ab6d900..5867cd4a0 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -24,15 +24,19 @@ namespace MediaBrowser.Common.Json var options = new JsonSerializerOptions { ReadCommentHandling = JsonCommentHandling.Disallow, - WriteIndented = false + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + NumberHandling = JsonNumberHandling.AllowReadingFromString }; + // Get built-in converters for fallback converting. + var baseNullableInt32Converter = (JsonConverter)options.GetConverter(typeof(int?)); + var baseNullableInt64Converter = (JsonConverter)options.GetConverter(typeof(long?)); + options.Converters.Add(new JsonGuidConverter()); - options.Converters.Add(new JsonInt32Converter()); options.Converters.Add(new JsonStringEnumConverter()); - options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory()); - options.Converters.Add(new JsonInt64Converter()); - options.Converters.Add(new JsonDoubleConverter()); + options.Converters.Add(new JsonNullableStructConverter(baseNullableInt32Converter)); + options.Converters.Add(new JsonNullableStructConverter(baseNullableInt64Converter)); return options; } diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 7380f39fd..6e258371c 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -8,8 +8,9 @@ Jellyfin Contributors Jellyfin.Common - https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt + 10.7.0 https://github.com/jellyfin/jellyfin + GPL-3.0-only @@ -17,6 +18,7 @@ + @@ -31,6 +33,15 @@ false true true + true + true + true + snupkg + + + + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb diff --git a/MediaBrowser.Common/Net/DefaultHttpClientHandler.cs b/MediaBrowser.Common/Net/DefaultHttpClientHandler.cs new file mode 100644 index 000000000..e189d6e70 --- /dev/null +++ b/MediaBrowser.Common/Net/DefaultHttpClientHandler.cs @@ -0,0 +1,20 @@ +using System.Net; +using System.Net.Http; + +namespace MediaBrowser.Common.Net +{ + /// + /// Default http client handler. + /// + public class DefaultHttpClientHandler : HttpClientHandler + { + /// + /// Initializes a new instance of the class. + /// + public DefaultHttpClientHandler() + { + // TODO change to DecompressionMethods.All with .NET5 + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + } + } +} diff --git a/MediaBrowser.Common/Net/NamedClient.cs b/MediaBrowser.Common/Net/NamedClient.cs new file mode 100644 index 000000000..0f6161c32 --- /dev/null +++ b/MediaBrowser.Common/Net/NamedClient.cs @@ -0,0 +1,18 @@ +namespace MediaBrowser.Common.Net +{ + /// + /// Registered http client names. + /// + public static class NamedClient + { + /// + /// Gets the value for the default named http client. + /// + public const string Default = nameof(Default); + + /// + /// Gets the value for the MusicBrainz named http client. + /// + public const string MusicBrainz = nameof(MusicBrainz); + } +} diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs index f10a1918f..4b2918d08 100644 --- a/MediaBrowser.Common/Plugins/BasePlugin.cs +++ b/MediaBrowser.Common/Plugins/BasePlugin.cs @@ -6,6 +6,7 @@ using System.Reflection; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; +using Microsoft.Extensions.DependencyInjection; namespace MediaBrowser.Common.Plugins { @@ -81,6 +82,16 @@ namespace MediaBrowser.Common.Plugins { } + /// + public virtual void RegisterServices(IServiceCollection serviceCollection) + { + } + + /// + public virtual void UnregisterServices(IServiceCollection serviceCollection) + { + } + /// public void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion) { diff --git a/MediaBrowser.Common/Plugins/IPlugin.cs b/MediaBrowser.Common/Plugins/IPlugin.cs index 7bd37d210..1844eb124 100644 --- a/MediaBrowser.Common/Plugins/IPlugin.cs +++ b/MediaBrowser.Common/Plugins/IPlugin.cs @@ -2,6 +2,7 @@ using System; using MediaBrowser.Model.Plugins; +using Microsoft.Extensions.DependencyInjection; namespace MediaBrowser.Common.Plugins { @@ -61,6 +62,18 @@ namespace MediaBrowser.Common.Plugins /// Called when just before the plugin is uninstalled from the server. /// void OnUninstalling(); + + /// + /// Registers the plugin's services to the service collection. + /// + /// The service collection. + void RegisterServices(IServiceCollection serviceCollection); + + /// + /// Unregisters the plugin's services from the service collection. + /// + /// The service collection. + void UnregisterServices(IServiceCollection serviceCollection); } public interface IHasPluginConfiguration diff --git a/MediaBrowser.Common/Updates/InstallationEventArgs.cs b/MediaBrowser.Common/Updates/InstallationEventArgs.cs index 11eb2ad34..61178f631 100644 --- a/MediaBrowser.Common/Updates/InstallationEventArgs.cs +++ b/MediaBrowser.Common/Updates/InstallationEventArgs.cs @@ -1,10 +1,11 @@ #pragma warning disable CS1591 +using System; using MediaBrowser.Model.Updates; namespace MediaBrowser.Common.Updates { - public class InstallationEventArgs + public class InstallationEventArgs : EventArgs { public InstallationInfo InstallationInfo { get; set; } diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs index 15c902777..ecdffa2eb 100644 --- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs +++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Model.Users; @@ -11,7 +13,9 @@ namespace MediaBrowser.Controller.Authentication bool IsEnabled { get; } Task Authenticate(string username, string password); + bool HasPassword(User user); + Task ChangePassword(User user, string newPassword); } diff --git a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs index 693df80ac..6729b9115 100644 --- a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs +++ b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Threading.Tasks; using Jellyfin.Data.Entities; @@ -12,6 +14,7 @@ namespace MediaBrowser.Controller.Authentication bool IsEnabled { get; } Task StartForgotPasswordProcess(User user, bool isInNetwork); + Task RedeemPasswordResetPin(string pin); } diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs index dbb047804..129cdb519 100644 --- a/MediaBrowser.Controller/Channels/Channel.cs +++ b/MediaBrowser.Controller/Channels/Channel.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Globalization; using System.Linq; diff --git a/MediaBrowser.Controller/Channels/ChannelItemInfo.cs b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs index 00d4d9cb3..476992cbd 100644 --- a/MediaBrowser.Controller/Channels/ChannelItemInfo.cs +++ b/MediaBrowser.Controller/Channels/ChannelItemInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Channels/ChannelItemResult.cs b/MediaBrowser.Controller/Channels/ChannelItemResult.cs index c194c8e48..cee7b2003 100644 --- a/MediaBrowser.Controller/Channels/ChannelItemResult.cs +++ b/MediaBrowser.Controller/Channels/ChannelItemResult.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; namespace MediaBrowser.Controller.Channels diff --git a/MediaBrowser.Controller/Channels/ChannelItemType.cs b/MediaBrowser.Controller/Channels/ChannelItemType.cs index 833ae75fe..3ce920e23 100644 --- a/MediaBrowser.Controller/Channels/ChannelItemType.cs +++ b/MediaBrowser.Controller/Channels/ChannelItemType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Channels { public enum ChannelItemType diff --git a/MediaBrowser.Controller/Channels/ChannelParentalRating.cs b/MediaBrowser.Controller/Channels/ChannelParentalRating.cs index 1d189446d..f77d81c16 100644 --- a/MediaBrowser.Controller/Channels/ChannelParentalRating.cs +++ b/MediaBrowser.Controller/Channels/ChannelParentalRating.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Channels { public enum ChannelParentalRating diff --git a/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs b/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs index c02055b90..32469d4d7 100644 --- a/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs +++ b/MediaBrowser.Controller/Channels/ChannelSearchInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Channels { public class ChannelSearchInfo diff --git a/MediaBrowser.Controller/Channels/IChannel.cs b/MediaBrowser.Controller/Channels/IChannel.cs index c44e20d1a..2c0eadf95 100644 --- a/MediaBrowser.Controller/Channels/IChannel.cs +++ b/MediaBrowser.Controller/Channels/IChannel.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs index df508fbe2..9a9d22d33 100644 --- a/MediaBrowser.Controller/Channels/IChannelManager.cs +++ b/MediaBrowser.Controller/Channels/IChannelManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; @@ -31,6 +33,7 @@ namespace MediaBrowser.Controller.Channels ChannelFeatures[] GetAllChannelFeatures(); bool EnableMediaSourceDisplay(BaseItem item); + bool CanDelete(BaseItem item); Task DeleteItem(BaseItem item); diff --git a/MediaBrowser.Controller/Channels/IHasCacheKey.cs b/MediaBrowser.Controller/Channels/IHasCacheKey.cs index d86ad0dba..bf895a0ec 100644 --- a/MediaBrowser.Controller/Channels/IHasCacheKey.cs +++ b/MediaBrowser.Controller/Channels/IHasCacheKey.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Channels { public interface IHasCacheKey diff --git a/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs b/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs index deee46ff7..589295543 100644 --- a/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs +++ b/MediaBrowser.Controller/Channels/IRequiresMediaInfoCallback.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Channels/ISearchableChannel.cs b/MediaBrowser.Controller/Channels/ISearchableChannel.cs index 48043ad7a..b627ca1c2 100644 --- a/MediaBrowser.Controller/Channels/ISearchableChannel.cs +++ b/MediaBrowser.Controller/Channels/ISearchableChannel.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -30,6 +32,7 @@ namespace MediaBrowser.Controller.Channels public interface ISupportsDelete { bool CanDelete(BaseItem item); + Task DeleteItem(string id, CancellationToken cancellationToken); } diff --git a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs index 1f4a26064..1074ce435 100644 --- a/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs +++ b/MediaBrowser.Controller/Channels/InternalChannelFeatures.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Model.Channels; @@ -5,6 +7,14 @@ namespace MediaBrowser.Controller.Channels { public class InternalChannelFeatures { + public InternalChannelFeatures() + { + MediaTypes = new List(); + ContentTypes = new List(); + + DefaultSortFields = new List(); + } + /// /// Gets or sets the media types. /// @@ -48,13 +58,5 @@ namespace MediaBrowser.Controller.Channels /// /// true if [supports downloading]; otherwise, false. public bool SupportsContentDownloading { get; set; } - - public InternalChannelFeatures() - { - MediaTypes = new List(); - ContentTypes = new List(); - - DefaultSortFields = new List(); - } } } diff --git a/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs b/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs index 6d5ca75af..7e9bb28ed 100644 --- a/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs +++ b/MediaBrowser.Controller/Channels/InternalChannelItemQuery.cs @@ -1,7 +1,8 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Channels; - namespace MediaBrowser.Controller.Channels { public class InternalChannelItemQuery diff --git a/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs index 1e7549d2b..f6037d05e 100644 --- a/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs +++ b/MediaBrowser.Controller/Collections/CollectionCreationOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Entities; @@ -6,6 +8,13 @@ namespace MediaBrowser.Controller.Collections { public class CollectionCreationOptions : IHasProviderIds { + public CollectionCreationOptions() + { + ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); + ItemIdList = Array.Empty(); + UserIds = Array.Empty(); + } + public string Name { get; set; } public Guid? ParentId { get; set; } @@ -17,12 +26,5 @@ namespace MediaBrowser.Controller.Collections public string[] ItemIdList { get; set; } public Guid[] UserIds { get; set; } - - public CollectionCreationOptions() - { - ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); - ItemIdList = Array.Empty(); - UserIds = Array.Empty(); - } } } diff --git a/MediaBrowser.Controller/Collections/CollectionEvents.cs b/MediaBrowser.Controller/Collections/CollectionEvents.cs index bfc6af463..ce59b4ada 100644 --- a/MediaBrowser.Controller/Collections/CollectionEvents.cs +++ b/MediaBrowser.Controller/Collections/CollectionEvents.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs index 701423c0f..a6991e2ea 100644 --- a/MediaBrowser.Controller/Collections/ICollectionManager.cs +++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs @@ -1,5 +1,8 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -27,24 +30,23 @@ namespace MediaBrowser.Controller.Collections /// Creates the collection. /// /// The options. - BoxSet CreateCollection(CollectionCreationOptions options); + Task CreateCollectionAsync(CollectionCreationOptions options); /// /// Adds to collection. /// /// The collection identifier. /// The item ids. - void AddToCollection(Guid collectionId, IEnumerable itemIds); + /// representing the asynchronous operation. + Task AddToCollectionAsync(Guid collectionId, IEnumerable itemIds); /// /// Removes from collection. /// /// The collection identifier. /// The item ids. - void RemoveFromCollection(Guid collectionId, IEnumerable itemIds); - - void AddToCollection(Guid collectionId, IEnumerable itemIds); - void RemoveFromCollection(Guid collectionId, IEnumerable itemIds); + /// A representing the asynchronous operation. + Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable itemIds); /// /// Collapses the items within box sets. diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 7d279230b..8f0872dba 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -1,7 +1,9 @@ +#pragma warning disable CS1591 + using System; using Jellyfin.Data.Entities; +using Jellyfin.Data.Events; using MediaBrowser.Model.Devices; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; @@ -9,6 +11,8 @@ namespace MediaBrowser.Controller.Devices { public interface IDeviceManager { + event EventHandler>> DeviceOptionsUpdated; + /// /// Saves the capabilities. /// @@ -44,7 +48,7 @@ namespace MediaBrowser.Controller.Devices bool CanAccessDevice(User user, string deviceId); void UpdateDeviceOptions(string deviceId, DeviceOptions options); + DeviceOptions GetDeviceOptions(string deviceId); - event EventHandler>> DeviceOptionsUpdated; } } diff --git a/MediaBrowser.Controller/Dlna/IDlnaManager.cs b/MediaBrowser.Controller/Dlna/IDlnaManager.cs index 41a7686a3..dc2d5a356 100644 --- a/MediaBrowser.Controller/Dlna/IDlnaManager.cs +++ b/MediaBrowser.Controller/Dlna/IDlnaManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Dlna; diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index e09ccd204..f9b2e6fef 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Drawing; diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index 69d799165..b7edb1052 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; diff --git a/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs index 3f762fad0..fe0465d0d 100644 --- a/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageCollageOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Drawing { public class ImageCollageOptions @@ -7,16 +9,19 @@ namespace MediaBrowser.Controller.Drawing /// /// The input paths. public string[] InputPaths { get; set; } + /// /// Gets or sets the output path. /// /// The output path. public string OutputPath { get; set; } + /// /// Gets or sets the width. /// /// The width. public int Width { get; set; } + /// /// Gets or sets the height. /// diff --git a/MediaBrowser.Controller/Drawing/ImageHelper.cs b/MediaBrowser.Controller/Drawing/ImageHelper.cs index e1273fe7f..87c28d577 100644 --- a/MediaBrowser.Controller/Drawing/ImageHelper.cs +++ b/MediaBrowser.Controller/Drawing/ImageHelper.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Drawing; diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs index 31d2c1bd4..22105b7d7 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; diff --git a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs index df9050de5..d3a2b4dbf 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Drawing/ImageStream.cs b/MediaBrowser.Controller/Drawing/ImageStream.cs index 74ac24ec9..46f58ec15 100644 --- a/MediaBrowser.Controller/Drawing/ImageStream.cs +++ b/MediaBrowser.Controller/Drawing/ImageStream.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.IO; using MediaBrowser.Model.Drawing; @@ -11,6 +13,7 @@ namespace MediaBrowser.Controller.Drawing /// /// The stream. public Stream Stream { get; set; } + /// /// Gets or sets the format. /// diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs index cf301f1e4..76f20ace2 100644 --- a/MediaBrowser.Controller/Dto/DtoOptions.cs +++ b/MediaBrowser.Controller/Dto/DtoOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Linq; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index e1c800e61..6ebea5f44 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -57,6 +59,7 @@ namespace MediaBrowser.Controller.Entities private Guid[] _childrenIds = null; private readonly object _childIdsLock = new object(); + protected override List LoadChildren() { lock (_childIdsLock) diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 98f802b5d..2c6dea02c 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; diff --git a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs index 056f31f78..f6d3cd6cc 100644 --- a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; namespace MediaBrowser.Controller.Entities.Audio diff --git a/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs index 5ae056050..ac4dd1688 100644 --- a/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs +++ b/MediaBrowser.Controller/Entities/Audio/IHasMusicGenres.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Entities.Audio { public interface IHasMusicGenres diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index 5a1ddeece..48cd9371a 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index ea5c41caa..397a68ff7 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index 4f6aa0776..5a117a6b1 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Text.Json.Serialization; diff --git a/MediaBrowser.Controller/Entities/AudioBook.cs b/MediaBrowser.Controller/Entities/AudioBook.cs index 11ff8a257..f4bd851e1 100644 --- a/MediaBrowser.Controller/Entities/AudioBook.cs +++ b/MediaBrowser.Controller/Entities/AudioBook.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; @@ -15,8 +17,10 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } + [JsonIgnore] public string SeriesName { get; set; } + [JsonIgnore] public Guid SeriesId { get; set; } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index f34309c40..a5c22e50f 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -1390,7 +1392,7 @@ namespace MediaBrowser.Controller.Entities new List(); var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false); - LibraryManager.UpdateImages(this); // ensure all image properties in DB are fresh + await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh if (ownedItemsChanged) { @@ -2279,7 +2281,7 @@ namespace MediaBrowser.Controller.Entities /// /// The type. /// The index. - public void DeleteImage(ImageType type, int index) + public async Task DeleteImageAsync(ImageType type, int index) { var info = GetImageInfo(type, index); @@ -2297,7 +2299,7 @@ namespace MediaBrowser.Controller.Entities FileSystem.DeleteFile(info.Path); } - UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); } public void RemoveImage(ItemImageInfo image) @@ -2310,10 +2312,8 @@ namespace MediaBrowser.Controller.Entities ImageInfos = ImageInfos.Except(deletedImages).ToArray(); } - public virtual void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) - { - LibraryManager.UpdateItem(this, GetParent(), updateReason, cancellationToken); - } + public virtual Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) + => LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken); /// /// Validates that images within the item are still on the filesystem. @@ -2558,7 +2558,7 @@ namespace MediaBrowser.Controller.Entities return type == ImageType.Backdrop || type == ImageType.Screenshot || type == ImageType.Chapter; } - public void SwapImages(ImageType type, int index1, int index2) + public Task SwapImagesAsync(ImageType type, int index1, int index2) { if (!AllowsMultipleImages(type)) { @@ -2571,13 +2571,13 @@ namespace MediaBrowser.Controller.Entities if (info1 == null || info2 == null) { // Nothing to do - return; + return Task.CompletedTask; } if (!info1.IsLocalFile || !info2.IsLocalFile) { // TODO: Not supported yet - return; + return Task.CompletedTask; } var path1 = info1.Path; @@ -2594,7 +2594,7 @@ namespace MediaBrowser.Controller.Entities info2.Width = 0; info2.Height = 0; - UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + return UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None); } public virtual bool IsPlayed(User user) @@ -2633,6 +2633,7 @@ namespace MediaBrowser.Controller.Entities { return new T { + Path = Path, MetadataCountryCode = GetPreferredMetadataCountryCode(), MetadataLanguage = GetPreferredMetadataLanguage(), Name = GetNameForMetadataLookup(), diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs index 815239be2..8a69971d0 100644 --- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs +++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Linq; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; diff --git a/MediaBrowser.Controller/Entities/BasePluginFolder.cs b/MediaBrowser.Controller/Entities/BasePluginFolder.cs index 106385bc6..ef5a5a734 100644 --- a/MediaBrowser.Controller/Entities/BasePluginFolder.cs +++ b/MediaBrowser.Controller/Entities/BasePluginFolder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities @@ -26,13 +28,5 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public override bool SupportsPeople => 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 index 11c6c6e45..55945283c 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Linq; using System.Text.Json.Serialization; @@ -49,11 +51,13 @@ namespace MediaBrowser.Controller.Entities return SeriesId; } + /// public override bool CanDownload() { return IsFileProtocol; } + /// public override UnratedItem GetBlockUnratedType() { return UnratedItem.Book; diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 5c6a9d2a2..d25545a2f 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 6441340f9..11542c1ca 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -350,12 +350,12 @@ namespace MediaBrowser.Controller.Entities if (currentChild.UpdateFromResolvedItem(child) > ItemUpdateType.None) { - currentChild.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken); + await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); } else { // metadata is up-to-date; make sure DB has correct images dimensions and hash - LibraryManager.UpdateImages(currentChild); + await LibraryManager.UpdateImagesAsync(currentChild).ConfigureAwait(false); } continue; diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index 437def532..db6c85caf 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Text.Json.Serialization; diff --git a/MediaBrowser.Controller/Entities/ICollectionFolder.cs b/MediaBrowser.Controller/Entities/ICollectionFolder.cs index 245b23ff0..b84a9fa6f 100644 --- a/MediaBrowser.Controller/Entities/ICollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/ICollectionFolder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Entities diff --git a/MediaBrowser.Controller/Entities/IHasMediaSources.cs b/MediaBrowser.Controller/Entities/IHasMediaSources.cs index 213c0a794..a7b60d168 100644 --- a/MediaBrowser.Controller/Entities/IHasMediaSources.cs +++ b/MediaBrowser.Controller/Entities/IHasMediaSources.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Dto; @@ -7,15 +9,19 @@ namespace MediaBrowser.Controller.Entities { public interface IHasMediaSources { - /// - /// Gets the media sources. - /// - List GetMediaSources(bool enablePathSubstitution); - List GetMediaStreams(); Guid Id { get; set; } long? RunTimeTicks { get; set; } string Path { get; } + + /// + /// Gets the media sources. + /// + List GetMediaSources(bool enablePathSubstitution); + + List GetMediaStreams(); + + } } diff --git a/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs index fd1c19c97..f747b5149 100644 --- a/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs +++ b/MediaBrowser.Controller/Entities/IHasProgramAttributes.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.LiveTv; namespace MediaBrowser.Controller.Entities diff --git a/MediaBrowser.Controller/Entities/IHasSeries.cs b/MediaBrowser.Controller/Entities/IHasSeries.cs index 475a2ab85..5444f1f52 100644 --- a/MediaBrowser.Controller/Entities/IHasSeries.cs +++ b/MediaBrowser.Controller/Entities/IHasSeries.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Entities @@ -10,13 +12,16 @@ namespace MediaBrowser.Controller.Entities /// The name of the series. string SeriesName { get; set; } - string FindSeriesName(); - string FindSeriesSortName(); Guid SeriesId { get; set; } - Guid FindSeriesId(); string SeriesPresentationUniqueKey { get; set; } + string FindSeriesName(); + + string FindSeriesSortName(); + + Guid FindSeriesId(); + string FindSeriesPresentationUniqueKey(); } } diff --git a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs index 688439e6c..6a350212b 100644 --- a/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs +++ b/MediaBrowser.Controller/Entities/IHasSpecialFeatures.cs @@ -1,4 +1,7 @@ +#pragma warning disable CS1591 + using System; +using System.Collections.Generic; namespace MediaBrowser.Controller.Entities { @@ -8,6 +11,6 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the special feature ids. /// /// The special feature ids. - Guid[] SpecialFeatureIds { get; set; } + IReadOnlyList SpecialFeatureIds { get; set; } } } diff --git a/MediaBrowser.Controller/Entities/IHasStartDate.cs b/MediaBrowser.Controller/Entities/IHasStartDate.cs index 1ecde9af4..dab15eb01 100644 --- a/MediaBrowser.Controller/Entities/IHasStartDate.cs +++ b/MediaBrowser.Controller/Entities/IHasStartDate.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Entities diff --git a/MediaBrowser.Controller/Entities/IHasTrailers.cs b/MediaBrowser.Controller/Entities/IHasTrailers.cs index dd8e3c45f..d1f6f2b7e 100644 --- a/MediaBrowser.Controller/Entities/IHasTrailers.cs +++ b/MediaBrowser.Controller/Entities/IHasTrailers.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Entities/IItemByName.cs b/MediaBrowser.Controller/Entities/IItemByName.cs index 8ef5c8d96..cac8aa61a 100644 --- a/MediaBrowser.Controller/Entities/IItemByName.cs +++ b/MediaBrowser.Controller/Entities/IItemByName.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; namespace MediaBrowser.Controller.Entities diff --git a/MediaBrowser.Controller/Entities/IMetadataContainer.cs b/MediaBrowser.Controller/Entities/IMetadataContainer.cs index a384c0df3..77f5cfb79 100644 --- a/MediaBrowser.Controller/Entities/IMetadataContainer.cs +++ b/MediaBrowser.Controller/Entities/IMetadataContainer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs b/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs index 3e96771c3..cdda8ea39 100644 --- a/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs +++ b/MediaBrowser.Controller/Entities/ISupportsPlaceHolders.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Entities { public interface ISupportsPlaceHolders diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 466cda67c..904752a22 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs index 011975dd2..4e09ee573 100644 --- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Entities diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs index 12f5db2e0..570d8eec0 100644 --- a/MediaBrowser.Controller/Entities/ItemImageInfo.cs +++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Text.Json.Serialization; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs index 65753a26e..8e0f721e7 100644 --- a/MediaBrowser.Controller/Entities/LinkedChild.cs +++ b/MediaBrowser.Controller/Entities/LinkedChild.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index c131c5430..8de88cc1b 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 53badac4d..8b67aaccc 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -1,5 +1,8 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text.Json.Serialization; using System.Threading; @@ -17,8 +20,6 @@ namespace MediaBrowser.Controller.Entities.Movies /// public class Movie : Video, IHasSpecialFeatures, IHasTrailers, IHasLookupInfo, ISupportsBoxSetGrouping { - public Guid[] SpecialFeatureIds { get; set; } - public Movie() { SpecialFeatureIds = Array.Empty(); @@ -27,6 +28,9 @@ namespace MediaBrowser.Controller.Entities.Movies RemoteTrailerIds = Array.Empty(); } + /// + public IReadOnlyList SpecialFeatureIds { get; set; } + /// public IReadOnlyList LocalTrailerIds { get; set; } @@ -46,6 +50,9 @@ namespace MediaBrowser.Controller.Entities.Movies set => TmdbCollectionName = value; } + [JsonIgnore] + public override bool StopRefreshIfLocalMetadataFound => false; + public override double GetDefaultPrimaryImageAspectRatio() { // hack for tv plugins @@ -105,6 +112,7 @@ namespace MediaBrowser.Controller.Entities.Movies return itemsChanged; } + /// public override UnratedItem GetBlockUnratedType() { return UnratedItem.Movie; @@ -133,6 +141,7 @@ namespace MediaBrowser.Controller.Entities.Movies return info; } + /// public override bool BeforeMetadataRefresh(bool replaceAllMetdata) { var hasChanges = base.BeforeMetadataRefresh(replaceAllMetdata); @@ -169,6 +178,7 @@ namespace MediaBrowser.Controller.Entities.Movies return hasChanges; } + /// public override List GetRelatedUrls() { var list = base.GetRelatedUrls(); @@ -179,14 +189,11 @@ namespace MediaBrowser.Controller.Entities.Movies list.Add(new ExternalUrl { Name = "Trakt", - Url = string.Format("https://trakt.tv/movies/{0}", imdbId) + Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId) }); } return list; } - - [JsonIgnore] - public override bool StopRefreshIfLocalMetadataFound => false; } } diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs index 6e7f2812d..b278a0142 100644 --- a/MediaBrowser.Controller/Entities/MusicVideo.cs +++ b/MediaBrowser.Controller/Entities/MusicVideo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Text.Json.Serialization; diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs index c39495759..1f3758a73 100644 --- a/MediaBrowser.Controller/Entities/PeopleHelper.cs +++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index 331d17fc8..c4fcb0267 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Text.Json.Serialization; diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs index f3ec73b32..4ff9b0955 100644 --- a/MediaBrowser.Controller/Entities/PersonInfo.cs +++ b/MediaBrowser.Controller/Entities/PersonInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs index 82d0826c5..1485d4c79 100644 --- a/MediaBrowser.Controller/Entities/Photo.cs +++ b/MediaBrowser.Controller/Entities/Photo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Text.Json.Serialization; using MediaBrowser.Model.Drawing; diff --git a/MediaBrowser.Controller/Entities/PhotoAlbum.cs b/MediaBrowser.Controller/Entities/PhotoAlbum.cs index b86f1ac2a..a7ecb9061 100644 --- a/MediaBrowser.Controller/Entities/PhotoAlbum.cs +++ b/MediaBrowser.Controller/Entities/PhotoAlbum.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities diff --git a/MediaBrowser.Controller/Entities/Share.cs b/MediaBrowser.Controller/Entities/Share.cs index a51f2b452..50f1655f3 100644 --- a/MediaBrowser.Controller/Entities/Share.cs +++ b/MediaBrowser.Controller/Entities/Share.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Entities { public interface IHasShares diff --git a/MediaBrowser.Controller/Entities/SourceType.cs b/MediaBrowser.Controller/Entities/SourceType.cs index 927483b93..be19e1bda 100644 --- a/MediaBrowser.Controller/Entities/SourceType.cs +++ b/MediaBrowser.Controller/Entities/SourceType.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Entities { public enum SourceType diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs index 1f64de6a4..9018ddb75 100644 --- a/MediaBrowser.Controller/Entities/Studio.cs +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Text.Json.Serialization; diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 9a5f9097d..dc12fbbea 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 2aba1d03d..93bdd6e70 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 45daa8a53..72c696c1a 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -496,7 +498,7 @@ namespace MediaBrowser.Controller.Entities.TV list.Add(new ExternalUrl { Name = "Trakt", - Url = string.Format("https://trakt.tv/shows/{0}", imdbId) + Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/shows/{0}", imdbId) }); } diff --git a/MediaBrowser.Controller/Entities/TagExtensions.cs b/MediaBrowser.Controller/Entities/TagExtensions.cs index 97f590635..2ce396daf 100644 --- a/MediaBrowser.Controller/Entities/TagExtensions.cs +++ b/MediaBrowser.Controller/Entities/TagExtensions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Linq; diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs index 6b544afc6..9ae8ad708 100644 --- a/MediaBrowser.Controller/Entities/Trailer.cs +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -1,5 +1,8 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; +using System.Globalization; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; @@ -86,7 +89,7 @@ namespace MediaBrowser.Controller.Entities list.Add(new ExternalUrl { Name = "Trakt", - Url = string.Format("https://trakt.tv/movies/{0}", imdbId) + Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId) }); } diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs index 3298fa2d3..db63c42e4 100644 --- a/MediaBrowser.Controller/Entities/UserItemData.cs +++ b/MediaBrowser.Controller/Entities/UserItemData.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Text.Json.Serialization; @@ -24,6 +26,7 @@ namespace MediaBrowser.Controller.Entities /// The _rating. /// private double? _rating; + /// /// Gets or sets the users 0-10 rating. /// @@ -75,11 +78,13 @@ namespace MediaBrowser.Controller.Entities /// /// true if played; otherwise, false. public bool Played { get; set; } + /// /// Gets or sets the index of the audio stream. /// /// The index of the audio stream. public int? AudioStreamIndex { get; set; } + /// /// Gets or sets the index of the subtitle stream. /// diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index 39f4e0b6c..7f7224ae0 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index 1fba8a30f..b1da4d64c 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; @@ -6,7 +8,6 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Querying; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities { diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 22bb7fd55..b384b27d1 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -8,7 +10,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.TV; -using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; @@ -672,9 +673,7 @@ namespace MediaBrowser.Controller.Entities var isPlaceHolder = false; - var hasPlaceHolder = item as ISupportsPlaceHolders; - - if (hasPlaceHolder != null) + if (item is ISupportsPlaceHolders hasPlaceHolder) { isPlaceHolder = hasPlaceHolder.IsPlaceHolder; } @@ -689,13 +688,11 @@ namespace MediaBrowser.Controller.Entities { var filterValue = query.HasSpecialFeature.Value; - var movie = item as IHasSpecialFeatures; - - if (movie != null) + if (item is IHasSpecialFeatures movie) { var ok = filterValue - ? movie.SpecialFeatureIds.Length > 0 - : movie.SpecialFeatureIds.Length == 0; + ? movie.SpecialFeatureIds.Count > 0 + : movie.SpecialFeatureIds.Count == 0; if (!ok) { diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index b7d7e8e1a..07f381881 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -495,9 +497,10 @@ namespace MediaBrowser.Controller.Entities } } - public override void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) + /// + public override async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) { - base.UpdateToRepository(updateReason, cancellationToken); + await base.UpdateToRepositoryAsync(updateReason, cancellationToken).ConfigureAwait(false); var localAlternates = GetLocalAlternateVersionIds() .Select(i => LibraryManager.GetItemById(i)) @@ -514,7 +517,7 @@ namespace MediaBrowser.Controller.Entities item.Genres = Genres; item.ProviderIds = ProviderIds; - item.UpdateToRepository(ItemUpdateType.MetadataDownload, cancellationToken); + await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataDownload, cancellationToken).ConfigureAwait(false); } } diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs index c88498640..b2e4d307a 100644 --- a/MediaBrowser.Controller/Entities/Year.cs +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; diff --git a/MediaBrowser.Controller/Events/IEventConsumer.cs b/MediaBrowser.Controller/Events/IEventConsumer.cs new file mode 100644 index 000000000..5c4ab5d8d --- /dev/null +++ b/MediaBrowser.Controller/Events/IEventConsumer.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Events +{ + /// + /// An interface representing a type that consumes events of type T. + /// + /// The type of events this consumes. + public interface IEventConsumer + where T : EventArgs + { + /// + /// A method that is called when an event of type T is fired. + /// + /// The event. + /// A task representing the consumption of the event. + Task OnEvent(T eventArgs); + } +} diff --git a/MediaBrowser.Controller/Events/IEventManager.cs b/MediaBrowser.Controller/Events/IEventManager.cs new file mode 100644 index 000000000..a1f40b3a6 --- /dev/null +++ b/MediaBrowser.Controller/Events/IEventManager.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Events +{ + /// + /// An interface that handles eventing. + /// + public interface IEventManager + { + /// + /// Publishes an event. + /// + /// the event arguments. + /// The type of event. + void Publish(T eventArgs) + where T : EventArgs; + + /// + /// Publishes an event asynchronously. + /// + /// The event arguments. + /// The type of event. + /// A task representing the publishing of the event. + Task PublishAsync(T eventArgs) + where T : EventArgs; + } +} diff --git a/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs b/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs new file mode 100644 index 000000000..46d7e5a17 --- /dev/null +++ b/MediaBrowser.Controller/Events/Session/SessionEndedEventArgs.cs @@ -0,0 +1,19 @@ +using Jellyfin.Data.Events; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.Events.Session +{ + /// + /// An event that fires when a session is ended. + /// + public class SessionEndedEventArgs : GenericEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The session info. + public SessionEndedEventArgs(SessionInfo arg) : base(arg) + { + } + } +} diff --git a/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs b/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs new file mode 100644 index 000000000..aab19cc46 --- /dev/null +++ b/MediaBrowser.Controller/Events/Session/SessionStartedEventArgs.cs @@ -0,0 +1,19 @@ +using Jellyfin.Data.Events; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.Events.Session +{ + /// + /// An event that fires when a session is started. + /// + public class SessionStartedEventArgs : GenericEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The session info. + public SessionStartedEventArgs(SessionInfo arg) : base(arg) + { + } + } +} diff --git a/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs new file mode 100644 index 000000000..b06046c05 --- /dev/null +++ b/MediaBrowser.Controller/Events/Updates/PluginInstallationCancelledEventArgs.cs @@ -0,0 +1,19 @@ +using Jellyfin.Data.Events; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Controller.Events.Updates +{ + /// + /// An event that occurs when a plugin installation is cancelled. + /// + public class PluginInstallationCancelledEventArgs : GenericEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The installation info. + public PluginInstallationCancelledEventArgs(InstallationInfo arg) : base(arg) + { + } + } +} diff --git a/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs new file mode 100644 index 000000000..dfadc9f61 --- /dev/null +++ b/MediaBrowser.Controller/Events/Updates/PluginInstalledEventArgs.cs @@ -0,0 +1,19 @@ +using Jellyfin.Data.Events; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Controller.Events.Updates +{ + /// + /// An event that occurs when a plugin is installed. + /// + public class PluginInstalledEventArgs : GenericEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The installation info. + public PluginInstalledEventArgs(InstallationInfo arg) : base(arg) + { + } + } +} diff --git a/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs new file mode 100644 index 000000000..045a60027 --- /dev/null +++ b/MediaBrowser.Controller/Events/Updates/PluginInstallingEventArgs.cs @@ -0,0 +1,19 @@ +using Jellyfin.Data.Events; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Controller.Events.Updates +{ + /// + /// An event that occurs when a plugin is installing. + /// + public class PluginInstallingEventArgs : GenericEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The installation info. + public PluginInstallingEventArgs(InstallationInfo arg) : base(arg) + { + } + } +} diff --git a/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs new file mode 100644 index 000000000..7510b62b8 --- /dev/null +++ b/MediaBrowser.Controller/Events/Updates/PluginUninstalledEventArgs.cs @@ -0,0 +1,19 @@ +using Jellyfin.Data.Events; +using MediaBrowser.Common.Plugins; + +namespace MediaBrowser.Controller.Events.Updates +{ + /// + /// An event that occurs when a plugin is uninstalled. + /// + public class PluginUninstalledEventArgs : GenericEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The plugin. + public PluginUninstalledEventArgs(IPlugin arg) : base(arg) + { + } + } +} diff --git a/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs b/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs new file mode 100644 index 000000000..661ca066a --- /dev/null +++ b/MediaBrowser.Controller/Events/Updates/PluginUpdatedEventArgs.cs @@ -0,0 +1,19 @@ +using Jellyfin.Data.Events; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Controller.Events.Updates +{ + /// + /// An event that occurs when a plugin is updated. + /// + public class PluginUpdatedEventArgs : GenericEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The installation info. + public PluginUpdatedEventArgs(InstallationInfo arg) : base(arg) + { + } + } +} diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index 4c2209b67..f9285c768 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -8,6 +8,12 @@ namespace MediaBrowser.Controller.Extensions /// public static class ConfigurationExtensions { + /// + /// The key for a setting that specifies the default redirect path + /// to use for requests where the URL base prefix is invalid or missing.. + /// + public const string DefaultRedirectKey = "DefaultRedirectPath"; + /// /// The key for a setting that indicates whether the application should host web client content. /// diff --git a/MediaBrowser.Controller/Extensions/StringExtensions.cs b/MediaBrowser.Controller/Extensions/StringExtensions.cs index e09543e14..3cc1f328a 100644 --- a/MediaBrowser.Controller/Extensions/StringExtensions.cs +++ b/MediaBrowser.Controller/Extensions/StringExtensions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Globalization; using System.Linq; diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs index b6bfed3e5..856b91b5d 100644 --- a/MediaBrowser.Controller/IDisplayPreferencesManager.cs +++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Jellyfin.Data.Entities; diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs index e655f50eb..9bc4cac39 100644 --- a/MediaBrowser.Controller/IO/FileData.cs +++ b/MediaBrowser.Controller/IO/FileData.cs @@ -8,28 +8,17 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.IO { /// - /// Provides low level File access that is much faster than the File/Directory api's + /// Provides low level File access that is much faster than the File/Directory api's. /// public static class FileData { - private static Dictionary GetFileSystemDictionary(FileSystemMetadata[] list) - { - var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var file in list) - { - dict[file.FullName] = file; - } - - return dict; - } - /// /// Gets the filtered file system entries. /// /// The directory service. /// The path. /// The file system. + /// The application host. /// The logger. /// The args. /// The flatten folder depth. diff --git a/MediaBrowser.Controller/IResourceFileManager.cs b/MediaBrowser.Controller/IResourceFileManager.cs index 69a51cec8..26f0424b7 100644 --- a/MediaBrowser.Controller/IResourceFileManager.cs +++ b/MediaBrowser.Controller/IResourceFileManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller { public interface IResourceFileManager diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index abdb0f695..9f4c00e1c 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Net; @@ -16,13 +18,9 @@ namespace MediaBrowser.Controller { event EventHandler HasUpdateAvailableChanged; - /// - /// Gets the system info. - /// - /// SystemInfo. - Task GetSystemInfo(CancellationToken cancellationToken); + IServiceProvider ServiceProvider { get; } - Task GetPublicSystemInfo(CancellationToken cancellationToken); + bool CoreStartupHasCompleted { get; } bool CanLaunchWebBrowser { get; } @@ -55,6 +53,14 @@ namespace MediaBrowser.Controller /// The name of the friendly. string FriendlyName { get; } + /// + /// Gets the system info. + /// + /// SystemInfo. + Task GetSystemInfo(CancellationToken cancellationToken); + + Task GetPublicSystemInfo(CancellationToken cancellationToken); + /// /// Gets all the local IP addresses of this API instance. Each address is validated by sending a 'ping' request /// to the API that should exist at the address. @@ -113,8 +119,7 @@ namespace MediaBrowser.Controller IEnumerable GetWakeOnLanInfo(); string ExpandVirtualPath(string path); - string ReverseVirtualPath(string path); - Task ExecuteHttpHandlerAsync(HttpContext context, Func next); + string ReverseVirtualPath(string path); } } diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs index 155bf9177..be57d6bca 100644 --- a/MediaBrowser.Controller/IServerApplicationPaths.cs +++ b/MediaBrowser.Controller/IServerApplicationPaths.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Common.Configuration; namespace MediaBrowser.Controller @@ -81,6 +83,10 @@ namespace MediaBrowser.Controller /// The internal metadata path. string InternalMetadataPath { get; } + /// + /// Gets the virtual internal metadata path, either a custom path or the default. + /// + /// The virtual internal metadata path. string VirtualInternalMetadataPath { get; } /// diff --git a/MediaBrowser.Controller/Library/DeleteOptions.cs b/MediaBrowser.Controller/Library/DeleteOptions.cs index 2944d8259..b7417efcb 100644 --- a/MediaBrowser.Controller/Library/DeleteOptions.cs +++ b/MediaBrowser.Controller/Library/DeleteOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Library { public class DeleteOptions diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 9abcf2b62..d2f937d4f 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; @@ -63,6 +65,7 @@ namespace MediaBrowser.Controller.Library /// Finds the by path. /// /// The path. + /// true is the path is a directory; otherwise false. /// BaseItem. BaseItem FindByPath(string path, bool? isFolder); @@ -72,6 +75,7 @@ namespace MediaBrowser.Controller.Library /// The name. /// Task{Artist}. MusicArtist GetArtist(string name); + MusicArtist GetArtist(string name, DtoOptions options); /// /// Gets a Studio. @@ -124,7 +128,7 @@ namespace MediaBrowser.Controller.Library /// void QueueLibraryScan(); - void UpdateImages(BaseItem item, bool forceUpdate = false); + Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false); /// /// Gets the default view. @@ -179,6 +183,7 @@ namespace MediaBrowser.Controller.Library /// The sort order. /// IEnumerable{BaseItem}. IEnumerable Sort(IEnumerable items, User user, IEnumerable sortBy, SortOrder sortOrder); + IEnumerable Sort(IEnumerable items, User user, IEnumerable> orderBy); /// @@ -200,9 +205,16 @@ namespace MediaBrowser.Controller.Library /// /// Updates the item. /// - void UpdateItems(IReadOnlyList items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); + Task UpdateItemsAsync(IReadOnlyList items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); - void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); + /// + /// Updates the item. + /// + /// The item. + /// The parent item. + /// The update reason. + /// The cancellation token. + Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); /// /// Retrieves the item. @@ -317,7 +329,8 @@ namespace MediaBrowser.Controller.Library /// The name. /// Type of the view. /// Name of the sort. - UserView GetNamedView(string name, + UserView GetNamedView( + string name, string viewType, string sortName); @@ -329,7 +342,8 @@ namespace MediaBrowser.Controller.Library /// Type of the view. /// Name of the sort. /// The unique identifier. - UserView GetNamedView(string name, + UserView GetNamedView( + string name, Guid parentId, string viewType, string sortName, @@ -341,7 +355,8 @@ namespace MediaBrowser.Controller.Library /// The parent. /// Type of the view. /// Name of the sort. - UserView GetShadowView(BaseItem parent, + UserView GetShadowView( + BaseItem parent, string viewType, string sortName); @@ -393,7 +408,9 @@ namespace MediaBrowser.Controller.Library /// The file system children. /// The directory service. /// IEnumerable<Trailer>. - IEnumerable readonly string _name; + /// /// The stopwatch. /// @@ -44,6 +46,7 @@ namespace MediaBrowser.Controller.Library public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } /// @@ -58,13 +61,19 @@ namespace MediaBrowser.Controller.Library string message; if (_stopwatch.ElapsedMilliseconds > 300000) { - message = string.Format("{0} took {1} minutes.", - _name, ((float)_stopwatch.ElapsedMilliseconds / 60000).ToString("F")); + message = string.Format( + CultureInfo.InvariantCulture, + "{0} took {1} minutes.", + _name, + ((float)_stopwatch.ElapsedMilliseconds / 60000).ToString("F", CultureInfo.InvariantCulture)); } else { - message = string.Format("{0} took {1} seconds.", - _name, ((float)_stopwatch.ElapsedMilliseconds / 1000).ToString("#0.000")); + message = string.Format( + CultureInfo.InvariantCulture, + "{0} took {1} seconds.", + _name, + ((float)_stopwatch.ElapsedMilliseconds / 1000).ToString("#0.000", CultureInfo.InvariantCulture)); } _logger.LogInformation(message); diff --git a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs index fa0192784..cd9109753 100644 --- a/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs +++ b/MediaBrowser.Controller/Library/UserDataSaveEventArgs.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs index 67d0df4fd..d7afd2118 100644 --- a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.LiveTv; namespace MediaBrowser.Controller.LiveTv diff --git a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs index 2ea0a748e..038ff2eae 100644 --- a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs +++ b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index f619b011b..55c330931 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -1,13 +1,15 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Events; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; @@ -105,6 +107,7 @@ namespace MediaBrowser.Controller.LiveTv /// /// The identifier. /// The media source identifier. + /// The current live streams. /// The cancellation token. /// Task{StreamResponseInfo}. Task> GetChannelStream(string id, string mediaSourceId, List currentLiveStreams, CancellationToken cancellationToken); diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs index b71a76648..3ca1d165e 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs index 3679e4f78..ff92bf856 100644 --- a/MediaBrowser.Controller/LiveTv/ITunerHost.cs +++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -14,28 +16,37 @@ namespace MediaBrowser.Controller.LiveTv /// /// The name. string Name { get; } + /// /// Gets the type. /// /// The type. string Type { get; } + + bool IsSupported { get; } + /// /// Gets the channels. /// /// Task<IEnumerable<ChannelInfo>>. Task> GetChannels(bool enableCache, CancellationToken cancellationToken); + /// /// Gets the tuner infos. /// /// The cancellation token. /// Task<List<LiveTvTunerInfo>>. Task> GetTunerInfos(CancellationToken cancellationToken); + /// /// Gets the channel stream. /// /// The channel identifier. /// The stream identifier. + /// The current live streams. + /// The cancellation token to cancel operation. Task GetChannelStream(string channelId, string streamId, List currentLiveStreams, CancellationToken cancellationToken); + /// /// Gets the channel stream media sources. /// @@ -45,10 +56,7 @@ namespace MediaBrowser.Controller.LiveTv Task> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken); Task> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken); - bool IsSupported - { - get; - } + } public interface IConfigurableTunerHost diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs index 10af98121..ec933caf3 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -63,7 +65,7 @@ namespace MediaBrowser.Controller.LiveTv if (double.TryParse(Number, NumberStyles.Any, CultureInfo.InvariantCulture, out number)) { - return string.Format("{0:00000.0}", number) + "-" + (Name ?? string.Empty); + return string.Format(CultureInfo.InvariantCulture, "{0:00000.0}", number) + "-" + (Name ?? string.Empty); } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs b/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs index 0e09d1aeb..881c42c73 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvConflictException.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.LiveTv diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index 472b061e6..43af495dd 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -261,7 +263,7 @@ namespace MediaBrowser.Controller.LiveTv list.Add(new ExternalUrl { Name = "Trakt", - Url = string.Format("https://trakt.tv/movies/{0}", imdbId) + Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/movies/{0}", imdbId) }); } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs index 67b2f0eb1..02178297b 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Model.LiveTv; @@ -5,6 +7,12 @@ namespace MediaBrowser.Controller.LiveTv { public class LiveTvServiceStatusInfo { + public LiveTvServiceStatusInfo() + { + Tuners = new List(); + IsVisible = true; + } + /// /// Gets or sets the status. /// @@ -39,11 +47,5 @@ namespace MediaBrowser.Controller.LiveTv /// /// true if this instance is visible; otherwise, false. public bool IsVisible { get; set; } - - public LiveTvServiceStatusInfo() - { - Tuners = new List(); - IsVisible = true; - } } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs index 2857f73f6..739978e7c 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Model.LiveTv; @@ -5,6 +7,11 @@ namespace MediaBrowser.Controller.LiveTv { public class LiveTvTunerInfo { + public LiveTvTunerInfo() + { + Clients = new List(); + } + /// /// Gets or sets the type of the source. /// @@ -64,10 +71,5 @@ namespace MediaBrowser.Controller.LiveTv /// /// true if this instance can reset; otherwise, false. public bool CanReset { get; set; } - - public LiveTvTunerInfo() - { - Clients = new List(); - } } } diff --git a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs index d06a15323..bdcffd5ca 100644 --- a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.LiveTv; diff --git a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs index b9e0218ab..303882b7e 100644 --- a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs +++ b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.LiveTv; diff --git a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs b/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs index 99460a686..847c0ea8c 100644 --- a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs +++ b/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.LiveTv; diff --git a/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs index 6e7acaae3..1343ecd98 100644 --- a/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/SeriesTimerInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.LiveTv; diff --git a/MediaBrowser.Controller/LiveTv/TimerInfo.cs b/MediaBrowser.Controller/LiveTv/TimerInfo.cs index df98bb6af..bcef4666d 100644 --- a/MediaBrowser.Controller/LiveTv/TimerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/TimerInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; diff --git a/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs b/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs index df3f55c26..2759b314f 100644 --- a/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs +++ b/MediaBrowser.Controller/LiveTv/TunerChannelMapping.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.LiveTv { public class TunerChannelMapping diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 67f17f7a5..3674181b1 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -8,11 +8,13 @@ Jellyfin Contributors Jellyfin.Controller - https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt + 10.7.0 https://github.com/jellyfin/jellyfin + GPL-3.0-only + @@ -30,6 +32,16 @@ netstandard2.1 false true + true + true + true + true + snupkg + + + + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 2dd21be3c..550916f82 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -675,7 +677,7 @@ namespace MediaBrowser.Controller.MediaEncoding // } // } - // fallbackFontParam = string.Format(":force_style='FontName=Droid Sans Fallback':fontsdir='{0}'", _mediaEncoder.EscapeSubtitleFilterPath(_fileSystem.GetDirectoryName(fallbackFontPath))); + // fallbackFontParam = string.Format(CultureInfo.InvariantCulture, ":force_style='FontName=Droid Sans Fallback':fontsdir='{0}'", _mediaEncoder.EscapeSubtitleFilterPath(_fileSystem.GetDirectoryName(fallbackFontPath))); if (state.SubtitleStream.IsExternal) { @@ -880,7 +882,7 @@ namespace MediaBrowser.Controller.MediaEncoding 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}", + param += string.Format(CultureInfo.InvariantCulture, "-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}", profileScore.ToString(_usCulture), crf, qmin, @@ -904,7 +906,7 @@ namespace MediaBrowser.Controller.MediaEncoding var framerate = GetFramerateParam(state); if (framerate.HasValue) { - param += string.Format(" -r {0}", framerate.Value.ToString(_usCulture)); + param += string.Format(CultureInfo.InvariantCulture, " -r {0}", framerate.Value.ToString(_usCulture)); } var targetVideoCodec = state.ActualOutputVideoCodec; @@ -1484,7 +1486,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (time > 0) { - return string.Format("-ss {0}", _mediaEncoder.GetTimeParameter(time)); + return string.Format(CultureInfo.InvariantCulture, "-ss {0}", _mediaEncoder.GetTimeParameter(time)); } return string.Empty; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index b971b7c4b..68bc502a0 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs index 8f6fcb9ab..1f3abe8f4 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs @@ -1,8 +1,9 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Services; namespace MediaBrowser.Controller.MediaEncoding { @@ -61,26 +62,20 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets or sets the id. /// /// The id. - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public Guid Id { get; set; } - [ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public string MediaSourceId { get; set; } - [ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string DeviceId { get; set; } - [ApiMember(Name = "Container", Description = "Container", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] public string Container { get; set; } /// /// Gets or sets the audio codec. /// /// The audio codec. - [ApiMember(Name = "AudioCodec", Description = "Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string AudioCodec { get; set; } - [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; } @@ -93,7 +88,6 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets or sets the audio sample rate. /// /// The audio sample rate. - [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; } @@ -102,105 +96,86 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets or sets the audio bit rate. /// /// The audio bit rate. - [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; } /// /// Gets or sets the audio channels. /// /// The audio channels. - [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; } /// /// Gets or sets the profile. /// /// The profile. - [ApiMember(Name = "Profile", Description = "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string Profile { get; set; } /// /// Gets or sets the level. /// /// The level. - [ApiMember(Name = "Level", Description = "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string Level { get; set; } /// /// Gets or sets the framerate. /// /// The framerate. - [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; } /// /// Gets or sets the start time ticks. /// /// The start time ticks. - [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; } /// /// Gets or sets the width. /// /// The width. - [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; } /// /// Gets or sets the height. /// /// The height. - [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; } /// /// Gets or sets the width of the max. /// /// The width of the max. - [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; } /// /// Gets or sets the height of the max. /// /// The height of the max. - [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; } /// /// Gets or sets the video bit rate. /// /// The video bit rate. - [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; } /// /// Gets or sets the index of the subtitle stream. /// /// The index of the subtitle stream. - [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; } @@ -221,7 +196,6 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets or sets the video codec. /// /// The video codec. - [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: h265, h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string VideoCodec { get; set; } public string SubtitleCodec { get; set; } @@ -232,14 +206,12 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets or sets the index of the audio stream. /// /// The index of the audio stream. - [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; } /// /// Gets or sets the index of the video stream. /// /// The index of the video stream. - [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; } diff --git a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs index 7c7e84de6..fbc827534 100644 --- a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs +++ b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.IO; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index f60e70239..17d6dc5d2 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; diff --git a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs index 174e74f34..6ebf7f159 100644 --- a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.IO; using System.Threading; using System.Threading.Tasks; @@ -12,7 +14,8 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets the subtitles. /// /// Task{Stream}. - Task GetSubtitles(BaseItem item, + Task GetSubtitles( + BaseItem item, string mediaSourceId, int subtitleStreamIndex, string outputFormat, @@ -25,6 +28,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets the subtitle language encoding parameter. /// /// The path. + /// The language. /// The protocol. /// The cancellation token. /// System.String. diff --git a/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs b/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs index 361dd79dc..e7b4c8c15 100644 --- a/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/ImageEncodingOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.MediaEncoding { public class ImageEncodingOptions diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index c9f64c707..ac520c5c4 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Globalization; using System.IO; diff --git a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs index 6c9bbb043..ce53c23ad 100644 --- a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs +++ b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; diff --git a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs index 39a47792a..59729de49 100644 --- a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs +++ b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs deleted file mode 100644 index 87a7f7e10..000000000 --- a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; - -namespace MediaBrowser.Controller.Net -{ - public class AuthenticatedAttribute : Attribute, IHasRequestFilter, IAuthenticationAttributes - { - public static IAuthService AuthService { get; set; } - - /// - /// Gets or sets the roles. - /// - /// The roles. - public string Roles { get; set; } - - /// - /// Gets or sets a value indicating whether [escape parental control]. - /// - /// true if [escape parental control]; otherwise, false. - public bool EscapeParentalControl { get; set; } - - /// - /// Gets or sets a value indicating whether [allow before startup wizard]. - /// - /// true if [allow before startup wizard]; otherwise, false. - public bool AllowBeforeStartupWizard { get; set; } - - public bool AllowLocal { get; set; } - - /// - /// The request filter is executed before the service. - /// - /// The http request wrapper. - /// The http response wrapper. - /// The request DTO. - public void RequestFilter(IRequest request, HttpResponse response, object requestDto) - { - AuthService.Authenticate(request, this); - } - - /// - /// Order in which Request Filters are executed. - /// <0 Executed before global request filters - /// >0 Executed after global request filters - /// - /// The priority. - public int Priority => 0; - - public string[] GetRoles() - { - return (Roles ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - } - - public bool IgnoreLegacyAuth { get; set; } - - public bool AllowLocalOnly { get; set; } - } - - public interface IAuthenticationAttributes - { - bool EscapeParentalControl { get; } - - bool AllowBeforeStartupWizard { get; } - - bool AllowLocal { get; } - - bool AllowLocalOnly { get; } - - string[] GetRoles(); - - bool IgnoreLegacyAuth { get; } - } -} diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs index 4361e253b..735c46ef8 100644 --- a/MediaBrowser.Controller/Net/AuthorizationInfo.cs +++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using Jellyfin.Data.Entities; diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index a54f6d57b..916dea58b 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs index 2055a656a..04b2e13e8 100644 --- a/MediaBrowser.Controller/Net/IAuthService.cs +++ b/MediaBrowser.Controller/Net/IAuthService.cs @@ -1,7 +1,5 @@ #nullable enable -using Jellyfin.Data.Entities; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net @@ -11,21 +9,6 @@ namespace MediaBrowser.Controller.Net /// public interface IAuthService { - /// - /// Authenticate and authorize request. - /// - /// Request. - /// Authorization attributes. - void Authenticate(IRequest request, IAuthenticationAttributes authAttribtutes); - - /// - /// Authenticate and authorize request. - /// - /// Request. - /// Authorization attributes. - /// Authenticated user. - User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtutes); - /// /// Authenticate request. /// diff --git a/MediaBrowser.Controller/Net/IAuthorizationContext.cs b/MediaBrowser.Controller/Net/IAuthorizationContext.cs index 37a7425b9..0d310548d 100644 --- a/MediaBrowser.Controller/Net/IAuthorizationContext.cs +++ b/MediaBrowser.Controller/Net/IAuthorizationContext.cs @@ -1,4 +1,3 @@ -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net @@ -13,14 +12,7 @@ namespace MediaBrowser.Controller.Net /// /// The request context. /// AuthorizationInfo. - AuthorizationInfo GetAuthorizationInfo(object requestContext); - - /// - /// Gets the authorization information. - /// - /// The request context. - /// AuthorizationInfo. - AuthorizationInfo GetAuthorizationInfo(IRequest requestContext); + AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext); /// /// Gets the authorization information. diff --git a/MediaBrowser.Controller/Net/IHasResultFactory.cs b/MediaBrowser.Controller/Net/IHasResultFactory.cs deleted file mode 100644 index b8cf8cd78..000000000 --- a/MediaBrowser.Controller/Net/IHasResultFactory.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MediaBrowser.Model.Services; - -namespace MediaBrowser.Controller.Net -{ - /// - /// Interface IHasResultFactory - /// Services that require a ResultFactory should implement this - /// - public interface IHasResultFactory : IRequiresRequest - { - /// - /// Gets or sets the result factory. - /// - /// The result factory. - IHttpResultFactory ResultFactory { get; set; } - } -} diff --git a/MediaBrowser.Controller/Net/IHttpResultFactory.cs b/MediaBrowser.Controller/Net/IHttpResultFactory.cs deleted file mode 100644 index 609bd5f59..000000000 --- a/MediaBrowser.Controller/Net/IHttpResultFactory.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; - -namespace MediaBrowser.Controller.Net -{ - /// - /// Interface IHttpResultFactory. - /// - public interface IHttpResultFactory - { - /// - /// Gets the result. - /// - /// The content. - /// Type of the content. - /// The response headers. - /// System.Object. - object GetResult(string content, string contentType, IDictionary responseHeaders = null); - - object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary responseHeaders = null); - object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary responseHeaders = null); - object GetResult(IRequest requestContext, string content, string contentType, IDictionary responseHeaders = null); - - object GetRedirectResult(string url); - - object GetResult(IRequest requestContext, T result, IDictionary responseHeaders = null) - where T : class; - - /// - /// Gets the static result. - /// - /// The request context. - /// The cache key. - /// The last date modified. - /// Duration of the cache. - /// Type of the content. - /// The factory fn. - /// The response headers. - /// if set to true [is head request]. - /// System.Object. - Task GetStaticResult(IRequest requestContext, - Guid cacheKey, - DateTime? lastDateModified, - TimeSpan? cacheDuration, - string contentType, Func> factoryFn, - IDictionary responseHeaders = null, - bool isHeadRequest = false); - - /// - /// Gets the static result. - /// - /// The request context. - /// The options. - /// System.Object. - Task GetStaticResult(IRequest requestContext, StaticResultOptions options); - - /// - /// Gets the static file result. - /// - /// The request context. - /// The path. - /// The file share. - /// System.Object. - Task GetStaticFileResult(IRequest requestContext, string path, FileShare fileShare = FileShare.Read); - - /// - /// Gets the static file result. - /// - /// The request context. - /// The options. - /// System.Object. - Task GetStaticFileResult(IRequest requestContext, - StaticFileResultOptions options); - } -} diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs deleted file mode 100644 index e6609fae3..000000000 --- a/MediaBrowser.Controller/Net/IHttpServer.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; - -namespace MediaBrowser.Controller.Net -{ - /// - /// Interface IHttpServer. - /// - public interface IHttpServer - { - /// - /// Gets the URL prefix. - /// - /// The URL prefix. - string[] UrlPrefixes { get; } - - /// - /// Occurs when [web socket connected]. - /// - event EventHandler> WebSocketConnected; - - /// - /// Inits this instance. - /// - void Init(IEnumerable serviceTypes, IEnumerable listener, IEnumerable urlPrefixes); - - /// - /// If set, all requests will respond with this message. - /// - string GlobalResponse { get; set; } - - /// - /// The HTTP request handler. - /// - /// - /// - Task RequestHandler(HttpContext context); - - /// - /// Get the default CORS headers. - /// - /// - /// - IDictionary GetDefaultCorsHeaders(IRequest req); - } -} diff --git a/MediaBrowser.Controller/Net/ISessionContext.cs b/MediaBrowser.Controller/Net/ISessionContext.cs index 421ac3fe2..a60dc2ea1 100644 --- a/MediaBrowser.Controller/Net/ISessionContext.cs +++ b/MediaBrowser.Controller/Net/ISessionContext.cs @@ -1,15 +1,19 @@ +#pragma warning disable CS1591 + using Jellyfin.Data.Entities; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { public interface ISessionContext { SessionInfo GetSession(object requestContext); + User GetUser(object requestContext); - SessionInfo GetSession(IRequest requestContext); - User GetUser(IRequest requestContext); + SessionInfo GetSession(HttpContext requestContext); + + User GetUser(HttpContext requestContext); } } diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index 3ef8e5f6d..e87f3bca6 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + #nullable enable using System; diff --git a/MediaBrowser.Controller/Net/IWebSocketManager.cs b/MediaBrowser.Controller/Net/IWebSocketManager.cs new file mode 100644 index 000000000..e9f00ae88 --- /dev/null +++ b/MediaBrowser.Controller/Net/IWebSocketManager.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Jellyfin.Data.Events; +using Microsoft.AspNetCore.Http; + +namespace MediaBrowser.Controller.Net +{ + /// + /// Interface IHttpServer. + /// + public interface IWebSocketManager + { + /// + /// Occurs when [web socket connected]. + /// + event EventHandler> WebSocketConnected; + + /// + /// Inits this instance. + /// + /// The websocket listeners. + void Init(IEnumerable listeners); + + /// + /// The HTTP request handler. + /// + /// The current HTTP context. + /// The task. + Task WebSocketRequestHandler(HttpContext context); + } +} diff --git a/MediaBrowser.Controller/Net/SecurityException.cs b/MediaBrowser.Controller/Net/SecurityException.cs index f0d0b45a0..c6347133a 100644 --- a/MediaBrowser.Controller/Net/SecurityException.cs +++ b/MediaBrowser.Controller/Net/SecurityException.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; namespace MediaBrowser.Controller.Net diff --git a/MediaBrowser.Controller/Net/StaticResultOptions.cs b/MediaBrowser.Controller/Net/StaticResultOptions.cs deleted file mode 100644 index 85772e036..000000000 --- a/MediaBrowser.Controller/Net/StaticResultOptions.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace MediaBrowser.Controller.Net -{ - public class StaticResultOptions - { - public string ContentType { get; set; } - - public TimeSpan? CacheDuration { get; set; } - - public DateTime? DateLastModified { get; set; } - - public Func> ContentFactory { get; set; } - - public bool IsHeadRequest { get; set; } - - public IDictionary ResponseHeaders { get; set; } - - public Action OnComplete { get; set; } - - public Action OnError { get; set; } - - public string Path { get; set; } - - public long? ContentLength { get; set; } - - public FileShare FileShare { get; set; } - - public StaticResultOptions() - { - ResponseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - FileShare = FileShare.Read; - } - } - - public class StaticFileResultOptions : StaticResultOptions - { - } -} diff --git a/MediaBrowser.Controller/Notifications/INotificationManager.cs b/MediaBrowser.Controller/Notifications/INotificationManager.cs index 44defbe0b..08d9bc12a 100644 --- a/MediaBrowser.Controller/Notifications/INotificationManager.cs +++ b/MediaBrowser.Controller/Notifications/INotificationManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Notifications/INotificationService.cs b/MediaBrowser.Controller/Notifications/INotificationService.cs index ab5eb13cd..fa947220a 100644 --- a/MediaBrowser.Controller/Notifications/INotificationService.cs +++ b/MediaBrowser.Controller/Notifications/INotificationService.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; diff --git a/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs b/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs index 9f1d2841d..52a3e120b 100644 --- a/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs +++ b/MediaBrowser.Controller/Notifications/INotificationTypeFactory.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Model.Notifications; diff --git a/MediaBrowser.Controller/Notifications/UserNotification.cs b/MediaBrowser.Controller/Notifications/UserNotification.cs index a1029589b..d768abfe7 100644 --- a/MediaBrowser.Controller/Notifications/UserNotification.cs +++ b/MediaBrowser.Controller/Notifications/UserNotification.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using Jellyfin.Data.Entities; using MediaBrowser.Model.Notifications; diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 0ae1b8bbf..ebc37bd1f 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; @@ -156,15 +158,23 @@ namespace MediaBrowser.Controller.Persistence int GetCount(InternalItemsQuery query); QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query); + QueryResult<(BaseItem, ItemCounts)> GetMusicGenres(InternalItemsQuery query); + QueryResult<(BaseItem, ItemCounts)> GetStudios(InternalItemsQuery query); + QueryResult<(BaseItem, ItemCounts)> GetArtists(InternalItemsQuery query); + QueryResult<(BaseItem, ItemCounts)> GetAlbumArtists(InternalItemsQuery query); + QueryResult<(BaseItem, ItemCounts)> GetAllArtists(InternalItemsQuery query); List GetMusicGenreNames(); + List GetStudioNames(); + List GetGenreNames(); + List GetAllArtistNames(); } } diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs index ba7c9fd50..81ba513ce 100644 --- a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs +++ b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs @@ -24,9 +24,15 @@ namespace MediaBrowser.Controller.Persistence /// /// The user id. /// The key. - /// Task{UserItemData}. + /// The user data. UserItemData GetUserData(long userId, string key); + /// + /// Gets the user data. + /// + /// The user id. + /// The keys. + /// The user data. UserItemData GetUserData(long userId, List keys); /// diff --git a/MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs b/MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs index e3b2d4665..e07e96f73 100644 --- a/MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs +++ b/MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Persistence diff --git a/MediaBrowser.Controller/Persistence/MediaStreamQuery.cs b/MediaBrowser.Controller/Persistence/MediaStreamQuery.cs index 7dc563b3a..f9295c8fd 100644 --- a/MediaBrowser.Controller/Persistence/MediaStreamQuery.cs +++ b/MediaBrowser.Controller/Persistence/MediaStreamQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index 544cd2643..fbf2c5213 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -29,7 +31,7 @@ namespace MediaBrowser.Controller.Playlists /// The item ids. /// The user identifier. /// Task. - void AddToPlaylist(string playlistId, ICollection itemIds, Guid userId); + Task AddToPlaylistAsync(Guid playlistId, ICollection itemIds, Guid userId); /// /// Removes from playlist. @@ -37,7 +39,7 @@ namespace MediaBrowser.Controller.Playlists /// The playlist identifier. /// The entry ids. /// Task. - void RemoveFromPlaylist(string playlistId, IEnumerable entryIds); + Task RemoveFromPlaylistAsync(string playlistId, IEnumerable entryIds); /// /// Gets the playlists folder. @@ -53,6 +55,6 @@ namespace MediaBrowser.Controller.Playlists /// The entry identifier. /// The new index. /// Task. - void MoveItem(string playlistId, string entryId, int newIndex); + Task MoveItemAsync(string playlistId, string entryId, int newIndex); } } diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 0fd63770f..216dd2709 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; diff --git a/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs b/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs index 5deb165f6..bf15fe040 100644 --- a/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs +++ b/MediaBrowser.Controller/Plugins/ILocalizablePlugin.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.IO; using System.Reflection; diff --git a/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs b/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs index 077f5ab63..93eab42cc 100644 --- a/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs +++ b/MediaBrowser.Controller/Plugins/IPluginConfigurationPage.cs @@ -42,6 +42,7 @@ namespace MediaBrowser.Controller.Plugins /// The plugin configuration. /// PluginConfiguration, + /// /// The none. /// diff --git a/MediaBrowser.Controller/Providers/AlbumInfo.cs b/MediaBrowser.Controller/Providers/AlbumInfo.cs index dbda4843f..276bcf125 100644 --- a/MediaBrowser.Controller/Providers/AlbumInfo.cs +++ b/MediaBrowser.Controller/Providers/AlbumInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Providers/ArtistInfo.cs b/MediaBrowser.Controller/Providers/ArtistInfo.cs index 08bf3982b..adf885baa 100644 --- a/MediaBrowser.Controller/Providers/ArtistInfo.cs +++ b/MediaBrowser.Controller/Providers/ArtistInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; namespace MediaBrowser.Controller.Providers diff --git a/MediaBrowser.Controller/Providers/BookInfo.cs b/MediaBrowser.Controller/Providers/BookInfo.cs index 03a6737c5..cce0a25fc 100644 --- a/MediaBrowser.Controller/Providers/BookInfo.cs +++ b/MediaBrowser.Controller/Providers/BookInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public class BookInfo : ItemLookupInfo diff --git a/MediaBrowser.Controller/Providers/BoxSetInfo.cs b/MediaBrowser.Controller/Providers/BoxSetInfo.cs index d23f2b9bf..f43ea6717 100644 --- a/MediaBrowser.Controller/Providers/BoxSetInfo.cs +++ b/MediaBrowser.Controller/Providers/BoxSetInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public class BoxSetInfo : ItemLookupInfo diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index b7640c205..f77455485 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; diff --git a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs index 7c1371702..006174be8 100644 --- a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs +++ b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.IO; using MediaBrowser.Model.Drawing; diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs index 55c41ff82..a4c8dab7e 100644 --- a/MediaBrowser.Controller/Providers/EpisodeInfo.cs +++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs b/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs index 6b4c9feb5..32a9cbef2 100644 --- a/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs index 949a17740..f06481c7a 100644 --- a/MediaBrowser.Controller/Providers/IDirectoryService.cs +++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Model.IO; diff --git a/MediaBrowser.Controller/Providers/IDynamicImageProvider.cs b/MediaBrowser.Controller/Providers/IDynamicImageProvider.cs index dec327d66..ab66462fa 100644 --- a/MediaBrowser.Controller/Providers/IDynamicImageProvider.cs +++ b/MediaBrowser.Controller/Providers/IDynamicImageProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Providers/IHasItemChangeMonitor.cs b/MediaBrowser.Controller/Providers/IHasItemChangeMonitor.cs index 68acb3910..a0e20e312 100644 --- a/MediaBrowser.Controller/Providers/IHasItemChangeMonitor.cs +++ b/MediaBrowser.Controller/Providers/IHasItemChangeMonitor.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Providers diff --git a/MediaBrowser.Controller/Providers/IHasLookupInfo.cs b/MediaBrowser.Controller/Providers/IHasLookupInfo.cs index 4c0c38442..42cb52371 100644 --- a/MediaBrowser.Controller/Providers/IHasLookupInfo.cs +++ b/MediaBrowser.Controller/Providers/IHasLookupInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public interface IHasLookupInfo diff --git a/MediaBrowser.Controller/Providers/IHasOrder.cs b/MediaBrowser.Controller/Providers/IHasOrder.cs index a3db61225..9fde0e695 100644 --- a/MediaBrowser.Controller/Providers/IHasOrder.cs +++ b/MediaBrowser.Controller/Providers/IHasOrder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public interface IHasOrder diff --git a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs index 463c81376..c129eddb3 100644 --- a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs +++ b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs b/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs index 44fb1b394..e771c881d 100644 --- a/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Providers/IMetadataProvider.cs b/MediaBrowser.Controller/Providers/IMetadataProvider.cs index 62b16dadd..1a87e0625 100644 --- a/MediaBrowser.Controller/Providers/IMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/IMetadataProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Providers diff --git a/MediaBrowser.Controller/Providers/IMetadataService.cs b/MediaBrowser.Controller/Providers/IMetadataService.cs index 21204e6d3..5f3d4274e 100644 --- a/MediaBrowser.Controller/Providers/IMetadataService.cs +++ b/MediaBrowser.Controller/Providers/IMetadataService.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs b/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs index 28da27ae7..6d98af33e 100644 --- a/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs +++ b/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public interface IPreRefreshProvider : ICustomMetadataProvider diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs index 8ba01d773..996ec27c0 100644 --- a/MediaBrowser.Controller/Providers/IProviderManager.cs +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; @@ -5,12 +7,12 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Events; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.Providers @@ -72,7 +74,7 @@ namespace MediaBrowser.Controller.Providers /// Task. Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken); - Task SaveImage(User user, Stream source, string mimeType, string path); + Task SaveImage(Stream source, string mimeType, string path); /// /// Adds the metadata providers. diff --git a/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs b/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs index c143b15cd..f146decb6 100644 --- a/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/IRemoteMetadataProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs b/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs index 17ad9e4a3..9592baa7c 100644 --- a/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs +++ b/MediaBrowser.Controller/Providers/IRemoteSearchProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Net.Http; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs index 3f8c409f5..9fc379f04 100644 --- a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Linq; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Providers/ItemInfo.cs b/MediaBrowser.Controller/Providers/ItemInfo.cs index d61153dfa..b50def043 100644 --- a/MediaBrowser.Controller/Providers/ItemInfo.cs +++ b/MediaBrowser.Controller/Providers/ItemInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs index 4707b0c7f..b777cc1d3 100644 --- a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs +++ b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Entities; @@ -12,6 +14,12 @@ namespace MediaBrowser.Controller.Providers /// The name. public string Name { get; set; } + /// + /// Gets or sets the path. + /// + /// The path. + public string Path { get; set; } + /// /// Gets or sets the metadata language. /// diff --git a/MediaBrowser.Controller/Providers/LocalImageInfo.cs b/MediaBrowser.Controller/Providers/LocalImageInfo.cs index 184281025..41801862f 100644 --- a/MediaBrowser.Controller/Providers/LocalImageInfo.cs +++ b/MediaBrowser.Controller/Providers/LocalImageInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs b/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs index 6d49b5510..920e3da5b 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshMode.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public enum MetadataRefreshMode diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs index 0a473b80c..b92b83701 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Linq; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs index 270ea2444..1c695cafa 100644 --- a/MediaBrowser.Controller/Providers/MetadataResult.cs +++ b/MediaBrowser.Controller/Providers/MetadataResult.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; diff --git a/MediaBrowser.Controller/Providers/MovieInfo.cs b/MediaBrowser.Controller/Providers/MovieInfo.cs index 5b2c3ed03..20e6b697a 100644 --- a/MediaBrowser.Controller/Providers/MovieInfo.cs +++ b/MediaBrowser.Controller/Providers/MovieInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public class MovieInfo : ItemLookupInfo diff --git a/MediaBrowser.Controller/Providers/MusicVideoInfo.cs b/MediaBrowser.Controller/Providers/MusicVideoInfo.cs index 9835351fc..0b927f6eb 100644 --- a/MediaBrowser.Controller/Providers/MusicVideoInfo.cs +++ b/MediaBrowser.Controller/Providers/MusicVideoInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; namespace MediaBrowser.Controller.Providers diff --git a/MediaBrowser.Controller/Providers/PersonLookupInfo.cs b/MediaBrowser.Controller/Providers/PersonLookupInfo.cs index a6218c039..11cb71f90 100644 --- a/MediaBrowser.Controller/Providers/PersonLookupInfo.cs +++ b/MediaBrowser.Controller/Providers/PersonLookupInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public class PersonLookupInfo : ItemLookupInfo diff --git a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs index a2ac6c9ae..9653bc1c4 100644 --- a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs +++ b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Providers diff --git a/MediaBrowser.Controller/Providers/SeasonInfo.cs b/MediaBrowser.Controller/Providers/SeasonInfo.cs index dd2ef9ad7..2a4c1f03c 100644 --- a/MediaBrowser.Controller/Providers/SeasonInfo.cs +++ b/MediaBrowser.Controller/Providers/SeasonInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Providers/SeriesInfo.cs b/MediaBrowser.Controller/Providers/SeriesInfo.cs index 6c206e031..976fa175a 100644 --- a/MediaBrowser.Controller/Providers/SeriesInfo.cs +++ b/MediaBrowser.Controller/Providers/SeriesInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public class SeriesInfo : ItemLookupInfo diff --git a/MediaBrowser.Controller/Providers/SongInfo.cs b/MediaBrowser.Controller/Providers/SongInfo.cs index 50615b0bd..58f76dca9 100644 --- a/MediaBrowser.Controller/Providers/SongInfo.cs +++ b/MediaBrowser.Controller/Providers/SongInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; diff --git a/MediaBrowser.Controller/Providers/TrailerInfo.cs b/MediaBrowser.Controller/Providers/TrailerInfo.cs index 13f07562d..630850f9d 100644 --- a/MediaBrowser.Controller/Providers/TrailerInfo.cs +++ b/MediaBrowser.Controller/Providers/TrailerInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Controller.Providers { public class TrailerInfo : ItemLookupInfo diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs new file mode 100644 index 000000000..959a2d771 --- /dev/null +++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs @@ -0,0 +1,87 @@ +using System; +using MediaBrowser.Model.QuickConnect; + +namespace MediaBrowser.Controller.QuickConnect +{ + /// + /// Quick connect standard interface. + /// + public interface IQuickConnect + { + /// + /// Gets or sets the length of user facing codes. + /// + int CodeLength { get; set; } + + /// + /// Gets or sets the name of internal access tokens. + /// + string TokenName { get; set; } + + /// + /// Gets the current state of quick connect. + /// + QuickConnectState State { get; } + + /// + /// Gets or sets the time (in minutes) before quick connect will automatically deactivate. + /// + int Timeout { get; set; } + + /// + /// Assert that quick connect is currently active and throws an exception if it is not. + /// + void AssertActive(); + + /// + /// Temporarily activates quick connect for a short amount of time. + /// + void Activate(); + + /// + /// Changes the state of quick connect. + /// + /// New state to change to. + void SetState(QuickConnectState newState); + + /// + /// Initiates a new quick connect request. + /// + /// A quick connect result with tokens to proceed or throws an exception if not active. + QuickConnectResult TryConnect(); + + /// + /// Checks the status of an individual request. + /// + /// Unique secret identifier of the request. + /// Quick connect result. + QuickConnectResult CheckRequestStatus(string secret); + + /// + /// Authorizes a quick connect request to connect as the calling user. + /// + /// User id. + /// Identifying code for the request. + /// A boolean indicating if the authorization completed successfully. + bool AuthorizeRequest(Guid userId, string code); + + /// + /// Expire quick connect requests that are over the time limit. If is true, all requests are unconditionally expired. + /// + /// If true, all requests will be expired. + void ExpireRequests(bool expireAll = false); + + /// + /// Deletes all quick connect access tokens for the provided user. + /// + /// Guid of the user to delete tokens for. + /// A count of the deleted tokens. + int DeleteAllDevices(Guid user); + + /// + /// Generates a short code to display to the user to uniquely identify this request. + /// + /// A short, unique alphanumeric string. + string GenerateCode(); + } +} diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs index a73937b3e..b99c46843 100644 --- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs +++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; diff --git a/MediaBrowser.Controller/Resolvers/ResolverPriority.cs b/MediaBrowser.Controller/Resolvers/ResolverPriority.cs index 1911e5c1d..ac73a5ea8 100644 --- a/MediaBrowser.Controller/Resolvers/ResolverPriority.cs +++ b/MediaBrowser.Controller/Resolvers/ResolverPriority.cs @@ -9,15 +9,22 @@ namespace MediaBrowser.Controller.Resolvers /// The first. /// First = 1, + /// /// The second. /// Second = 2, + /// /// The third. /// Third = 3, + + /// + /// The Fourth. + /// Fourth = 4, + /// /// The last. /// diff --git a/MediaBrowser.Controller/Security/AuthenticationInfo.cs b/MediaBrowser.Controller/Security/AuthenticationInfo.cs index 1d0b959b7..efac9273e 100644 --- a/MediaBrowser.Controller/Security/AuthenticationInfo.cs +++ b/MediaBrowser.Controller/Security/AuthenticationInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Security diff --git a/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs b/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs index 2bd17eb26..c5f3da0b1 100644 --- a/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs +++ b/MediaBrowser.Controller/Security/AuthenticationInfoQuery.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Security diff --git a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs b/MediaBrowser.Controller/Security/IAuthenticationRepository.cs index 6a9625613..883b74165 100644 --- a/MediaBrowser.Controller/Security/IAuthenticationRepository.cs +++ b/MediaBrowser.Controller/Security/IAuthenticationRepository.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Devices; using MediaBrowser.Model.Querying; @@ -29,6 +31,7 @@ namespace MediaBrowser.Controller.Security void Delete(AuthenticationInfo info); DeviceOptions GetDeviceOptions(string deviceId); + void UpdateDeviceOptions(string deviceId, DeviceOptions options); } } diff --git a/MediaBrowser.Controller/Session/AuthenticationRequest.cs b/MediaBrowser.Controller/Session/AuthenticationRequest.cs index 685ca3bdd..cc321fd22 100644 --- a/MediaBrowser.Controller/Session/AuthenticationRequest.cs +++ b/MediaBrowser.Controller/Session/AuthenticationRequest.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Session diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs index 04450085b..22d6e2a04 100644 --- a/MediaBrowser.Controller/Session/ISessionController.cs +++ b/MediaBrowser.Controller/Session/ISessionController.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index e54f21050..228b2331d 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -1,12 +1,14 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Security; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Session; using MediaBrowser.Model.SyncPlay; @@ -264,6 +266,14 @@ namespace MediaBrowser.Controller.Session /// Task{SessionInfo}. Task AuthenticateNewSession(AuthenticationRequest request); + /// + /// Authenticates a new session with quick connect. + /// + /// The request. + /// Quick connect access token. + /// Task{SessionInfo}. + Task AuthenticateQuickConnect(AuthenticationRequest request, string token); + /// /// Creates the new session. /// diff --git a/MediaBrowser.Controller/Session/SessionEventArgs.cs b/MediaBrowser.Controller/Session/SessionEventArgs.cs index 08baaf647..097e32eae 100644 --- a/MediaBrowser.Controller/Session/SessionEventArgs.cs +++ b/MediaBrowser.Controller/Session/SessionEventArgs.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Controller.Session diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 4b088998c..054fd33d9 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Linq; using System.Text.Json.Serialization; diff --git a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs index de7f72d1c..70cb9eebe 100644 --- a/MediaBrowser.Controller/Sorting/AlphanumComparator.cs +++ b/MediaBrowser.Controller/Sorting/AlphanumComparator.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + #nullable enable using System; @@ -127,7 +129,7 @@ namespace MediaBrowser.Controller.Sorting } /// - public int Compare(string x, string y) + public int Compare(string? x, string? y) { return CompareValues(x, y); } diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs index 2a68f4678..88467814c 100644 --- a/MediaBrowser.Controller/Sorting/SortExtensions.cs +++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; @@ -7,6 +9,7 @@ namespace MediaBrowser.Controller.Sorting public static class SortExtensions { private static readonly AlphanumComparator _comparer = new AlphanumComparator(); + public static IEnumerable OrderByString(this IEnumerable list, Func getName) { return list.OrderBy(getName, _comparer); diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs index 39538aacd..f43d523a6 100644 --- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs +++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs b/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs index 8ffd7c530..a633262de 100644 --- a/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs +++ b/MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/Subtitles/SubtitleDownloadEventArgs.cs b/MediaBrowser.Controller/Subtitles/SubtitleDownloadEventArgs.cs deleted file mode 100644 index 5703aea17..000000000 --- a/MediaBrowser.Controller/Subtitles/SubtitleDownloadEventArgs.cs +++ /dev/null @@ -1,27 +0,0 @@ -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/SubtitleDownloadFailureEventArgs.cs b/MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs new file mode 100644 index 000000000..ce8141219 --- /dev/null +++ b/MediaBrowser.Controller/Subtitles/SubtitleDownloadFailureEventArgs.cs @@ -0,0 +1,26 @@ +using System; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Subtitles +{ + /// + /// An event that occurs when subtitle downloading fails. + /// + public class SubtitleDownloadFailureEventArgs : EventArgs + { + /// + /// Gets or sets the item. + /// + public BaseItem Item { get; set; } + + /// + /// Gets or sets the provider. + /// + public string Provider { get; set; } + + /// + /// Gets or sets the exception. + /// + public Exception Exception { get; set; } + } +} diff --git a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs index ad6025927..a86b05778 100644 --- a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs +++ b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.IO; namespace MediaBrowser.Controller.Subtitles diff --git a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs index a202723b9..7d3c20e8f 100644 --- a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs +++ b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Controller.Providers; diff --git a/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs b/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs index d6bac23be..e7395b136 100644 --- a/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs +++ b/MediaBrowser.Controller/Sync/IHasDynamicAccess.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Sync; diff --git a/MediaBrowser.Controller/Sync/IServerSyncProvider.cs b/MediaBrowser.Controller/Sync/IServerSyncProvider.cs index 8b2d5d779..c97fd7044 100644 --- a/MediaBrowser.Controller/Sync/IServerSyncProvider.cs +++ b/MediaBrowser.Controller/Sync/IServerSyncProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.IO; using System.Threading; diff --git a/MediaBrowser.Controller/Sync/ISyncProvider.cs b/MediaBrowser.Controller/Sync/ISyncProvider.cs index 56f6f3729..950cc73e8 100644 --- a/MediaBrowser.Controller/Sync/ISyncProvider.cs +++ b/MediaBrowser.Controller/Sync/ISyncProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Model.Sync; diff --git a/MediaBrowser.Controller/Sync/SyncedFileInfo.cs b/MediaBrowser.Controller/Sync/SyncedFileInfo.cs index 687a46d78..a626738fb 100644 --- a/MediaBrowser.Controller/Sync/SyncedFileInfo.cs +++ b/MediaBrowser.Controller/Sync/SyncedFileInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Model.MediaInfo; @@ -5,6 +7,11 @@ namespace MediaBrowser.Controller.Sync { public class SyncedFileInfo { + public SyncedFileInfo() + { + RequiredHttpHeaders = new Dictionary(); + } + /// /// Gets or sets the path. /// @@ -12,25 +19,23 @@ namespace MediaBrowser.Controller.Sync public string Path { get; set; } public string[] PathParts { get; set; } + /// /// Gets or sets the protocol. /// /// The protocol. public MediaProtocol Protocol { get; set; } + /// /// Gets or sets the required HTTP headers. /// /// The required HTTP headers. public Dictionary RequiredHttpHeaders { get; set; } + /// /// Gets or sets the identifier. /// /// The identifier. public string Id { get; set; } - - public SyncedFileInfo() - { - RequiredHttpHeaders = new Dictionary(); - } } } diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs index 45c543806..60d17fe36 100644 --- a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs @@ -64,4 +64,4 @@ namespace MediaBrowser.Controller.SyncPlay /// The group info for the clients. GroupInfoView GetInfo(); } -} \ No newline at end of file +} diff --git a/MediaBrowser.Controller/TV/ITVSeriesManager.cs b/MediaBrowser.Controller/TV/ITVSeriesManager.cs index 09a0f6fea..291dea04e 100644 --- a/MediaBrowser.Controller/TV/ITVSeriesManager.cs +++ b/MediaBrowser.Controller/TV/ITVSeriesManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Querying; diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index a8ebe6bc5..21b5d0c5b 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -240,11 +240,11 @@ namespace MediaBrowser.MediaEncoding.Attachments if (protocol == MediaProtocol.File) { var date = _fileSystem.GetLastWriteTimeUtc(mediaPath); - filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D"); + filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture); } else { - filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D"); + filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture); } var prefix = filename.Substring(0, 1); diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 75123a843..c8bf5557b 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Linq; -using System.Text; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; @@ -87,19 +87,17 @@ namespace MediaBrowser.MediaEncoding.Encoder "hevc_videotoolbox" }; - // Try and use the individual library versions to determine a FFmpeg version - // This lookup table is to be maintained with the following command line: - // $ ffmpeg -version | perl -ne ' print "$1=$2.$3," if /^(lib\w+)\s+(\d+)\.\s*(\d+)/' - private static readonly IReadOnlyDictionary _ffmpegVersionMap = new Dictionary + // These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below + private static readonly IReadOnlyDictionary _ffmpegMinimumLibraryVersions = new Dictionary { - { "libavutil=56.51,libavcodec=58.91,libavformat=58.45,libavdevice=58.10,libavfilter=7.85,libswscale=5.7,libswresample=3.7,libpostproc=55.7,", new Version(4, 3) }, - { "libavutil=56.31,libavcodec=58.54,libavformat=58.29,libavdevice=58.8,libavfilter=7.57,libswscale=5.5,libswresample=3.5,libpostproc=55.5,", new Version(4, 2) }, - { "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3,", new Version(4, 1) }, - { "libavutil=56.14,libavcodec=58.18,libavformat=58.12,libavdevice=58.3,libavfilter=7.16,libswscale=5.1,libswresample=3.1,libpostproc=55.1,", new Version(4, 0) }, - { "libavutil=55.78,libavcodec=57.107,libavformat=57.83,libavdevice=57.10,libavfilter=6.107,libswscale=4.8,libswresample=2.9,libpostproc=54.7,", new Version(3, 4) }, - { "libavutil=55.58,libavcodec=57.89,libavformat=57.71,libavdevice=57.6,libavfilter=6.82,libswscale=4.6,libswresample=2.7,libpostproc=54.5,", new Version(3, 3) }, - { "libavutil=55.34,libavcodec=57.64,libavformat=57.56,libavdevice=57.1,libavfilter=6.65,libswscale=4.2,libswresample=2.3,libpostproc=54.1,", new Version(3, 2) }, - { "libavutil=54.31,libavcodec=56.60,libavformat=56.40,libavdevice=56.4,libavfilter=5.40,libswscale=3.1,libswresample=1.2,libpostproc=53.3,", new Version(2, 8) } + { "libavutil", new Version(56, 14) }, + { "libavcodec", new Version(58, 18) }, + { "libavformat", new Version(58, 12) }, + { "libavdevice", new Version(58, 3) }, + { "libavfilter", new Version(7, 16) }, + { "libswscale", new Version(5, 1) }, + { "libswresample", new Version(3, 1) }, + { "libpostproc", new Version(55, 1) } }; private readonly ILogger _logger; @@ -118,6 +116,7 @@ namespace MediaBrowser.MediaEncoding.Encoder Decoder } + // When changing this, also change the minimum library versions in _ffmpegMinimumLibraryVersions public static Version MinVersion { get; } = new Version(4, 0); public static Version MaxVersion { get; } = null; @@ -156,32 +155,36 @@ namespace MediaBrowser.MediaEncoding.Encoder // Work out what the version under test is var version = GetFFmpegVersion(versionOutput); - _logger.LogInformation("Found ffmpeg version {0}", version != null ? version.ToString() : "unknown"); + _logger.LogInformation("Found ffmpeg version {Version}", version != null ? version.ToString() : "unknown"); if (version == null) { - if (MinVersion != null && MaxVersion != null) // Version is unknown + if (MaxVersion != null) // Version is unknown { if (MinVersion == MaxVersion) { - _logger.LogWarning("FFmpeg validation: We recommend ffmpeg version {0}", MinVersion); + _logger.LogWarning("FFmpeg validation: We recommend version {MinVersion}", MinVersion); } else { - _logger.LogWarning("FFmpeg validation: We recommend a minimum of {0} and maximum of {1}", MinVersion, MaxVersion); + _logger.LogWarning("FFmpeg validation: We recommend a minimum of {MinVersion} and maximum of {MaxVersion}", MinVersion, MaxVersion); } } + else + { + _logger.LogWarning("FFmpeg validation: We recommend minimum version {MinVersion}", MinVersion); + } return false; } - else if (MinVersion != null && version < MinVersion) // Version is below what we recommend + else if (version < MinVersion) // Version is below what we recommend { - _logger.LogWarning("FFmpeg validation: The minimum recommended ffmpeg version is {0}", MinVersion); + _logger.LogWarning("FFmpeg validation: The minimum recommended version is {MinVersion}", MinVersion); return false; } else if (MaxVersion != null && version > MaxVersion) // Version is above what we recommend { - _logger.LogWarning("FFmpeg validation: The maximum recommended ffmpeg version is {0}", MaxVersion); + _logger.LogWarning("FFmpeg validation: The maximum recommended version is {MaxVersion}", MaxVersion); return false; } @@ -197,13 +200,12 @@ namespace MediaBrowser.MediaEncoding.Encoder /// /// Using the output from "ffmpeg -version" work out the FFmpeg version. /// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy - /// to parse. If this is not available, then we try to match known library versions to FFmpeg versions. - /// If that fails then we use one of the main libraries to determine if it's new/older than the latest - /// we have stored. + /// to parse. If this is not available, then we try to match known library versions to FFmpeg versions. + /// If that fails then we test the libraries to determine if they're newer than our minimum versions. /// /// The output from "ffmpeg -version". /// The FFmpeg version. - internal static Version GetFFmpegVersion(string output) + internal Version GetFFmpegVersion(string output) { // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output var match = Regex.Match(output, @"^ffmpeg version n?((?:[0-9]+\.?)+)"); @@ -212,14 +214,33 @@ namespace MediaBrowser.MediaEncoding.Encoder { return new Version(match.Groups[1].Value); } - else - { - // Create a reduced version string and lookup key from dictionary - var reducedVersion = GetLibrariesVersionString(output); - // Try to lookup the string and return Key, otherwise if not found returns null - return _ffmpegVersionMap.TryGetValue(reducedVersion, out Version version) ? version : null; + var versionMap = GetFFmpegLibraryVersions(output); + + var allVersionsValidated = true; + + foreach (var minimumVersion in _ffmpegMinimumLibraryVersions) + { + if (versionMap.TryGetValue(minimumVersion.Key, out var foundVersion)) + { + if (foundVersion >= minimumVersion.Value) + { + _logger.LogInformation("Found {Library} version {FoundVersion} ({MinimumVersion})", minimumVersion.Key, foundVersion, minimumVersion.Value); + } + else + { + _logger.LogWarning("Found {Library} version {FoundVersion} lower than recommended version {MinimumVersion}", minimumVersion.Key, foundVersion, minimumVersion.Value); + allVersionsValidated = false; + } + } + else + { + _logger.LogError("{Library} version not found", minimumVersion.Key); + allVersionsValidated = false; + } } + + return allVersionsValidated ? MinVersion : null; } /// @@ -228,23 +249,23 @@ namespace MediaBrowser.MediaEncoding.Encoder /// /// The 'ffmpeg -version' output. /// The library names and major.minor version numbers. - private static string GetLibrariesVersionString(string output) + private static IReadOnlyDictionary GetFFmpegLibraryVersions(string output) { - var rc = new StringBuilder(144); - foreach (Match m in Regex.Matches( + var map = new Dictionary(); + + foreach (Match match in Regex.Matches( output, @"((?lib\w+)\s+(?[0-9]+)\.\s*(?[0-9]+))", RegexOptions.Multiline)) { - rc.Append(m.Groups["name"]) - .Append('=') - .Append(m.Groups["major"]) - .Append('.') - .Append(m.Groups["minor"]) - .Append(','); + var version = new Version( + int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture), + int.Parse(match.Groups["minor"].Value, CultureInfo.InvariantCulture)); + + map.Add(match.Groups["name"].Value, version); } - return rc.Length == 0 ? null : rc.ToString(); + return map; } private IEnumerable GetHwaccelTypes() diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs index 7c2d9f1fd..63310fdf6 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs @@ -1,6 +1,8 @@ #pragma warning disable CS1591 +using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using MediaBrowser.Model.MediaInfo; @@ -14,7 +16,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { var url = inputFiles[0]; - return string.Format("\"{0}\"", url); + return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", url); } return GetConcatInputArgument(inputFiles); @@ -33,7 +35,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { var files = string.Join("|", inputFiles.Select(NormalizePath)); - return string.Format("concat:\"{0}\"", files); + return string.Format(CultureInfo.InvariantCulture, "concat:\"{0}\"", files); } // Determine the input path for video files @@ -47,15 +49,15 @@ namespace MediaBrowser.MediaEncoding.Encoder /// System.String. private static string GetFileInputArgument(string path) { - if (path.IndexOf("://") != -1) + if (path.IndexOf("://", StringComparison.Ordinal) != -1) { - return string.Format("\"{0}\"", path); + return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", path); } // Quotes are valid path characters in linux and they need to be escaped here with a leading \ path = NormalizePath(path); - return string.Format("file:\"{0}\"", path); + return string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", path); } /// @@ -66,7 +68,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private static string NormalizePath(string path) { // Quotes are valid path characters in linux and they need to be escaped here with a leading \ - return path.Replace("\"", "\\\""); + return path.Replace("\"", "\\\"", StringComparison.Ordinal); } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 778c0b18c..5a3a9185d 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Json; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Probing; @@ -54,6 +55,9 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly object _runningProcessesLock = new object(); private readonly List _runningProcesses = new List(); + // MediaEncoder is registered as a Singleton + private readonly JsonSerializerOptions _jsonSerializerOptions; + private List _encoders = new List(); private List _decoders = new List(); private List _hwaccels = new List(); @@ -75,6 +79,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _localization = localization; _encodingHelperFactory = encodingHelperFactory; _startupOptionFFmpegPath = config.GetValue(Controller.Extensions.ConfigurationExtensions.FfmpegPathKey) ?? string.Empty; + _jsonSerializerOptions = JsonDefaults.GetOptions(); } private EncodingHelper EncodingHelper => _encodingHelperFactory.Value; @@ -196,9 +201,6 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger.LogWarning("FFmpeg: {Location}: Failed version check: {Path}", location, path); } - // ToDo - Enable the ffmpeg validator. At the moment any version can be used. - rc = true; - _ffmpegPath = path; EncoderLocation = location; } @@ -377,7 +379,7 @@ namespace MediaBrowser.MediaEncoding.Encoder var args = extractChapters ? "{0} -i {1} -threads 0 -v warning -print_format json -show_streams -show_chapters -show_format" : "{0} -i {1} -threads 0 -v warning -print_format json -show_streams -show_format"; - args = string.Format(args, probeSizeArgument, inputPath).Trim(); + args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath).Trim(); var process = new Process { @@ -417,6 +419,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { result = await JsonSerializer.DeserializeAsync( process.StandardOutput.BaseStream, + _jsonSerializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false); } catch @@ -552,8 +555,8 @@ namespace MediaBrowser.MediaEncoding.Encoder // Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case. var thumbnail = enableThumbnail ? ",thumbnail=24" : string.Empty; - var args = useIFrame ? string.Format("-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2}{4}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, thumbnail) : - string.Format("-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg); + var args = useIFrame ? string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2}{4}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, thumbnail) : + string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg); var probeSizeArgument = EncodingHelper.GetProbeSizeArgument(1); var analyzeDurationArgument = EncodingHelper.GetAnalyzeDurationArgument(1); @@ -570,7 +573,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (offset.HasValue) { - args = string.Format("-ss {0} ", GetTimeParameter(offset.Value)) + args; + args = string.Format(CultureInfo.InvariantCulture, "-ss {0} ", GetTimeParameter(offset.Value)) + args; } if (videoStream != null) @@ -641,7 +644,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (exitCode == -1 || !file.Exists || file.Length == 0) { - var msg = string.Format("ffmpeg image extraction failed for {0}", inputPath); + var msg = string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputPath); _logger.LogError(msg); @@ -684,13 +687,13 @@ namespace MediaBrowser.MediaEncoding.Encoder { var maxWidthParam = maxWidth.Value.ToString(_usCulture); - vf += string.Format(",scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam); + vf += string.Format(CultureInfo.InvariantCulture, ",scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam); } Directory.CreateDirectory(targetDirectory); var outputPath = Path.Combine(targetDirectory, filenamePrefix + "%05d.jpg"); - var args = string.Format("-i {0} -threads 0 -v quiet -vf \"{2}\" -f image2 \"{1}\"", inputArgument, outputPath, vf); + var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads 0 -v quiet -vf \"{2}\" -f image2 \"{1}\"", inputArgument, outputPath, vf); var probeSizeArgument = EncodingHelper.GetProbeSizeArgument(1); var analyzeDurationArgument = EncodingHelper.GetAnalyzeDurationArgument(1); @@ -790,7 +793,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (exitCode == -1) { - var msg = string.Format("ffmpeg image extraction failed for {0}", inputArgument); + var msg = string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", inputArgument); _logger.LogError(msg); @@ -856,7 +859,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // https://ffmpeg.org/ffmpeg-filters.html#Notes-on-filtergraph-escaping // We need to double escape - return path.Replace('\\', '/').Replace(":", "\\:").Replace("'", "'\\\\\\''"); + return path.Replace('\\', '/').Replace(":", "\\:", StringComparison.Ordinal).Replace("'", "'\\\\\\''", StringComparison.Ordinal); } /// diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 017f917e2..814edd732 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -34,7 +34,7 @@ - + diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs index 8996d3b09..b7b23deff 100644 --- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs +++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs @@ -133,7 +133,6 @@ namespace MediaBrowser.MediaEncoding.Probing /// /// The bits_per_raw_sample. [JsonPropertyName("bits_per_raw_sample")] - [JsonConverter(typeof(JsonInt32Converter))] public int BitsPerRawSample { get; set; } /// diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 19e3bd8e6..40a3b43e1 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -42,7 +42,8 @@ namespace MediaBrowser.MediaEncoding.Probing var info = new MediaInfo { Path = path, - Protocol = protocol + Protocol = protocol, + VideoType = videoType }; FFProbeHelpers.NormalizeFFProbeResult(data); @@ -1133,7 +1134,7 @@ namespace MediaBrowser.MediaEncoding.Probing { // Only use the comma as a delimeter if there are no slashes or pipes. // We want to be careful not to split names that have commas in them - var delimeter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.IndexOf(i) != -1) ? + var delimeter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.IndexOf(i, StringComparison.Ordinal) != -1) ? _nameDelimiters : new[] { ',' }; @@ -1377,8 +1378,8 @@ namespace MediaBrowser.MediaEncoding.Probing if (subtitle.Contains('/', StringComparison.Ordinal)) // It contains a episode number and season number { string[] numbers = subtitle.Split(' '); - video.IndexNumber = int.Parse(numbers[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/')[0]); - int totalEpisodesInSeason = int.Parse(numbers[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/')[1]); + video.IndexNumber = int.Parse(numbers[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/')[0], CultureInfo.InvariantCulture); + int totalEpisodesInSeason = int.Parse(numbers[0].Replace(".", string.Empty, StringComparison.Ordinal).Split('/')[1], CultureInfo.InvariantCulture); description = string.Join(" ", numbers, 1, numbers.Length - 1).Trim(); // Skip the first, concatenate the rest, clean up spaces and save it } diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs index 308b62886..86b87fddd 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs @@ -86,9 +86,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles private void RemoteNativeFormatting(SubtitleTrackEvent p) { - int indexOfBegin = p.Text.IndexOf('{'); + int indexOfBegin = p.Text.IndexOf('{', StringComparison.Ordinal); string pre = string.Empty; - while (indexOfBegin >= 0 && p.Text.IndexOf('}') > indexOfBegin) + while (indexOfBegin >= 0 && p.Text.IndexOf('}', StringComparison.Ordinal) > indexOfBegin) { string s = p.Text.Substring(indexOfBegin); if (s.StartsWith("{\\an1}", StringComparison.Ordinal) || @@ -116,10 +116,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles pre = s.Substring(0, 5) + "}"; } - int indexOfEnd = p.Text.IndexOf('}'); + int indexOfEnd = p.Text.IndexOf('}', StringComparison.Ordinal); p.Text = p.Text.Remove(indexOfBegin, (indexOfEnd - indexOfBegin) + 1); - indexOfBegin = p.Text.IndexOf('{'); + indexOfBegin = p.Text.IndexOf('{', StringComparison.Ordinal); } p.Text = pre + p.Text; diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs index 6b7a81e6e..a5d641747 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Text; using System.Threading; @@ -50,14 +51,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles { eventsStarted = true; } - else if (!string.IsNullOrEmpty(line) && line.Trim().StartsWith(";")) + else if (!string.IsNullOrEmpty(line) && line.Trim().StartsWith(";", StringComparison.Ordinal)) { // skip comment lines } else if (eventsStarted && line.Trim().Length > 0) { string s = line.Trim().ToLowerInvariant(); - if (s.StartsWith("format:")) + if (s.StartsWith("format:", StringComparison.Ordinal)) { if (line.Length > 10) { @@ -103,7 +104,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles string[] splittedLine; - if (s.StartsWith("dialogue:")) + if (s.StartsWith("dialogue:", StringComparison.Ordinal)) { splittedLine = line.Substring(10).Split(','); } @@ -181,10 +182,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles string[] timeCode = time.Split(':', '.'); return new TimeSpan( 0, - int.Parse(timeCode[0]), - int.Parse(timeCode[1]), - int.Parse(timeCode[2]), - int.Parse(timeCode[3]) * 10).Ticks; + int.Parse(timeCode[0], CultureInfo.InvariantCulture), + int.Parse(timeCode[1], CultureInfo.InvariantCulture), + int.Parse(timeCode[2], CultureInfo.InvariantCulture), + int.Parse(timeCode[3], CultureInfo.InvariantCulture) * 10).Ticks; } private static string GetFormattedText(string text) @@ -193,11 +194,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles for (int i = 0; i < 10; i++) // just look ten times... { - if (text.Contains(@"{\fn")) + if (text.Contains(@"{\fn", StringComparison.Ordinal)) { - int start = text.IndexOf(@"{\fn"); + int start = text.IndexOf(@"{\fn", StringComparison.Ordinal); int end = text.IndexOf('}', start); - if (end > 0 && !text.Substring(start).StartsWith("{\\fn}")) + if (end > 0 && !text.Substring(start).StartsWith("{\\fn}", StringComparison.Ordinal)) { string fontName = text.Substring(start + 4, end - (start + 4)); string extraTags = string.Empty; @@ -212,7 +213,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles text = text.Insert(start, ""); } - int indexOfEndTag = text.IndexOf("{\\fn}", start); + int indexOfEndTag = text.IndexOf("{\\fn}", start, StringComparison.Ordinal); if (indexOfEndTag > 0) { text = text.Remove(indexOfEndTag, "{\\fn}".Length).Insert(indexOfEndTag, ""); @@ -224,11 +225,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - if (text.Contains(@"{\fs")) + if (text.Contains(@"{\fs", StringComparison.Ordinal)) { - int start = text.IndexOf(@"{\fs"); + int start = text.IndexOf(@"{\fs", StringComparison.Ordinal); int end = text.IndexOf('}', start); - if (end > 0 && !text.Substring(start).StartsWith("{\\fs}")) + if (end > 0 && !text.Substring(start).StartsWith("{\\fs}", StringComparison.Ordinal)) { string fontSize = text.Substring(start + 4, end - (start + 4)); string extraTags = string.Empty; @@ -245,7 +246,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles text = text.Insert(start, ""); } - int indexOfEndTag = text.IndexOf("{\\fs}", start); + int indexOfEndTag = text.IndexOf("{\\fs}", start, StringComparison.Ordinal); if (indexOfEndTag > 0) { text = text.Remove(indexOfEndTag, "{\\fs}".Length).Insert(indexOfEndTag, ""); @@ -258,17 +259,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - if (text.Contains(@"{\c")) + if (text.Contains(@"{\c", StringComparison.Ordinal)) { - int start = text.IndexOf(@"{\c"); + int start = text.IndexOf(@"{\c", StringComparison.Ordinal); int end = text.IndexOf('}', start); - if (end > 0 && !text.Substring(start).StartsWith("{\\c}")) + if (end > 0 && !text.Substring(start).StartsWith("{\\c}", StringComparison.Ordinal)) { string color = text.Substring(start + 4, end - (start + 4)); string extraTags = string.Empty; CheckAndAddSubTags(ref color, ref extraTags, out bool italic); - color = color.Replace("&", string.Empty).TrimStart('H'); + color = color.Replace("&", string.Empty, StringComparison.Ordinal).TrimStart('H'); color = color.PadLeft(6, '0'); // switch to rrggbb from bbggrr @@ -285,7 +286,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles text = text.Insert(start, ""); } - int indexOfEndTag = text.IndexOf("{\\c}", start); + int indexOfEndTag = text.IndexOf("{\\c}", start, StringComparison.Ordinal); if (indexOfEndTag > 0) { text = text.Remove(indexOfEndTag, "{\\c}".Length).Insert(indexOfEndTag, ""); @@ -297,17 +298,17 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - if (text.Contains(@"{\1c")) // "1" specifices primary color + if (text.Contains(@"{\1c", StringComparison.Ordinal)) // "1" specifices primary color { - int start = text.IndexOf(@"{\1c"); + int start = text.IndexOf(@"{\1c", StringComparison.Ordinal); int end = text.IndexOf('}', start); - if (end > 0 && !text.Substring(start).StartsWith("{\\1c}")) + if (end > 0 && !text.Substring(start).StartsWith("{\\1c}", StringComparison.Ordinal)) { string color = text.Substring(start + 5, end - (start + 5)); string extraTags = string.Empty; CheckAndAddSubTags(ref color, ref extraTags, out bool italic); - color = color.Replace("&", string.Empty).TrimStart('H'); + color = color.Replace("&", string.Empty, StringComparison.Ordinal).TrimStart('H'); color = color.PadLeft(6, '0'); // switch to rrggbb from bbggrr @@ -329,25 +330,25 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - text = text.Replace(@"{\i1}", ""); - text = text.Replace(@"{\i0}", ""); - text = text.Replace(@"{\i}", ""); + text = text.Replace(@"{\i1}", "", StringComparison.Ordinal); + text = text.Replace(@"{\i0}", "", StringComparison.Ordinal); + text = text.Replace(@"{\i}", "", StringComparison.Ordinal); if (CountTagInText(text, "") > CountTagInText(text, "")) { text += ""; } - text = text.Replace(@"{\u1}", ""); - text = text.Replace(@"{\u0}", ""); - text = text.Replace(@"{\u}", ""); + text = text.Replace(@"{\u1}", "", StringComparison.Ordinal); + text = text.Replace(@"{\u0}", "", StringComparison.Ordinal); + text = text.Replace(@"{\u}", "", StringComparison.Ordinal); if (CountTagInText(text, "") > CountTagInText(text, "")) { text += ""; } - text = text.Replace(@"{\b1}", ""); - text = text.Replace(@"{\b0}", ""); - text = text.Replace(@"{\b}", ""); + text = text.Replace(@"{\b1}", "", StringComparison.Ordinal); + text = text.Replace(@"{\b0}", "", StringComparison.Ordinal); + text = text.Replace(@"{\b}", "", StringComparison.Ordinal); if (CountTagInText(text, "") > CountTagInText(text, "")) { text += ""; @@ -362,7 +363,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles private static int CountTagInText(string text, string tag) { int count = 0; - int index = text.IndexOf(tag); + int index = text.IndexOf(tag, StringComparison.Ordinal); while (index >= 0) { count++; @@ -371,7 +372,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles return count; } - index = text.IndexOf(tag, index + 1); + index = text.IndexOf(tag, index + 1, StringComparison.Ordinal); } return count; @@ -380,7 +381,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles private static void CheckAndAddSubTags(ref string tagName, ref string extraTags, out bool italic) { italic = false; - int indexOfSPlit = tagName.IndexOf(@"\"); + int indexOfSPlit = tagName.IndexOf('\\', StringComparison.Ordinal); if (indexOfSPlit > 0) { string rest = tagName.Substring(indexOfSPlit).TrimStart('\\'); @@ -388,9 +389,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles for (int i = 0; i < 10; i++) { - if (rest.StartsWith("fs") && rest.Length > 2) + if (rest.StartsWith("fs", StringComparison.Ordinal) && rest.Length > 2) { - indexOfSPlit = rest.IndexOf(@"\"); + indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal); string fontSize = rest; if (indexOfSPlit > 0) { @@ -404,9 +405,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles extraTags += " size=\"" + fontSize.Substring(2) + "\""; } - else if (rest.StartsWith("fn") && rest.Length > 2) + else if (rest.StartsWith("fn", StringComparison.Ordinal) && rest.Length > 2) { - indexOfSPlit = rest.IndexOf(@"\"); + indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal); string fontName = rest; if (indexOfSPlit > 0) { @@ -420,9 +421,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles extraTags += " face=\"" + fontName.Substring(2) + "\""; } - else if (rest.StartsWith("c") && rest.Length > 2) + else if (rest.StartsWith("c", StringComparison.Ordinal) && rest.Length > 2) { - indexOfSPlit = rest.IndexOf(@"\"); + indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal); string fontColor = rest; if (indexOfSPlit > 0) { @@ -435,7 +436,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles } string color = fontColor.Substring(2); - color = color.Replace("&", string.Empty).TrimStart('H'); + color = color.Replace("&", string.Empty, StringComparison.Ordinal).TrimStart('H'); color = color.PadLeft(6, '0'); // switch to rrggbb from bbggrr color = "#" + color.Remove(color.Length - 6) + color.Substring(color.Length - 2, 2) + color.Substring(color.Length - 4, 2) + color.Substring(color.Length - 6, 2); @@ -443,9 +444,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles extraTags += " color=\"" + color + "\""; } - else if (rest.StartsWith("i1") && rest.Length > 1) + else if (rest.StartsWith("i1", StringComparison.Ordinal) && rest.Length > 1) { - indexOfSPlit = rest.IndexOf(@"\"); + indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal); italic = true; if (indexOfSPlit > 0) { @@ -456,9 +457,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles rest = string.Empty; } } - else if (rest.Length > 0 && rest.Contains("\\")) + else if (rest.Length > 0 && rest.Contains('\\', StringComparison.Ordinal)) { - indexOfSPlit = rest.IndexOf(@"\"); + indexOfSPlit = rest.IndexOf('\\', StringComparison.Ordinal); rest = rest.Substring(indexOfSPlit).TrimStart('\\'); } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 374e35b96..6ac5ac2ff 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -415,7 +415,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles // FFmpeg automatically convert character encoding when it is UTF-16 // If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to recode subtitle event" - if ((inputPath.EndsWith(".smi") || inputPath.EndsWith(".sami")) && + if ((inputPath.EndsWith(".smi", StringComparison.Ordinal) || inputPath.EndsWith(".sami", StringComparison.Ordinal)) && (encodingParam.Equals("UTF-16BE", StringComparison.OrdinalIgnoreCase) || encodingParam.Equals("UTF-16LE", StringComparison.OrdinalIgnoreCase))) { @@ -435,7 +435,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles CreateNoWindow = true, UseShellExecute = false, FileName = _mediaEncoder.EncoderPath, - Arguments = string.Format("{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath), + Arguments = string.Format(CultureInfo.InvariantCulture, "{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath), WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false }, @@ -506,7 +506,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle conversion failed for {0}", inputPath)); } - await SetAssFont(outputPath).ConfigureAwait(false); + await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false); _logger.LogInformation("ffmpeg subtitle conversion succeeded for {Path}", inputPath); } @@ -668,7 +668,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (string.Equals(outputCodec, "ass", StringComparison.OrdinalIgnoreCase)) { - await SetAssFont(outputPath).ConfigureAwait(false); + await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false); } } @@ -676,8 +676,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// Sets the ass font. /// /// The file. + /// The token to monitor for cancellation requests. The default value is System.Threading.CancellationToken.None. /// Task. - private async Task SetAssFont(string file) + private async Task SetAssFont(string file, CancellationToken cancellationToken = default) { _logger.LogInformation("Setting ass font within {File}", file); @@ -692,14 +693,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles text = await reader.ReadToEndAsync().ConfigureAwait(false); } - var newText = text.Replace(",Arial,", ",Arial Unicode MS,"); + var newText = text.Replace(",Arial,", ",Arial Unicode MS,", StringComparison.Ordinal); - if (!string.Equals(text, newText)) + if (!string.Equals(text, newText, StringComparison.Ordinal)) { using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read)) using (var writer = new StreamWriter(fileStream, encoding)) { - writer.Write(newText); + await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false); } } } @@ -736,7 +737,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName; // UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding - if ((path.EndsWith(".ass") || path.EndsWith(".ssa") || path.EndsWith(".srt")) + if ((path.EndsWith(".ass", StringComparison.Ordinal) || path.EndsWith(".ssa", StringComparison.Ordinal) || path.EndsWith(".srt", StringComparison.Ordinal)) && (string.Equals(charset, "utf-16le", StringComparison.OrdinalIgnoreCase) || string.Equals(charset, "utf-16be", StringComparison.OrdinalIgnoreCase))) { diff --git a/MediaBrowser.Model/Activity/IActivityManager.cs b/MediaBrowser.Model/Activity/IActivityManager.cs index 9dab5e77b..d5344494e 100644 --- a/MediaBrowser.Model/Activity/IActivityManager.cs +++ b/MediaBrowser.Model/Activity/IActivityManager.cs @@ -4,7 +4,7 @@ using System; using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Entities; -using MediaBrowser.Model.Events; +using Jellyfin.Data.Events; using MediaBrowser.Model.Querying; namespace MediaBrowser.Model.Activity @@ -13,8 +13,6 @@ namespace MediaBrowser.Model.Activity { event EventHandler> EntryCreated; - void Create(ActivityLog entry); - Task CreateAsync(ActivityLog entry); QueryResult GetPagedResult(int? startIndex, int? limit); diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index c66091f9d..33975bc1e 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -78,6 +78,11 @@ namespace MediaBrowser.Model.Configuration /// true if this instance is port authorized; otherwise, false. public bool IsPortAuthorized { get; set; } + /// + /// Gets or sets if quick connect is available for use on this server. + /// + public bool QuickConnectAvailable { get; set; } + public bool AutoRunWebApp { get; set; } public bool EnableRemoteAccess { get; set; } @@ -299,6 +304,7 @@ namespace MediaBrowser.Model.Configuration AutoRunWebApp = true; EnableRemoteAccess = true; + QuickConnectAvailable = false; EnableUPnP = false; MinResumePct = 5; diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs index a579f8464..93e60753a 100644 --- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs +++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs @@ -157,7 +157,7 @@ namespace MediaBrowser.Model.Dlna // flagValue = flagValue | DlnaFlags.TimeBasedSeek; //} - string dlnaflags = string.Format(";DLNA.ORG_FLAGS={0}", + string dlnaflags = string.Format(CultureInfo.InvariantCulture, ";DLNA.ORG_FLAGS={0}", DlnaMaps.FlagsToString(flagValue)); ResponseProfile mediaProfile = _profile.GetVideoMediaProfile(container, diff --git a/MediaBrowser.Model/Dlna/DlnaMaps.cs b/MediaBrowser.Model/Dlna/DlnaMaps.cs index 052b4b78b..95cd0ac27 100644 --- a/MediaBrowser.Model/Dlna/DlnaMaps.cs +++ b/MediaBrowser.Model/Dlna/DlnaMaps.cs @@ -1,18 +1,20 @@ #pragma warning disable CS1591 +using System.Globalization; + namespace MediaBrowser.Model.Dlna { public static class DlnaMaps { private static readonly string DefaultStreaming = - FlagsToString(DlnaFlags.StreamingTransferMode | + FlagsToString(DlnaFlags.StreamingTransferMode | DlnaFlags.BackgroundTransferMode | DlnaFlags.ConnectionStall | DlnaFlags.ByteBasedSeek | DlnaFlags.DlnaV15); private static readonly string DefaultInteractive = - FlagsToString(DlnaFlags.InteractiveTransferMode | + FlagsToString(DlnaFlags.InteractiveTransferMode | DlnaFlags.BackgroundTransferMode | DlnaFlags.ConnectionStall | DlnaFlags.ByteBasedSeek | @@ -20,7 +22,7 @@ namespace MediaBrowser.Model.Dlna public static string FlagsToString(DlnaFlags flags) { - return string.Format("{0:X8}{1:D24}", (ulong)flags, 0); + return string.Format(CultureInfo.InvariantCulture, "{0:X8}{1:D24}", (ulong)flags, 0); } public static string GetOrgOpValue(bool hasKnownRuntime, bool isDirectStream, TranscodeSeekInfo profileTranscodeSeekInfo) diff --git a/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs b/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs index 76c9a4b04..05209e53d 100644 --- a/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs +++ b/MediaBrowser.Model/Dlna/IDeviceDiscovery.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 using System; -using MediaBrowser.Model.Events; +using Jellyfin.Data.Events; namespace MediaBrowser.Model.Dlna { diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs index bdc5f8bb7..3c955989a 100644 --- a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs +++ b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using MediaBrowser.Model.MediaInfo; @@ -142,26 +143,26 @@ namespace MediaBrowser.Model.Dlna { 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(CultureInfo.InvariantCulture, "AVC_TS_HP_{0}D_MPEG1_L2_ISO", resolution)) }; } - return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_HP_{0}D_MPEG1_L2_T", resolution)) }; + return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_HP_{0}D_MPEG1_L2_T", resolution)) }; } if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) { - return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_MP_{0}D_AAC_MULT5{1}", resolution, suffix)) }; + return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_MP_{0}D_AAC_MULT5{1}", resolution, suffix)) }; } if (string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) { - return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_MP_{0}D_MPEG1_L3{1}", resolution, suffix)) }; + return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_MP_{0}D_MPEG1_L3{1}", resolution, suffix)) }; } if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) { - return new MediaFormatProfile[] { ValueOf(string.Format("AVC_TS_MP_{0}D_AC3{1}", resolution, suffix)) }; + return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "AVC_TS_MP_{0}D_AC3{1}", resolution, suffix)) }; } } else if (string.Equals(videoCodec, "vc1", StringComparison.OrdinalIgnoreCase)) @@ -180,29 +181,29 @@ namespace MediaBrowser.Model.Dlna { suffix = string.Equals(suffix, "_ISO", StringComparison.OrdinalIgnoreCase) ? suffix : "_T"; - return new MediaFormatProfile[] { ValueOf(string.Format("VC1_TS_HD_DTS{0}", suffix)) }; + return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "VC1_TS_HD_DTS{0}", suffix)) }; } } else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase)) { if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) { - return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_AAC{0}", suffix)) }; + return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "MPEG4_P2_TS_ASP_AAC{0}", suffix)) }; } if (string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) { - return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_MPEG1_L3{0}", suffix)) }; + return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "MPEG4_P2_TS_ASP_MPEG1_L3{0}", suffix)) }; } if (string.Equals(audioCodec, "mp2", StringComparison.OrdinalIgnoreCase)) { - return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_MPEG2_L2{0}", suffix)) }; + return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "MPEG4_P2_TS_ASP_MPEG2_L2{0}", suffix)) }; } if (string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) { - return new MediaFormatProfile[] { ValueOf(string.Format("MPEG4_P2_TS_ASP_AC3{0}", suffix)) }; + return new MediaFormatProfile[] { ValueOf(string.Format(CultureInfo.InvariantCulture, "MPEG4_P2_TS_ASP_AC3{0}", suffix)) }; } } diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 204340c46..94d53ab70 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -191,7 +191,7 @@ namespace MediaBrowser.Model.Dlna var encodedValue = pair.Value.Replace(" ", "%20"); - list.Add(string.Format("{0}={1}", pair.Name, encodedValue)); + list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue)); } string queryString = string.Join("&", list.ToArray()); @@ -214,18 +214,18 @@ namespace MediaBrowser.Model.Dlna { if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) { - return string.Format("{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); + return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); } - return string.Format("{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); + return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); } if (string.Equals(SubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) { - return string.Format("{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); + return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); } - return string.Format("{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); + return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); } private static List BuildParams(StreamInfo item, string accessToken) @@ -457,7 +457,7 @@ namespace MediaBrowser.Model.Dlna { if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal) { - info.Url = string.Format("{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}", + info.Url = string.Format(CultureInfo.InvariantCulture, "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}", baseUrl, ItemId, MediaSourceId, diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 902e29b20..4ae38ade9 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -8,8 +8,9 @@ Jellyfin Contributors Jellyfin.Model - https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt + 10.7.0 https://github.com/jellyfin/jellyfin + GPL-3.0-only @@ -19,13 +20,23 @@ true enable latest + true + true + true + snupkg + + + + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + - + diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs new file mode 100644 index 000000000..0fa40b6a7 --- /dev/null +++ b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs @@ -0,0 +1,40 @@ +using System; + +namespace MediaBrowser.Model.QuickConnect +{ + /// + /// Stores the result of an incoming quick connect request. + /// + public class QuickConnectResult + { + /// + /// Gets a value indicating whether this request is authorized. + /// + public bool Authenticated => !string.IsNullOrEmpty(Authentication); + + /// + /// Gets or sets the secret value used to uniquely identify this request. Can be used to retrieve authentication information. + /// + public string? Secret { get; set; } + + /// + /// Gets or sets the user facing code used so the user can quickly differentiate this request from others. + /// + public string? Code { get; set; } + + /// + /// Gets or sets the private access token. + /// + public string? Authentication { get; set; } + + /// + /// Gets or sets an error message. + /// + public string? Error { get; set; } + + /// + /// Gets or sets the DateTime that this request was created. + /// + public DateTime? DateAdded { get; set; } + } +} diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectState.cs b/MediaBrowser.Model/QuickConnect/QuickConnectState.cs new file mode 100644 index 000000000..f1074f25f --- /dev/null +++ b/MediaBrowser.Model/QuickConnect/QuickConnectState.cs @@ -0,0 +1,23 @@ +namespace MediaBrowser.Model.QuickConnect +{ + /// + /// Quick connect state. + /// + public enum QuickConnectState + { + /// + /// This feature has not been opted into and is unavailable until the server administrator chooses to opt-in. + /// + Unavailable = 0, + + /// + /// The feature is enabled for use on the server but is not currently accepting connection requests. + /// + Available = 1, + + /// + /// The feature is actively accepting connection requests. + /// + Active = 2 + } +} diff --git a/MediaBrowser.Model/Services/ApiMemberAttribute.cs b/MediaBrowser.Model/Services/ApiMemberAttribute.cs deleted file mode 100644 index 63f3ecd55..000000000 --- a/MediaBrowser.Model/Services/ApiMemberAttribute.cs +++ /dev/null @@ -1,65 +0,0 @@ -#nullable disable -using System; - -namespace MediaBrowser.Model.Services -{ - /// - /// Identifies a single API endpoint. - /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] - public class ApiMemberAttribute : Attribute - { - /// - /// Gets or sets verb to which applies attribute. By default applies to all verbs. - /// - public string Verb { get; set; } - - /// - /// Gets or sets parameter type: It can be only one of the following: path, query, body, form, or header. - /// - public string ParameterType { get; set; } - - /// - /// Gets or sets unique name for the parameter. Each name must be unique, even if they are associated with different paramType values. - /// - /// - /// - /// 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. - /// - /// - public string Name { get; set; } - - /// - /// Gets or sets the human-readable description for the parameter. - /// - public string Description { get; set; } - - /// - /// For path, query, and header paramTypes, this field must be a primitive. For body, this can be a complex or container datatype. - /// - public string DataType { get; set; } - - /// - /// For path, this is always true. Otherwise, this field tells the client whether or not the field must be supplied. - /// - public bool IsRequired { get; set; } - - /// - /// 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. - /// - public bool AllowMultiple { get; set; } - - /// - /// Gets or sets route to which applies attribute, matches using StartsWith. By default applies to all routes. - /// - public string Route { get; set; } - - /// - /// Whether to exclude this property from being included in the ModelSchema. - /// - public bool ExcludeInSchema { get; set; } - } -} diff --git a/MediaBrowser.Model/Services/IAsyncStreamWriter.cs b/MediaBrowser.Model/Services/IAsyncStreamWriter.cs deleted file mode 100644 index afbca78a2..000000000 --- a/MediaBrowser.Model/Services/IAsyncStreamWriter.cs +++ /dev/null @@ -1,13 +0,0 @@ -#pragma warning disable CS1591 - -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 deleted file mode 100644 index 313f34b41..000000000 --- a/MediaBrowser.Model/Services/IHasHeaders.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Model.Services -{ - public interface IHasHeaders - { - IDictionary Headers { get; } - } -} diff --git a/MediaBrowser.Model/Services/IHasRequestFilter.cs b/MediaBrowser.Model/Services/IHasRequestFilter.cs deleted file mode 100644 index b83d3b075..000000000 --- a/MediaBrowser.Model/Services/IHasRequestFilter.cs +++ /dev/null @@ -1,24 +0,0 @@ -#pragma warning disable CS1591 - -using Microsoft.AspNetCore.Http; - -namespace MediaBrowser.Model.Services -{ - public interface IHasRequestFilter - { - /// - /// Gets the order in which Request Filters are executed. - /// <0 Executed before global request filters. - /// >0 Executed after global request filters. - /// - int Priority { get; } - - /// - /// The request filter is executed before the service. - /// - /// The http request wrapper. - /// The http response wrapper. - /// The request DTO. - void RequestFilter(IRequest req, HttpResponse res, object requestDto); - } -} diff --git a/MediaBrowser.Model/Services/IHttpRequest.cs b/MediaBrowser.Model/Services/IHttpRequest.cs deleted file mode 100644 index 3ea65195c..000000000 --- a/MediaBrowser.Model/Services/IHttpRequest.cs +++ /dev/null @@ -1,17 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Services -{ - public interface IHttpRequest : IRequest - { - /// - /// Gets the HTTP Verb. - /// - string HttpMethod { get; } - - /// - /// Gets the value of the Accept HTTP Request Header. - /// - string Accept { get; } - } -} diff --git a/MediaBrowser.Model/Services/IHttpResult.cs b/MediaBrowser.Model/Services/IHttpResult.cs deleted file mode 100644 index abc581d8e..000000000 --- a/MediaBrowser.Model/Services/IHttpResult.cs +++ /dev/null @@ -1,35 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System.Net; - -namespace MediaBrowser.Model.Services -{ - public interface IHttpResult : IHasHeaders - { - /// - /// The HTTP Response Status. - /// - int Status { get; set; } - - /// - /// The HTTP Response Status Code. - /// - HttpStatusCode StatusCode { get; set; } - - /// - /// The HTTP Response ContentType. - /// - string ContentType { get; set; } - - /// - /// Response DTO. - /// - object Response { get; set; } - - /// - /// Holds the request call context. - /// - IRequest RequestContext { get; set; } - } -} diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs deleted file mode 100644 index 8bc1d3668..000000000 --- a/MediaBrowser.Model/Services/IRequest.cs +++ /dev/null @@ -1,93 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.AspNetCore.Http; - -namespace MediaBrowser.Model.Services -{ - public interface IRequest - { - HttpResponse Response { get; } - - /// - /// The name of the service being called (e.g. Request DTO Name) - /// - string OperationName { get; set; } - - /// - /// The Verb / HttpMethod or Action for this request - /// - string Verb { get; } - - /// - /// The request ContentType. - /// - string ContentType { get; } - - bool IsLocal { get; } - - string UserAgent { get; } - - /// - /// The expected Response ContentType for this request. - /// - string ResponseContentType { get; set; } - - /// - /// Attach any data to this request that all filters and services can access. - /// - Dictionary Items { get; } - - IHeaderDictionary Headers { get; } - - IQueryCollection QueryString { get; } - - string RawUrl { get; } - - string AbsoluteUri { get; } - - /// - /// The Remote Ip as reported by X-Forwarded-For, X-Real-IP or Request.UserHostAddress - /// - string RemoteIp { get; } - - /// - /// The value of the Authorization Header used to send the Api Key, null if not available. - /// - string Authorization { get; } - - string[] AcceptTypes { get; } - - string PathInfo { get; } - - Stream InputStream { get; } - - long ContentLength { get; } - - /// - /// The value of the Referrer, null if not available. - /// - 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; } - } -} diff --git a/MediaBrowser.Model/Services/IRequiresRequestStream.cs b/MediaBrowser.Model/Services/IRequiresRequestStream.cs deleted file mode 100644 index 3e5f2da42..000000000 --- a/MediaBrowser.Model/Services/IRequiresRequestStream.cs +++ /dev/null @@ -1,14 +0,0 @@ -#pragma warning disable CS1591 - -using System.IO; - -namespace MediaBrowser.Model.Services -{ - public interface IRequiresRequestStream - { - /// - /// The raw Http Request Input Stream. - /// - Stream RequestStream { get; set; } - } -} diff --git a/MediaBrowser.Model/Services/IService.cs b/MediaBrowser.Model/Services/IService.cs deleted file mode 100644 index 5233f57ab..000000000 --- a/MediaBrowser.Model/Services/IService.cs +++ /dev/null @@ -1,15 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Services -{ - // marker interface - public interface IService - { - } - - public interface IReturn { } - - public interface IReturn : IReturn { } - - public interface IReturnVoid : IReturn { } -} diff --git a/MediaBrowser.Model/Services/IStreamWriter.cs b/MediaBrowser.Model/Services/IStreamWriter.cs deleted file mode 100644 index 3ebfef66b..000000000 --- a/MediaBrowser.Model/Services/IStreamWriter.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -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 deleted file mode 100644 index bdb0cabdf..000000000 --- a/MediaBrowser.Model/Services/QueryParamCollection.cs +++ /dev/null @@ -1,147 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Linq; -using MediaBrowser.Model.Dto; - -namespace MediaBrowser.Model.Services -{ - // Remove this garbage class, it's just a bastard copy of NameValueCollection - public class QueryParamCollection : List - { - public QueryParamCollection() - { - } - - private static StringComparison GetStringComparison() - { - return StringComparison.OrdinalIgnoreCase; - } - - /// - /// Adds a new query parameter. - /// - public void Add(string key, string value) - { - Add(new NameValuePair(key, value)); - } - - private 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); - } - - private string Get(string name) - { - var stringComparison = GetStringComparison(); - - foreach (var pair in this) - { - if (string.Equals(pair.Name, name, stringComparison)) - { - return pair.Value; - } - } - - return null; - } - - private List GetItems(string name) - { - var stringComparison = GetStringComparison(); - - var list = new List(); - - foreach (var pair in this) - { - if (string.Equals(pair.Name, name, stringComparison)) - { - list.Add(pair); - } - } - - return list; - } - - public virtual List GetValues(string name) - { - var stringComparison = GetStringComparison(); - - var list = new List(); - - foreach (var pair in this) - { - if (string.Equals(pair.Name, name, stringComparison)) - { - list.Add(pair.Value); - } - } - - return list; - } - - public IEnumerable Keys - { - get - { - var keys = new string[this.Count]; - - for (var i = 0; i < keys.Length; i++) - { - keys[i] = this[i].Name; - } - - return keys; - } - } - - /// - /// Gets or sets a query parameter value by name. A query may contain multiple values of the same name - /// (i.e. "x=1&x=2"), in which case the value is an array, which works for both getting and setting. - /// - /// The query parameter name. - /// The query parameter value or array of values. - public string this[string name] - { - get => Get(name); - set => Set(name, value); - } - - private string GetQueryStringValue(NameValuePair pair) - { - return pair.Name + "=" + pair.Value; - } - - public override string ToString() - { - var vals = this.Select(GetQueryStringValue).ToArray(); - - return string.Join("&", vals); - } - } -} diff --git a/MediaBrowser.Model/Services/RouteAttribute.cs b/MediaBrowser.Model/Services/RouteAttribute.cs deleted file mode 100644 index f8bf51112..000000000 --- a/MediaBrowser.Model/Services/RouteAttribute.cs +++ /dev/null @@ -1,163 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Services -{ - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public class RouteAttribute : Attribute - { - /// - /// Initializes an instance of the class. - /// - /// - /// The path template to map to the request. See - /// RouteAttribute.Path - /// for details on the correct format. - /// - public RouteAttribute(string path) - : this(path, null) - { - } - - /// - /// Initializes an instance of the class. - /// - /// - /// The path template to map to the request. See - /// RouteAttribute.Path - /// for details on the correct format. - /// - /// A comma-delimited list of HTTP verbs supported by the - /// service. If unspecified, all verbs are assumed to be supported. - public RouteAttribute(string path, string verbs) - { - Path = path; - Verbs = verbs; - } - - /// - /// Gets or sets the path template to be mapped to the request. - /// - /// - /// A value providing the path mapped to - /// the request. Never . - /// - /// - /// Some examples of valid paths are: - /// - /// - /// "/Inventory" - /// "/Inventory/{Category}/{ItemId}" - /// "/Inventory/{ItemPath*}" - /// - /// - /// 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. - /// - /// 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&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. - /// - /// 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. - /// - /// 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. - /// - public string Path { get; set; } - - /// - /// Gets or sets short summary of what the route does. - /// - public string Summary { get; set; } - - public string Description { get; set; } - - public bool IsHidden { get; set; } - - /// - /// Gets or sets longer text to explain the behaviour of the route. - /// - public string Notes { get; set; } - - /// - /// Gets or sets a comma-delimited list of HTTP verbs supported by the service, such as - /// "GET,PUT,POST,DELETE". - /// - /// - /// A providing a comma-delimited list of HTTP verbs supported - /// by the service, or empty if all verbs are supported. - /// - public string Verbs { get; set; } - - /// - /// Used to rank the precedences of route definitions in reverse routing. - /// i.e. Priorities below 0 are auto-generated have less precedence. - /// - 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/PlayRequest.cs b/MediaBrowser.Model/Session/PlayRequest.cs index d57bed171..6a66465a2 100644 --- a/MediaBrowser.Model/Session/PlayRequest.cs +++ b/MediaBrowser.Model/Session/PlayRequest.cs @@ -2,7 +2,6 @@ #pragma warning disable CS1591 using System; -using MediaBrowser.Model.Services; namespace MediaBrowser.Model.Session { @@ -15,21 +14,18 @@ namespace MediaBrowser.Model.Session /// Gets or sets the item ids. /// /// The item ids. - [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; } /// /// Gets or sets the start position ticks that the first item should be played at. /// /// The start position ticks. - [ApiMember(Name = "StartPositionTicks", Description = "The starting position of the first item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] public long? StartPositionTicks { get; set; } /// /// Gets or sets the play command. /// /// The play command. - [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; } /// diff --git a/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs b/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs index b08acba2c..2f05e08c5 100644 --- a/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs +++ b/MediaBrowser.Model/Tasks/IScheduledTaskWorker.cs @@ -1,6 +1,6 @@ #nullable disable using System; -using MediaBrowser.Model.Events; +using Jellyfin.Data.Events; namespace MediaBrowser.Model.Tasks { diff --git a/MediaBrowser.Model/Tasks/ITaskManager.cs b/MediaBrowser.Model/Tasks/ITaskManager.cs index 363773ff7..02b29074e 100644 --- a/MediaBrowser.Model/Tasks/ITaskManager.cs +++ b/MediaBrowser.Model/Tasks/ITaskManager.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using MediaBrowser.Model.Events; +using Jellyfin.Data.Events; namespace MediaBrowser.Model.Tasks { diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index caf2e0f54..a1f01f7e8 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -80,11 +80,11 @@ namespace MediaBrowser.Model.Users public bool EnableAllDevices { get; set; } - public string[] EnabledChannels { get; set; } + public Guid[] EnabledChannels { get; set; } public bool EnableAllChannels { get; set; } - public string[] EnabledFolders { get; set; } + public Guid[] EnabledFolders { get; set; } public bool EnableAllFolders { get; set; } @@ -94,9 +94,9 @@ namespace MediaBrowser.Model.Users public bool EnablePublicSharing { get; set; } - public string[] BlockedMediaFolders { get; set; } + public Guid[] BlockedMediaFolders { get; set; } - public string[] BlockedChannels { get; set; } + public Guid[] BlockedChannels { get; set; } public int RemoteClientBitrateLimit { get; set; } @@ -145,10 +145,10 @@ namespace MediaBrowser.Model.Users LoginAttemptsBeforeLockout = -1; EnableAllChannels = true; - EnabledChannels = Array.Empty(); + EnabledChannels = Array.Empty(); EnableAllFolders = true; - EnabledFolders = Array.Empty(); + EnabledFolders = Array.Empty(); EnabledDevices = Array.Empty(); EnableAllDevices = true; diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index 26b50784b..413d297cb 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -187,7 +187,7 @@ namespace MediaBrowser.Providers.Manager } } - public async Task SaveImage(User user, Stream source, string path) + public async Task SaveImage(Stream source, string path) { await SaveImageToLocation(source, path, path, CancellationToken.None).ConfigureAwait(false); } @@ -355,7 +355,7 @@ namespace MediaBrowser.Providers.Manager if (string.IsNullOrWhiteSpace(extension)) { - throw new ArgumentException(string.Format("Unable to determine image file extension from mime type {0}", mimeType)); + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Unable to determine image file extension from mime type {0}", mimeType)); } if (type == ImageType.Thumb && saveLocally) diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index a5eb095c4..9227b6d93 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -54,7 +54,12 @@ namespace MediaBrowser.Providers.Manager return hasChanges; } - public async Task RefreshImages(BaseItem item, LibraryOptions libraryOptions, List providers, ImageRefreshOptions refreshOptions, MetadataOptions savedOptions, CancellationToken cancellationToken) + public async Task RefreshImages( + BaseItem item, + LibraryOptions libraryOptions, + List providers, + ImageRefreshOptions refreshOptions, + CancellationToken cancellationToken) { if (refreshOptions.IsReplacingImage(ImageType.Backdrop)) { @@ -78,19 +83,15 @@ namespace MediaBrowser.Providers.Manager foreach (var provider in providers) { - var remoteProvider = provider as IRemoteImageProvider; - - if (remoteProvider != null) + if (provider is IRemoteImageProvider remoteProvider) { await RefreshFromProvider(item, libraryOptions, remoteProvider, refreshOptions, typeOptions, backdropLimit, screenshotLimit, downloadedImages, result, cancellationToken).ConfigureAwait(false); continue; } - var dynamicImageProvider = provider as IDynamicImageProvider; - - if (dynamicImageProvider != null) + if (provider is IDynamicImageProvider dynamicImageProvider) { - await RefreshFromProvider(item, dynamicImageProvider, refreshOptions, typeOptions, libraryOptions, downloadedImages, result, cancellationToken).ConfigureAwait(false); + await RefreshFromProvider(item, dynamicImageProvider, refreshOptions, typeOptions, downloadedImages, result, cancellationToken).ConfigureAwait(false); } } @@ -100,11 +101,11 @@ namespace MediaBrowser.Providers.Manager /// /// Refreshes from provider. /// - private async Task RefreshFromProvider(BaseItem item, + private async Task RefreshFromProvider( + BaseItem item, IDynamicImageProvider provider, ImageRefreshOptions refreshOptions, TypeOptions savedOptions, - LibraryOptions libraryOptions, ICollection downloadedImages, RefreshResult result, CancellationToken cancellationToken) @@ -115,7 +116,7 @@ namespace MediaBrowser.Providers.Manager foreach (var imageType in images) { - if (!IsEnabled(savedOptions, imageType, item)) + if (!IsEnabled(savedOptions, imageType)) { continue; } @@ -133,12 +134,13 @@ namespace MediaBrowser.Providers.Manager if (response.Protocol == MediaProtocol.Http) { _logger.LogDebug("Setting image url into item {0}", item.Id); - item.SetImage(new ItemImageInfo - { - Path = response.Path, - Type = imageType - - }, 0); + item.SetImage( + new ItemImageInfo + { + Path = response.Path, + Type = imageType + }, + 0); } else { @@ -157,7 +159,7 @@ namespace MediaBrowser.Providers.Manager } downloadedImages.Add(imageType); - result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate; + result.UpdateType |= ItemUpdateType.ImageUpdate; } } } @@ -279,7 +281,7 @@ namespace MediaBrowser.Providers.Manager foreach (var imageType in _singularImages) { - if (!IsEnabled(savedOptions, imageType, item)) + if (!IsEnabled(savedOptions, imageType)) { continue; } @@ -299,8 +301,7 @@ namespace MediaBrowser.Providers.Manager minWidth = savedOptions.GetMinWidth(ImageType.Backdrop); await DownloadBackdrops(item, libraryOptions, ImageType.Backdrop, backdropLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false); - var hasScreenshots = item as IHasScreenshots; - if (hasScreenshots != null) + if (item is IHasScreenshots hasScreenshots) { minWidth = savedOptions.GetMinWidth(ImageType.Screenshot); await DownloadBackdrops(item, libraryOptions, ImageType.Screenshot, screenshotLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false); @@ -317,7 +318,7 @@ namespace MediaBrowser.Providers.Manager } } - private bool IsEnabled(TypeOptions options, ImageType type, BaseItem item) + private bool IsEnabled(TypeOptions options, ImageType type) { return options.IsEnabled(type); } @@ -452,10 +453,10 @@ namespace MediaBrowser.Providers.Manager .Where(i => i.Type == type && !(i.Width.HasValue && i.Width.Value < minWidth)) .ToList(); - if (EnableImageStub(item, type, libraryOptions) && eligibleImages.Count > 0) + if (EnableImageStub(item, libraryOptions) && eligibleImages.Count > 0) { SaveImageStub(item, type, eligibleImages.Select(i => i.Url)); - result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate; + result.UpdateType |= ItemUpdateType.ImageUpdate; return true; } @@ -476,7 +477,7 @@ namespace MediaBrowser.Providers.Manager null, cancellationToken).ConfigureAwait(false); - result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate; + result.UpdateType |= ItemUpdateType.ImageUpdate; return true; } catch (HttpException ex) @@ -495,7 +496,7 @@ namespace MediaBrowser.Providers.Manager return false; } - private bool EnableImageStub(BaseItem item, ImageType type, LibraryOptions libraryOptions) + private bool EnableImageStub(BaseItem item, LibraryOptions libraryOptions) { if (item is LiveTvProgram) { @@ -563,10 +564,10 @@ namespace MediaBrowser.Providers.Manager var url = image.Url; - if (EnableImageStub(item, imageType, libraryOptions)) + if (EnableImageStub(item, libraryOptions)) { SaveImageStub(item, imageType, new[] { url }); - result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate; + result.UpdateType |= ItemUpdateType.ImageUpdate; continue; } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 3b0c7b56c..d0de58427 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -52,7 +52,6 @@ namespace MediaBrowser.Providers.Manager public async Task RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) { var itemOfType = (TItemType)item; - var config = ProviderManager.GetMetadataOptions(item); var updateType = ItemUpdateType.None; var requiresRefresh = false; @@ -86,7 +85,7 @@ namespace MediaBrowser.Providers.Manager // Always validate images and check for new locally stored ones. if (itemImageProvider.ValidateImages(item, allImageProviders.OfType(), refreshOptions.DirectoryService)) { - updateType = updateType | ItemUpdateType.ImageUpdate; + updateType |= ItemUpdateType.ImageUpdate; } } catch (Exception ex) @@ -102,7 +101,7 @@ namespace MediaBrowser.Providers.Manager bool hasRefreshedMetadata = true; bool hasRefreshedImages = true; - var isFirstRefresh = item.DateLastRefreshed == default(DateTime); + var isFirstRefresh = item.DateLastRefreshed == default; // Next run metadata providers if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None) @@ -114,7 +113,7 @@ namespace MediaBrowser.Providers.Manager { if (item.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata)) { - updateType = updateType | ItemUpdateType.MetadataImport; + updateType |= ItemUpdateType.MetadataImport; } } @@ -132,7 +131,7 @@ namespace MediaBrowser.Providers.Manager var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, itemImageProvider, cancellationToken).ConfigureAwait(false); - updateType = updateType | result.UpdateType; + updateType |= result.UpdateType; if (result.Failures > 0) { hasRefreshedMetadata = false; @@ -147,9 +146,9 @@ namespace MediaBrowser.Providers.Manager if (providers.Count > 0) { - var result = await itemImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, config, cancellationToken).ConfigureAwait(false); + var result = await itemImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, cancellationToken).ConfigureAwait(false); - updateType = updateType | result.UpdateType; + updateType |= result.UpdateType; if (result.Failures > 0) { hasRefreshedImages = false; @@ -158,7 +157,7 @@ namespace MediaBrowser.Providers.Manager } var beforeSaveResult = BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh || refreshOptions.ForceSave, updateType); - updateType = updateType | beforeSaveResult; + updateType |= beforeSaveResult; // Save if changes were made, or it's never been saved before if (refreshOptions.ForceSave || updateType > ItemUpdateType.None || isFirstRefresh || refreshOptions.ReplaceAllMetadata || requiresRefresh) @@ -175,7 +174,7 @@ namespace MediaBrowser.Providers.Manager // If any of these properties are set then make sure the updateType is not None, just to force everything to save if (refreshOptions.ForceSave || refreshOptions.ReplaceAllMetadata) { - updateType = updateType | ItemUpdateType.MetadataDownload; + updateType |= ItemUpdateType.MetadataDownload; } if (hasRefreshedMetadata && hasRefreshedImages) @@ -184,11 +183,11 @@ namespace MediaBrowser.Providers.Manager } else { - item.DateLastRefreshed = default(DateTime); + item.DateLastRefreshed = default; } // Save to database - SaveItem(metadataResult, libraryOptions, updateType, cancellationToken); + await SaveItemAsync(metadataResult, libraryOptions, updateType, cancellationToken).ConfigureAwait(false); } await AfterMetadataRefresh(itemOfType, refreshOptions, cancellationToken).ConfigureAwait(false); @@ -203,26 +202,26 @@ namespace MediaBrowser.Providers.Manager lookupInfo.Year = result.ProductionYear; } - protected void SaveItem(MetadataResult result, LibraryOptions libraryOptions, ItemUpdateType reason, CancellationToken cancellationToken) + protected async Task SaveItemAsync(MetadataResult result, LibraryOptions libraryOptions, ItemUpdateType reason, CancellationToken cancellationToken) { if (result.Item.SupportsPeople && result.People != null) { var baseItem = result.Item; LibraryManager.UpdatePeople(baseItem, result.People); - SavePeopleMetadata(result.People, libraryOptions, cancellationToken); + await SavePeopleMetadataAsync(result.People, libraryOptions, cancellationToken).ConfigureAwait(false); } - result.Item.UpdateToRepository(reason, cancellationToken); + await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false); } - private void SavePeopleMetadata(List people, LibraryOptions libraryOptions, CancellationToken cancellationToken) + private async Task SavePeopleMetadataAsync(List people, LibraryOptions libraryOptions, CancellationToken cancellationToken) { foreach (var person in people) { cancellationToken.ThrowIfCancellationRequested(); - if (person.ProviderIds.Any() || !string.IsNullOrWhiteSpace(person.ImageUrl)) + if (person.ProviderIds.Count > 0 || !string.IsNullOrWhiteSpace(person.ImageUrl)) { var updateType = ItemUpdateType.MetadataDownload; @@ -239,40 +238,42 @@ namespace MediaBrowser.Providers.Manager if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary)) { - AddPersonImage(personEntity, libraryOptions, person.ImageUrl, cancellationToken); + await AddPersonImageAsync(personEntity, libraryOptions, person.ImageUrl, cancellationToken).ConfigureAwait(false); saveEntity = true; - updateType = updateType | ItemUpdateType.ImageUpdate; + updateType |= ItemUpdateType.ImageUpdate; } if (saveEntity) { - personEntity.UpdateToRepository(updateType, cancellationToken); + await personEntity.UpdateToRepositoryAsync(updateType, cancellationToken).ConfigureAwait(false); } } } } - private void AddPersonImage(Person personEntity, LibraryOptions libraryOptions, string imageUrl, CancellationToken cancellationToken) + private async Task AddPersonImageAsync(Person personEntity, LibraryOptions libraryOptions, string imageUrl, CancellationToken cancellationToken) { - // if (libraryOptions.DownloadImagesInAdvance) - //{ - // try - // { - // await ProviderManager.SaveImage(personEntity, imageUrl, ImageType.Primary, null, cancellationToken).ConfigureAwait(false); - // return; - // } - // catch (Exception ex) - // { - // Logger.LogError(ex, "Error in AddPersonImage"); - // } - //} - - personEntity.SetImage(new ItemImageInfo + if (libraryOptions.DownloadImagesInAdvance) { - Path = imageUrl, - Type = ImageType.Primary - }, 0); + try + { + await ProviderManager.SaveImage(personEntity, imageUrl, ImageType.Primary, null, cancellationToken).ConfigureAwait(false); + return; + } + catch (Exception ex) + { + Logger.LogError(ex, "Error in AddPersonImage"); + } + } + + personEntity.SetImage( + new ItemImageInfo + { + Path = imageUrl, + Type = ImageType.Primary + }, + 0); } protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index e67d1b8c3..155e8cb8a 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -10,6 +10,7 @@ using System.Net.Mime; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Events; using MediaBrowser.Common.Net; using MediaBrowser.Common.Progress; using MediaBrowser.Controller; @@ -23,7 +24,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; @@ -210,10 +210,10 @@ namespace MediaBrowser.Providers.Manager } /// - public Task SaveImage(User user, Stream source, string mimeType, string path) + public Task SaveImage(Stream source, string mimeType, string path) { return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger) - .SaveImage(user, source, path); + .SaveImage(source, path); } /// @@ -563,7 +563,7 @@ namespace MediaBrowser.Providers.Manager var pluginList = summary.Plugins.ToList(); AddMetadataPlugins(pluginList, dummy, libraryOptions, options); - AddImagePlugins(pluginList, dummy, imageProviders); + AddImagePlugins(pluginList, imageProviders); var subtitleProviders = _subtitleManager.GetSupportedProviders(dummy); @@ -594,14 +594,14 @@ namespace MediaBrowser.Providers.Manager var providers = GetMetadataProvidersInternal(item, libraryOptions, options, true, true).ToList(); // Locals - list.AddRange(providers.Where(i => (i is ILocalMetadataProvider)).Select(i => new MetadataPlugin + list.AddRange(providers.Where(i => i is ILocalMetadataProvider).Select(i => new MetadataPlugin { Name = i.Name, Type = MetadataPluginType.LocalMetadataProvider })); // Fetchers - list.AddRange(providers.Where(i => (i is IRemoteMetadataProvider)).Select(i => new MetadataPlugin + list.AddRange(providers.Where(i => i is IRemoteMetadataProvider).Select(i => new MetadataPlugin { Name = i.Name, Type = MetadataPluginType.MetadataFetcher @@ -615,11 +615,10 @@ namespace MediaBrowser.Providers.Manager })); } - private void AddImagePlugins(List list, T item, List imageProviders) - where T : BaseItem + private void AddImagePlugins(List list, List imageProviders) { // Locals - list.AddRange(imageProviders.Where(i => (i is ILocalImageProvider)).Select(i => new MetadataPlugin + list.AddRange(imageProviders.Where(i => i is ILocalImageProvider).Select(i => new MetadataPlugin { Name = i.Name, Type = MetadataPluginType.LocalImageProvider @@ -1166,12 +1165,32 @@ namespace MediaBrowser.Providers.Manager /// public void Dispose() { - _disposed = true; + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and optionally managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } if (!_disposeCancellationTokenSource.IsCancellationRequested) { _disposeCancellationTokenSource.Cancel(); } + + if (disposing) + { + _disposeCancellationTokenSource.Dispose(); + } + + _disposed = true; } } } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs index 69c6fd722..77f03580a 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs @@ -2,11 +2,9 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -17,7 +15,6 @@ using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Providers.MediaInfo { @@ -25,19 +22,17 @@ namespace MediaBrowser.Providers.MediaInfo { private readonly IMediaEncoder _mediaEncoder; private readonly IItemRepository _itemRepo; - private readonly IApplicationPaths _appPaths; - private readonly IJsonSerializer _json; private readonly ILibraryManager _libraryManager; private readonly IMediaSourceManager _mediaSourceManager; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - public FFProbeAudioInfo(IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IApplicationPaths appPaths, IJsonSerializer json, ILibraryManager libraryManager) + public FFProbeAudioInfo( + IMediaSourceManager mediaSourceManager, + IMediaEncoder mediaEncoder, + IItemRepository itemRepo, + ILibraryManager libraryManager) { _mediaEncoder = mediaEncoder; _itemRepo = itemRepo; - _appPaths = appPaths; - _json = json; _libraryManager = libraryManager; _mediaSourceManager = mediaSourceManager; } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs index 4fabe709b..9926275ae 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs @@ -40,19 +40,15 @@ namespace MediaBrowser.Providers.MediaInfo IHasItemChangeMonitor { private readonly ILogger _logger; - private readonly IIsoManager _isoManager; private readonly IMediaEncoder _mediaEncoder; private readonly IItemRepository _itemRepo; private readonly IBlurayExaminer _blurayExaminer; private readonly ILocalizationManager _localization; - private readonly IApplicationPaths _appPaths; - private readonly IJsonSerializer _json; private readonly IEncodingManager _encodingManager; private readonly IServerConfigurationManager _config; private readonly ISubtitleManager _subtitleManager; private readonly IChapterManager _chapterManager; private readonly ILibraryManager _libraryManager; - private readonly IChannelManager _channelManager; private readonly IMediaSourceManager _mediaSourceManager; public string Name => "ffprobe"; @@ -126,14 +122,10 @@ namespace MediaBrowser.Providers.MediaInfo public FFProbeProvider( ILogger logger, IMediaSourceManager mediaSourceManager, - IChannelManager channelManager, - IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, - IApplicationPaths appPaths, - IJsonSerializer json, IEncodingManager encodingManager, IServerConfigurationManager config, ISubtitleManager subtitleManager, @@ -141,19 +133,15 @@ namespace MediaBrowser.Providers.MediaInfo ILibraryManager libraryManager) { _logger = logger; - _isoManager = isoManager; _mediaEncoder = mediaEncoder; _itemRepo = itemRepo; _blurayExaminer = blurayExaminer; _localization = localization; - _appPaths = appPaths; - _json = json; _encodingManager = encodingManager; _config = config; _subtitleManager = subtitleManager; _chapterManager = chapterManager; _libraryManager = libraryManager; - _channelManager = channelManager; _mediaSourceManager = mediaSourceManager; _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager); @@ -211,9 +199,9 @@ namespace MediaBrowser.Providers.MediaInfo private string NormalizeStrmLine(string line) { - return line.Replace("\t", string.Empty) - .Replace("\r", string.Empty) - .Replace("\n", string.Empty) + return line.Replace("\t", string.Empty, StringComparison.Ordinal) + .Replace("\r", string.Empty, StringComparison.Ordinal) + .Replace("\n", string.Empty, StringComparison.Ordinal) .Trim(); } @@ -242,10 +230,11 @@ namespace MediaBrowser.Providers.MediaInfo FetchShortcutInfo(item); } - var prober = new FFProbeAudioInfo(_mediaSourceManager, _mediaEncoder, _itemRepo, _appPaths, _json, _libraryManager); + var prober = new FFProbeAudioInfo(_mediaSourceManager, _mediaEncoder, _itemRepo, _libraryManager); return prober.Probe(item, options, cancellationToken); } + // Run last public int Order => 100; } diff --git a/MediaBrowser.Providers/Music/Extensions.cs b/MediaBrowser.Providers/Music/Extensions.cs index b57d35256..dddfd02e4 100644 --- a/MediaBrowser.Providers/Music/Extensions.cs +++ b/MediaBrowser.Providers/Music/Extensions.cs @@ -6,7 +6,7 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Providers.Music { - public static class Extensions + public static class AlbumInfoExtensions { public static string GetAlbumArtist(this AlbumInfo info) { @@ -18,7 +18,7 @@ namespace MediaBrowser.Providers.Music return id; } - return info.AlbumArtists.FirstOrDefault(); + return info.AlbumArtists.Count > 0 ? info.AlbumArtists[0] : default; } public static string GetReleaseGroupId(this AlbumInfo info) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs index 23acb7fd6..7f10e6922 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs @@ -276,7 +276,7 @@ namespace MediaBrowser.Providers.Music private async Task GetReleaseResult(string albumName, string artistId, CancellationToken cancellationToken) { - var url = string.Format("/ws/2/release/?query=\"{0}\" AND arid:{1}", + var url = string.Format(CultureInfo.InvariantCulture, "/ws/2/release/?query=\"{0}\" AND arid:{1}", WebUtility.UrlEncode(albumName), artistId); @@ -740,11 +740,11 @@ namespace MediaBrowser.Providers.Music // MusicBrainz request a contact email address is supplied, as comment, in user agent field: // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent - options.Headers.UserAgent.Add(new ProductInfoHeaderValue(string.Format( + options.Headers.UserAgent.ParseAdd(string.Format( CultureInfo.InvariantCulture, "{0} ( {1} )", _appHost.ApplicationUserAgent, - _appHost.ApplicationUserAgentAddress))); + _appHost.ApplicationUserAgentAddress)); HttpResponseMessage response; var attempts = 0u; diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs index b829ed378..781b71640 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs @@ -46,7 +46,7 @@ namespace MediaBrowser.Providers.Music // They seem to throw bad request failures on any term with a slash var nameToSearch = searchInfo.Name.Replace('/', ' '); - var url = string.Format("/ws/2/artist/?query=\"{0}\"&dismax=true", UrlEncode(nameToSearch)); + var url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=\"{0}\"&dismax=true", UrlEncode(nameToSearch)); using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) await using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) @@ -62,7 +62,7 @@ namespace MediaBrowser.Providers.Music if (HasDiacritics(searchInfo.Name)) { // Try again using the search with accent characters url - url = string.Format("/ws/2/artist/?query=artistaccent:\"{0}\"", UrlEncode(nameToSearch)); + url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=artistaccent:\"{0}\"", UrlEncode(nameToSearch)); using var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs index 41e664aac..c18725e0a 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Net.Http; +using System.Globalization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; @@ -70,7 +71,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb list.Add(new RemoteImageInfo { ProviderName = Name, - Url = string.Format("https://img.omdbapi.com/?i={0}&apikey=2c9d9507", imdbId) + Url = string.Format(CultureInfo.InvariantCulture, "https://img.omdbapi.com/?i={0}&apikey=2c9d9507", imdbId) }); } } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs index d2823a08c..102ad82e1 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs @@ -127,7 +127,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb } } - var url = OmdbProvider.GetOmdbUrl(urlQuery, _appHost, cancellationToken); + var url = OmdbProvider.GetOmdbUrl(urlQuery); using var response = await OmdbProvider.GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index 6ad5298de..c45149c3a 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -256,16 +256,16 @@ namespace MediaBrowser.Providers.Plugins.Omdb return false; } - public static string GetOmdbUrl(string query, IApplicationHost appHost, CancellationToken cancellationToken) + public static string GetOmdbUrl(string query) { - const string url = "https://www.omdbapi.com?apikey=2c9d9507"; + const string Url = "https://www.omdbapi.com?apikey=2c9d9507"; if (string.IsNullOrWhiteSpace(query)) { - return url; + return Url; } - return url + "&" + query; + return Url + "&" + query; } private async Task EnsureItemInfo(string imdbId, CancellationToken cancellationToken) @@ -290,7 +290,11 @@ namespace MediaBrowser.Providers.Plugins.Omdb } } - var url = GetOmdbUrl(string.Format("i={0}&plot=short&tomatoes=true&r=json", imdbParam), _appHost, cancellationToken); + var url = GetOmdbUrl( + string.Format( + CultureInfo.InvariantCulture, + "i={0}&plot=short&tomatoes=true&r=json", + imdbParam)); using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); @@ -323,7 +327,12 @@ namespace MediaBrowser.Providers.Plugins.Omdb } } - var url = GetOmdbUrl(string.Format("i={0}&season={1}&detail=full", imdbParam, seasonId), _appHost, cancellationToken); + var url = GetOmdbUrl( + string.Format( + CultureInfo.InvariantCulture, + "i={0}&season={1}&detail=full", + imdbParam, + seasonId)); using var response = await GetOmdbResponse(_httpClientFactory.CreateClient(), url, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); @@ -348,7 +357,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb var dataPath = Path.Combine(_configurationManager.ApplicationPaths.CachePath, "omdb"); - var filename = string.Format("{0}.json", imdbId); + var filename = string.Format(CultureInfo.InvariantCulture, "{0}.json", imdbId); return Path.Combine(dataPath, filename); } @@ -362,7 +371,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb var dataPath = Path.Combine(_configurationManager.ApplicationPaths.CachePath, "omdb"); - var filename = string.Format("{0}_season_{1}.json", imdbId, seasonId); + var filename = string.Format(CultureInfo.InvariantCulture, "{0}_season_{1}.json", imdbId, seasonId); return Path.Combine(dataPath, filename); } diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs index cd2f96f14..f22d484ab 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; @@ -19,7 +20,6 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb { private const string DefaultLanguage = "en"; - private readonly SemaphoreSlim _cacheWriteLock = new SemaphoreSlim(1, 1); private readonly IMemoryCache _cache; private readonly TvDbClient _tvDbClient; private DateTime _tokenCreatedAt; @@ -176,7 +176,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb string language, CancellationToken cancellationToken) { - searchInfo.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), + searchInfo.SeriesProviderIds.TryGetValue(nameof(MetadataProvider.Tvdb), out var seriesTvdbId); var episodeQuery = new EpisodeQuery(); @@ -203,10 +203,10 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb else if (searchInfo.PremiereDate.HasValue) { // tvdb expects yyyy-mm-dd format - episodeQuery.FirstAired = searchInfo.PremiereDate.Value.ToString("yyyy-MM-dd"); + episodeQuery.FirstAired = searchInfo.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); } - return GetEpisodeTvdbId(Convert.ToInt32(seriesTvdbId), episodeQuery, language, cancellationToken); + return GetEpisodeTvdbId(Convert.ToInt32(seriesTvdbId, CultureInfo.InvariantCulture), episodeQuery, language, cancellationToken); } public async Task GetEpisodeTvdbId( @@ -218,7 +218,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb var episodePage = await GetEpisodesPageAsync(Convert.ToInt32(seriesTvdbId), episodeQuery, language, cancellationToken) .ConfigureAwait(false); - return episodePage.Data.FirstOrDefault()?.Id.ToString(); + return episodePage.Data.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture); } public Task> GetEpisodesPageAsync( @@ -276,23 +276,10 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb return cachedValue; } - await _cacheWriteLock.WaitAsync().ConfigureAwait(false); - try - { - if (_cache.TryGetValue(key, out cachedValue)) - { - return cachedValue; - } - - _tvDbClient.AcceptedLanguage = TvdbUtils.NormalizeLanguage(language) ?? DefaultLanguage; - var result = await resultFactory.Invoke().ConfigureAwait(false); - _cache.Set(key, result, TimeSpan.FromHours(1)); - return result; - } - finally - { - _cacheWriteLock.Release(); - } + _tvDbClient.AcceptedLanguage = TvdbUtils.NormalizeLanguage(language) ?? DefaultLanguage; + var result = await resultFactory.Invoke().ConfigureAwait(false); + _cache.Set(key, result, TimeSpan.FromHours(1)); + return result; } private static string GenerateKey(params object[] objects) diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs index 4d38d38dc..4e7c0e5a6 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Net.Http; +using System.Globalization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; @@ -76,7 +77,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb var episodeResult = await _tvdbClientManager - .GetEpisodesAsync(Convert.ToInt32(episodeTvdbId), language, cancellationToken) + .GetEpisodesAsync(Convert.ToInt32(episodeTvdbId, CultureInfo.InvariantCulture), language, cancellationToken) .ConfigureAwait(false); var image = GetImageInfo(episodeResult.Data); @@ -103,8 +104,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb return new RemoteImageInfo { - Width = Convert.ToInt32(episode.ThumbWidth), - Height = Convert.ToInt32(episode.ThumbHeight), + Width = Convert.ToInt32(episode.ThumbWidth, CultureInfo.InvariantCulture), + Height = Convert.ToInt32(episode.ThumbHeight, CultureInfo.InvariantCulture), ProviderName = Name, Url = TvdbUtils.BannerUrl + episode.Filename, Type = ImageType.Primary diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs index 4f86a0293..4da2c042f 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs @@ -180,7 +180,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets if (!string.IsNullOrEmpty(language)) { - url += string.Format("&language={0}", TmdbMovieProvider.NormalizeLanguage(language)); + url += string.Format(CultureInfo.InvariantCulture, "&language={0}", TmdbMovieProvider.NormalizeLanguage(language)); // Get images in english and with no language url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); @@ -250,7 +250,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets { var path = GetDataPath(appPaths, tmdbId); - var filename = string.Format("all-{0}.json", preferredLanguage ?? string.Empty); + var filename = string.Format(CultureInfo.InvariantCulture, "all-{0}.json", preferredLanguage ?? string.Empty); return Path.Combine(path, filename); } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs index 27ca3759e..01a887eed 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs @@ -300,7 +300,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies { movie.RemoteTrailers = movieData.Trailers.Youtube.Select(i => new MediaUrl { - Url = string.Format("https://www.youtube.com/watch?v={0}", i.Source), + Url = string.Format(CultureInfo.InvariantCulture, "https://www.youtube.com/watch?v={0}", i.Source), Name = i.Name }).ToArray(); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 685561e2d..d8918bb6b 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -30,6 +30,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies /// public class TmdbMovieProvider : IRemoteMetadataProvider, IHasOrder { + private const string TmdbConfigUrl = TmdbUtils.BaseTmdbApiUrl + "3/configuration?api_key={0}"; + private const string GetMovieInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/movie/{0}?api_key={1}&append_to_response=casts,releases,images,keywords,trailers"; + internal static TmdbMovieProvider Current { get; private set; } private readonly IJsonSerializer _jsonSerializer; @@ -157,9 +160,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies return _tmdbSettings; } - private const string TmdbConfigUrl = TmdbUtils.BaseTmdbApiUrl + "3/configuration?api_key={0}"; - private const string GetMovieInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/movie/{0}?api_key={1}&append_to_response=casts,releases,images,keywords,trailers"; - /// /// Gets the movie data path. /// @@ -334,7 +334,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); } - using var mainResponse = await GetMovieDbResponse(requestMessage); + using var mainResponse = await GetMovieDbResponse(requestMessage).ConfigureAwait(false); if (mainResponse.StatusCode == HttpStatusCode.NotFound) { return null; @@ -367,7 +367,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); } - using var langResponse = await GetMovieDbResponse(langRequestMessage); + using var langResponse = await GetMovieDbResponse(langRequestMessage).ConfigureAwait(false); await using var langStream = await langResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); var langResult = await _jsonSerializer.DeserializeFromStreamAsync(stream).ConfigureAwait(false); @@ -380,10 +380,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies /// /// Gets the movie db response. /// - internal async Task GetMovieDbResponse(HttpRequestMessage message) + internal Task GetMovieDbResponse(HttpRequestMessage message) { - message.Headers.UserAgent.Add(new ProductInfoHeaderValue(_appHost.ApplicationUserAgent)); - return await _httpClientFactory.CreateClient().SendAsync(message); + message.Headers.UserAgent.ParseAdd(_appHost.ApplicationUserAgent); + return _httpClientFactory.CreateClient().SendAsync(message); } /// diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs index 48f2a68a6..2a6c6d035 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs @@ -22,11 +22,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies { public class TmdbSearch { + private const string SearchUrl = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}"; + private const string SearchUrlTvWithYear = TmdbUtils.BaseTmdbApiUrl + @"3/search/tv?api_key={1}&query={0}&language={2}&first_air_date_year={3}"; + private const string SearchUrlMovieWithYear = TmdbUtils.BaseTmdbApiUrl + @"3/search/movie?api_key={1}&query={0}&language={2}&primary_release_year={3}"; + private static readonly CultureInfo _usCulture = new CultureInfo("en-US"); private static readonly Regex _cleanEnclosed = new Regex(@"\p{Ps}.*\p{Pe}", RegexOptions.Compiled); private static readonly Regex _cleanNonWord = new Regex(@"[\W_]+", RegexOptions.Compiled); - private static readonly Regex _cleanStopWords = new Regex(@"\b( # Start at word boundary + private static readonly Regex _cleanStopWords = new Regex( + @"\b( # Start at word boundary 19[0-9]{2}|20[0-9]{2}| # 1900-2099 S[0-9]{2}| # Season E[0-9]{2}| # Episode @@ -37,8 +42,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies ).* # Match rest of string", RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase); - private const string _searchURL = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}"; - private readonly ILogger _logger; private readonly IJsonSerializer _json; private readonly ILibraryManager _libraryManager; @@ -124,7 +127,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies name2 = name2.Trim(); // Search again if the new name is different - if (!string.Equals(name2, name) && !string.IsNullOrWhiteSpace(name2)) + if (!string.Equals(name2, name, StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(name2)) { _logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name2, year); results = await GetSearchResults(name2, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false); @@ -164,12 +167,32 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies { if (string.IsNullOrWhiteSpace(name)) { - throw new ArgumentException("name"); + throw new ArgumentException("String can't be null or empty.", nameof(name)); } - var url3 = string.Format(_searchURL, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, type); + string url3; + if (year != null && string.Equals(type, "movie", StringComparison.OrdinalIgnoreCase)) + { + url3 = string.Format( + CultureInfo.InvariantCulture, + SearchUrlMovieWithYear, + WebUtility.UrlEncode(name), + TmdbUtils.ApiKey, + language, + year); + } + else + { + url3 = string.Format( + CultureInfo.InvariantCulture, + SearchUrl, + WebUtility.UrlEncode(name), + TmdbUtils.ApiKey, + language, + type); + } - var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3); + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3); foreach (var header in TmdbUtils.AcceptHeaders) { requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); @@ -207,12 +230,32 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies { if (string.IsNullOrWhiteSpace(name)) { - throw new ArgumentException("name"); + throw new ArgumentException("String can't be null or empty.", nameof(name)); } - var url3 = string.Format(_searchURL, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, "tv"); + string url3; + if (year == null) + { + url3 = string.Format( + CultureInfo.InvariantCulture, + SearchUrl, + WebUtility.UrlEncode(name), + TmdbUtils.ApiKey, + language, + "tv"); + } + else + { + url3 = string.Format( + CultureInfo.InvariantCulture, + SearchUrlTvWithYear, + WebUtility.UrlEncode(name), + TmdbUtils.ApiKey, + language, + year); + } - var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3); + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3); foreach (var header in TmdbUtils.AcceptHeaders) { requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header)); @@ -227,7 +270,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies return results .Select(i => { - var remoteResult = new RemoteSearchResult {SearchProviderName = TmdbMovieProvider.Current.Name, Name = i.Name ?? i.Original_Name, ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path}; + var remoteResult = new RemoteSearchResult + { + SearchProviderName = TmdbMovieProvider.Current.Name, + Name = i.Name ?? i.Original_Name, + ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path + }; if (!string.IsNullOrWhiteSpace(i.First_Air_Date)) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index b4aef4542..90e3cea93 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -131,7 +131,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV { if (video.Site.Equals("youtube", System.StringComparison.OrdinalIgnoreCase)) { - var videoUrl = string.Format("http://www.youtube.com/watch?v={0}", video.Key); + var videoUrl = string.Format(CultureInfo.InvariantCulture, "http://www.youtube.com/watch?v={0}", video.Key); item.AddTrailerUrl(videoUrl); } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs index 154664321..5705885b4 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs @@ -92,7 +92,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); - var filename = string.Format("season-{0}-episode-{1}-{2}.json", + var filename = string.Format(CultureInfo.InvariantCulture, "season-{0}-episode-{1}-{2}.json", seasonNumber.ToString(CultureInfo.InvariantCulture), episodeNumber.ToString(CultureInfo.InvariantCulture), preferredLanguage); @@ -116,7 +116,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (!string.IsNullOrEmpty(language)) { - url += string.Format("&language={0}", language); + url += string.Format(CultureInfo.InvariantCulture, "&language={0}", language); } var includeImageLanguageParam = TmdbMovieProvider.GetImageLanguagesParam(language); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs index 2b9077f55..e59504cc6 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs @@ -180,7 +180,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); - var filename = string.Format("season-{0}-{1}.json", + var filename = string.Format(CultureInfo.InvariantCulture, "season-{0}-{1}.json", seasonNumber.ToString(CultureInfo.InvariantCulture), preferredLanguage); @@ -203,7 +203,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (!string.IsNullOrEmpty(language)) { - url += string.Format("&language={0}", TmdbMovieProvider.NormalizeLanguage(language)); + url += string.Format(CultureInfo.InvariantCulture, "&language={0}", TmdbMovieProvider.NormalizeLanguage(language)); } var includeImageLanguageParam = TmdbMovieProvider.GetImageLanguagesParam(language); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index ac577b125..0eded3233 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -496,7 +496,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var path = GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); - var filename = string.Format("series-{0}.json", preferredLanguage ?? string.Empty); + var filename = string.Format(CultureInfo.InvariantCulture, "series-{0}.json", preferredLanguage ?? string.Empty); return Path.Combine(path, filename); } diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs index dad155c81..321153c6b 100644 --- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; @@ -100,7 +101,7 @@ namespace MediaBrowser.Providers.Studios private string GetUrl(string image, string filename) { - return string.Format("https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname/studios/{0}/{1}.jpg", image, filename); + return string.Format(CultureInfo.InvariantCulture, "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname/studios/{0}/{1}.jpg", image, filename); } private Task EnsureThumbsList(string file, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/TV/DummySeasonProvider.cs b/MediaBrowser.Providers/TV/DummySeasonProvider.cs index df09d13dd..0c09cdef6 100644 --- a/MediaBrowser.Providers/TV/DummySeasonProvider.cs +++ b/MediaBrowser.Providers/TV/DummySeasonProvider.cs @@ -87,7 +87,7 @@ namespace MediaBrowser.Providers.TV else if (existingSeason.IsVirtualItem) { existingSeason.IsVirtualItem = false; - existingSeason.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken); + await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); seasons = null; } } @@ -113,7 +113,7 @@ namespace MediaBrowser.Providers.TV else if (existingSeason.IsVirtualItem) { existingSeason.IsVirtualItem = false; - existingSeason.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken); + await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); seasons = null; } } diff --git a/bump_version b/bump_version index 1c943f691..d2de5a0bd 100755 --- a/bump_version +++ b/bump_version @@ -20,6 +20,8 @@ fi shared_version_file="./SharedVersion.cs" build_file="./build.yaml" +# csproj files for nuget packages +jellyfin_subprojects=( MediaBrowser.Common/MediaBrowser.Common.csproj Jellyfin.Data/Jellyfin.Data.csproj MediaBrowser.Controller/MediaBrowser.Controller.csproj MediaBrowser.Model/MediaBrowser.Model.csproj Emby.Naming/Emby.Naming.csproj ) new_version="$1" @@ -45,6 +47,22 @@ echo $old_version old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars sed -i "s/${old_version_sed}/${new_version}/g" ${build_file} +# update nuget package version +for subproject in ${jellyfin_subprojects[@]}; do +do + echo ${subproject} + # Parse the version from the *.csproj file + old_version="$( + grep "VersionPrefix" ${subproject} \ + | awk '{$1=$1};1' \ + | sed -E 's/([0-9\.]+[-a-z0-9]*)<\/VersionPrefix>/\1/' + )" + echo old nuget version: $old_version + + # Set the nuget version to the specified new_version + sed -i "s|${old_version}|${new_version}|g" ${subproject} +done + if [[ ${new_version} == *"-"* ]]; then new_version_deb="$( sed 's/-/~/g' <<<"${new_version}" )" else diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index b0a729a9e..a1e0b5f14 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -74,6 +74,9 @@ EOF %{__install} -D -m 0755 %{SOURCE14} %{buildroot}%{_libexecdir}/jellyfin/restart.sh %{__install} -D -m 0644 %{SOURCE16} %{buildroot}%{_prefix}/lib/firewalld/services/jellyfin.xml +%files +# empty as this is just a meta-package + %files server %attr(755,root,root) %{_bindir}/jellyfin %{_libdir}/jellyfin/*.json diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs index 9eb601edf..39fd8afda 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs @@ -13,15 +13,18 @@ namespace Jellyfin.MediaEncoding.Tests [ClassData(typeof(GetFFmpegVersionTestData))] public void GetFFmpegVersionTest(string versionOutput, Version? version) { - Assert.Equal(version, EncoderValidator.GetFFmpegVersion(versionOutput)); + var val = new EncoderValidator(new NullLogger()); + Assert.Equal(version, val.GetFFmpegVersion(versionOutput)); } [Theory] + [InlineData(EncoderValidatorTestsData.FFmpegV431Output, true)] [InlineData(EncoderValidatorTestsData.FFmpegV43Output, true)] [InlineData(EncoderValidatorTestsData.FFmpegV421Output, true)] [InlineData(EncoderValidatorTestsData.FFmpegV42Output, true)] [InlineData(EncoderValidatorTestsData.FFmpegV414Output, true)] [InlineData(EncoderValidatorTestsData.FFmpegV404Output, true)] + [InlineData(EncoderValidatorTestsData.FFmpegGitUnknownOutput2, true)] [InlineData(EncoderValidatorTestsData.FFmpegGitUnknownOutput, false)] public void ValidateVersionInternalTest(string versionOutput, bool valid) { @@ -33,11 +36,13 @@ namespace Jellyfin.MediaEncoding.Tests { public IEnumerator GetEnumerator() { + yield return new object?[] { EncoderValidatorTestsData.FFmpegV431Output, new Version(4, 3, 1) }; yield return new object?[] { EncoderValidatorTestsData.FFmpegV43Output, new Version(4, 3) }; yield return new object?[] { EncoderValidatorTestsData.FFmpegV421Output, new Version(4, 2, 1) }; yield return new object?[] { EncoderValidatorTestsData.FFmpegV42Output, new Version(4, 2) }; yield return new object?[] { EncoderValidatorTestsData.FFmpegV414Output, new Version(4, 1, 4) }; yield return new object?[] { EncoderValidatorTestsData.FFmpegV404Output, new Version(4, 0, 4) }; + yield return new object?[] { EncoderValidatorTestsData.FFmpegGitUnknownOutput2, new Version(4, 0) }; yield return new object?[] { EncoderValidatorTestsData.FFmpegGitUnknownOutput, null }; } diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs index f5ff3d723..9f5bef9a8 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs @@ -2,6 +2,18 @@ namespace Jellyfin.MediaEncoding.Tests { internal static class EncoderValidatorTestsData { + public const string FFmpegV431Output = @"ffmpeg version n4.3.1 Copyright (c) 2000-2020 the FFmpeg developers +built with gcc 10.1.0 (GCC) +configuration: --prefix=/usr --disable-debug --disable-static --disable-stripping --enable-avisynth --enable-fontconfig --enable-gmp --enable-gnutls --enable-gpl --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libdav1d --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libiec61883 --enable-libjack --enable-libmfx --enable-libmodplug --enable-libmp3lame --enable-libopencore_amrnb --enable-libopencore_amrwb --enable-libopenjpeg --enable-libopus --enable-libpulse --enable-librav1e --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libv4l2 --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxcb --enable-libxml2 --enable-libxvid --enable-nvdec --enable-nvenc --enable-omx --enable-shared --enable-version3 +libavutil 56. 51.100 / 56. 51.100 +libavcodec 58. 91.100 / 58. 91.100 +libavformat 58. 45.100 / 58. 45.100 +libavdevice 58. 10.100 / 58. 10.100 +libavfilter 7. 85.100 / 7. 85.100 +libswscale 5. 7.100 / 5. 7.100 +libswresample 3. 7.100 / 3. 7.100 +libpostproc 55. 7.100 / 55. 7.100"; + public const string FFmpegV43Output = @"ffmpeg version 4.3 Copyright (c) 2000-2020 the FFmpeg developers built with gcc 7 (Ubuntu 7.5.0-3ubuntu1~18.04) configuration: --prefix=/usr/lib/jellyfin-ffmpeg --target-os=linux --disable-doc --disable-ffplay --disable-shared --disable-libxcb --disable-vdpau --disable-sdl2 --disable-xlib --enable-gpl --enable-version3 --enable-static --enable-libfontconfig --enable-fontconfig --enable-gmp --enable-gnutls --enable-libass --enable-libbluray --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libx264 --enable-libx265 --enable-libzvbi --arch=amd64 --enable-amf --enable-nvenc --enable-nvdec --enable-vaapi --enable-opencl @@ -63,7 +75,7 @@ libswscale 5. 1.100 / 5. 1.100 libswresample 3. 1.100 / 3. 1.100 libpostproc 55. 1.100 / 55. 1.100"; - public const string FFmpegGitUnknownOutput = @"ffmpeg version N-94303-g7cb4f8c962 Copyright (c) 2000-2019 the FFmpeg developers + public const string FFmpegGitUnknownOutput2 = @"ffmpeg version N-94303-g7cb4f8c962 Copyright (c) 2000-2019 the FFmpeg developers built with gcc 9.1.1 (GCC) 20190716 configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-amf --enable-ffnvcodec --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth --enable-libopenmpt libavutil 56. 30.100 / 56. 30.100 @@ -74,5 +86,17 @@ libavfilter 7. 56.101 / 7. 56.101 libswscale 5. 4.101 / 5. 4.101 libswresample 3. 4.100 / 3. 4.100 libpostproc 55. 4.100 / 55. 4.100"; + + public const string FFmpegGitUnknownOutput = @"ffmpeg version N-45325-gb173e0353-static https://johnvansickle.com/ffmpeg/ Copyright (c) 2000-2018 the FFmpeg developers +built with gcc 6.3.0 (Debian 6.3.0-18+deb9u1) 20170516 +configuration: --enable-gpl --enable-version3 --enable-static --disable-debug --disable-ffplay --disable-indev=sndio --disable-outdev=sndio --cc=gcc-6 --enable-fontconfig --enable-frei0r --enable-gnutls --enable-gray --enable-libfribidi --enable-libass --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-librubberband --enable-libsoxr --enable-libspeex --enable-libvorbis --enable-libopus --enable-libtheora --enable-libvidstab --enable-libvo-amrwbenc --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxvid --enable-libzimg +libavutil 56. 9.100 / 56. 9.100 +libavcodec 58. 14.100 / 58. 14.100 +libavformat 58. 10.100 / 58. 10.100 +libavdevice 58. 2.100 / 58. 2.100 +libavfilter 7. 13.100 / 7. 13.100 +libswscale 5. 0.102 / 5. 0.102 +libswresample 3. 0.101 / 3. 0.101 +libpostproc 55. 0.100 / 55. 0.100"; } } diff --git a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs index 2032f6cec..c39ef0ce9 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs @@ -1,6 +1,7 @@ using System.IO; using System.Text.Json; using System.Threading.Tasks; +using MediaBrowser.Common.Json; using MediaBrowser.MediaEncoding.Probing; using Xunit; @@ -15,7 +16,7 @@ namespace Jellyfin.MediaEncoding.Tests var path = Path.Join("Test Data", fileName); using (var stream = File.OpenRead(path)) { - await JsonSerializer.DeserializeAsync(stream).ConfigureAwait(false); + await JsonSerializer.DeserializeAsync(stream, JsonDefaults.GetOptions()).ConfigureAwait(false); } } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs b/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs deleted file mode 100644 index 39bd94b59..000000000 --- a/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Emby.Server.Implementations.HttpServer; -using Xunit; - -namespace Jellyfin.Server.Implementations.Tests.HttpServer -{ - public class ResponseFilterTests - { - [Theory] - [InlineData(null, null)] - [InlineData("", "")] - [InlineData("This is a clean string.", "This is a clean string.")] - [InlineData("This isn't \n\ra clean string.", "This isn't a clean string.")] - public void RemoveControlCharacters_ValidArgs_Correct(string? input, string? result) - { - Assert.Equal(result, ResponseFilter.RemoveControlCharacters(input)); - } - } -} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs index b4e6db8f3..09eb22328 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs @@ -30,8 +30,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/movies/.@__thumb/foo-bar-thumbnail.png", true)] [InlineData("/media/music/Foo B.A.R./epic.flac", false)] [InlineData("/media/music/Foo B.A.R", false)] - // This test is pending an upstream fix: https://github.com/dazinator/DotNet.Glob/issues/78 - // [InlineData("/media/music/Foo B.A.R.", false)] + [InlineData("/media/music/Foo B.A.R.", false)] public void PathIgnored(string path, bool expected) { Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path)); diff --git a/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs b/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs index c39ed07de..2029f88e9 100644 --- a/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs +++ b/tests/MediaBrowser.Api.Tests/JellyfinApplicationFactory.cs @@ -72,6 +72,7 @@ namespace MediaBrowser.Api.Tests var startupConfig = Program.CreateAppConfiguration(commandLineOpts, appPaths); ILoggerFactory loggerFactory = new SerilogLoggerFactory(); + var serviceCollection = new ServiceCollection(); _disposableComponents.Add(loggerFactory); // Create the app host and initialize it @@ -80,10 +81,10 @@ namespace MediaBrowser.Api.Tests loggerFactory, commandLineOpts, new ManagedFileSystem(loggerFactory.CreateLogger(), appPaths), - new NetworkManager(loggerFactory.CreateLogger())); + new NetworkManager(loggerFactory.CreateLogger()), + serviceCollection); _disposableComponents.Add(appHost); - var serviceCollection = new ServiceCollection(); - appHost.Init(serviceCollection); + appHost.Init(); // Configure the web host builder Program.ConfigureWebHostBuilder(builder, appHost, serviceCollection, commandLineOpts, startupConfig, appPaths);