From d4b614f2ed87901f8c609b30cad9372d1973fa2e Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 19 Mar 2020 14:09:51 +0000 Subject: [PATCH 01/14] more pipeline stuff --- .azuredevops/pipelines/build.yml | 0 .azuredevops/pipelines/main.yml | 18 -- .azuredevops/pipelines/publish-job.yml | 8 + .../pipelines/templates/build-steps.yml | 9 - .../pipelines/templates/publish-steps.yml | 171 ------------------ 5 files changed, 8 insertions(+), 198 deletions(-) delete mode 100644 .azuredevops/pipelines/build.yml delete mode 100644 .azuredevops/pipelines/main.yml delete mode 100644 .azuredevops/pipelines/templates/publish-steps.yml diff --git a/.azuredevops/pipelines/build.yml b/.azuredevops/pipelines/build.yml deleted file mode 100644 index e69de29bb..000000000 diff --git a/.azuredevops/pipelines/main.yml b/.azuredevops/pipelines/main.yml deleted file mode 100644 index 9a6f953fe..000000000 --- a/.azuredevops/pipelines/main.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: '$(Build.SourceBranchName)_$(Date:yyyy.MM.dd)$(Rev:.r)' - -trigger: none - -variables: - - template: templates/variables.yml - -jobs: -- job: Build - pool: - vmImage: ${{ variables.vmImage }} - steps: - - template: templates/build-steps.yml -- job: Publish - pool: - vmImage: ${{ variables.vmImage }} - steps: - - template: templates/publish-stepsnew.yml \ No newline at end of file diff --git a/.azuredevops/pipelines/publish-job.yml b/.azuredevops/pipelines/publish-job.yml index 43bdcf547..43d6f3b0b 100644 --- a/.azuredevops/pipelines/publish-job.yml +++ b/.azuredevops/pipelines/publish-job.yml @@ -42,6 +42,14 @@ stages: buildType: 'current' targetPath: '$(System.ArtifactsDirectory)' + - task: PowerShell@2 + displayName: 'Get Release Notes' + inputs: + targetType: 'inline' + script: | + $response = Invoke-WebRequest -Uri "https://ombireleasenote.azurewebsites.net/api/ReleaseNotesFunction?buildId=$(Build.BuildId)" + Write-Host "##vso[task.setvariable variable=ReleaseNotes;]$response" + - task: GitHubRelease@1 inputs: gitHubConnection: 'github.com_tidusjar' diff --git a/.azuredevops/pipelines/templates/build-steps.yml b/.azuredevops/pipelines/templates/build-steps.yml index 92eee9c88..50ddb8a50 100644 --- a/.azuredevops/pipelines/templates/build-steps.yml +++ b/.azuredevops/pipelines/templates/build-steps.yml @@ -12,15 +12,6 @@ steps: packageType: 'sdk' version: '2.1.x' - -- task: PowerShell@2 - displayName: 'Get Release Notes' - inputs: - targetType: 'inline' - script: | - $response = Invoke-WebRequest -Uri "https://ombireleasenote.azurewebsites.net/api/ReleaseNotesFunction?buildId=$(Build.BuildId)" - Write-Host "##vso[task.setvariable variable=ReleaseNotes;]$response" - - task: PowerShell@2 displayName: 'Set Version' inputs: diff --git a/.azuredevops/pipelines/templates/publish-steps.yml b/.azuredevops/pipelines/templates/publish-steps.yml deleted file mode 100644 index 80ec2ce5c..000000000 --- a/.azuredevops/pipelines/templates/publish-steps.yml +++ /dev/null @@ -1,171 +0,0 @@ -steps: - -- task: DotNetCoreCLI@2 - displayName: Publish Win10-x64 - inputs: - command: 'publish' - publishWebProjects: true - arguments: '-c $(BuildConfiguration) -r "win10-x64" -o $(Build.ArtifactStagingDirectory)/win-64' - zipAfterPublish: false - modifyOutputPath: false - -- task: CopyFiles@2 - displayName: 'Copy Angular App Win10-x64' - inputs: - SourceFolder: '$(UiLocation)dist' - Contents: '**' - TargetFolder: '$(Build.ArtifactStagingDirectory)/win-64/ClientApp/dist' - -- task: DotNetCoreCLI@2 - displayName: Publish Win10-x86 - inputs: - command: 'publish' - publishWebProjects: true - arguments: '-c $(BuildConfiguration) -r "win10-x86" -o $(Build.ArtifactStagingDirectory)/win-86' - zipAfterPublish: false - modifyOutputPath: false - -- task: CopyFiles@2 - displayName: 'Copy Angular App Win10-x86' - inputs: - SourceFolder: '$(UiLocation)dist' - Contents: '**' - TargetFolder: '$(Build.ArtifactStagingDirectory)/win-86/ClientApp/dist' - -- task: DotNetCoreCLI@2 - displayName: Publish OSX-x64 - inputs: - command: 'publish' - publishWebProjects: true - arguments: '-c $(BuildConfiguration) -r "osx-x64" -o $(Build.ArtifactStagingDirectory)/osx-64' - zipAfterPublish: false - modifyOutputPath: false - -- task: CopyFiles@2 - displayName: 'Copy Angular App OSX-x64' - inputs: - SourceFolder: '$(UiLocation)dist' - Contents: '**' - TargetFolder: '$(Build.ArtifactStagingDirectory)/osx-64/ClientApp/dist' - -- task: DotNetCoreCLI@2 - displayName: Publish Linux-x64 - inputs: - command: 'publish' - publishWebProjects: true - arguments: '-c $(BuildConfiguration) -r "linux-x64" -o $(Build.ArtifactStagingDirectory)/linux-64' - zipAfterPublish: false - modifyOutputPath: false - -- task: CopyFiles@2 - displayName: 'Copy Angular App Linux-x64' - inputs: - SourceFolder: '$(UiLocation)dist' - Contents: '**' - TargetFolder: '$(Build.ArtifactStagingDirectory)/linux-64/ClientApp/dist' - -- task: DotNetCoreCLI@2 - displayName: Publish Linux-ARM - inputs: - command: 'publish' - publishWebProjects: true - arguments: '-c $(BuildConfiguration) -r "linux-arm" -o $(Build.ArtifactStagingDirectory)/linux-arm' - zipAfterPublish: false - modifyOutputPath: false - -- task: CopyFiles@2 - displayName: 'Copy Angular App Linux-ARM' - inputs: - SourceFolder: '$(UiLocation)dist' - Contents: '**' - TargetFolder: '$(Build.ArtifactStagingDirectory)/linux-arm/ClientApp/dist' - -- task: DotNetCoreCLI@2 - displayName: Publish Linux-ARM-x64 - inputs: - command: 'publish' - publishWebProjects: true - arguments: '-c $(BuildConfiguration) -r "linux-arm64" -o $(Build.ArtifactStagingDirectory)/linux-arm64' - zipAfterPublish: false - modifyOutputPath: false - -- task: CopyFiles@2 - displayName: 'Copy Angular App Linux-ARM64' - inputs: - SourceFolder: '$(UiLocation)dist' - Contents: '**' - TargetFolder: '$(Build.ArtifactStagingDirectory)/linux-arm64/ClientApp/dist' - -### Zip them up - -- task: ArchiveFiles@2 - displayName: Zip Win-x64 - inputs: - rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/win-64' - includeRootFolder: false - archiveType: 'zip' - archiveFile: '$(Build.ArtifactStagingDirectory)/win-x64-$(Build.BuildId).zip' - replaceExistingArchive: true - -- task: ArchiveFiles@2 - displayName: Zip Win-x86 - inputs: - rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/win-86' - includeRootFolder: false - archiveType: 'zip' - archiveFile: '$(Build.ArtifactStagingDirectory)/win-x86-$(Build.BuildId).zip' - replaceExistingArchive: true - -- task: ArchiveFiles@2 - displayName: Zip OSX-x64 - inputs: - rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/osx-64' - includeRootFolder: false - archiveType: 'tar' - archiveFile: '$(Build.ArtifactStagingDirectory)/osx-x64-$(Build.BuildId).tar.gz' - replaceExistingArchive: true - -- task: ArchiveFiles@2 - displayName: Zip Linux-x64 - inputs: - rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/linux-64' - includeRootFolder: false - archiveType: 'tar' - archiveFile: '$(Build.ArtifactStagingDirectory)/linux-x64-$(Build.BuildId).tar.gz' - replaceExistingArchive: true - -- task: ArchiveFiles@2 - displayName: Zip Linux-ARM - inputs: - rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/linux-arm' - includeRootFolder: false - archiveType: 'tar' - archiveFile: '$(Build.ArtifactStagingDirectory)/linux-arm-$(Build.BuildId).tar.gz' - replaceExistingArchive: true - -- task: ArchiveFiles@2 - displayName: Zip Linux-ARM-x64 - inputs: - rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/linux-arm64' - includeRootFolder: false - archiveType: 'tar' - archiveFile: '$(Build.ArtifactStagingDirectory)/linux-arm64-$(Build.BuildId).tar.gz' - replaceExistingArchive: true - -- task: GitHubRelease@1 - inputs: - gitHubConnection: 'github.com_tidusjar' - repositoryName: 'tidusjar/Ombi.Releases' - action: 'create' - target: 'c7fcbb77b58aef1076d635a9ef99e4374abc8672' - tagSource: 'userSpecifiedTag' - tag: '$(gitTag)' - releaseNotesSource: 'inline' - releaseNotesInline: '$(ReleaseNotes)' - assets: | - $(Build.ArtifactStagingDirectory)/*.zip - $(Build.ArtifactStagingDirectory)/*.gz - isPreRelease: true - changeLogCompareToRelease: 'lastNonDraftRelease' - changeLogType: 'commitBased' - condition: and(succeeded(), eq(variables['PublishToGithub'], 'true')) \ No newline at end of file From 7385aa82f22f666cf8d7f681b38c5ec2ec4c273e Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 Mar 2020 10:10:26 +0000 Subject: [PATCH 02/14] Should fix https://github.com/tidusjar/Ombi/issues/3417 --- src/Ombi/ClientApp/src/app/auth/cookie.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ombi/ClientApp/src/app/auth/cookie.component.ts b/src/Ombi/ClientApp/src/app/auth/cookie.component.ts index 515f8c603..1a6cf9144 100644 --- a/src/Ombi/ClientApp/src/app/auth/cookie.component.ts +++ b/src/Ombi/ClientApp/src/app/auth/cookie.component.ts @@ -16,7 +16,7 @@ export class CookieComponent implements OnInit { if (cookie.Auth) { const jwtVal = cookie.Auth; this.store.save("id_token", jwtVal); - this.router.navigate(["search"]); + this.router.navigate(["discover"]); } else { this.router.navigate(["login"]); } From 2a31d3a437f03e345abd3236cd334d342a59b4d5 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 Mar 2020 16:05:40 +0000 Subject: [PATCH 03/14] Fixed the format of the compressed files --- .azuredevops/pipelines/publish-job.yml | 6 ++++++ .azuredevops/pipelines/templates/publish-os-steps.yml | 2 +- .azuredevops/pipelines/templates/variables.yml | 5 +---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.azuredevops/pipelines/publish-job.yml b/.azuredevops/pipelines/publish-job.yml index 43d6f3b0b..f7a880c23 100644 --- a/.azuredevops/pipelines/publish-job.yml +++ b/.azuredevops/pipelines/publish-job.yml @@ -18,16 +18,22 @@ stages: matrix: win10-x64: runtime: win10-x64 + format: zip win10-x86: runtime: win10-x86 + format: zip osx-x64: runtime: osx-x64 + format: tar.gz linux-x64: runtime: linux-x64 + format: tar.gz linux-arm: runtime: linux-arm + format: tar.gz linux-arm64: runtime: linux-arm64 + format: tar.gz pool: vmImage: ${{ variables.vmImage }} steps: diff --git a/.azuredevops/pipelines/templates/publish-os-steps.yml b/.azuredevops/pipelines/templates/publish-os-steps.yml index b0e4a45a8..12d89b241 100644 --- a/.azuredevops/pipelines/templates/publish-os-steps.yml +++ b/.azuredevops/pipelines/templates/publish-os-steps.yml @@ -27,7 +27,7 @@ steps: rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/$(runtime)' includeRootFolder: false archiveType: 'zip' - archiveFile: '$(Build.ArtifactStagingDirectory)/$(runtime).zip' + archiveFile: '$(Build.ArtifactStagingDirectory)/$(runtime).$(format)' replaceExistingArchive: true - task: PublishPipelineArtifact@1 diff --git a/.azuredevops/pipelines/templates/variables.yml b/.azuredevops/pipelines/templates/variables.yml index 2a42b65f3..737f7b9d1 100644 --- a/.azuredevops/pipelines/templates/variables.yml +++ b/.azuredevops/pipelines/templates/variables.yml @@ -24,7 +24,4 @@ variables: value: "$(Build.SourcesDirectory)/src/Ombi/ClientApp/" - name: "BuildVersion" - value: "4.0.$(Build.BuildId)" - - - name: "ReleaseNotes" - value: "" \ No newline at end of file + value: "4.0.$(Build.BuildId)" \ No newline at end of file From df942ba1b0613d3550fe0bef6641c211630da95e Mon Sep 17 00:00:00 2001 From: Jamie Date: Fri, 20 Mar 2020 21:10:07 +0000 Subject: [PATCH 04/14] Update publish-os-steps.yml --- .azuredevops/pipelines/templates/publish-os-steps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azuredevops/pipelines/templates/publish-os-steps.yml b/.azuredevops/pipelines/templates/publish-os-steps.yml index 12d89b241..6725b712f 100644 --- a/.azuredevops/pipelines/templates/publish-os-steps.yml +++ b/.azuredevops/pipelines/templates/publish-os-steps.yml @@ -32,6 +32,6 @@ steps: - task: PublishPipelineArtifact@1 inputs: - targetPath: '$(Build.ArtifactStagingDirectory)/$(runtime).zip' + targetPath: '$(Build.ArtifactStagingDirectory)/$(runtime).$(format)' artifact: '$(runtime)' publishLocation: 'pipeline' From ef7e834706a9a1640062376a3aaab6a0ef7a652b Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 Mar 2020 21:31:09 +0000 Subject: [PATCH 05/14] upload gz too --- .azuredevops/pipelines/publish-job.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azuredevops/pipelines/publish-job.yml b/.azuredevops/pipelines/publish-job.yml index f7a880c23..31d13a3f9 100644 --- a/.azuredevops/pipelines/publish-job.yml +++ b/.azuredevops/pipelines/publish-job.yml @@ -68,7 +68,7 @@ stages: releaseNotesInline: '$(ReleaseNotes)' assets: | $(System.ArtifactsDirectory)/**/*.zip - $(System.ArtifactsDirectory)/S**/*.gz + $(System.ArtifactsDirectory)/S**/*.tar.gz isPreRelease: true changeLogCompareToRelease: 'lastNonDraftRelease' changeLogType: 'commitBased' From 7ffbdc7c488600d1e33abf70726d47fe81d1f09c Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 Mar 2020 21:45:02 +0000 Subject: [PATCH 06/14] ffs --- .azuredevops/pipelines/publish-job.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azuredevops/pipelines/publish-job.yml b/.azuredevops/pipelines/publish-job.yml index 31d13a3f9..a8fcaf1ce 100644 --- a/.azuredevops/pipelines/publish-job.yml +++ b/.azuredevops/pipelines/publish-job.yml @@ -68,7 +68,7 @@ stages: releaseNotesInline: '$(ReleaseNotes)' assets: | $(System.ArtifactsDirectory)/**/*.zip - $(System.ArtifactsDirectory)/S**/*.tar.gz + $(System.ArtifactsDirectory)/**/*.tar.gz isPreRelease: true changeLogCompareToRelease: 'lastNonDraftRelease' changeLogType: 'commitBased' From eb9420f6a736661688b37ab32b862ebb35713776 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 Mar 2020 23:19:54 +0000 Subject: [PATCH 07/14] Fix versioning --- .azuredevops/pipelines/templates/build-steps.yml | 13 ------------- .../pipelines/templates/publish-os-steps.yml | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.azuredevops/pipelines/templates/build-steps.yml b/.azuredevops/pipelines/templates/build-steps.yml index 50ddb8a50..719b0470b 100644 --- a/.azuredevops/pipelines/templates/build-steps.yml +++ b/.azuredevops/pipelines/templates/build-steps.yml @@ -6,19 +6,6 @@ steps: inputs: packageType: 'sdk' version: '3.x' -- task: DotNetCoreInstaller@1 - displayName: 'Use .NET Core sdk for versioning' - inputs: - packageType: 'sdk' - version: '2.1.x' - -- task: PowerShell@2 - displayName: 'Set Version' - inputs: - targetType: 'inline' - script: | - dotnet tool install -g dotnet-setversion - setversion -r $(BuildVersion) - task: Yarn@3 displayName: 'Install UI Dependancies' diff --git a/.azuredevops/pipelines/templates/publish-os-steps.yml b/.azuredevops/pipelines/templates/publish-os-steps.yml index 6725b712f..dde225ac8 100644 --- a/.azuredevops/pipelines/templates/publish-os-steps.yml +++ b/.azuredevops/pipelines/templates/publish-os-steps.yml @@ -1,4 +1,19 @@ steps: + +- task: DotNetCoreInstaller@1 + displayName: 'Use .NET Core sdk for versioning' + inputs: + packageType: 'sdk' + version: '2.1.x' + +- task: PowerShell@2 + displayName: 'Set Version' + inputs: + targetType: 'inline' + script: | + dotnet tool install -g dotnet-setversion + setversion -r $(BuildVersion) + - task: DotNetCoreCLI@2 displayName: 'publish $(runtime)' inputs: From 20130a06cee0a868a035571ce5bb9a1df0f74977 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 20 Mar 2020 23:35:54 +0000 Subject: [PATCH 08/14] include net core 3.1 --- .azuredevops/pipelines/templates/publish-os-steps.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.azuredevops/pipelines/templates/publish-os-steps.yml b/.azuredevops/pipelines/templates/publish-os-steps.yml index dde225ac8..77df59efb 100644 --- a/.azuredevops/pipelines/templates/publish-os-steps.yml +++ b/.azuredevops/pipelines/templates/publish-os-steps.yml @@ -1,4 +1,9 @@ steps: +- task: DotNetCoreInstaller@1 + displayName: 'Use .NET Core sdk ' + inputs: + packageType: 'sdk' + version: '3.x' - task: DotNetCoreInstaller@1 displayName: 'Use .NET Core sdk for versioning' From 82f41074ab23017d7d108a0bdd77bfa349344587 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 23 Mar 2020 10:31:34 +0000 Subject: [PATCH 09/14] Fixed build --- .azuredevops/pipelines/publish-job.yml | 6 ++++++ .azuredevops/pipelines/templates/publish-os-steps.yml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.azuredevops/pipelines/publish-job.yml b/.azuredevops/pipelines/publish-job.yml index a8fcaf1ce..3356685f5 100644 --- a/.azuredevops/pipelines/publish-job.yml +++ b/.azuredevops/pipelines/publish-job.yml @@ -19,21 +19,27 @@ stages: win10-x64: runtime: win10-x64 format: zip + compression: zip win10-x86: runtime: win10-x86 format: zip + compression: zip osx-x64: runtime: osx-x64 format: tar.gz + compression: tar linux-x64: runtime: linux-x64 format: tar.gz + compression: tar linux-arm: runtime: linux-arm format: tar.gz + compression: tar linux-arm64: runtime: linux-arm64 format: tar.gz + compression: tar pool: vmImage: ${{ variables.vmImage }} steps: diff --git a/.azuredevops/pipelines/templates/publish-os-steps.yml b/.azuredevops/pipelines/templates/publish-os-steps.yml index 77df59efb..ad72212cf 100644 --- a/.azuredevops/pipelines/templates/publish-os-steps.yml +++ b/.azuredevops/pipelines/templates/publish-os-steps.yml @@ -46,7 +46,7 @@ steps: inputs: rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/$(runtime)' includeRootFolder: false - archiveType: 'zip' + archiveType: $(compression) archiveFile: '$(Build.ArtifactStagingDirectory)/$(runtime).$(format)' replaceExistingArchive: true From 4340dc44bfd2efa7d295920a2387d6f0c53f525c Mon Sep 17 00:00:00 2001 From: tidusjar Date: Tue, 24 Mar 2020 23:44:26 +0000 Subject: [PATCH 10/14] Fixed #3245 --- src/Ombi.Api.Emby/EmbyApiFactory.cs | 41 ++++ src/Ombi.Api.Emby/IBaseEmbyApi.cs | 33 ++++ src/Ombi.Api.Emby/IEmbyApi.cs | 30 +-- src/Ombi.Api.Emby/JellyfinApi.cs | 180 ++++++++++++++++++ .../Authentication/OmbiUserManager.cs | 12 +- src/Ombi.DependencyInjection/IocExtensions.cs | 2 + .../Checks/EmbyHealthCheck.cs | 7 +- .../Jobs/Emby/EmbyContentSync.cs | 20 +- .../Jobs/Emby/EmbyEpisodeSync.cs | 14 +- .../Jobs/Emby/EmbyUserImporter.cs | 11 +- .../Jobs/Ombi/RefreshMetadata.cs | 10 +- .../Controllers/V1/External/EmbyController.cs | 13 +- .../V1/External/TesterController.cs | 8 +- .../Controllers/V1/LandingPageController.cs | 7 +- src/Ombi/Controllers/V1/SettingsController.cs | 7 +- 15 files changed, 322 insertions(+), 73 deletions(-) create mode 100644 src/Ombi.Api.Emby/EmbyApiFactory.cs create mode 100644 src/Ombi.Api.Emby/IBaseEmbyApi.cs create mode 100644 src/Ombi.Api.Emby/JellyfinApi.cs diff --git a/src/Ombi.Api.Emby/EmbyApiFactory.cs b/src/Ombi.Api.Emby/EmbyApiFactory.cs new file mode 100644 index 000000000..c5c6e1c02 --- /dev/null +++ b/src/Ombi.Api.Emby/EmbyApiFactory.cs @@ -0,0 +1,41 @@ +using Ombi.Api; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using System.Threading.Tasks; + +namespace Ombi.Api.Emby +{ + public class EmbyApiFactory : IEmbyApiFactory + { + private readonly ISettingsService _embySettings; + private readonly IApi _api; + + // TODO, if we need to derive futher, need to rework + public EmbyApiFactory(ISettingsService embySettings, IApi api) + { + _embySettings = embySettings; + _api = api; + } + + public async Task CreateClient() + { + var settings = await _embySettings.GetSettingsAsync(); + return CreateClient(settings); + } + + public IEmbyApi CreateClient(EmbySettings settings) + { + if (settings.IsJellyfin) + { + return new JellyfinApi(_api); + } + return new EmbyApi(_api); + } + } + + public interface IEmbyApiFactory + { + Task CreateClient(); + IEmbyApi CreateClient(EmbySettings settings); + } +} diff --git a/src/Ombi.Api.Emby/IBaseEmbyApi.cs b/src/Ombi.Api.Emby/IBaseEmbyApi.cs new file mode 100644 index 000000000..6f9a6bc7f --- /dev/null +++ b/src/Ombi.Api.Emby/IBaseEmbyApi.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Ombi.Api.Emby.Models; +using Ombi.Api.Emby.Models.Media.Tv; +using Ombi.Api.Emby.Models.Movie; + +namespace Ombi.Api.Emby +{ + public interface IBaseEmbyApi + { + Task GetSystemInformation(string apiKey, string baseUrl); + Task> GetUsers(string baseUri, string apiKey); + Task LogIn(string username, string password, string apiKey, string baseUri); + + Task> GetAllMovies(string apiKey, int startIndex, int count, string userId, + string baseUri); + + Task> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, + string baseUri); + + Task> GetAllShows(string apiKey, int startIndex, int count, string userId, + string baseUri); + + Task> GetCollection(string mediaId, + string apiKey, string userId, string baseUrl); + + Task GetSeriesInformation(string mediaId, string apiKey, string userId, string baseUrl); + Task GetMovieInformation(string mediaId, string apiKey, string userId, string baseUrl); + Task GetEpisodeInformation(string mediaId, string apiKey, string userId, string baseUrl); + Task GetPublicInformation(string baseUrl); + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Emby/IEmbyApi.cs b/src/Ombi.Api.Emby/IEmbyApi.cs index 3c29878b7..e7803116d 100644 --- a/src/Ombi.Api.Emby/IEmbyApi.cs +++ b/src/Ombi.Api.Emby/IEmbyApi.cs @@ -1,34 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; +using System.Threading.Tasks; using Ombi.Api.Emby.Models; -using Ombi.Api.Emby.Models.Media.Tv; -using Ombi.Api.Emby.Models.Movie; namespace Ombi.Api.Emby { - public interface IEmbyApi - { - Task GetSystemInformation(string apiKey, string baseUrl); - Task> GetUsers(string baseUri, string apiKey); - Task LogIn(string username, string password, string apiKey, string baseUri); + public interface IEmbyApi : IBaseEmbyApi + { Task LoginConnectUser(string username, string password); - - Task> GetAllMovies(string apiKey, int startIndex, int count, string userId, - string baseUri); - - Task> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, - string baseUri); - - Task> GetAllShows(string apiKey, int startIndex, int count, string userId, - string baseUri); - - Task> GetCollection(string mediaId, - string apiKey, string userId, string baseUrl); - - Task GetSeriesInformation(string mediaId, string apiKey, string userId, string baseUrl); - Task GetMovieInformation(string mediaId, string apiKey, string userId, string baseUrl); - Task GetEpisodeInformation(string mediaId, string apiKey, string userId, string baseUrl); - Task GetPublicInformation(string baseUrl); } } \ No newline at end of file diff --git a/src/Ombi.Api.Emby/JellyfinApi.cs b/src/Ombi.Api.Emby/JellyfinApi.cs new file mode 100644 index 000000000..3fbec3806 --- /dev/null +++ b/src/Ombi.Api.Emby/JellyfinApi.cs @@ -0,0 +1,180 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Internal; +using Newtonsoft.Json; +using Ombi.Api.Emby.Models; +using Ombi.Api.Emby.Models.Media.Tv; +using Ombi.Api.Emby.Models.Movie; +using Ombi.Helpers; + +namespace Ombi.Api.Emby +{ + public class JellyfinApi : IEmbyApi + { + public JellyfinApi(IApi api) + { + Api = api; + } + + private IApi Api { get; } + + /// + /// Returns all users from the Emby Instance + /// + /// + /// + public async Task> GetUsers(string baseUri, string apiKey) + { + var request = new Request("jellyfin/users", baseUri, HttpMethod.Get); + + AddHeaders(request, apiKey); + var obj = await Api.Request>(request); + + return obj; + } + + public async Task GetSystemInformation(string apiKey, string baseUrl) + { + var request = new Request("jellyfin/System/Info", baseUrl, HttpMethod.Get); + + AddHeaders(request, apiKey); + + var obj = await Api.Request(request); + + return obj; + } + + public async Task GetPublicInformation(string baseUrl) + { + var request = new Request("jellyfin/System/Info/public", baseUrl, HttpMethod.Get); + + AddHeaders(request, string.Empty); + + var obj = await Api.Request(request); + + return obj; + } + + public async Task LogIn(string username, string password, string apiKey, string baseUri) + { + var request = new Request("jellyfin/users/authenticatebyname", baseUri, HttpMethod.Post); + var body = new + { + username, + pw = password, + }; + + request.AddJsonBody(body); + + request.AddHeader("X-Emby-Authorization", + $"MediaBrowser Client=\"Ombi\", Device=\"Ombi\", DeviceId=\"v3\", Version=\"v3\""); + AddHeaders(request, apiKey); + + var obj = await Api.Request(request); + return obj; + } + + public async Task> GetCollection(string mediaId, string apiKey, string userId, string baseUrl) + { + var request = new Request($"jellyfin/users/{userId}/items?parentId={mediaId}", baseUrl, HttpMethod.Get); + AddHeaders(request, apiKey); + + request.AddQueryString("Fields", "ProviderIds,Overview"); + + request.AddQueryString("IsVirtualItem", "False"); + + return await Api.Request>(request); + } + + public async Task> GetAllMovies(string apiKey, int startIndex, int count, string userId, string baseUri) + { + return await GetAll("Movie", apiKey, userId, baseUri, true, startIndex, count); + } + + public async Task> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, string baseUri) + { + return await GetAll("Episode", apiKey, userId, baseUri, false, startIndex, count); + } + + public async Task> GetAllShows(string apiKey, int startIndex, int count, string userId, string baseUri) + { + return await GetAll("Series", apiKey, userId, baseUri, false, startIndex, count); + } + + public async Task GetSeriesInformation(string mediaId, string apiKey, string userId, string baseUrl) + { + return await GetInformation(mediaId, apiKey, userId, baseUrl); + } + public async Task GetMovieInformation(string mediaId, string apiKey, string userId, string baseUrl) + { + return await GetInformation(mediaId, apiKey, userId, baseUrl); + } + + public async Task GetEpisodeInformation(string mediaId, string apiKey, string userId, string baseUrl) + { + return await GetInformation(mediaId, apiKey, userId, baseUrl); + } + + private async Task GetInformation(string mediaId, string apiKey, string userId, string baseUrl) + { + var request = new Request($"jellyfin/users/{userId}/items/{mediaId}", baseUrl, HttpMethod.Get); + + AddHeaders(request, apiKey); + var response = await Api.RequestContent(request); + + return JsonConvert.DeserializeObject(response); + } + + private async Task> GetAll(string type, string apiKey, string userId, string baseUri, bool includeOverview = false) + { + var request = new Request($"jellyfin/users/{userId}/items", baseUri, HttpMethod.Get); + + request.AddQueryString("Recursive", true.ToString()); + request.AddQueryString("IncludeItemTypes", type); + request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview" : "ProviderIds"); + + request.AddQueryString("IsVirtualItem", "False"); + + AddHeaders(request, apiKey); + + + var obj = await Api.Request>(request); + return obj; + } + private async Task> GetAll(string type, string apiKey, string userId, string baseUri, bool includeOverview, int startIndex, int count) + { + var request = new Request($"jellyfin/users/{userId}/items", baseUri, HttpMethod.Get); + + request.AddQueryString("Recursive", true.ToString()); + request.AddQueryString("IncludeItemTypes", type); + request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview" : "ProviderIds"); + request.AddQueryString("startIndex", startIndex.ToString()); + request.AddQueryString("limit", count.ToString()); + + request.AddQueryString("IsVirtualItem", "False"); + + AddHeaders(request, apiKey); + + + var obj = await Api.Request>(request); + return obj; + } + + private static void AddHeaders(Request req, string apiKey) + { + if (!string.IsNullOrEmpty(apiKey)) + { + req.AddHeader("X-MediaBrowser-Token", apiKey); + } + req.AddHeader("Accept", "application/json"); + req.AddContentHeader("Content-Type", "application/json"); + req.AddHeader("Device", "Ombi"); + } + + public Task LoginConnectUser(string username, string password) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/src/Ombi.Core/Authentication/OmbiUserManager.cs b/src/Ombi.Core/Authentication/OmbiUserManager.cs index 2c78f39bf..135196051 100644 --- a/src/Ombi.Core/Authentication/OmbiUserManager.cs +++ b/src/Ombi.Core/Authentication/OmbiUserManager.cs @@ -49,7 +49,7 @@ namespace Ombi.Core.Authentication IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger, IPlexApi plexApi, - IEmbyApi embyApi, ISettingsService embySettings, ISettingsService auth) + IEmbyApiFactory embyApi, ISettingsService embySettings, ISettingsService auth) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) { _plexApi = plexApi; @@ -59,7 +59,7 @@ namespace Ombi.Core.Authentication } private readonly IPlexApi _plexApi; - private readonly IEmbyApi _embyApi; + private readonly IEmbyApiFactory _embyApi; private readonly ISettingsService _embySettings; private readonly ISettingsService _authSettings; @@ -146,9 +146,12 @@ namespace Ombi.Core.Authentication /// private async Task CheckEmbyPasswordAsync(OmbiUser user, string password) { + var embySettings = await _embySettings.GetSettingsAsync(); + var client = _embyApi.CreateClient(embySettings); + if (user.IsEmbyConnect) { - var result = await _embyApi.LoginConnectUser(user.UserName, password); + var result = await client.LoginConnectUser(user.UserName, password); if (result.AccessToken.HasValue()) { // We cannot update the email address in the user importer due to there is no way @@ -165,12 +168,11 @@ namespace Ombi.Core.Authentication } } - var embySettings = await _embySettings.GetSettingsAsync(); foreach (var server in embySettings.Servers) { try { - var result = await _embyApi.LogIn(user.UserName, password, server.ApiKey, server.FullUri); + var result = await client.LogIn(user.UserName, password, server.ApiKey, server.FullUri); if (result != null) { return true; diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index 22129f287..76f2f42bc 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -151,6 +151,8 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); } public static void RegisterStore(this IServiceCollection services) { diff --git a/src/Ombi.HealthChecks/Checks/EmbyHealthCheck.cs b/src/Ombi.HealthChecks/Checks/EmbyHealthCheck.cs index d601cdeef..45c227033 100644 --- a/src/Ombi.HealthChecks/Checks/EmbyHealthCheck.cs +++ b/src/Ombi.HealthChecks/Checks/EmbyHealthCheck.cs @@ -25,17 +25,18 @@ namespace Ombi.HealthChecks.Checks using (var scope = CreateScope()) { var settingsProvider = scope.ServiceProvider.GetRequiredService>(); - var api = scope.ServiceProvider.GetRequiredService(); + var api = scope.ServiceProvider.GetRequiredService(); var settings = await settingsProvider.GetSettingsAsync(); if (settings == null) { return HealthCheckResult.Healthy("Emby is not configured."); } - + + var client = api.CreateClient(settings); var taskResult = new List>(); foreach (var server in settings.Servers) { - taskResult.Add(api.GetSystemInformation(server.ApiKey, server.FullUri)); + taskResult.Add(client.GetSystemInformation(server.ApiKey, server.FullUri)); } try diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs index 0968ccbb4..8fa02d5ac 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs @@ -20,28 +20,30 @@ namespace Ombi.Schedule.Jobs.Emby { public class EmbyContentSync : IEmbyContentSync { - public EmbyContentSync(ISettingsService settings, IEmbyApi api, ILogger logger, + public EmbyContentSync(ISettingsService settings, IEmbyApiFactory api, ILogger logger, IEmbyContentRepository repo, IHubContext notification) { _logger = logger; _settings = settings; - _api = api; + _apiFactory = api; _repo = repo; _notification = notification; } private readonly ILogger _logger; private readonly ISettingsService _settings; - private readonly IEmbyApi _api; + private readonly IEmbyApiFactory _apiFactory; private readonly IEmbyContentRepository _repo; private readonly IHubContext _notification; + private IEmbyApi Api { get; set; } public async Task Execute(IJobExecutionContext job) { var embySettings = await _settings.GetSettingsAsync(); if (!embySettings.Enable) return; - + + Api = _apiFactory.CreateClient(embySettings); await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) .SendAsync(NotificationHub.NotificationEvent, "Emby Content Sync Started"); @@ -76,7 +78,7 @@ namespace Ombi.Schedule.Jobs.Emby //await _repo.ExecuteSql("DELETE FROM EmbyEpisode"); //await _repo.ExecuteSql("DELETE FROM EmbyContent"); - var movies = await _api.GetAllMovies(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); + var movies = await Api.GetAllMovies(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); var totalCount = movies.TotalRecordCount; var processed = 1; @@ -89,7 +91,7 @@ namespace Ombi.Schedule.Jobs.Emby if (movie.Type.Equals("boxset", StringComparison.InvariantCultureIgnoreCase)) { var movieInfo = - await _api.GetCollection(movie.Id, server.ApiKey, server.AdministratorId, server.FullUri); + await Api.GetCollection(movie.Id, server.ApiKey, server.AdministratorId, server.FullUri); foreach (var item in movieInfo.Items) { await ProcessMovies(item, mediaToAdd, server); @@ -106,7 +108,7 @@ namespace Ombi.Schedule.Jobs.Emby } // Get the next batch - movies = await _api.GetAllMovies(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); + movies = await Api.GetAllMovies(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); await _repo.AddRange(mediaToAdd); mediaToAdd.Clear(); @@ -114,7 +116,7 @@ namespace Ombi.Schedule.Jobs.Emby // TV Time - var tv = await _api.GetAllShows(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); + var tv = await Api.GetAllShows(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); var totalTv = tv.TotalRecordCount; processed = 1; while (processed < totalTv) @@ -160,7 +162,7 @@ namespace Ombi.Schedule.Jobs.Emby } } // Get the next batch - tv = await _api.GetAllShows(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); + tv = await Api.GetAllShows(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); await _repo.AddRange(mediaToAdd); mediaToAdd.Clear(); } diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs index 292c9ed13..6dda89979 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs @@ -44,10 +44,10 @@ namespace Ombi.Schedule.Jobs.Emby { public class EmbyEpisodeSync : IEmbyEpisodeSync { - public EmbyEpisodeSync(ISettingsService s, IEmbyApi api, ILogger l, IEmbyContentRepository repo + public EmbyEpisodeSync(ISettingsService s, IEmbyApiFactory api, ILogger l, IEmbyContentRepository repo , IHubContext notification) { - _api = api; + _apiFactory = api; _logger = l; _settings = s; _repo = repo; @@ -55,16 +55,18 @@ namespace Ombi.Schedule.Jobs.Emby } private readonly ISettingsService _settings; - private readonly IEmbyApi _api; + private readonly IEmbyApiFactory _apiFactory; private readonly ILogger _logger; private readonly IEmbyContentRepository _repo; private readonly IHubContext _notification; + private IEmbyApi Api { get; set; } public async Task Execute(IJobExecutionContext job) { var settings = await _settings.GetSettingsAsync(); - + + Api = _apiFactory.CreateClient(settings); await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) .SendAsync(NotificationHub.NotificationEvent, "Emby Episode Sync Started"); foreach (var server in settings.Servers) @@ -80,7 +82,7 @@ namespace Ombi.Schedule.Jobs.Emby private async Task CacheEpisodes(EmbyServers server) { - var allEpisodes = await _api.GetAllEpisodes(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); + var allEpisodes = await Api.GetAllEpisodes(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); var total = allEpisodes.TotalRecordCount; var processed = 1; var epToAdd = new HashSet(); @@ -147,7 +149,7 @@ namespace Ombi.Schedule.Jobs.Emby await _repo.AddRange(epToAdd); epToAdd.Clear(); - allEpisodes = await _api.GetAllEpisodes(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); + allEpisodes = await Api.GetAllEpisodes(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); } if (epToAdd.Any()) diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyUserImporter.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyUserImporter.cs index cd0bd0b8e..c5f8ad862 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyUserImporter.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyUserImporter.cs @@ -45,10 +45,10 @@ namespace Ombi.Schedule.Jobs.Emby { public class EmbyUserImporter : IEmbyUserImporter { - public EmbyUserImporter(IEmbyApi api, UserManager um, ILogger log, + public EmbyUserImporter(IEmbyApiFactory api, UserManager um, ILogger log, ISettingsService embySettings, ISettingsService ums, IHubContext notification) { - _api = api; + _apiFactory = api; _userManager = um; _log = log; _embySettings = embySettings; @@ -56,12 +56,13 @@ namespace Ombi.Schedule.Jobs.Emby _notification = notification; } - private readonly IEmbyApi _api; + private readonly IEmbyApiFactory _apiFactory; private readonly UserManager _userManager; private readonly ILogger _log; private readonly ISettingsService _embySettings; private readonly ISettingsService _userManagementSettings; private readonly IHubContext _notification; + private IEmbyApi Api { get; set; } public async Task Execute(IJobExecutionContext job) { @@ -76,6 +77,8 @@ namespace Ombi.Schedule.Jobs.Emby return; } + Api = _apiFactory.CreateClient(settings); + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) .SendAsync(NotificationHub.NotificationEvent, "Emby User Importer Started"); var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.EmbyUser).ToListAsync(); @@ -86,7 +89,7 @@ namespace Ombi.Schedule.Jobs.Emby continue; } - var embyUsers = await _api.GetUsers(server.FullUri, server.ApiKey); + var embyUsers = await Api.GetUsers(server.FullUri, server.ApiKey); foreach (var embyUser in embyUsers) { // Check if we should import this user diff --git a/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs index a24914e99..55d09894e 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs @@ -23,7 +23,7 @@ namespace Ombi.Schedule.Jobs.Ombi { public RefreshMetadata(IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, ILogger log, ITvMazeApi tvApi, ISettingsService plexSettings, - IMovieDbApi movieApi, ISettingsService embySettings, IEmbyApi embyApi, IHubContext notification) + IMovieDbApi movieApi, ISettingsService embySettings, IEmbyApiFactory embyApi, IHubContext notification) { _plexRepo = plexRepo; _embyRepo = embyRepo; @@ -32,7 +32,7 @@ namespace Ombi.Schedule.Jobs.Ombi _tvApi = tvApi; _plexSettings = plexSettings; _embySettings = embySettings; - _embyApi = embyApi; + _embyApiFactory = embyApi; _notification = notification; } @@ -43,8 +43,9 @@ namespace Ombi.Schedule.Jobs.Ombi private readonly ITvMazeApi _tvApi; private readonly ISettingsService _plexSettings; private readonly ISettingsService _embySettings; - private readonly IEmbyApi _embyApi; + private readonly IEmbyApiFactory _embyApiFactory; private readonly IHubContext _notification; + private IEmbyApi EmbyApi { get; set; } public async Task Execute(IJobExecutionContext job) { @@ -94,6 +95,7 @@ namespace Ombi.Schedule.Jobs.Ombi private async Task StartEmby(EmbySettings s) { + EmbyApi = _embyApiFactory.CreateClient(s); await StartEmbyMovies(s); await StartEmbyTv(); } @@ -221,7 +223,7 @@ namespace Ombi.Schedule.Jobs.Ombi foreach (var server in settings.Servers) { _log.LogInformation($"Checking server {server.Name} for upto date metadata"); - var movieInfo = await _embyApi.GetMovieInformation(movie.EmbyId, server.ApiKey, server.AdministratorId, + var movieInfo = await EmbyApi.GetMovieInformation(movie.EmbyId, server.ApiKey, server.AdministratorId, server.FullUri); if (movieInfo.ProviderIds?.Imdb.HasValue() ?? false) diff --git a/src/Ombi/Controllers/V1/External/EmbyController.cs b/src/Ombi/Controllers/V1/External/EmbyController.cs index d8ef5c158..4b201fe7c 100644 --- a/src/Ombi/Controllers/V1/External/EmbyController.cs +++ b/src/Ombi/Controllers/V1/External/EmbyController.cs @@ -24,13 +24,13 @@ namespace Ombi.Controllers.V1.External /// /// /// - public EmbyController(IEmbyApi emby, ISettingsService embySettings) + public EmbyController(IEmbyApiFactory emby, ISettingsService embySettings) { EmbyApi = emby; EmbySettings = embySettings; } - private IEmbyApi EmbyApi { get; } + private IEmbyApiFactory EmbyApi { get; } private ISettingsService EmbySettings { get; } /// @@ -46,10 +46,11 @@ namespace Ombi.Controllers.V1.External var settings = await EmbySettings.GetSettingsAsync(); if (settings?.Servers?.Any() ?? false) return null; + var client = await EmbyApi.CreateClient(); request.Enable = true; var firstServer = request.Servers.FirstOrDefault(); // Test that we can connect - var result = await EmbyApi.GetUsers(firstServer.FullUri, firstServer.ApiKey); + var result = await client.GetUsers(firstServer.FullUri, firstServer.ApiKey); if (result != null && result.Any()) { @@ -64,7 +65,8 @@ namespace Ombi.Controllers.V1.External [HttpPost("info")] public async Task GetServerInfo([FromBody] EmbyServers server) { - var result = await EmbyApi.GetPublicInformation(server.FullUri); + var client = await EmbyApi.CreateClient(); + var result = await client.GetPublicInformation(server.FullUri); return result; } @@ -77,9 +79,10 @@ namespace Ombi.Controllers.V1.External { var vm = new List(); var s = await EmbySettings.GetSettingsAsync(); + var client = EmbyApi.CreateClient(s); foreach (var server in s?.Servers ?? new List()) { - var users = await EmbyApi.GetUsers(server.FullUri, server.ApiKey); + var users = await client.GetUsers(server.FullUri, server.ApiKey); if (users != null && users.Any()) { vm.AddRange(users.Select(u => new UsersViewModel diff --git a/src/Ombi/Controllers/V1/External/TesterController.cs b/src/Ombi/Controllers/V1/External/TesterController.cs index d66fa29e9..0dbaac688 100644 --- a/src/Ombi/Controllers/V1/External/TesterController.cs +++ b/src/Ombi/Controllers/V1/External/TesterController.cs @@ -42,7 +42,7 @@ namespace Ombi.Controllers.V1.External /// public TesterController(INotificationService service, IDiscordNotification notification, IEmailNotification emailN, IPushbulletNotification pushbullet, ISlackNotification slack, IPushoverNotification po, IMattermostNotification mm, - IPlexApi plex, IEmbyApi emby, IRadarrApi radarr, ISonarrApi sonarr, ILogger log, IEmailProvider provider, + IPlexApi plex, IEmbyApiFactory emby, IRadarrApi radarr, ISonarrApi sonarr, ILogger log, IEmailProvider provider, ICouchPotatoApi cpApi, ITelegramNotification telegram, ISickRageApi srApi, INewsletterJob newsletter, ILegacyMobileNotification mobileNotification, ILidarrApi lidarrApi, IGotifyNotification gotifyNotification, IWhatsAppApi whatsAppApi, OmbiUserManager um, IWebhookNotification webhookNotification) { @@ -82,7 +82,7 @@ namespace Ombi.Controllers.V1.External private IMattermostNotification MattermostNotification { get; } private IPlexApi PlexApi { get; } private IRadarrApi RadarrApi { get; } - private IEmbyApi EmbyApi { get; } + private IEmbyApiFactory EmbyApi { get; } private ISonarrApi SonarrApi { get; } private ICouchPotatoApi CouchPotatoApi { get; } private ILogger Log { get; } @@ -322,8 +322,8 @@ namespace Ombi.Controllers.V1.External { try { - - var result = await EmbyApi.GetUsers(settings.FullUri, settings.ApiKey); + var client = await EmbyApi.CreateClient(); + var result = await client.GetUsers(settings.FullUri, settings.ApiKey); return result.Any(); } catch (Exception e) diff --git a/src/Ombi/Controllers/V1/LandingPageController.cs b/src/Ombi/Controllers/V1/LandingPageController.cs index 48d10d727..54e1638b4 100644 --- a/src/Ombi/Controllers/V1/LandingPageController.cs +++ b/src/Ombi/Controllers/V1/LandingPageController.cs @@ -18,7 +18,7 @@ namespace Ombi.Controllers.V1 public class LandingPageController : ControllerBase { public LandingPageController(ISettingsService plex, ISettingsService emby, - IPlexApi plexApi, IEmbyApi embyApi) + IPlexApi plexApi, IEmbyApiFactory embyApi) { _plexSettings = plex; _embySettings = emby; @@ -27,7 +27,7 @@ namespace Ombi.Controllers.V1 } private readonly IPlexApi _plexApi; - private readonly IEmbyApi _embyApi; + private readonly IEmbyApiFactory _embyApi; private readonly ISettingsService _plexSettings; private readonly ISettingsService _embySettings; @@ -65,11 +65,12 @@ namespace Ombi.Controllers.V1 var emby = await _embySettings.GetSettingsAsync(); if (emby.Enable) { + var client = _embyApi.CreateClient(emby); foreach (var server in emby.Servers) { try { - var result = await _embyApi.GetUsers(server.FullUri, server.ApiKey); + var result = await client.GetUsers(server.FullUri, server.ApiKey); if (result.Any()) { model.ServersAvailable++; diff --git a/src/Ombi/Controllers/V1/SettingsController.cs b/src/Ombi/Controllers/V1/SettingsController.cs index 200fc59d9..8a0e52018 100644 --- a/src/Ombi/Controllers/V1/SettingsController.cs +++ b/src/Ombi/Controllers/V1/SettingsController.cs @@ -45,7 +45,7 @@ namespace Ombi.Controllers.V1 public SettingsController(ISettingsResolver resolver, IMapper mapper, INotificationTemplatesRepository templateRepo, - IEmbyApi embyApi, + IEmbyApiFactory embyApi, ICacheService memCache, IGithubApi githubApi, IRecentlyAddedEngine engine) @@ -62,7 +62,7 @@ namespace Ombi.Controllers.V1 private ISettingsResolver SettingsResolver { get; } private IMapper Mapper { get; } private INotificationTemplatesRepository TemplateRepository { get; } - private readonly IEmbyApi _embyApi; + private readonly IEmbyApiFactory _embyApi; private readonly ICacheService _cache; private readonly IGithubApi _githubApi; private readonly IRecentlyAddedEngine _recentlyAdded; @@ -212,9 +212,10 @@ namespace Ombi.Controllers.V1 { if (emby.Enable) { + var client = await _embyApi.CreateClient(); foreach (var server in emby.Servers) { - var users = await _embyApi.GetUsers(server.FullUri, server.ApiKey); + var users = await client.GetUsers(server.FullUri, server.ApiKey); var admin = users.FirstOrDefault(x => x.Policy.IsAdministrator); server.AdministratorId = admin?.Id; } From 2cba74e096e0de697f6bec5d2e19de629d1579ea Mon Sep 17 00:00:00 2001 From: tidusjar Date: Wed, 25 Mar 2020 11:21:05 +0000 Subject: [PATCH 11/14] Should fix the newsletter issue from #3445 --- src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs index 751c2d0e3..504be9814 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs @@ -138,8 +138,8 @@ namespace Ombi.Schedule.Jobs.Ombi // Filter out the ones that we haven't sent yet - var plexContentLocalDataset = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie && x.HasTheMovieDb).ToHashSet(); - var embyContentLocalDataset = embyContent.Where(x => x.Type == EmbyMediaType.Movie && x.HasTheMovieDb).ToHashSet(); + var plexContentLocalDataset = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet(); + var embyContentLocalDataset = embyContent.Where(x => x.Type == EmbyMediaType.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet(); var plexContentMoviesToSend = plexContentLocalDataset.Where(x => !addedPlexMovieLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId))).ToHashSet(); var embyContentMoviesToSend = embyContentLocalDataset.Where(x => !addedEmbyMoviesLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId))).ToHashSet(); var lidarrContentAlbumsToSend = (await lidarrContent.ToListAsync()).Where(x => !addedAlbumLogIds.Contains(x.ForeignAlbumId)).ToHashSet(); @@ -148,16 +148,16 @@ namespace Ombi.Schedule.Jobs.Ombi _log.LogInformation("Albums to send: {0}", lidarrContentAlbumsToSend.Count()); // Find the movies that do not yet have MovieDbIds - var needsMovieDbPlex = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie && !x.HasTheMovieDb).ToHashSet(); - var needsMovieDbEmby = embyContent.Where(x => x.Type == EmbyMediaType.Movie && !x.HasTheMovieDb).ToHashSet(); + var needsMovieDbPlex = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet(); + var needsMovieDbEmby = embyContent.Where(x => x.Type == EmbyMediaType.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet(); var newPlexMovies = await GetMoviesWithoutId(addedPlexMovieLogIds, needsMovieDbPlex); var newEmbyMovies = await GetMoviesWithoutId(addedEmbyMoviesLogIds, needsMovieDbEmby); plexContentMoviesToSend = plexContentMoviesToSend.Union(newPlexMovies).ToHashSet(); embyContentMoviesToSend = embyContentMoviesToSend.Union(newEmbyMovies).ToHashSet(); var plexEpisodesToSend = - FilterPlexEpisodes(_plex.GetAllEpisodes().Include(x => x.Series).Where(x => x.Series.HasTvDb).AsNoTracking(), addedPlexEpisodesLogIds); - var embyEpisodesToSend = FilterEmbyEpisodes(_emby.GetAllEpisodes().Include(x => x.Series).Where(x => x.Series.HasTvDb).AsNoTracking(), + FilterPlexEpisodes(_plex.GetAllEpisodes().Include(x => x.Series).AsNoTracking(), addedPlexEpisodesLogIds); + var embyEpisodesToSend = FilterEmbyEpisodes(_emby.GetAllEpisodes().Include(x => x.Series).AsNoTracking(), addedEmbyEpisodesLogIds); _log.LogInformation("Plex Episodes to send: {0}", plexEpisodesToSend.Count()); @@ -386,7 +386,7 @@ namespace Ombi.Schedule.Jobs.Ombi private HashSet FilterPlexEpisodes(IEnumerable source, IQueryable recentlyAdded) { var itemsToReturn = new HashSet(); - foreach (var ep in source) + foreach (var ep in source.Where(x => x.Series.HasTvDb)) { var tvDbId = StringHelper.IntParseLinq(ep.Series.TvDbId); if (recentlyAdded.Any(x => x.ContentId == tvDbId && x.EpisodeNumber == ep.EpisodeNumber && x.SeasonNumber == ep.SeasonNumber)) @@ -403,7 +403,7 @@ namespace Ombi.Schedule.Jobs.Ombi private HashSet FilterEmbyEpisodes(IEnumerable source, IQueryable recentlyAdded) { var itemsToReturn = new HashSet(); - foreach (var ep in source) + foreach (var ep in source.Where(x => x.Series.HasTvDb)) { var tvDbId = StringHelper.IntParseLinq(ep.Series.TvDbId); if (recentlyAdded.Any(x => x.ContentId == tvDbId && x.EpisodeNumber == ep.EpisodeNumber && x.SeasonNumber == ep.SeasonNumber)) From e080afe05aec1456cfd54cdac8cdcac48080fd39 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Wed, 25 Mar 2020 22:14:45 +0000 Subject: [PATCH 12/14] Added the ability for the discover options to stay/stick --- .../components/discover/discover.component.ts | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts index 47695073b..aaa44171e 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts +++ b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts @@ -3,6 +3,7 @@ import { SearchV2Service } from "../../../services"; import { ISearchMovieResult, ISearchTvResult, RequestType } from "../../../interfaces"; import { IDiscoverCardResult, DiscoverOption } from "../../interfaces"; import { trigger, transition, style, animate } from "@angular/animations"; +import { StorageService } from "../../../shared/storage/storage-service"; @Component({ templateUrl: "./discover.component.html", @@ -21,13 +22,13 @@ export class DiscoverComponent implements OnInit { public discoverResults: IDiscoverCardResult[] = []; public movies: ISearchMovieResult[] = []; public tvShows: ISearchTvResult[] = []; - + public discoverOptions: DiscoverOption = DiscoverOption.Combined; public DiscoverOption = DiscoverOption; public defaultTvPoster: string; - public popularActive: boolean = true; + public popularActive: boolean; public trendingActive: boolean; public upcomingActive: boolean; @@ -36,22 +37,28 @@ export class DiscoverComponent implements OnInit { private contentLoaded: number; private isScrolling: boolean = false; + private mediaTypeStorageKey = "DiscoverOptions"; - constructor(private searchService: SearchV2Service) { } + constructor(private searchService: SearchV2Service, + private storageService: StorageService) { } public async ngOnInit() { this.loading() + const localDiscoverOptions = +this.storageService.get(this.mediaTypeStorageKey); + if (localDiscoverOptions) { + this.discoverOptions = DiscoverOption[DiscoverOption[localDiscoverOptions]]; + } this.scrollDisabled = true; switch (this.discoverOptions) { case DiscoverOption.Combined: - this.movies = await this.searchService.popularMoviesByPage(0,12); - this.tvShows = await this.searchService.popularTvByPage(0,12); + this.movies = await this.searchService.popularMoviesByPage(0, 12); + this.tvShows = await this.searchService.popularTvByPage(0, 12); break; case DiscoverOption.Movie: - this.movies = await this.searchService.popularMoviesByPage(0,12); + this.movies = await this.searchService.popularMoviesByPage(0, 12); break; case DiscoverOption.Tv: - this.tvShows = await this.searchService.popularTvByPage(0,12); + this.tvShows = await this.searchService.popularTvByPage(0, 12); break; } @@ -108,7 +115,7 @@ export class DiscoverComponent implements OnInit { case DiscoverOption.Tv: this.tvShows = await this.searchService.anticipatedTvByPage(this.contentLoaded, 12); break; - } + } } this.contentLoaded += 12; @@ -199,7 +206,8 @@ export class DiscoverComponent implements OnInit { public async switchDiscoverMode(newMode: DiscoverOption) { this.loading(); this.clear(); - this.discoverOptions = newMode; + this.discoverOptions = newMode; + this.storageService.save(this.mediaTypeStorageKey, newMode.toString()); await this.ngOnInit(); this.finishLoading(); } From 942a4c5ae2b1db37a0cc4d44e72baff77d69a0ab Mon Sep 17 00:00:00 2001 From: tidusjar Date: Wed, 25 Mar 2020 22:31:33 +0000 Subject: [PATCH 13/14] Made the sorting and sorting direction on the request lists stick --- .../movies-grid/movies-grid.component.html | 4 +-- .../movies-grid/movies-grid.component.ts | 31 +++++++++++++++---- .../components/requests-list.component.html | 2 +- .../components/tv-grid/tv-grid.component.html | 4 +-- .../components/tv-grid/tv-grid.component.ts | 30 +++++++++++++++--- 5 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.html b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.html index 7c91e9900..228ae9cdb 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.html +++ b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.html @@ -10,8 +10,8 @@ - +
diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.ts b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.ts index 513558fa0..4443cd4ed 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.ts +++ b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.ts @@ -1,4 +1,4 @@ -import { Component, AfterViewInit, ViewChild, EventEmitter, Output, ChangeDetectorRef } from "@angular/core"; +import { Component, AfterViewInit, ViewChild, EventEmitter, Output, ChangeDetectorRef, OnInit } from "@angular/core"; import { IMovieRequests, IRequestsViewModel } from "../../../interfaces"; import { MatPaginator, MatSort } from "@angular/material"; import { merge, Observable, of as observableOf } from 'rxjs'; @@ -6,13 +6,14 @@ import { catchError, map, startWith, switchMap } from 'rxjs/operators'; import { RequestServiceV2 } from "../../../services/requestV2.service"; import { AuthService } from "../../../auth/auth.service"; +import { StorageService } from "../../../shared/storage/storage-service"; @Component({ templateUrl: "./movies-grid.component.html", selector: "movies-grid", styleUrls: ["../requests-list.component.scss"] }) -export class MoviesGridComponent implements AfterViewInit { +export class MoviesGridComponent implements OnInit, AfterViewInit { public dataSource: IMovieRequests[] = []; public resultsLength: number; public isLoadingResults = true; @@ -20,6 +21,11 @@ export class MoviesGridComponent implements AfterViewInit { public gridCount: string = "15"; public showUnavailableRequests: boolean; public isAdmin: boolean; + public defaultSort: string = "requestedDate"; + public defaultOrder: string = "desc"; + + private storageKey = "Movie_DefaultRequestListSort"; + private storageKeyOrder = "Movie_DefaultRequestListSortOrder"; @Output() public onOpenOptions = new EventEmitter<{ request: any, filter: any, onChange: any }>(); @@ -27,9 +33,20 @@ export class MoviesGridComponent implements AfterViewInit { @ViewChild(MatSort, { static: false }) sort: MatSort; constructor(private requestService: RequestServiceV2, private ref: ChangeDetectorRef, - private auth: AuthService) { + private auth: AuthService, private storageService: StorageService) { } + + public ngOnInit() { + const defaultSort = this.storageService.get(this.storageKey); + const defaultOrder = this.storageService.get(this.storageKeyOrder); + if (defaultSort) { + this.defaultSort = defaultSort; + } + if (defaultOrder) { + this.defaultOrder = defaultOrder; + } + } public async ngAfterViewInit() { // const results = await this.requestService.getMovieRequests(this.gridCount, 0, OrderType.RequestedDateDesc, @@ -45,10 +62,12 @@ export class MoviesGridComponent implements AfterViewInit { merge(this.sort.sortChange, this.paginator.page) .pipe( startWith({}), - switchMap(() => { + switchMap((value: any) => { this.isLoadingResults = true; - // eturn this.exampleDatabase!.getRepoIssues( - // this.sort.active, this.sort.direction, this.paginator.pageIndex); + if (value.active || value.direction) { + this.storageService.save(this.storageKey, value.active); + this.storageService.save(this.storageKeyOrder, value.direction); + } return this.loadData(); }), map((data: IRequestsViewModel) => { diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/requests-list.component.html b/src/Ombi/ClientApp/src/app/requests-list/components/requests-list.component.html index 381865c0c..a5b9364fe 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/requests-list.component.html +++ b/src/Ombi/ClientApp/src/app/requests-list/components/requests-list.component.html @@ -11,7 +11,7 @@ -

Some more tab content

+

Coming soon

...

diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.html b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.html index 343b9f255..2b9cec0d6 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.html +++ b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.html @@ -11,8 +11,8 @@ -
+
diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.ts b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.ts index 78dd23c1a..3e13a5ce2 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.ts +++ b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.ts @@ -1,4 +1,4 @@ -import { Component, AfterViewInit, ViewChild, Output, EventEmitter, ChangeDetectorRef } from "@angular/core"; +import { Component, AfterViewInit, ViewChild, Output, EventEmitter, ChangeDetectorRef, OnInit } from "@angular/core"; import { IRequestsViewModel, IChildRequests } from "../../../interfaces"; import { MatPaginator, MatSort } from "@angular/material"; import { merge, of as observableOf, Observable } from 'rxjs'; @@ -6,13 +6,14 @@ import { catchError, map, startWith, switchMap } from 'rxjs/operators'; import { RequestServiceV2 } from "../../../services/requestV2.service"; import { AuthService } from "../../../auth/auth.service"; +import { StorageService } from "../../../shared/storage/storage-service"; @Component({ templateUrl: "./tv-grid.component.html", selector: "tv-grid", styleUrls: ["../requests-list.component.scss"] }) -export class TvGridComponent implements AfterViewInit { +export class TvGridComponent implements OnInit, AfterViewInit { public dataSource: IChildRequests[] = []; public resultsLength: number; public isLoadingResults = true; @@ -20,6 +21,11 @@ export class TvGridComponent implements AfterViewInit { public gridCount: string = "15"; public showUnavailableRequests: boolean; public isAdmin: boolean; + public defaultSort: string = "requestedDate"; + public defaultOrder: string = "desc"; + + private storageKey = "Tv_DefaultRequestListSort"; + private storageKeyOrder = "Tv_DefaultRequestListSortOrder"; @Output() public onOpenOptions = new EventEmitter<{request: any, filter: any, onChange: any}>(); @@ -27,8 +33,19 @@ export class TvGridComponent implements AfterViewInit { @ViewChild(MatSort, {static: false}) sort: MatSort; constructor(private requestService: RequestServiceV2, private auth: AuthService, - private ref: ChangeDetectorRef) { + private ref: ChangeDetectorRef, private storageService: StorageService) { + + } + public ngOnInit() { + const defaultSort = this.storageService.get(this.storageKey); + const defaultOrder = this.storageService.get(this.storageKeyOrder); + if (defaultSort) { + this.defaultSort = defaultSort; + } + if (defaultOrder) { + this.defaultOrder = defaultOrder; + } } public async ngAfterViewInit() { @@ -40,8 +57,13 @@ export class TvGridComponent implements AfterViewInit { merge(this.sort.sortChange, this.paginator.page) .pipe( startWith({}), - switchMap(() => { + switchMap((value: any) => { this.isLoadingResults = true; + + if (value.active || value.direction) { + this.storageService.save(this.storageKey, value.active); + this.storageService.save(this.storageKeyOrder, value.direction); + } return this.loadData(); }), map((data: IRequestsViewModel) => { From 10430cfb4055de12b1cc5aa2dcc8b133817e26d0 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Wed, 25 Mar 2020 22:34:35 +0000 Subject: [PATCH 14/14] put back missing flag --- .../src/app/discover/components/discover/discover.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts index aaa44171e..3238ea5ed 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts +++ b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts @@ -28,7 +28,7 @@ export class DiscoverComponent implements OnInit { public defaultTvPoster: string; - public popularActive: boolean; + public popularActive: boolean = true; public trendingActive: boolean; public upcomingActive: boolean;