diff --git a/.azuredevops/pipelines/publish-job.yml b/.azuredevops/pipelines/publish-job.yml index 24c24d385..f7f2d4605 100644 --- a/.azuredevops/pipelines/publish-job.yml +++ b/.azuredevops/pipelines/publish-job.yml @@ -63,22 +63,11 @@ stages: $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' - # repositoryName: 'tidusjar/Ombi' - # action: 'create' - # target: '$(Build.SourceVersion)' - # tagSource: 'userSpecifiedTag' - # tag: '$(gitTag)' - # isDraft: true - # changeLogCompareToRelease: 'lastNonDraftRelease' - # changeLogType: 'commitBased' - - task: GitHubRelease@1 + displayName: 'Ombi.Releases Release' inputs: - gitHubConnection: 'github.com_tidusjar' - repositoryName: 'tidusjar/Ombi.Releases' + gitHubConnection: 'PAT' + repositoryName: 'Ombi-app/Ombi.Releases' action: 'create' target: 'c7fcbb77b58aef1076d635a9ef99e4374abc8672' tagSource: 'userSpecifiedTag' @@ -91,3 +80,38 @@ stages: isPreRelease: true changeLogCompareToRelease: 'lastNonDraftRelease' changeLogType: 'commitBased' + + - task: GitHubRelease@1 + displayName: 'Ombi Release' + inputs: + gitHubConnection: 'PAT' + repositoryName: 'Ombi-app/Ombi' + action: 'create' + target: '$(Build.SourceVersion)' + tagSource: 'userSpecifiedTag' + tag: '$(gitTag)' + releaseNotesSource: 'inline' + releaseNotesInline: '$(ReleaseNotes)' + assets: | + $(System.ArtifactsDirectory)/**/*.zip + $(System.ArtifactsDirectory)/**/*.tar.gz + isPreRelease: true + changeLogCompareToRelease: 'lastNonDraftRelease' + changeLogType: 'commitBased' + + - task: PowerShell@2 + displayName: "Trigger APT build" + inputs: + targetType: 'inline' + script: | + $body = @{ + "ref"="main" + "inputs"= @{"version"= "$(gitTag)"} + } | ConvertTo-Json + + $header = @{ + "Accept"="application/vnd.github.v3+json" + "Authorization"="Bearer ${env:APTPAT}" + } + + Invoke-RestMethod -Uri "https://api.github.com/repos/Ombi-app/Ombi.Apt/actions/workflows/build-deb.yml/dispatches" -Method 'Post' -Body $body -Headers $header \ No newline at end of file diff --git a/.azuredevops/pipelines/templates/variables.yml b/.azuredevops/pipelines/templates/variables.yml index c33cd7999..205e68eaa 100644 --- a/.azuredevops/pipelines/templates/variables.yml +++ b/.azuredevops/pipelines/templates/variables.yml @@ -27,4 +27,4 @@ variables: value: "4.0.$(Build.BuildId)" - name: isMain - value: $[eq(variables['Build.SourceBranch'], 'refs/heads/feature/v4')] \ No newline at end of file + value: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/develop'), eq(variables['Build.SourceBranch'], 'refs/heads/main'))] \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8f206e648..dfe94f0f0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -27,7 +27,7 @@ If applicable, a snippet of the logs that seems relevant to the bug if present. - OS: [e.g. iOS] **Ombi Version (please complete the following information):** - - Version [e.g. 3.0.1158] + - Version [e.g. 4.0.958] - Media Server [e.g. Plex] - Database Type: SQLite (Please change if using MySQL) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 82f01f053..a9da96171 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - - name: Wiki - url: https://github.com/tidusjar/Ombi/wiki - about: The Ombi wiki should help guide you through installation and setup as well as help resolve common problems and answer frequently asked questions + - name: Docs + url: https://docs.ombi.app/ + about: The Ombi documentation should help guide you through installation and setup as well as help resolve common problems and answer frequently asked questions - name: Reddit support url: https://www.reddit.com/r/Ombi about: Ask questions about Ombi diff --git a/CHANGELOG.md b/CHANGELOG.md index 6febb6683..d4abf4412 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -661,7 +661,7 @@ - Added capture of anonymous analytical data. [tidusjar] -- Added {AvailableDate} as a Notification Variable, this is the date the request was marked as available. See here: https://github.com/tidusjar/Ombi/wiki/Notification-Template-Variables. [tidusjar] +- Added {AvailableDate} as a Notification Variable, this is the date the request was marked as available. See here: https://docs.ombi.app/info/notification-template-variables/. [tidusjar] - Added the ability to search movies via the movie db with a different language! [tidusjar] diff --git a/README.md b/README.md index fdb90a5fc..d81c9a67d 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,25 @@ + ![](http://i.imgur.com/qQsN78U.png) ____ [![Discord](https://img.shields.io/discord/270828201473736705.svg)](https://discord.gg/Sa7wNWb) [![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/ombi.svg)](https://hub.docker.com/r/linuxserver/ombi/) -[![Github All Releases](https://img.shields.io/github/downloads/tidusjar/Ombi/total.svg)](https://github.com/tidusjar/Ombi) +[![Github All Releases](https://img.shields.io/github/downloads/tidusjar/Ombi/total.svg)](https://github.com/ombi-app/Ombi) [![firsttimersonly](http://img.shields.io/badge/first--timers--only-friendly-blue.svg)](http://www.firsttimersonly.com/) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/ombi/localized.svg)](https://crowdin.com/project/ombi) [![Patreon](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://patreon.com/tidusjar/Ombi) [![Paypal](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://paypal.me/PlexRequestsNet) +# Welcome + +Ombi is your friendly media request tool, automatically syncs with your media with your media servers! +Don't worry, it's grandma friendly, and more importantly; has wife approval certification 😂 + +| Service | Stable | Develop +|----------|:---------------------------:|:----------------------------:| +| Build Status | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/master?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/master) | [![Build Status](https://dev.azure.com/tidusjar/Ombi/_apis/build/status/Ombi%20CI?repoName=Ombi-app%2FOmbi&branchName=develop)](https://dev.azure.com/tidusjar/Ombi/_build/latest?definitionId=18&repoName=Ombi-app%2FOmbi&branchName=develop) | [![Build Status](https://dev.azure.com/tidusjar/Ombi/_apis/build/status/Ombi%20CI?branchName=feature%2Fv4)](https://dev.azure.com/tidusjar/Ombi/_build/latest?definitionId=18&branchName=feature%2Fv4) +| Download |[![Download](http://i.imgur.com/odToka3.png)](https://github.com/Ombi-app/Ombi/releases) | [![Download](http://i.imgur.com/odToka3.png)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/develop/artifacts) | [![Download](http://i.imgur.com/odToka3.png)](https://github.com/ombi-app/ombi/releases) | + # Feature Requests Feature requests are handled on Feature Upvote. @@ -16,14 +27,13 @@ Search the existing requests to see if your suggestion has already been submitte (If a similar request exists, please vote, or add additional comments to the request) #### [![Feature Requests](https://cloud.githubusercontent.com/assets/390379/10127973/045b3a96-6560-11e5-9b20-31a2032956b2.png)](https://features.ombi.io) -___ -[![Twitter](https://img.shields.io/twitter/follow/tidusjar.svg?style=social)](https://twitter.com/intent/follow?screen_name=tidusjar) + -Follow me developing Ombi! + -[![Twitch](https://img.shields.io/badge/Twitch-Watch-blue.svg?style=flat-square&logo=twitch)](https://www.twitch.tv/tidusjar) + ___ @@ -31,23 +41,9 @@ ___ _**Note:** There is no longer an iOS app due to complications outside of our control._ -___ - -We also now have merch up on Teespring! -[EU Store](https://teespring.com/stores/ombi-eu) -[US Store](https://teespring.com/stores/ombi-us) - -___ - - -| Service | Stable | Develop | V4 | -|----------|:---------------------------:|:----------------------------:|:----------------------------:| -| Build Status | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/master?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/master) | [![Build status](https://ci.appveyor.com/api/projects/status/hgj8j6lcea7j0yhn/branch/develop?svg=true)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/develop) | [![Build Status](https://dev.azure.com/tidusjar/Ombi/_apis/build/status/Ombi%20CI?branchName=feature%2Fv4)](https://dev.azure.com/tidusjar/Ombi/_build/latest?definitionId=18&branchName=feature%2Fv4) -| Download |[![Download](http://i.imgur.com/odToka3.png)](https://github.com/tidusjar/Ombi/releases) | [![Download](http://i.imgur.com/odToka3.png)](https://ci.appveyor.com/project/tidusjar/requestplex/branch/develop/artifacts) | [![Download](http://i.imgur.com/odToka3.png)](https://github.com/tidusjar/ombi.releases/releases) | # Features -Here are some of the features Ombi V3 has: -* Now working without crashes on Linux. +Here are some of the features Ombi has: * Lets users request Movies, Music, and TV Shows (whether it being the entire series, an entire season, or even single episodes.) * Easily manage your requests * Allows you to set specific users to automatically have requests approved and added to the relevant service (Sonarr/Radarr/Lidarr/Couchpotato etc) @@ -58,72 +54,16 @@ Here are some of the features Ombi V3 has: * Will show if the request is already on plex or even if it's already monitored. * Automatically updates the status of requests when they are available on Plex/Emby * Slick, responsive and mobile friendly UI -* Ombi will automatically update itself :) (YMMV) -* Very fast! - -### Integration -We integrate with the following applications: -* Plex Media Server -* Emby -* Jellyfin -* Sonarr V2 and V3 -* Radarr V2 -* Lidarr -* DogNzb -* Couch Potato - - -### Notifications -Supported notifications: -* Mobile -* SMTP Notifications (Email) -* Discord -* Slack -* Pushbullet -* Pushover -* Mattermost -* Telegram -* Gotify -* Twilio -* Webhook - -### The difference between Version 4 and 3 - -Over the last year, we focused on the main functions on Ombi, a complete rewrite while making it better, faster and more stable. -We have already done most of the work, but some features are still be missing in this first version. -We are planning to bring back these features in V3 but for now you can find a list below with a quick comparison of features between v4 and v3. - - -| Service | Version 4 (Beta) | Version 3 (Stable)| -|----------|:----------:|:----------:| -| Multiple Plex/Emby/Jellyfin Servers | Yes | Yes | -| Emby/Jellyfin & Plex support | Yes | Yes | -| Mono dependency | No | No | -| Plex OAuth support | Yes | Yes | -| Login page | Yes (brand new) | Yes | -| Discovery page | Yes (brand new) | No | -| Request a movie collection | Yes (brand new) | No | -| Auto Delete Available Requests | Yes (brand new) | No | -| Report issues | Yes | Yes | -| Notifications support | Yes | Yes | -| Custom Notification Messages | Yes | Yes | -| Sending newsletters | Yes | Yes | -| Send a Mass Email | Yes | Yes | -| SickRage | Yes | Yes | -| CouchPotato | Yes | Yes | -| DogNzb | Yes | Yes | -| Headphones | No | Yes | -| Lidarr | Yes | Yes | + # Preview -![Preview](http://i.imgur.com/Nn1BwAM.gif) +![Preview](https://i.imgur.com/kBXIqer.png) # Installation -[Installation Guide](https://github.com/tidusjar/Ombi/wiki/Installation) -[Here for Reverse Proxy Config Examples](https://github.com/tidusjar/Ombi/wiki/Reverse-Proxy-Examples) -[PlexGuide.com - Ombi Deployment & 101 Demonstration!](https://www.youtube.com/watch?v=QPNlqqkjNJw&feature=youtu.be) +[Installation Guide](https://docs.ombi.app/installation/) +[Here for Reverse Proxy Config Examples](https://docs.ombi.app/info/reverse-proxy/) # Contributors @@ -139,9 +79,3 @@ If you feel like donating you can donate with the below buttons! [![Paypal](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://paypal.me/PlexRequestsNet) ### A massive thanks to everyone for all their help! - - -### Sponsors ### -- [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools - - [ReSharper](http://www.jetbrains.com/resharper/) -- [BrowserStack](https://www.browserstack.com) for allowing us to use their platform for testing diff --git a/src/Ombi.Api.Emby/EmbyApiFactory.cs b/src/Ombi.Api.Emby/EmbyApiFactory.cs index c5c6e1c02..6f71938bf 100644 --- a/src/Ombi.Api.Emby/EmbyApiFactory.cs +++ b/src/Ombi.Api.Emby/EmbyApiFactory.cs @@ -25,10 +25,6 @@ namespace Ombi.Api.Emby public IEmbyApi CreateClient(EmbySettings settings) { - if (settings.IsJellyfin) - { - return new JellyfinApi(_api); - } return new EmbyApi(_api); } } diff --git a/src/Ombi.Api.Emby/Models/PublicInfo.cs b/src/Ombi.Api.Emby/Models/PublicInfo.cs index 01432d3c5..23db412b5 100644 --- a/src/Ombi.Api.Emby/Models/PublicInfo.cs +++ b/src/Ombi.Api.Emby/Models/PublicInfo.cs @@ -5,15 +5,8 @@ public string LocalAddress { get; set; } public string ServerName { get; set; } public string Version { get; set; } - /// - /// Only populated for Jellyfin - /// - public string ProductName { get; set; } - - public bool IsJellyfin => !string.IsNullOrEmpty(ProductName) && ProductName.Contains("Jellyfin"); - public string OperatingSystem { get; set; } public string Id { get; set; } } -} \ No newline at end of file +} diff --git a/src/Ombi.Api.Jellyfin/IBaseJellyfinApi.cs b/src/Ombi.Api.Jellyfin/IBaseJellyfinApi.cs new file mode 100644 index 000000000..7a9ee8a5a --- /dev/null +++ b/src/Ombi.Api.Jellyfin/IBaseJellyfinApi.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Ombi.Api.Jellyfin.Models; +using Ombi.Api.Jellyfin.Models.Media.Tv; +using Ombi.Api.Jellyfin.Models.Movie; + +namespace Ombi.Api.Jellyfin +{ + public interface IBaseJellyfinApi + { + 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.Jellyfin/IJellyfinApi.cs b/src/Ombi.Api.Jellyfin/IJellyfinApi.cs new file mode 100644 index 000000000..72d45877c --- /dev/null +++ b/src/Ombi.Api.Jellyfin/IJellyfinApi.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Ombi.Api.Jellyfin.Models; + +namespace Ombi.Api.Jellyfin +{ + public interface IJellyfinApi : IBaseJellyfinApi + { + Task LoginConnectUser(string username, string password); + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Emby/JellyfinApi.cs b/src/Ombi.Api.Jellyfin/JellyfinApi.cs similarity index 66% rename from src/Ombi.Api.Emby/JellyfinApi.cs rename to src/Ombi.Api.Jellyfin/JellyfinApi.cs index 197fba684..a8d94eca6 100644 --- a/src/Ombi.Api.Emby/JellyfinApi.cs +++ b/src/Ombi.Api.Jellyfin/JellyfinApi.cs @@ -3,14 +3,14 @@ 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.Api.Jellyfin.Models; +using Ombi.Api.Jellyfin.Models.Media.Tv; +using Ombi.Api.Jellyfin.Models.Movie; using Ombi.Helpers; -namespace Ombi.Api.Emby +namespace Ombi.Api.Jellyfin { - public class JellyfinApi : IEmbyApi + public class JellyfinApi : IJellyfinApi { public JellyfinApi(IApi api) { @@ -20,27 +20,27 @@ namespace Ombi.Api.Emby private IApi Api { get; } /// - /// Returns all users from the Emby Instance + /// Returns all users from the Jellyfin Instance /// /// /// - public async Task> GetUsers(string baseUri, string apiKey) + public async Task> GetUsers(string baseUri, string apiKey) { var request = new Request("users", baseUri, HttpMethod.Get); AddHeaders(request, apiKey); - var obj = await Api.Request>(request); + var obj = await Api.Request>(request); return obj; } - public async Task GetSystemInformation(string apiKey, string baseUrl) + public async Task GetSystemInformation(string apiKey, string baseUrl) { var request = new Request("System/Info", baseUrl, HttpMethod.Get); AddHeaders(request, apiKey); - var obj = await Api.Request(request); + var obj = await Api.Request(request); return obj; } @@ -56,7 +56,7 @@ namespace Ombi.Api.Emby return obj; } - public async Task LogIn(string username, string password, string apiKey, string baseUri) + public async Task LogIn(string username, string password, string apiKey, string baseUri) { var request = new Request("users/authenticatebyname", baseUri, HttpMethod.Post); var body = new @@ -71,11 +71,11 @@ namespace Ombi.Api.Emby $"MediaBrowser Client=\"Ombi\", Device=\"Ombi\", DeviceId=\"v3\", Version=\"v3\""); AddHeaders(request, apiKey); - var obj = await Api.Request(request); + var obj = await Api.Request(request); return obj; } - public async Task> GetCollection(string mediaId, string apiKey, string userId, string baseUrl) + public async Task> GetCollection(string mediaId, string apiKey, string userId, string baseUrl) { var request = new Request($"users/{userId}/items?parentId={mediaId}", baseUrl, HttpMethod.Get); AddHeaders(request, apiKey); @@ -84,22 +84,22 @@ namespace Ombi.Api.Emby request.AddQueryString("IsVirtualItem", "False"); - return await Api.Request>(request); + return await Api.Request>(request); } - public async Task> GetAllMovies(string apiKey, int startIndex, int count, string userId, string baseUri) + public async Task> GetAllMovies(string apiKey, int startIndex, int count, string userId, string baseUri) { - return await GetAll("Movie", apiKey, userId, baseUri, true, startIndex, count); + return await GetAll("Movie", apiKey, userId, baseUri, true, startIndex, count); } - public async Task> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, string baseUri) + public async Task> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, string baseUri) { - return await GetAll("Episode", apiKey, userId, baseUri, false, startIndex, count); + return await GetAll("Episode", apiKey, userId, baseUri, false, startIndex, count); } - public async Task> GetAllShows(string apiKey, int startIndex, int count, string userId, string baseUri) + public async Task> GetAllShows(string apiKey, int startIndex, int count, string userId, string baseUri) { - return await GetAll("Series", apiKey, userId, baseUri, false, startIndex, count); + return await GetAll("Series", apiKey, userId, baseUri, false, startIndex, count); } public async Task GetSeriesInformation(string mediaId, string apiKey, string userId, string baseUrl) @@ -126,7 +126,7 @@ namespace Ombi.Api.Emby return JsonConvert.DeserializeObject(response); } - private async Task> GetAll(string type, string apiKey, string userId, string baseUri, bool includeOverview = false) + private async Task> GetAll(string type, string apiKey, string userId, string baseUri, bool includeOverview = false) { var request = new Request($"users/{userId}/items", baseUri, HttpMethod.Get); @@ -139,10 +139,10 @@ namespace Ombi.Api.Emby AddHeaders(request, apiKey); - var obj = await Api.Request>(request); + 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) + private async Task> GetAll(string type, string apiKey, string userId, string baseUri, bool includeOverview, int startIndex, int count) { var request = new Request($"users/{userId}/items", baseUri, HttpMethod.Get); @@ -157,7 +157,7 @@ namespace Ombi.Api.Emby AddHeaders(request, apiKey); - var obj = await Api.Request>(request); + var obj = await Api.Request>(request); return obj; } @@ -172,7 +172,7 @@ namespace Ombi.Api.Emby req.AddHeader("Device", "Ombi"); } - public Task LoginConnectUser(string username, string password) + public Task LoginConnectUser(string username, string password) { throw new System.NotImplementedException(); } diff --git a/src/Ombi.Api.Jellyfin/JellyfinApiFactory.cs b/src/Ombi.Api.Jellyfin/JellyfinApiFactory.cs new file mode 100644 index 000000000..cc604b871 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/JellyfinApiFactory.cs @@ -0,0 +1,37 @@ +using Ombi.Api; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using System.Threading.Tasks; + +namespace Ombi.Api.Jellyfin +{ + public class JellyfinApiFactory : IJellyfinApiFactory + { + private readonly ISettingsService _jellyfinSettings; + private readonly IApi _api; + + // TODO, if we need to derive futher, need to rework + public JellyfinApiFactory(ISettingsService jellyfinSettings, IApi api) + { + _jellyfinSettings = jellyfinSettings; + _api = api; + } + + public async Task CreateClient() + { + var settings = await _jellyfinSettings.GetSettingsAsync(); + return CreateClient(settings); + } + + public IJellyfinApi CreateClient(JellyfinSettings settings) + { + return new JellyfinApi(_api); + } + } + + public interface IJellyfinApiFactory + { + Task CreateClient(); + IJellyfinApi CreateClient(JellyfinSettings settings); + } +} diff --git a/src/Ombi.Api.Jellyfin/Models/JellyfinConfiguration.cs b/src/Ombi.Api.Jellyfin/Models/JellyfinConfiguration.cs new file mode 100644 index 000000000..b7d2e39cb --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/JellyfinConfiguration.cs @@ -0,0 +1,45 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: JellyfinConfiguration.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace Ombi.Api.Jellyfin.Models +{ + public class JellyfinConfiguration + { + public bool PlayDefaultAudioTrack { get; set; } + public bool DisplayMissingEpisodes { get; set; } + public bool DisplayUnairedEpisodes { get; set; } + public object[] GroupedFolders { get; set; } + public string SubtitleMode { get; set; } + public bool DisplayCollectionsView { get; set; } + public bool EnableLocalPassword { get; set; } + public object[] OrderedViews { get; set; } + public object[] LatestItemsExcludes { get; set; } + public bool HidePlayedInLatest { get; set; } + public bool RememberAudioSelections { get; set; } + public bool RememberSubtitleSelections { get; set; } + public bool EnableNextEpisodeAutoPlay { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/JellyfinConnectUser.cs b/src/Ombi.Api.Jellyfin/Models/JellyfinConnectUser.cs new file mode 100644 index 000000000..a19418e3d --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/JellyfinConnectUser.cs @@ -0,0 +1,47 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: JellyfinConnectUser.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace Ombi.Api.Jellyfin.Models +{ + public class JellyfinConnectUser + { + public string AccessToken { get; set; } + public User User { get; set; } + } + + public class User + { + public string Id { get; set; } + public string Name { get; set; } + public string DisplayName { get; set; } + public string Email { get; set; } + public string IsActive { get; set; } + public string ImageUrl { get; set; } + public object IsSupporter { get; set; } + public object ExpDate { get; set; } + } + +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/JellyfinItemContainer.cs b/src/Ombi.Api.Jellyfin/Models/JellyfinItemContainer.cs new file mode 100644 index 000000000..940d568fa --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/JellyfinItemContainer.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Ombi.Api.Jellyfin.Models +{ + public class JellyfinItemContainer + { + public List Items { get; set; } + public int TotalRecordCount { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/JellyfinMediaType.cs b/src/Ombi.Api.Jellyfin/Models/JellyfinMediaType.cs new file mode 100644 index 000000000..2c4f75be0 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/JellyfinMediaType.cs @@ -0,0 +1,10 @@ +namespace Ombi.Api.Jellyfin.Models +{ + public enum JellyfinMediaType + { + Movie = 0, + Series = 1, + Music = 2, + Episode = 3 + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/JellyfinPolicy.cs b/src/Ombi.Api.Jellyfin/Models/JellyfinPolicy.cs new file mode 100644 index 000000000..8091b8b34 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/JellyfinPolicy.cs @@ -0,0 +1,59 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: JellyfinPolicy.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace Ombi.Api.Jellyfin.Models +{ + public class JellyfinPolicy + { + public bool IsAdministrator { get; set; } + public bool IsHidden { get; set; } + public bool IsDisabled { get; set; } + public object[] BlockedTags { get; set; } + public bool EnableUserPreferenceAccess { get; set; } + public object[] AccessSchedules { get; set; } + public object[] BlockUnratedItems { get; set; } + public bool EnableRemoteControlOfOtherUsers { get; set; } + public bool EnableSharedDeviceControl { get; set; } + public bool EnableLiveTvManagement { get; set; } + public bool EnableLiveTvAccess { get; set; } + public bool EnableMediaPlayback { get; set; } + public bool EnableAudioPlaybackTranscoding { get; set; } + public bool EnableVideoPlaybackTranscoding { get; set; } + public bool EnablePlaybackRemuxing { get; set; } + public bool EnableContentDeletion { get; set; } + public bool EnableContentDownloading { get; set; } + public bool EnableSync { get; set; } + public bool EnableSyncTranscoding { get; set; } + public object[] EnabledDevices { get; set; } + public bool EnableAllDevices { get; set; } + public object[] EnabledChannels { get; set; } + public bool EnableAllChannels { get; set; } + public object[] EnabledFolders { get; set; } + public bool EnableAllFolders { get; set; } + public int InvalidLoginAttemptCount { get; set; } + public bool EnablePublicSharing { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/JellyfinSystemInfo.cs b/src/Ombi.Api.Jellyfin/Models/JellyfinSystemInfo.cs new file mode 100644 index 000000000..1cedee722 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/JellyfinSystemInfo.cs @@ -0,0 +1,63 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: JellyfinSystemInfo.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace Ombi.Api.Jellyfin.Models +{ + public class JellyfinSystemInfo + { + public string SystemUpdateLevel { get; set; } + public string OperatingSystemDisplayName { get; set; } + public bool SupportsRunningAsService { get; set; } + public string MacAddress { get; set; } + public bool HasPendingRestart { get; set; } + public bool SupportsLibraryMonitor { get; set; } + public object[] InProgressInstallations { get; set; } + public int WebSocketPortNumber { get; set; } + public object[] CompletedInstallations { get; set; } + public bool CanSelfRestart { get; set; } + public bool CanSelfUpdate { get; set; } + public object[] FailedPluginAssemblies { get; set; } + public string ProgramDataPath { get; set; } + public string ItemsByNamePath { get; set; } + public string CachePath { get; set; } + public string LogPath { get; set; } + public string InternalMetadataPath { get; set; } + public string TranscodingTempPath { get; set; } + public int HttpServerPortNumber { get; set; } + public bool SupportsHttps { get; set; } + public int HttpsPortNumber { get; set; } + public bool HasUpdateAvailable { get; set; } + public bool SupportsAutoRunAtStartup { get; set; } + public string EncoderLocationType { get; set; } + public string SystemArchitecture { get; set; } + public string LocalAddress { get; set; } + public string WanAddress { get; set; } + public string ServerName { get; set; } + public string Version { get; set; } + public string OperatingSystem { get; set; } + public string Id { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/JellyfinUser.cs b/src/Ombi.Api.Jellyfin/Models/JellyfinUser.cs new file mode 100644 index 000000000..51078f36c --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/JellyfinUser.cs @@ -0,0 +1,47 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: JellyfinUser.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; + +namespace Ombi.Api.Jellyfin.Models +{ + public class JellyfinUser + { + public string Name { get; set; } + public string ServerId { get; set; } + public string ConnectUserName { get; set; } + public string ConnectLinkType { get; set; } + public string Id { get; set; } + public bool HasPassword { get; set; } + public bool HasConfiguredPassword { get; set; } + public bool HasConfiguredEasyPassword { get; set; } + public DateTime LastLoginDate { get; set; } + public DateTime LastActivityDate { get; set; } + public JellyfinConfiguration Configuration { get; set; } + public JellyfinPolicy Policy { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/JellyfinUserLogin.cs b/src/Ombi.Api.Jellyfin/Models/JellyfinUserLogin.cs new file mode 100644 index 000000000..580cc45af --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/JellyfinUserLogin.cs @@ -0,0 +1,7 @@ +namespace Ombi.Api.Jellyfin.Models +{ + public class JellyfinUserLogin + { + public JellyfinUser User { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinChapter.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinChapter.cs new file mode 100644 index 000000000..332268ae3 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinChapter.cs @@ -0,0 +1,8 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinChapter + { + public long StartPositionTicks { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinExternalurl.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinExternalurl.cs new file mode 100644 index 000000000..c53ec3cdf --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinExternalurl.cs @@ -0,0 +1,8 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinExternalurl + { + public string Name { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinImagetags.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinImagetags.cs new file mode 100644 index 000000000..d2dcc0f8a --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinImagetags.cs @@ -0,0 +1,10 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinImagetags + { + public string Primary { get; set; } + public string Logo { get; set; } + public string Thumb { get; set; } + public string Banner { get; set; } + } +} diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinMediasource.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinMediasource.cs new file mode 100644 index 000000000..3cdec7c4c --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinMediasource.cs @@ -0,0 +1,30 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinMediasource + { + public string Protocol { get; set; } + public string Id { get; set; } + public string Path { get; set; } + public string Type { get; set; } + public string Container { get; set; } + public string Name { get; set; } + public bool IsRemote { get; set; } + public string ETag { get; set; } + public long RunTimeTicks { get; set; } + public bool ReadAtNativeFramerate { get; set; } + public bool SupportsTranscoding { get; set; } + public bool SupportsDirectStream { get; set; } + public bool SupportsDirectPlay { get; set; } + public bool IsInfiniteStream { get; set; } + public bool RequiresOpening { get; set; } + public bool RequiresClosing { get; set; } + public bool SupportsProbing { get; set; } + public string VideoType { get; set; } + public JellyfinMediastream[] MediaStreams { get; set; } + public object[] PlayableStreamFileNames { get; set; } + public object[] Formats { get; set; } + public int Bitrate { get; set; } + public int DefaultAudioStreamIndex { get; set; } + + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinMediastream.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinMediastream.cs new file mode 100644 index 000000000..89da2651a --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinMediastream.cs @@ -0,0 +1,36 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinMediastream + { + public string Codec { get; set; } + public string Language { get; set; } + public string TimeBase { get; set; } + public string CodecTimeBase { get; set; } + public string NalLengthSize { get; set; } + public bool IsInterlaced { get; set; } + public bool IsAVC { get; set; } + public int BitRate { get; set; } + public int BitDepth { get; set; } + public int RefFrames { get; set; } + public bool IsDefault { get; set; } + public bool IsForced { get; set; } + public int Height { get; set; } + public int Width { get; set; } + public float AverageFrameRate { get; set; } + public float RealFrameRate { get; set; } + public string Profile { get; set; } + public string Type { get; set; } + public string AspectRatio { get; set; } + public int Index { get; set; } + public bool IsExternal { get; set; } + public bool IsTextSubtitleStream { get; set; } + public bool SupportsExternalStream { get; set; } + public string PixelFormat { get; set; } + public int Level { get; set; } + public bool IsAnamorphic { get; set; } + public string DisplayTitle { get; set; } + public string ChannelLayout { get; set; } + public int Channels { get; set; } + public int SampleRate { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinPerson.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinPerson.cs new file mode 100644 index 000000000..19bdd3f81 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinPerson.cs @@ -0,0 +1,11 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinPerson + { + public string Name { get; set; } + public string Id { get; set; } + public string Role { get; set; } + public string Type { get; set; } + public string PrimaryImageTag { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinProviderids.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinProviderids.cs new file mode 100644 index 000000000..9b47f9a1a --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinProviderids.cs @@ -0,0 +1,13 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinProviderids + { + public string Tmdb { get; set; } + public string Imdb { get; set; } + public string TmdbCollection { get; set; } + + public string Tvdb { get; set; } + public string Zap2It { get; set; } + public string TvRage { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinRemotetrailer.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinRemotetrailer.cs new file mode 100644 index 000000000..325ef8519 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinRemotetrailer.cs @@ -0,0 +1,8 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinRemotetrailer + { + public string Url { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinStudio.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinStudio.cs new file mode 100644 index 000000000..c0d561102 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinStudio.cs @@ -0,0 +1,8 @@ +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinStudio + { + public string Name { get; set; } + public string Id { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/JellyfinUserdata.cs b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinUserdata.cs new file mode 100644 index 000000000..502c67822 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/JellyfinUserdata.cs @@ -0,0 +1,15 @@ +using System; + +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinUserdata + { + public double PlaybackPositionTicks { get; set; } + public int PlayCount { get; set; } + public bool IsFavorite { get; set; } + public bool Played { get; set; } + public string Key { get; set; } + public DateTime LastPlayedDate { get; set; } + public int UnplayedItemCount { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Movie/JellyfinMovie.cs b/src/Ombi.Api.Jellyfin/Models/Media/Movie/JellyfinMovie.cs new file mode 100644 index 000000000..d86bf5047 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/Movie/JellyfinMovie.cs @@ -0,0 +1,34 @@ +using System; + +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class JellyfinMovie + { + public string Name { get; set; } + public string ServerId { get; set; } + public string Id { get; set; } + public string Container { get; set; } + public DateTime PremiereDate { get; set; } + public object[] ProductionLocations { get; set; } + public string OfficialRating { get; set; } + public float CommunityRating { get; set; } + public long RunTimeTicks { get; set; } + public string PlayAccess { get; set; } + public int ProductionYear { get; set; } + public bool IsPlaceHolder { get; set; } + public bool IsHD { get; set; } + public bool IsFolder { get; set; } + public string Type { get; set; } + public int LocalTrailerCount { get; set; } + public JellyfinUserdata UserData { get; set; } + public string VideoType { get; set; } + public JellyfinImagetags ImageTags { get; set; } + public string[] BackdropImageTags { get; set; } + public string LocationType { get; set; } + public string MediaType { get; set; } + public bool HasSubtitles { get; set; } + public int CriticRating { get; set; } + public string Overview { get; set; } + public JellyfinProviderids ProviderIds { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Movie/MovieInformation.cs b/src/Ombi.Api.Jellyfin/Models/Media/Movie/MovieInformation.cs new file mode 100644 index 000000000..3dba49bea --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/Movie/MovieInformation.cs @@ -0,0 +1,60 @@ +using System; + +namespace Ombi.Api.Jellyfin.Models.Movie +{ + public class MovieInformation + { + public string Name { get; set; } + public string OriginalTitle { get; set; } + public string ServerId { get; set; } + public string Id { get; set; } + public string Etag { get; set; } + public DateTime DateCreated { get; set; } + public bool CanDelete { get; set; } + public bool CanDownload { get; set; } + public bool SupportsSync { get; set; } + public string Container { get; set; } + public string SortName { get; set; } + public DateTime PremiereDate { get; set; } + public JellyfinExternalurl[] ExternalUrls { get; set; } + public JellyfinMediasource[] MediaSources { get; set; } + public string[] ProductionLocations { get; set; } + public string Path { get; set; } + public string OfficialRating { get; set; } + public string Overview { get; set; } + public string[] Taglines { get; set; } + public string[] Genres { get; set; } + public float CommunityRating { get; set; } + public int VoteCount { get; set; } + public long RunTimeTicks { get; set; } + public string PlayAccess { get; set; } + public int ProductionYear { get; set; } + public bool IsPlaceHolder { get; set; } + public JellyfinRemotetrailer[] RemoteTrailers { get; set; } + public JellyfinProviderids ProviderIds { get; set; } + public bool IsHD { get; set; } + public bool IsFolder { get; set; } + public string ParentId { get; set; } + public string Type { get; set; } + public JellyfinPerson[] People { get; set; } + public JellyfinStudio[] Studios { get; set; } + public int LocalTrailerCount { get; set; } + public JellyfinUserdata UserData { get; set; } + public string DisplayPreferencesId { get; set; } + public object[] Tags { get; set; } + public string[] Keywords { get; set; } + public JellyfinMediastream[] MediaStreams { get; set; } + public string VideoType { get; set; } + public JellyfinImagetags ImageTags { get; set; } + public string[] BackdropImageTags { get; set; } + public object[] ScreenshotImageTags { get; set; } + public JellyfinChapter[] Chapters { get; set; } + public string LocationType { get; set; } + public string MediaType { get; set; } + public string HomePageUrl { get; set; } + public int Budget { get; set; } + public float Revenue { get; set; } + public object[] LockedFields { get; set; } + public bool LockData { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Tv/EpisodeInformation.cs b/src/Ombi.Api.Jellyfin/Models/Media/Tv/EpisodeInformation.cs new file mode 100644 index 000000000..4df1e9bcc --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/Tv/EpisodeInformation.cs @@ -0,0 +1,71 @@ +using System; +using Ombi.Api.Jellyfin.Models.Movie; + +namespace Ombi.Api.Jellyfin.Models.Media.Tv +{ + public class EpisodeInformation + { + public string Name { get; set; } + public string ServerId { get; set; } + public string Id { get; set; } + public string Etag { get; set; } + public DateTime DateCreated { get; set; } + public bool CanDelete { get; set; } + public bool CanDownload { get; set; } + public bool SupportsSync { get; set; } + public string Container { get; set; } + public string SortName { get; set; } + public DateTime PremiereDate { get; set; } + public JellyfinExternalurl[] ExternalUrls { get; set; } + public JellyfinMediasource[] MediaSources { get; set; } + public string Path { get; set; } + public string Overview { get; set; } + public object[] Taglines { get; set; } + public object[] Genres { get; set; } + public string[] SeriesGenres { get; set; } + public float CommunityRating { get; set; } + public int VoteCount { get; set; } + public long RunTimeTicks { get; set; } + public string PlayAccess { get; set; } + public int ProductionYear { get; set; } + public bool IsPlaceHolder { get; set; } + public int IndexNumber { get; set; } + public int ParentIndexNumber { get; set; } + public object[] RemoteTrailers { get; set; } + public JellyfinProviderids ProviderIds { get; set; } + public bool IsHD { get; set; } + public bool IsFolder { get; set; } + public string ParentId { get; set; } + public string Type { get; set; } + public object[] People { get; set; } + public object[] Studios { get; set; } + public string ParentLogoItemId { get; set; } + public string ParentBackdropItemId { get; set; } + public string[] ParentBackdropImageTags { get; set; } + public int LocalTrailerCount { get; set; } + public JellyfinUserdata UserData { get; set; } + public string SeriesName { get; set; } + public string SeriesId { get; set; } + public string SeasonId { get; set; } + public string DisplayPreferencesId { get; set; } + public object[] Tags { get; set; } + public object[] Keywords { get; set; } + public string SeriesPrimaryImageTag { get; set; } + public string SeasonName { get; set; } + public JellyfinMediastream[] MediaStreams { get; set; } + public string VideoType { get; set; } + public JellyfinImagetags ImageTags { get; set; } + public object[] BackdropImageTags { get; set; } + public object[] ScreenshotImageTags { get; set; } + public string ParentLogoImageTag { get; set; } + public string SeriesStudio { get; set; } + public JellyfinSeriesstudioinfo SeriesStudioInfo { get; set; } + public string ParentThumbItemId { get; set; } + public string ParentThumbImageTag { get; set; } + public JellyfinChapter[] Chapters { get; set; } + public string LocationType { get; set; } + public string MediaType { get; set; } + public object[] LockedFields { get; set; } + public bool LockData { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinEpisodes.cs b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinEpisodes.cs new file mode 100644 index 000000000..a2769138e --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinEpisodes.cs @@ -0,0 +1,45 @@ +using Ombi.Api.Jellyfin.Models.Movie; +using System; + +namespace Ombi.Api.Jellyfin.Models.Media.Tv +{ + public class JellyfinEpisodes + { + public string Name { get; set; } + public string ServerId { get; set; } + public string Id { get; set; } + public string Container { get; set; } + public DateTime PremiereDate { get; set; } + public float CommunityRating { get; set; } + public long RunTimeTicks { get; set; } + public string PlayAccess { get; set; } + public int ProductionYear { get; set; } + public bool IsPlaceHolder { get; set; } + public int IndexNumber { get; set; } + public int? IndexNumberEnd { get; set; } + public int ParentIndexNumber { get; set; } + public bool IsHD { get; set; } + public bool IsFolder { get; set; } + public string Type { get; set; } + public string ParentLogoItemId { get; set; } + public string ParentBackdropItemId { get; set; } + public string[] ParentBackdropImageTags { get; set; } + public int LocalTrailerCount { get; set; } + public JellyfinUserdata UserData { get; set; } + public string SeriesName { get; set; } + public string SeriesId { get; set; } + public string SeasonId { get; set; } + public string SeriesPrimaryImageTag { get; set; } + public string SeasonName { get; set; } + public string VideoType { get; set; } + public JellyfinImagetags ImageTags { get; set; } + public object[] BackdropImageTags { get; set; } + public string ParentLogoImageTag { get; set; } + public string ParentThumbItemId { get; set; } + public string ParentThumbImageTag { get; set; } + public string LocationType { get; set; } + public string MediaType { get; set; } + public bool HasSubtitles { get; set; } + public JellyfinProviderids ProviderIds { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinRemotetrailer.cs b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinRemotetrailer.cs new file mode 100644 index 000000000..663dc4ce3 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinRemotetrailer.cs @@ -0,0 +1,8 @@ +namespace Ombi.Api.Jellyfin.Models.Media.Tv +{ + public class JellyfinRemotetrailer + { + public string Url { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinSeries.cs b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinSeries.cs new file mode 100644 index 000000000..0d0fb21be --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinSeries.cs @@ -0,0 +1,32 @@ +using Ombi.Api.Jellyfin.Models.Movie; +using System; + +namespace Ombi.Api.Jellyfin.Models.Media.Tv +{ + public class JellyfinSeries + { + public string Name { get; set; } + public string ServerId { get; set; } + public string Id { get; set; } + public DateTime PremiereDate { get; set; } + public string OfficialRating { get; set; } + public float CommunityRating { get; set; } + public long RunTimeTicks { get; set; } + public string PlayAccess { get; set; } + public int ProductionYear { get; set; } + public bool IsFolder { get; set; } + public string Type { get; set; } + public int LocalTrailerCount { get; set; } + public JellyfinUserdata UserData { get; set; } + public int ChildCount { get; set; } + public string Status { get; set; } + public string AirTime { get; set; } + public string[] AirDays { get; set; } + public JellyfinImagetags ImageTags { get; set; } + public string[] BackdropImageTags { get; set; } + public string LocationType { get; set; } + public DateTime EndDate { get; set; } + + public JellyfinProviderids ProviderIds { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinSeriesstudioinfo.cs b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinSeriesstudioinfo.cs new file mode 100644 index 000000000..ada6be68d --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinSeriesstudioinfo.cs @@ -0,0 +1,8 @@ +namespace Ombi.Api.Jellyfin.Models.Media.Tv +{ + public class JellyfinSeriesstudioinfo + { + public string Name { get; set; } + public string Id { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Tv/SeriesInformation.cs b/src/Ombi.Api.Jellyfin/Models/Media/Tv/SeriesInformation.cs new file mode 100644 index 000000000..efb48560e --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/Media/Tv/SeriesInformation.cs @@ -0,0 +1,59 @@ +using System; +using Ombi.Api.Jellyfin.Models.Movie; + +namespace Ombi.Api.Jellyfin.Models.Media.Tv +{ + public class SeriesInformation + { + + public string Name { get; set; } + public string ServerId { get; set; } + public string Id { get; set; } + public string Etag { get; set; } + public DateTime DateCreated { get; set; } + public DateTime DateLastMediaAdded { get; set; } + public bool CanDelete { get; set; } + public bool CanDownload { get; set; } + public bool SupportsSync { get; set; } + public string SortName { get; set; } + public DateTime PremiereDate { get; set; } + public JellyfinExternalurl[] ExternalUrls { get; set; } + public string Path { get; set; } + public string OfficialRating { get; set; } + public string Overview { get; set; } + public string ShortOverview { get; set; } + public object[] Taglines { get; set; } + public string[] Genres { get; set; } + public float CommunityRating { get; set; } + public int VoteCount { get; set; } + public long CumulativeRunTimeTicks { get; set; } + public long RunTimeTicks { get; set; } + public string PlayAccess { get; set; } + public int ProductionYear { get; set; } + public JellyfinRemotetrailer[] RemoteTrailers { get; set; } + public JellyfinProviderids ProviderIds { get; set; } + public bool IsFolder { get; set; } + public string ParentId { get; set; } + public string Type { get; set; } + public JellyfinPerson[] People { get; set; } + public JellyfinStudio[] Studios { get; set; } + public int LocalTrailerCount { get; set; } + public JellyfinUserdata UserData { get; set; } + public int RecursiveItemCount { get; set; } + public int ChildCount { get; set; } + public string DisplayPreferencesId { get; set; } + public string Status { get; set; } + public string AirTime { get; set; } + public string[] AirDays { get; set; } + public object[] Tags { get; set; } + public object[] Keywords { get; set; } + public JellyfinImagetags ImageTags { get; set; } + public string[] BackdropImageTags { get; set; } + public object[] ScreenshotImageTags { get; set; } + public string LocationType { get; set; } + public string HomePageUrl { get; set; } + public object[] LockedFields { get; set; } + public bool LockData { get; set; } + + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/PublicInfo.cs b/src/Ombi.Api.Jellyfin/Models/PublicInfo.cs new file mode 100644 index 000000000..6687cf3c9 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/PublicInfo.cs @@ -0,0 +1,12 @@ +namespace Ombi.Api.Jellyfin.Models +{ + public class PublicInfo + { + public string LocalAddress { get; set; } + public string ServerName { get; set; } + public string Version { get; set; } + public string OperatingSystem { get; set; } + public string Id { get; set; } + } + +} diff --git a/src/Ombi.Api.Jellyfin/Ombi.Api.Jellyfin.csproj b/src/Ombi.Api.Jellyfin/Ombi.Api.Jellyfin.csproj new file mode 100644 index 000000000..5457b0290 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Ombi.Api.Jellyfin.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + 3.0.0.0 + 3.0.0.0 + + + 8.0 + + + + + + + + \ No newline at end of file diff --git a/src/Ombi.Api.MusicBrainz/IMusicBrainzApi.cs b/src/Ombi.Api.MusicBrainz/IMusicBrainzApi.cs index 69d8231ad..3b47cdd28 100644 --- a/src/Ombi.Api.MusicBrainz/IMusicBrainzApi.cs +++ b/src/Ombi.Api.MusicBrainz/IMusicBrainzApi.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using Hqub.MusicBrainz.API.Entities; +using Hqub.MusicBrainz.API.Entities.Collections; using Ombi.Api.MusicBrainz.Models; namespace Ombi.Api.MusicBrainz @@ -11,6 +12,7 @@ namespace Ombi.Api.MusicBrainz Task> SearchArtist(string artistQuery); Task> GetReleaseForArtist(string artistId); Task GetArtistInformation(string artistId); + Task GetAlbumInformation(string albumId); Task GetCoverArtForReleaseGroup(string musicBrainzId, CancellationToken token); } } \ No newline at end of file diff --git a/src/Ombi.Api.MusicBrainz/MusicBrainzApi.cs b/src/Ombi.Api.MusicBrainz/MusicBrainzApi.cs index 3c86d7882..e870eb07f 100644 --- a/src/Ombi.Api.MusicBrainz/MusicBrainzApi.cs +++ b/src/Ombi.Api.MusicBrainz/MusicBrainzApi.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Hqub.MusicBrainz.API; using Hqub.MusicBrainz.API.Entities; +using Hqub.MusicBrainz.API.Entities.Collections; using Newtonsoft.Json; using Ombi.Api.MusicBrainz.Models; @@ -20,6 +21,12 @@ namespace Ombi.Api.MusicBrainz _api = api; } + public Task GetAlbumInformation(string albumId) + { + var album = Release.GetAsync(albumId); + return album; + } + public async Task> SearchArtist(string artistQuery) { var artist = await Artist.SearchAsync(artistQuery, 10); diff --git a/src/Ombi.Api.Radarr/Models/V2/SystemStatus.cs b/src/Ombi.Api.Radarr/Models/V2/SystemStatus.cs index 6877708c5..699b37b3c 100644 --- a/src/Ombi.Api.Radarr/Models/V2/SystemStatus.cs +++ b/src/Ombi.Api.Radarr/Models/V2/SystemStatus.cs @@ -3,5 +3,6 @@ public class SystemStatus { public string version { get; set; } + public string urlBase { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Api.RottenTomatoes/IRottenTomatoesApi.cs b/src/Ombi.Api.RottenTomatoes/IRottenTomatoesApi.cs new file mode 100644 index 000000000..4466832b1 --- /dev/null +++ b/src/Ombi.Api.RottenTomatoes/IRottenTomatoesApi.cs @@ -0,0 +1,14 @@ +using Ombi.Api.RottenTomatoes.Models; +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Ombi.Api.RottenTomatoes +{ + public interface IRottenTomatoesApi + { + Task GetMovieRatings(string movieName, int movieYear); + Task GetTvRatings(string showName, int showYear); + } +} diff --git a/src/Ombi.Api.RottenTomatoes/Models/RottenTomatoesMovieResponse.cs b/src/Ombi.Api.RottenTomatoes/Models/RottenTomatoesMovieResponse.cs new file mode 100644 index 000000000..ceea5000b --- /dev/null +++ b/src/Ombi.Api.RottenTomatoes/Models/RottenTomatoesMovieResponse.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace Ombi.Api.RottenTomatoes.Models +{ + public class RottenTomatoesMovieResponse + { + public int total { get; set; } + public List movies { get; set; } + } + + public class Movie + { + public string id { get; set; } + public string title { get; set; } + public int year { get; set; } + public string mpaa_rating { get; set; } + public object runtime { get; set; } + public string critics_consensus { get; set; } + public MovieRatings ratings { get; set; } + public Links links { get; set; } + } + + public class MovieRatings + { + public string critics_rating { get; set; } + public int critics_score { get; set; } + public string audience_rating { get; set; } + public int audience_score { get; set; } + } + + public class Links + { + public string alternate { get; set; } + } +} diff --git a/src/Ombi.Api.RottenTomatoes/Models/RottenTomatoesTvResponse.cs b/src/Ombi.Api.RottenTomatoes/Models/RottenTomatoesTvResponse.cs new file mode 100644 index 000000000..234e958be --- /dev/null +++ b/src/Ombi.Api.RottenTomatoes/Models/RottenTomatoesTvResponse.cs @@ -0,0 +1,20 @@ +namespace Ombi.Api.RottenTomatoes.Models +{ + public class RottenTomatoesTvResponse + { + public int tvCount { get; set; } + public TvSeries[] tvSeries { get; set; } + } + + public class TvSeries + { + public string title { get; set; } + public int startYear { get; set; } + public int endYear { get; set; } + public string url { get; set; } + public string meterClass { get; set; } + public int meterScore { get; set; } + public string image { get; set; } + } + +} diff --git a/src/Ombi.Api.RottenTomatoes/Models/TvRatings.cs b/src/Ombi.Api.RottenTomatoes/Models/TvRatings.cs new file mode 100644 index 000000000..902d532d7 --- /dev/null +++ b/src/Ombi.Api.RottenTomatoes/Models/TvRatings.cs @@ -0,0 +1,8 @@ +namespace Ombi.Api.RottenTomatoes.Models +{ + public class TvRatings + { + public string Class { get; set; } + public int Score { get; set; } + } +} diff --git a/src/Ombi.Api.RottenTomatoes/Ombi.Api.RottenTomatoes.csproj b/src/Ombi.Api.RottenTomatoes/Ombi.Api.RottenTomatoes.csproj new file mode 100644 index 000000000..8cb4799bc --- /dev/null +++ b/src/Ombi.Api.RottenTomatoes/Ombi.Api.RottenTomatoes.csproj @@ -0,0 +1,12 @@ + + + + net5.0 + 8.0 + + + + + + + diff --git a/src/Ombi.Api.RottenTomatoes/RottenTomatoesApi.cs b/src/Ombi.Api.RottenTomatoes/RottenTomatoesApi.cs new file mode 100644 index 000000000..88dfa2f79 --- /dev/null +++ b/src/Ombi.Api.RottenTomatoes/RottenTomatoesApi.cs @@ -0,0 +1,56 @@ +using Ombi.Api.RottenTomatoes.Models; +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Ombi.Api.RottenTomatoes +{ + public class RottenTomatoesApi : IRottenTomatoesApi + { + public RottenTomatoesApi(IApi api) + { + _api = api; + } + + private string Endpoint => "https://www.rottentomatoes.com/api/private"; + private IApi _api { get; } + + public async Task GetMovieRatings(string movieName, int movieYear) + { + var request = new Request("/v1.0/movies", Endpoint, HttpMethod.Get); + request.AddHeader("Accept", "application/json"); + request.AddQueryString("q", movieName); + var result = await _api.Request(request); + + var movieFound = result.movies.FirstOrDefault(x => x.year == movieYear); + if (movieFound == null) + { + return null; + } + + return movieFound.ratings; + } + + public async Task GetTvRatings(string showName, int showYear) + { + var request = new Request("/v2.0/search/", Endpoint, HttpMethod.Get); + request.AddHeader("Accept", "application/json"); + request.AddQueryString("q", showName); + request.AddQueryString("limit", 10.ToString()); + var result = await _api.Request(request); + + var showFound = result.tvSeries.FirstOrDefault(x => x.startYear == showYear); + if (showFound == null) + { + return null; + } + + return new TvRatings + { + Class = showFound.meterClass, + Score = showFound.meterScore + }; + } + } +} diff --git a/src/Ombi.Core.Tests/Authentication/OmbiUserManagerTests.cs b/src/Ombi.Core.Tests/Authentication/OmbiUserManagerTests.cs index bd6e8d5a2..f4e6f59a3 100644 --- a/src/Ombi.Core.Tests/Authentication/OmbiUserManagerTests.cs +++ b/src/Ombi.Core.Tests/Authentication/OmbiUserManagerTests.cs @@ -30,7 +30,7 @@ namespace Ombi.Core.Tests.Authentication AuthenticationSettings.Setup(x => x.GetSettingsAsync()) .ReturnsAsync(new AuthenticationSettings()); _um = new OmbiUserManager(UserStore.Object, null, null, null, null, null, null, null, null, - PlexApi.Object, null, null, AuthenticationSettings.Object); + PlexApi.Object, null, null, null, null, AuthenticationSettings.Object); } public OmbiUserManager _um { get; set; } diff --git a/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs b/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs index 8e5c57d67..132f51e49 100644 --- a/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs +++ b/src/Ombi.Core.Tests/Rule/Search/EmbyAvailabilityRuleTests.cs @@ -115,4 +115,4 @@ namespace Ombi.Core.Tests.Rule.Search Assert.False(search.Available); } } -} \ No newline at end of file +} diff --git a/src/Ombi.Core.Tests/Rule/Search/JellyfinAvailabilityRuleTests.cs b/src/Ombi.Core.Tests/Rule/Search/JellyfinAvailabilityRuleTests.cs new file mode 100644 index 000000000..1b838f102 --- /dev/null +++ b/src/Ombi.Core.Tests/Rule/Search/JellyfinAvailabilityRuleTests.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Moq; +using NUnit.Framework; +using Ombi.Core.Models.Search; +using Ombi.Core.Rule.Rules.Search; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Store.Entities; +using Ombi.Store.Repository; +using Ombi.Store.Repository.Requests; + +namespace Ombi.Core.Tests.Rule.Search +{ + public class JellyfinAvailabilityRuleTests + { + [SetUp] + public void Setup() + { + ContextMock = new Mock(); + SettingsMock = new Mock>(); + Rule = new JellyfinAvailabilityRule(ContextMock.Object, SettingsMock.Object); + } + + private JellyfinAvailabilityRule Rule { get; set; } + private Mock ContextMock { get; set; } + private Mock> SettingsMock { get; set; } + + [Test] + public async Task Movie_ShouldBe_Available_WhenFoundInJellyfin() + { + SettingsMock.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new JellyfinSettings()); + ContextMock.Setup(x => x.GetByTheMovieDbId(It.IsAny())).ReturnsAsync(new JellyfinContent + { + ProviderId = "123" + }); + var search = new SearchMovieViewModel() + { + TheMovieDbId = "123", + }; + var result = await Rule.Execute(search); + + Assert.True(result.Success); + Assert.True(search.Available); + } + + [Test] + public async Task Movie_Has_Custom_Url_When_Specified_In_Settings() + { + SettingsMock.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new JellyfinSettings + { + Enable = true, + Servers = new List + { + new JellyfinServers + { + ServerHostname = "http://test.com/", + ServerId = "8" + } + } + }); + ContextMock.Setup(x => x.GetByTheMovieDbId(It.IsAny())).ReturnsAsync(new JellyfinContent + { + ProviderId = "123", + JellyfinId = 1.ToString(), + }); + var search = new SearchMovieViewModel() + { + TheMovieDbId = "123", + }; + var result = await Rule.Execute(search); + + Assert.True(result.Success); + Assert.That(search.JellyfinUrl, Is.EqualTo("http://test.com/web/index.html#!/details?id=1&serverId=8")); + } + + [Test] + public async Task Movie_Uses_Default_Url_When() + { + SettingsMock.Setup(x => x.GetSettingsAsync()).ReturnsAsync(new JellyfinSettings + { + Enable = true, + Servers = new List + { + new JellyfinServers + { + Ip = "8080", + Port = 9090, + ServerHostname = string.Empty, + ServerId = "8" + } + } + }); + ContextMock.Setup(x => x.GetByTheMovieDbId(It.IsAny())).ReturnsAsync(new JellyfinContent + { + ProviderId = "123", + JellyfinId = 1.ToString() + }); + var search = new SearchMovieViewModel() + { + TheMovieDbId = "123", + }; + var result = await Rule.Execute(search); + + Assert.True(result.Success); + } + + [Test] + public async Task Movie_ShouldBe_NotAvailable_WhenNotFoundInJellyfin() + { + ContextMock.Setup(x => x.GetByTheMovieDbId(It.IsAny())).Returns(Task.FromResult(default(JellyfinContent))); + var search = new SearchMovieViewModel(); + var result = await Rule.Execute(search); + + Assert.True(result.Success); + Assert.False(search.Available); + } + } +} diff --git a/src/Ombi.Core.Tests/WatchProviderParserTests.cs b/src/Ombi.Core.Tests/WatchProviderParserTests.cs new file mode 100644 index 000000000..c56f64946 --- /dev/null +++ b/src/Ombi.Core.Tests/WatchProviderParserTests.cs @@ -0,0 +1,94 @@ +using NUnit.Framework; +using Ombi.Api.TheMovieDb.Models; +using Ombi.Core.Helpers; +using Ombi.Store.Entities; +using System.Collections.Generic; + +namespace Ombi.Core.Tests +{ + [TestFixture] + public class WatchProviderParserTests + { + [TestCase("GB", TestName = "UpperCase")] + [TestCase("gb", TestName = "LowerCase")] + [TestCase("gB", TestName = "MixedCase")] + public void GetValidStreamData(string streamingCountry) + { + var result = WatchProviderParser.GetUserWatchProviders(new WatchProviders + { + Results = new Results + { + GB = new WatchProviderData() + { + StreamInformation = new List + { + new StreamData + { + provider_name = "Netflix", + display_priority = 0, + logo_path = "logo", + provider_id = 8 + } + } + } + } + }, new OmbiUser { StreamingCountry = streamingCountry }); + + Assert.That(result[0].provider_name, Is.EqualTo("Netflix")); + } + + [TestCase("GB", TestName = "Missing_UpperCase")] + [TestCase("gb", TestName = "Missing_LowerCase")] + [TestCase("gB", TestName = "Missing_MixedCase")] + public void GetMissingStreamData(string streamingCountry) + { + var result = WatchProviderParser.GetUserWatchProviders(new WatchProviders + { + Results = new Results + { + AR = new WatchProviderData() + { + StreamInformation = new List + { + new StreamData + { + provider_name = "Netflix", + display_priority = 0, + logo_path = "logo", + provider_id = 8 + } + } + } + } + }, new OmbiUser { StreamingCountry = streamingCountry }); + + Assert.That(result, Is.Empty); + } + + [Test] + public void GetInvalidStreamData() + { + var result = WatchProviderParser.GetUserWatchProviders(new WatchProviders + { + Results = new Results + { + AR = new WatchProviderData() + { + StreamInformation = new List + { + new StreamData + { + provider_name = "Netflix", + display_priority = 0, + logo_path = "logo", + provider_id = 8 + } + } + } + } + }, new OmbiUser { StreamingCountry = "BLAH" }); + + Assert.That(result, Is.Empty); + } + } +} diff --git a/src/Ombi.Core/Authentication/OmbiUserManager.cs b/src/Ombi.Core/Authentication/OmbiUserManager.cs index 8313f359b..87f82c1de 100644 --- a/src/Ombi.Core/Authentication/OmbiUserManager.cs +++ b/src/Ombi.Core/Authentication/OmbiUserManager.cs @@ -33,6 +33,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Ombi.Api.Emby; +using Ombi.Api.Jellyfin; using Ombi.Api.Plex; using Ombi.Api.Plex.Models; using Ombi.Core.Settings; @@ -49,18 +50,24 @@ namespace Ombi.Core.Authentication IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger, IPlexApi plexApi, - IEmbyApiFactory embyApi, ISettingsService embySettings, ISettingsService auth) + IEmbyApiFactory embyApi, ISettingsService embySettings, + IJellyfinApiFactory jellyfinApi, ISettingsService jellyfinSettings, + ISettingsService auth) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) { _plexApi = plexApi; _embyApi = embyApi; + _jellyfinApi = jellyfinApi; _embySettings = embySettings; + _jellyfinSettings = jellyfinSettings; _authSettings = auth; } private readonly IPlexApi _plexApi; private readonly IEmbyApiFactory _embyApi; + private readonly IJellyfinApiFactory _jellyfinApi; private readonly ISettingsService _embySettings; + private readonly ISettingsService _jellyfinSettings; private readonly ISettingsService _authSettings; public override async Task CheckPasswordAsync(OmbiUser user, string password) @@ -83,6 +90,10 @@ namespace Ombi.Core.Authentication { return await CheckEmbyPasswordAsync(user, password); } + if (user.UserType == UserType.JellyfinUser) + { + return await CheckJellyfinPasswordAsync(user, password); + } return false; } @@ -185,5 +196,36 @@ namespace Ombi.Core.Authentication } return false; } + + /// + /// Sign the user into Jellyfin + /// We do not check if the user is in the owners "friends" since they must have a local user account to get this far. + /// We also have to try and authenticate them with every server, the first server that work we just say it was a success + /// + /// + /// + /// + private async Task CheckJellyfinPasswordAsync(OmbiUser user, string password) + { + var jellyfinSettings = await _jellyfinSettings.GetSettingsAsync(); + var client = _jellyfinApi.CreateClient(jellyfinSettings); + + foreach (var server in jellyfinSettings.Servers) + { + try + { + var result = await client.LogIn(user.UserName, password, server.ApiKey, server.FullUri); + if (result != null) + { + return true; + } + } + catch (Exception e) + { + Logger.LogError(e, "Jellyfin Login Failed"); + } + } + return false; + } } -} \ No newline at end of file +} diff --git a/src/Ombi.Core/Engine/BaseMediaEngine.cs b/src/Ombi.Core/Engine/BaseMediaEngine.cs index fc9847c7d..66e60767a 100644 --- a/src/Ombi.Core/Engine/BaseMediaEngine.cs +++ b/src/Ombi.Core/Engine/BaseMediaEngine.cs @@ -15,6 +15,8 @@ using Ombi.Core.Settings; using Ombi.Settings.Settings.Models; using Ombi.Store.Entities; using Ombi.Store.Repository; +using Ombi.Api.TheMovieDb.Models; +using Ombi.Core.Helpers; namespace Ombi.Core.Engine { @@ -179,6 +181,12 @@ namespace Ombi.Core.Engine return user.Language; } + protected async Task> GetUserWatchProvider(WatchProviders providers) + { + var user = await GetUser(); + return WatchProviderParser.GetUserWatchProviders(providers, user); + } + private OmbiSettings ombiSettings; protected async Task GetOmbiSettings() { diff --git a/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs b/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs index 3b8d97dc0..746045ef3 100644 --- a/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs +++ b/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs @@ -26,5 +26,6 @@ namespace Ombi.Core.Engine.Interfaces int ResultLimit { get; set; } Task GetMovieInfoByImdbId(string imdbId, CancellationToken requestAborted); + Task> GetStreamInformation(int movieDbId, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/Interfaces/IMusicSearchEngineV2.cs b/src/Ombi.Core/Engine/Interfaces/IMusicSearchEngineV2.cs index f9889ec9c..efd42dc68 100644 --- a/src/Ombi.Core/Engine/Interfaces/IMusicSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/Interfaces/IMusicSearchEngineV2.cs @@ -9,5 +9,6 @@ namespace Ombi.Core.Engine.Interfaces Task GetArtistInformation(string artistId); Task GetArtistInformationByRequestId(int requestId); Task GetReleaseGroupArt(string musicBrainzId, CancellationToken token); + Task GetAlbum(string albumId); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs b/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs index a8a27aa19..d2201825f 100644 --- a/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Ombi.Core.Models.Search.V2; namespace Ombi.Core @@ -7,5 +9,6 @@ namespace Ombi.Core { Task GetShowInformation(int tvdbid); Task GetShowByRequest(int requestId); + Task> GetStreamInformation(int tvDbId, int tvMazeId, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/MovieRequestEngine.cs b/src/Ombi.Core/Engine/MovieRequestEngine.cs index 741503795..c9dbde067 100644 --- a/src/Ombi.Core/Engine/MovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/MovieRequestEngine.cs @@ -67,6 +67,22 @@ namespace Ombi.Core.Engine $"{movieInfo.Title}{(!string.IsNullOrEmpty(movieInfo.ReleaseDate) ? $" ({DateTime.Parse(movieInfo.ReleaseDate).Year})" : string.Empty)}"; var userDetails = await GetUser(); + var canRequestOnBehalf = false; + + if (model.RequestOnBehalf.HasValue()) + { + canRequestOnBehalf = await UserManager.IsInRoleAsync(userDetails, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(userDetails, OmbiRoles.Admin); + + if (!canRequestOnBehalf) + { + return new RequestEngineResult + { + Result = false, + Message = "You do not have the correct permissions to request on behalf of users!", + ErrorMessage = $"You do not have the correct permissions to request on behalf of users!" + }; + } + } var requestModel = new MovieRequests { @@ -82,7 +98,7 @@ namespace Ombi.Core.Engine Status = movieInfo.Status, RequestedDate = DateTime.UtcNow, Approved = false, - RequestedUserId = userDetails.Id, + RequestedUserId = canRequestOnBehalf ? model.RequestOnBehalf : userDetails.Id, Background = movieInfo.BackdropPath, LangCode = model.LanguageCode, RequestedByAlias = model.RequestedByAlias @@ -103,7 +119,7 @@ namespace Ombi.Core.Engine if (requestModel.Approved) // The rules have auto approved this { - var requestEngineResult = await AddMovieRequest(requestModel, fullMovieName); + var requestEngineResult = await AddMovieRequest(requestModel, fullMovieName, model.RequestOnBehalf); if (requestEngineResult.Result) { var result = await ApproveMovie(requestModel); @@ -124,7 +140,7 @@ namespace Ombi.Core.Engine // If there are no providers then it's successful but movie has not been sent } - return await AddMovieRequest(requestModel, fullMovieName); + return await AddMovieRequest(requestModel, fullMovieName, model.RequestOnBehalf); } @@ -270,7 +286,7 @@ namespace Ombi.Core.Engine allRequests = allRequests.Where(x => x.Available); break; case RequestStatus.Denied: - allRequests = allRequests.Where(x => x.Denied.HasValue && x.Denied.Value && !x.Available); + allRequests = allRequests.Where(x => x.Denied.HasValue && x.Denied.Value && !x.Available); break; default: break; @@ -429,7 +445,7 @@ namespace Ombi.Core.Engine public async Task GetRequest(int requestId) { var request = await MovieRepository.GetWithUser().Where(x => x.Id == requestId).FirstOrDefaultAsync(); - await CheckForSubscription(new HideResult(), new List{request }); + await CheckForSubscription(new HideResult(), new List { request }); return request; } @@ -654,19 +670,19 @@ namespace Ombi.Core.Engine }; } - private async Task AddMovieRequest(MovieRequests model, string movieName) + private async Task AddMovieRequest(MovieRequests model, string movieName, string requestOnBehalf) { await MovieRepository.Add(model); var result = await RunSpecificRule(model, SpecificRules.CanSendNotification); if (result.Success) - { + { await NotificationHelper.NewRequest(model); } await _requestLog.Add(new RequestLog { - UserId = (await GetUser()).Id, + UserId = requestOnBehalf.HasValue() ? requestOnBehalf : (await GetUser()).Id, RequestDate = DateTime.UtcNow, RequestId = model.Id, RequestType = RequestType.Movie, diff --git a/src/Ombi.Core/Engine/RecentlyAddedEngine.cs b/src/Ombi.Core/Engine/RecentlyAddedEngine.cs index 114d32a24..d597ec80b 100644 --- a/src/Ombi.Core/Engine/RecentlyAddedEngine.cs +++ b/src/Ombi.Core/Engine/RecentlyAddedEngine.cs @@ -13,38 +13,45 @@ namespace Ombi.Core.Engine { public class RecentlyAddedEngine : IRecentlyAddedEngine { - public RecentlyAddedEngine(IPlexContentRepository plex, IEmbyContentRepository emby, IRepository recentlyAdded) + public RecentlyAddedEngine(IPlexContentRepository plex, IEmbyContentRepository emby, IJellyfinContentRepository jellyfin, IRepository recentlyAdded) { _plex = plex; _emby = emby; + _jellyfin = jellyfin; _recentlyAddedLog = recentlyAdded; } private readonly IPlexContentRepository _plex; private readonly IEmbyContentRepository _emby; + private readonly IJellyfinContentRepository _jellyfin; private readonly IRepository _recentlyAddedLog; + + public IEnumerable GetRecentlyAddedMovies(DateTime from, DateTime to) { var plexMovies = _plex.GetAll().Where(x => x.Type == PlexMediaTypeEntity.Movie && x.AddedAt > from && x.AddedAt < to); var embyMovies = _emby.GetAll().Where(x => x.Type == EmbyMediaType.Movie && x.AddedAt > from && x.AddedAt < to); + var jellyfinMovies = _jellyfin.GetAll().Where(x => x.Type == JellyfinMediaType.Movie && x.AddedAt > from && x.AddedAt < to); - return GetRecentlyAddedMovies(plexMovies, embyMovies).Take(30); + return GetRecentlyAddedMovies(plexMovies, embyMovies, jellyfinMovies).Take(30); } public IEnumerable GetRecentlyAddedMovies() { var plexMovies = _plex.GetAll().Where(x => x.Type == PlexMediaTypeEntity.Movie); var embyMovies = _emby.GetAll().Where(x => x.Type == EmbyMediaType.Movie); - return GetRecentlyAddedMovies(plexMovies, embyMovies); + var jellyfinMovies = _jellyfin.GetAll().Where(x => x.Type == JellyfinMediaType.Movie); + return GetRecentlyAddedMovies(plexMovies, embyMovies, jellyfinMovies); } public IEnumerable GetRecentlyAddedTv(DateTime from, DateTime to, bool groupBySeason) { var plexTv = _plex.GetAll().Include(x => x.Seasons).Include(x => x.Episodes).Where(x => x.Type == PlexMediaTypeEntity.Show && x.AddedAt > from && x.AddedAt < to); var embyTv = _emby.GetAll().Include(x => x.Episodes).Where(x => x.Type == EmbyMediaType.Series && x.AddedAt > from && x.AddedAt < to); + var jellyfinTv = _jellyfin.GetAll().Include(x => x.Episodes).Where(x => x.Type == JellyfinMediaType.Series && x.AddedAt > from && x.AddedAt < to); - return GetRecentlyAddedTv(plexTv, embyTv, groupBySeason).Take(30); + return GetRecentlyAddedTv(plexTv, embyTv, jellyfinTv, groupBySeason).Take(30); } @@ -52,14 +59,16 @@ namespace Ombi.Core.Engine { var plexTv = _plex.GetAll().Include(x => x.Seasons).Include(x => x.Episodes).Where(x => x.Type == PlexMediaTypeEntity.Show); var embyTv = _emby.GetAll().Include(x => x.Episodes).Where(x => x.Type == EmbyMediaType.Series); + var jellyfinTv = _jellyfin.GetAll().Include(x => x.Episodes).Where(x => x.Type == JellyfinMediaType.Series); - return GetRecentlyAddedTv(plexTv, embyTv, groupBySeason); + return GetRecentlyAddedTv(plexTv, embyTv, jellyfinTv, groupBySeason); } public async Task UpdateRecentlyAddedDatabase() { var plexContent = _plex.GetAll().Include(x => x.Episodes); var embyContent = _emby.GetAll().Include(x => x.Episodes); + var jellyfinContent = _jellyfin.GetAll().Include(x => x.Episodes); var recentlyAddedLog = new HashSet(); foreach (var p in plexContent) { @@ -136,17 +145,56 @@ namespace Ombi.Core.Engine } } } + + foreach (var e in jellyfinContent) + { + if (e.TheMovieDbId.IsNullOrEmpty()) + { + continue; + } + if (e.Type == JellyfinMediaType.Movie) + { + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Jellyfin, + ContentId = int.Parse(e.TheMovieDbId), + ContentType = ContentType.Parent + }); + } + else + { + // Add the episodes + foreach (var ep in e.Episodes) + { + if (ep.Series.TvDbId.IsNullOrEmpty()) + { + continue; + } + recentlyAddedLog.Add(new RecentlyAddedLog + { + AddedAt = DateTime.Now, + Type = RecentlyAddedType.Jellyfin, + ContentId = int.Parse(ep.Series.TvDbId), + ContentType = ContentType.Episode, + EpisodeNumber = ep.EpisodeNumber, + SeasonNumber = ep.SeasonNumber + }); + } + } + } await _recentlyAddedLog.AddRange(recentlyAddedLog); return true; } - private IEnumerable GetRecentlyAddedTv(IQueryable plexTv, IQueryable embyTv, + private IEnumerable GetRecentlyAddedTv(IQueryable plexTv, IQueryable embyTv, IQueryable jellyfinTv, bool groupBySeason) { var model = new HashSet(); TransformPlexShows(plexTv, model); TransformEmbyShows(embyTv, model); + TransformJellyfinShows(jellyfinTv, model); if (groupBySeason) { @@ -156,11 +204,12 @@ namespace Ombi.Core.Engine return model; } - private IEnumerable GetRecentlyAddedMovies(IQueryable plexMovies, IQueryable embyMovies) + private IEnumerable GetRecentlyAddedMovies(IQueryable plexMovies, IQueryable embyMovies, IQueryable jellyfinMovies) { var model = new HashSet(); TransformPlexMovies(plexMovies, model); TransformEmbyMovies(embyMovies, model); + TransformJellyfinMovies(jellyfinMovies, model); return model; } @@ -181,6 +230,22 @@ namespace Ombi.Core.Engine } } + private static void TransformJellyfinMovies(IQueryable jellyfinMovies, HashSet model) + { + foreach (var jellyfin in jellyfinMovies) + { + model.Add(new RecentlyAddedMovieModel + { + Id = jellyfin.Id, + ImdbId = jellyfin.ImdbId, + TheMovieDbId = jellyfin.TheMovieDbId, + TvDbId = jellyfin.TvDbId, + AddedAt = jellyfin.AddedAt, + Title = jellyfin.Title, + }); + } + } + private static void TransformPlexMovies(IQueryable plexMovies, HashSet model) { foreach (var plex in plexMovies) @@ -244,5 +309,26 @@ namespace Ombi.Core.Engine } } } + + private static void TransformJellyfinShows(IQueryable jellyfinShows, HashSet model) + { + foreach (var jellyfin in jellyfinShows) + { + foreach (var episode in jellyfin.Episodes) + { + model.Add(new RecentlyAddedTvModel + { + Id = jellyfin.Id, + ImdbId = jellyfin.ImdbId, + TvDbId = jellyfin.TvDbId, + TheMovieDbId = jellyfin.TheMovieDbId, + AddedAt = jellyfin.AddedAt, + Title = jellyfin.Title, + EpisodeNumber = episode.EpisodeNumber, + SeasonNumber = episode.SeasonNumber + }); + } + } + } } } diff --git a/src/Ombi.Core/Engine/TvRequestEngine.cs b/src/Ombi.Core/Engine/TvRequestEngine.cs index e066261f4..b06c99d49 100644 --- a/src/Ombi.Core/Engine/TvRequestEngine.cs +++ b/src/Ombi.Core/Engine/TvRequestEngine.cs @@ -51,12 +51,28 @@ namespace Ombi.Core.Engine public async Task RequestTvShow(TvRequestViewModel tv) { var user = await GetUser(); + var canRequestOnBehalf = false; + + if (tv.RequestOnBehalf.HasValue()) + { + canRequestOnBehalf = await UserManager.IsInRoleAsync(user, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(user, OmbiRoles.Admin); + + if (!canRequestOnBehalf) + { + return new RequestEngineResult + { + Result = false, + Message = "You do not have the correct permissions to request on behalf of users!", + ErrorMessage = $"You do not have the correct permissions to request on behalf of users!" + }; + } + } var tvBuilder = new TvShowRequestBuilder(TvApi, MovieDbApi); (await tvBuilder .GetShowInfo(tv.TvDbId)) .CreateTvList(tv) - .CreateChild(tv, user.Id); + .CreateChild(tv, canRequestOnBehalf ? tv.RequestOnBehalf : user.Id); await tvBuilder.BuildEpisodes(tv); @@ -124,12 +140,12 @@ namespace Ombi.Core.Engine ErrorMessage = "This has already been requested" }; } - return await AddExistingRequest(tvBuilder.ChildRequest, existingRequest); + return await AddExistingRequest(tvBuilder.ChildRequest, existingRequest, tv.RequestOnBehalf); } // This is a new request var newRequest = tvBuilder.CreateNewRequest(tv); - return await AddRequest(newRequest.NewRequest); + return await AddRequest(newRequest.NewRequest, tv.RequestOnBehalf); } public async Task> GetRequests(int count, int position, OrderFilterModel type) @@ -736,21 +752,21 @@ namespace Ombi.Core.Engine } } - private async Task AddExistingRequest(ChildRequests newRequest, TvRequests existingRequest) + private async Task AddExistingRequest(ChildRequests newRequest, TvRequests existingRequest, string requestOnBehalf) { // Add the child existingRequest.ChildRequests.Add(newRequest); await TvRepository.Update(existingRequest); - return await AfterRequest(newRequest); + return await AfterRequest(newRequest, requestOnBehalf); } - private async Task AddRequest(TvRequests model) + private async Task AddRequest(TvRequests model, string requestOnBehalf) { await TvRepository.Add(model); // This is a new request so we should only have 1 child - return await AfterRequest(model.ChildRequests.FirstOrDefault()); + return await AfterRequest(model.ChildRequests.FirstOrDefault(), requestOnBehalf); } private static List SortEpisodes(List items) @@ -766,7 +782,7 @@ namespace Ombi.Core.Engine } - private async Task AfterRequest(ChildRequests model) + private async Task AfterRequest(ChildRequests model, string requestOnBehalf) { var sendRuleResult = await RunSpecificRule(model, SpecificRules.CanSendNotification); if (sendRuleResult.Success) @@ -776,7 +792,7 @@ namespace Ombi.Core.Engine await _requestLog.Add(new RequestLog { - UserId = (await GetUser()).Id, + UserId = requestOnBehalf.HasValue() ? requestOnBehalf : (await GetUser()).Id, RequestDate = DateTime.UtcNow, RequestId = model.Id, RequestType = RequestType.TvShow, diff --git a/src/Ombi.Core/Engine/TvSearchEngine.cs b/src/Ombi.Core/Engine/TvSearchEngine.cs index 38ba8be1a..433cacee0 100644 --- a/src/Ombi.Core/Engine/TvSearchEngine.cs +++ b/src/Ombi.Core/Engine/TvSearchEngine.cs @@ -59,7 +59,12 @@ namespace Ombi.Core.Engine { continue; } - retVal.Add(await ProcessResult(tvMazeSearch, false)); + var mappedResult = await ProcessResult(tvMazeSearch, false); + if (mappedResult == null) + { + continue; + } + retVal.Add(mappedResult); } return retVal; } @@ -194,7 +199,7 @@ namespace Ombi.Core.Engine foreach (var tvMazeSearch in items) { var result = await ProcessResult(tvMazeSearch, includeImages); - if(settings.HideAvailableFromDiscover && result.Available) + if (result == null || settings.HideAvailableFromDiscover && result.Available) { continue; } @@ -211,15 +216,17 @@ namespace Ombi.Core.Engine private async Task ProcessResult(SearchTvShowViewModel item, bool includeImages) { + if (item.Id == 0) + { + return null; + } item.TheTvDbId = item.Id.ToString(); if (includeImages) { - - if (item.TheTvDbId.HasValue()) - { - item.BackdropPath = await _imageService.GetTvBackground(item.TheTvDbId); - } - + if (item.TheTvDbId.HasValue()) + { + item.BackdropPath = await _imageService.GetTvBackground(item.TheTvDbId); + } } await RunSearchRules(item); diff --git a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs index b0a78fbb3..dc009371a 100644 --- a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs @@ -249,6 +249,26 @@ namespace Ombi.Core.Engine.V2 return result; } + public async Task> GetStreamInformation(int movieDbId, CancellationToken cancellationToken) + { + var providers = await MovieApi.GetMovieWatchProviders(movieDbId, cancellationToken); + var results = await GetUserWatchProvider(providers); + + var data = new List(); + + foreach (var result in results) + { + data.Add(new StreamingData + { + Logo = result.logo_path, + Order = result.display_priority, + StreamingProvider = result.provider_name + }); + } + + return data; + } + protected async Task> TransformMovieResultsToResponse( IEnumerable movies) { @@ -287,6 +307,7 @@ namespace Ombi.Core.Engine.V2 mapped.Requested = viewMovie.Requested; mapped.PlexUrl = viewMovie.PlexUrl; mapped.EmbyUrl = viewMovie.EmbyUrl; + mapped.JellyfinUrl = viewMovie.JellyfinUrl; mapped.Subscribed = viewMovie.Subscribed; mapped.ShowSubscribe = viewMovie.ShowSubscribe; diff --git a/src/Ombi.Core/Engine/V2/MusicSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/MusicSearchEngineV2.cs index 13256be0a..b7d9575db 100644 --- a/src/Ombi.Core/Engine/V2/MusicSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/MusicSearchEngineV2.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; +using Newtonsoft.Json; using Ombi.Api.Lidarr; using Ombi.Api.Lidarr.Models; using Ombi.Api.MusicBrainz; @@ -41,6 +42,21 @@ namespace Ombi.Core.Engine.V2 _lidarrApi = lidarrApi; } + public async Task GetAlbum(string albumId) + { + var g = await _musicBrainzApi.GetAlbumInformation(albumId); + var release = new ReleaseGroup + { + ReleaseType = g.ReleaseGroup.PrimaryType, + Id = g.Id, + Title = g.Title, + ReleaseDate = g.ReleaseGroup.FirstReleaseDate, + }; + + await RunSearchRules(release); + return release; + } + public async Task GetArtistInformation(string artistId) { var artist = await _musicBrainzApi.GetArtistInformation(artistId); @@ -84,12 +100,19 @@ namespace Ombi.Core.Engine.V2 if (lidarrArtistTask != null) { - var artistResult = await lidarrArtistTask; - info.Banner = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("banner", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl(); - info.Logo = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("logo", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl(); - info.Poster = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("poster", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl(); - info.FanArt = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("fanart", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl(); - info.Overview = artistResult.overview; + try + { + var artistResult = await lidarrArtistTask; + info.Banner = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("banner", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl(); + info.Logo = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("logo", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl(); + info.Poster = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("poster", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl(); + info.FanArt = artistResult.images?.FirstOrDefault(x => x.coverType.Equals("fanart", StringComparison.InvariantCultureIgnoreCase))?.url.ToHttpsUrl(); + info.Overview = artistResult.overview; + } + catch (JsonSerializationException) + { + // swallow, Lidarr probably doesn't have this artist + } } return info; @@ -118,7 +141,7 @@ namespace Ombi.Core.Engine.V2 return new AlbumArt(); } - + public async Task GetArtistInformationByRequestId(int requestId) { var request = await RequestService.MusicRequestRepository.Find(requestId); diff --git a/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs index dd2ce22aa..29ea01879 100644 --- a/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs @@ -19,6 +19,8 @@ using Ombi.Core.Settings; using Ombi.Store.Repository; using TraktSharp.Entities; using Microsoft.EntityFrameworkCore; +using System.Threading; +using Ombi.Api.TheMovieDb; namespace Ombi.Core.Engine.V2 { @@ -27,15 +29,17 @@ namespace Ombi.Core.Engine.V2 private readonly ITvMazeApi _tvMaze; private readonly IMapper _mapper; private readonly ITraktApi _traktApi; + private readonly IMovieDbApi _movieApi; public TvSearchEngineV2(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper, ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um, ICacheService memCache, ISettingsService s, - IRepository sub) + IRepository sub, IMovieDbApi movieApi) : base(identity, service, r, um, memCache, s, sub) { _tvMaze = tvMaze; _mapper = mapper; _traktApi = trakt; + _movieApi = movieApi; } @@ -106,6 +110,39 @@ namespace Ombi.Core.Engine.V2 return await ProcessResult(mapped, traktInfoTask); } + public async Task> GetStreamInformation(int tvDbId, int tvMazeId, CancellationToken cancellationToken) + { + var tvdbshow = await Cache.GetOrAdd(nameof(GetShowInformation) + tvMazeId, + async () => await _tvMaze.ShowLookupByTheTvDbId(tvMazeId), DateTime.Now.AddHours(12)); + if (tvdbshow == null) + { + return null; + } + + /// this is a best effort guess since TV maze do not provide the TheMovieDbId + var movieDbResults = await _movieApi.SearchTv(tvdbshow.name, tvdbshow.premiered.Substring(0, 4)); + var potential = movieDbResults.FirstOrDefault(); + tvDbId = potential.Id; + // end guess + + var providers = await _movieApi.GetTvWatchProviders(tvDbId, cancellationToken); + var results = await GetUserWatchProvider(providers); + + var data = new List(); + + foreach (var result in results) + { + data.Add(new StreamingData + { + Logo = result.logo_path, + Order = result.display_priority, + StreamingProvider = result.provider_name + }); + } + + return data; + } + private IEnumerable ProcessResults(IEnumerable items) { var retVal = new List(); @@ -141,7 +178,7 @@ namespace Ombi.Core.Engine.V2 { item.Images.Medium = item.Images.Medium.ToHttpsUrl(); } - + if (item.Cast?.Any() ?? false) { foreach (var cast in item.Cast) diff --git a/src/Ombi.Core/Helpers/WatchProviderParser.cs b/src/Ombi.Core/Helpers/WatchProviderParser.cs new file mode 100644 index 000000000..68f3c26dd --- /dev/null +++ b/src/Ombi.Core/Helpers/WatchProviderParser.cs @@ -0,0 +1,35 @@ +using Ombi.Api.TheMovieDb.Models; +using Ombi.Store.Entities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Ombi.Core.Helpers +{ + public static class WatchProviderParser + { + public static List GetUserWatchProviders(WatchProviders providers, OmbiUser user) + { + var data = new List(); + + if (providers?.Results == null) + { + return data; + } + + var resultsProp = providers.Results.GetType().GetProperties(); + var matchingStreamingCountry = resultsProp.FirstOrDefault(x => x.Name.Equals(user.StreamingCountry, StringComparison.InvariantCultureIgnoreCase)); + if (matchingStreamingCountry == null) + { + return data; + } + + var result = (WatchProviderData)matchingStreamingCountry.GetValue(providers.Results); + if (result == null || result.StreamInformation == null) + { + return data; + } + return result.StreamInformation; + } + } +} diff --git a/src/Ombi.Core/Models/RecentlyAddedMovieModel.cs b/src/Ombi.Core/Models/RecentlyAddedMovieModel.cs index c63ea98d3..4ed713453 100644 --- a/src/Ombi.Core/Models/RecentlyAddedMovieModel.cs +++ b/src/Ombi.Core/Models/RecentlyAddedMovieModel.cs @@ -18,6 +18,7 @@ namespace Ombi.Core.Models public enum RecentlyAddedType { Plex, - Emby + Emby, + Jellyfin } -} \ No newline at end of file +} diff --git a/src/Ombi.Core/Models/Requests/MovieRequestViewModel.cs b/src/Ombi.Core/Models/Requests/MovieRequestViewModel.cs index 5a79d2982..22d1cc449 100644 --- a/src/Ombi.Core/Models/Requests/MovieRequestViewModel.cs +++ b/src/Ombi.Core/Models/Requests/MovieRequestViewModel.cs @@ -33,6 +33,7 @@ namespace Ombi.Core.Models.Requests { public int TheMovieDbId { get; set; } public string LanguageCode { get; set; } = "en"; + public string RequestOnBehalf { get; set; } /// /// This is only set from a HTTP Header diff --git a/src/Ombi.Core/Models/Requests/TvRequestViewModel.cs b/src/Ombi.Core/Models/Requests/TvRequestViewModel.cs index c17925b1b..15349462b 100644 --- a/src/Ombi.Core/Models/Requests/TvRequestViewModel.cs +++ b/src/Ombi.Core/Models/Requests/TvRequestViewModel.cs @@ -12,6 +12,8 @@ namespace Ombi.Core.Models.Requests public List Seasons { get; set; } = new List(); [JsonIgnore] public string RequestedByAlias { get; set; } + + public string RequestOnBehalf { get; set; } } public class SeasonsViewModel diff --git a/src/Ombi.Core/Models/Search/SearchViewModel.cs b/src/Ombi.Core/Models/Search/SearchViewModel.cs index 2f951c97e..4cf812982 100644 --- a/src/Ombi.Core/Models/Search/SearchViewModel.cs +++ b/src/Ombi.Core/Models/Search/SearchViewModel.cs @@ -14,11 +14,12 @@ namespace Ombi.Core.Models.Search public bool Available { get; set; } public string PlexUrl { get; set; } public string EmbyUrl { get; set; } + public string JellyfinUrl { get; set; } public string Quality { get; set; } public abstract RequestType Type { get; } /// - /// This is used for the PlexAvailabilityCheck/EmbyAvailabilityRule rule + /// This is used for the PlexAvailabilityCheck/EmbyAvailabilityRule/JellyfinAvailabilityRule rule /// /// /// The custom identifier. @@ -35,4 +36,4 @@ namespace Ombi.Core.Models.Search [NotMapped] public bool ShowSubscribe { get; set; } } -} \ No newline at end of file +} diff --git a/src/Ombi.Core/Models/Search/V2/StreamingData.cs b/src/Ombi.Core/Models/Search/V2/StreamingData.cs new file mode 100644 index 000000000..d9444c2ce --- /dev/null +++ b/src/Ombi.Core/Models/Search/V2/StreamingData.cs @@ -0,0 +1,9 @@ +namespace Ombi.Core.Models.Search.V2 +{ + public class StreamingData + { + public int Order { get; set; } + public string StreamingProvider { get; set; } + public string Logo { get; set; } + } +} diff --git a/src/Ombi.Core/Models/TesterResultModel.cs b/src/Ombi.Core/Models/TesterResultModel.cs new file mode 100644 index 000000000..f23e14d43 --- /dev/null +++ b/src/Ombi.Core/Models/TesterResultModel.cs @@ -0,0 +1,8 @@ +namespace Ombi.Core.Models +{ + public class TesterResultModel + { + public bool IsValid { get; set; } + public string ExpectedSubDir { get; set; } + } +} diff --git a/src/Ombi.Core/Models/UI/UserViewModel.cs b/src/Ombi.Core/Models/UI/UserViewModel.cs index e74e5037d..0c9be846a 100644 --- a/src/Ombi.Core/Models/UI/UserViewModel.cs +++ b/src/Ombi.Core/Models/UI/UserViewModel.cs @@ -18,6 +18,7 @@ namespace Ombi.Core.Models.UI public UserType UserType { get; set; } public int MovieRequestLimit { get; set; } public int EpisodeRequestLimit { get; set; } + public string StreamingCountry { get; set; } public RequestQuotaCountModel EpisodeRequestQuota { get; set; } public RequestQuotaCountModel MovieRequestQuota { get; set; } public RequestQuotaCountModel MusicRequestQuota { get; set; } @@ -30,4 +31,10 @@ namespace Ombi.Core.Models.UI public string Value { get; set; } public bool Enabled { get; set; } } + + public class UserViewModelDropdown + { + public string Id { get; set; } + public string Username { get; set; } + } } \ No newline at end of file diff --git a/src/Ombi.Core/Models/UserDto.cs b/src/Ombi.Core/Models/UserDto.cs index 5c629fb39..7fbdb3465 100644 --- a/src/Ombi.Core/Models/UserDto.cs +++ b/src/Ombi.Core/Models/UserDto.cs @@ -19,6 +19,7 @@ namespace Ombi.Core.Models { LocalUser = 1, PlexUser = 2, - EmbyUser = 3 + EmbyUser = 3, + JellyfinUser = 5 } -} \ No newline at end of file +} diff --git a/src/Ombi.Core/Ombi.Core.csproj b/src/Ombi.Core/Ombi.Core.csproj index 8acbaceb0..8fa0f976f 100644 --- a/src/Ombi.Core/Ombi.Core.csproj +++ b/src/Ombi.Core/Ombi.Core.csproj @@ -24,6 +24,7 @@ + @@ -40,4 +41,4 @@ - \ No newline at end of file + diff --git a/src/Ombi.Core/Rule/Rules/Search/AvailabilityRuleHelper.cs b/src/Ombi.Core/Rule/Rules/Search/AvailabilityRuleHelper.cs index 24faa3a97..9076cd232 100644 --- a/src/Ombi.Core/Rule/Rules/Search/AvailabilityRuleHelper.cs +++ b/src/Ombi.Core/Rule/Rules/Search/AvailabilityRuleHelper.cs @@ -108,10 +108,40 @@ namespace Ombi.Core.Rule.Rules.Search x.Series.TvDbId == item.TvDbId); } + if (epExists != null) + { + episode.Available = true; + } + } + public static async Task SingleEpisodeCheck(bool useImdb, IQueryable allEpisodes, EpisodeRequests episode, + SeasonRequests season, JellyfinContent item, bool useTheMovieDb, bool useTvDb) + { + JellyfinEpisode epExists = null; + if (useImdb) + { + epExists = await allEpisodes.FirstOrDefaultAsync(x => + x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && + x.Series.ImdbId == item.ImdbId); + } + + if (useTheMovieDb) + { + epExists = await allEpisodes.FirstOrDefaultAsync(x => + x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && + x.Series.TheMovieDbId == item.TheMovieDbId); + } + + if (useTvDb) + { + epExists = await allEpisodes.FirstOrDefaultAsync(x => + x.EpisodeNumber == episode.EpisodeNumber && x.SeasonNumber == season.SeasonNumber && + x.Series.TvDbId == item.TvDbId); + } + if (epExists != null) { episode.Available = true; } } } -} \ No newline at end of file +} diff --git a/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs b/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs index 75b6633bb..3fe11cbc4 100644 --- a/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs @@ -70,11 +70,11 @@ namespace Ombi.Core.Rule.Rules.Search var server = s.Servers.FirstOrDefault(x => x.ServerHostname != null); if ((server?.ServerHostname ?? string.Empty).HasValue()) { - obj.EmbyUrl = EmbyHelper.GetEmbyMediaUrl(item.EmbyId, server?.ServerId, server?.ServerHostname, s.IsJellyfin); + obj.EmbyUrl = EmbyHelper.GetEmbyMediaUrl(item.EmbyId, server?.ServerId, server?.ServerHostname); } else { - obj.EmbyUrl = EmbyHelper.GetEmbyMediaUrl(item.EmbyId, server?.ServerId, null, s.IsJellyfin); + obj.EmbyUrl = EmbyHelper.GetEmbyMediaUrl(item.EmbyId, server?.ServerId, null); } } @@ -100,4 +100,4 @@ namespace Ombi.Core.Rule.Rules.Search return Success(); } } -} \ No newline at end of file +} diff --git a/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs b/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs new file mode 100644 index 000000000..0447458d9 --- /dev/null +++ b/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs @@ -0,0 +1,104 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Ombi.Core.Models.Search; +using Ombi.Core.Rule.Interfaces; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Helpers; +using Ombi.Store.Entities; +using Ombi.Store.Repository; + +namespace Ombi.Core.Rule.Rules.Search +{ + public class JellyfinAvailabilityRule : BaseSearchRule, IRules + { + public JellyfinAvailabilityRule(IJellyfinContentRepository repo, ISettingsService s) + { + JellyfinContentRepository = repo; + JellyfinSettings = s; + } + + private IJellyfinContentRepository JellyfinContentRepository { get; } + private ISettingsService JellyfinSettings { get; } + + public async Task Execute(SearchViewModel obj) + { + JellyfinContent item = null; + var useImdb = false; + var useTheMovieDb = false; + var useTvDb = false; + + if (obj.ImdbId.HasValue()) + { + item = await JellyfinContentRepository.GetByImdbId(obj.ImdbId); + if (item != null) + { + useImdb = true; + } + } + if (item == null) + { + if (obj.TheMovieDbId.HasValue()) + { + item = await JellyfinContentRepository.GetByTheMovieDbId(obj.TheMovieDbId); + if (item != null) + { + useTheMovieDb = true; + } + } + + if (item == null) + { + if (obj.TheTvDbId.HasValue()) + { + item = await JellyfinContentRepository.GetByTvDbId(obj.TheTvDbId); + if (item != null) + { + useTvDb = true; + } + } + } + } + + if (item != null) + { + obj.Available = true; + var s = await JellyfinSettings.GetSettingsAsync(); + if (s.Enable) + { + var server = s.Servers.FirstOrDefault(x => x.ServerHostname != null); + if ((server?.ServerHostname ?? string.Empty).HasValue()) + { + obj.JellyfinUrl = JellyfinHelper.GetJellyfinMediaUrl(item.JellyfinId, server?.ServerId, server?.ServerHostname); + } + else + { + var firstServer = s.Servers?.FirstOrDefault(); + obj.JellyfinUrl = JellyfinHelper.GetJellyfinMediaUrl(item.JellyfinId, firstServer.ServerId, firstServer.FullUri); + } + } + + if (obj.Type == RequestType.TvShow) + { + var search = (SearchTvShowViewModel)obj; + // Let's go through the episodes now + if (search.SeasonRequests.Any()) + { + var allEpisodes = JellyfinContentRepository.GetAllEpisodes().Include(x => x.Series); + foreach (var season in search.SeasonRequests) + { + foreach (var episode in season.Episodes) + { + await AvailabilityRuleHelper.SingleEpisodeCheck(useImdb, allEpisodes, episode, season, item, useTheMovieDb, useTvDb); + } + } + } + + AvailabilityRuleHelper.CheckForUnairedEpisodes(search); + } + } + return Success(); + } + } +} diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index 76f4879ce..d5e577d84 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Ombi.Api.Discord; using Ombi.Api.Emby; +using Ombi.Api.Jellyfin; using Ombi.Api.Plex; using Ombi.Api.Radarr; using Ombi.Api.Sonarr; @@ -47,6 +48,7 @@ using Ombi.Core.Senders; using Ombi.Helpers; using Ombi.Schedule.Jobs.Couchpotato; using Ombi.Schedule.Jobs.Emby; +using Ombi.Schedule.Jobs.Jellyfin; using Ombi.Schedule.Jobs.Ombi; using Ombi.Schedule.Jobs.Plex; using Ombi.Schedule.Jobs.Sonarr; @@ -65,6 +67,7 @@ using Quartz.Spi; using Ombi.Api.MusicBrainz; using Ombi.Api.Twilio; using Ombi.Api.CloudService; +using Ombi.Api.RottenTomatoes; namespace Ombi.DependencyInjection { @@ -126,6 +129,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -153,8 +157,9 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); } public static void RegisterStore(this IServiceCollection services) { @@ -169,6 +174,7 @@ namespace Ombi.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -213,6 +219,9 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -220,6 +229,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj b/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj index bbfe532eb..ed1e9e4a2 100644 --- a/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj +++ b/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj @@ -32,6 +32,7 @@ + diff --git a/src/Ombi.HealthChecks/Checks/CouchPotatoHealthCheck.cs b/src/Ombi.HealthChecks/Checks/CouchPotatoHealthCheck.cs index a68ce327b..cb41ab31d 100644 --- a/src/Ombi.HealthChecks/Checks/CouchPotatoHealthCheck.cs +++ b/src/Ombi.HealthChecks/Checks/CouchPotatoHealthCheck.cs @@ -3,6 +3,8 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Ombi.Api.CouchPotato; using Ombi.Api.Emby; using Ombi.Api.Emby.Models; +using Ombi.Api.Jellyfin; +using Ombi.Api.Jellyfin.Models; using Ombi.Api.Plex; using Ombi.Api.Plex.Models.Status; using Ombi.Core.Settings; diff --git a/src/Ombi.HealthChecks/Checks/JellyfinHealthCheck.cs b/src/Ombi.HealthChecks/Checks/JellyfinHealthCheck.cs new file mode 100644 index 000000000..2f758961c --- /dev/null +++ b/src/Ombi.HealthChecks/Checks/JellyfinHealthCheck.cs @@ -0,0 +1,54 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Ombi.Api.Jellyfin; +using Ombi.Api.Jellyfin.Models; +using Ombi.Api.Plex; +using Ombi.Api.Plex.Models.Status; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Ombi.HealthChecks.Checks +{ + public class JellyfinHealthCheck : BaseHealthCheck + { + public JellyfinHealthCheck(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) + { + } + public override async Task CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default) + { + using (var scope = CreateScope()) + { + var settingsProvider = scope.ServiceProvider.GetRequiredService>(); + var api = scope.ServiceProvider.GetRequiredService(); + var settings = await settingsProvider.GetSettingsAsync(); + if (settings == null) + { + return HealthCheckResult.Healthy("Jellyfin is not configured."); + } + + var client = api.CreateClient(settings); + var taskResult = new List>(); + foreach (var server in settings.Servers) + { + taskResult.Add(client.GetSystemInformation(server.ApiKey, server.FullUri)); + } + + try + { + var result = await Task.WhenAll(taskResult.ToArray()); + return HealthCheckResult.Healthy(); + } + catch (Exception e) + { + return HealthCheckResult.Unhealthy("Could not communicate with Jellyfin", e); + } + } + } + } +} diff --git a/src/Ombi.HealthChecks/Checks/SickrageHealthCheck.cs b/src/Ombi.HealthChecks/Checks/SickrageHealthCheck.cs index c348e8edf..68fb5fa84 100644 --- a/src/Ombi.HealthChecks/Checks/SickrageHealthCheck.cs +++ b/src/Ombi.HealthChecks/Checks/SickrageHealthCheck.cs @@ -3,6 +3,8 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Ombi.Api.CouchPotato; using Ombi.Api.Emby; using Ombi.Api.Emby.Models; +using Ombi.Api.Jellyfin; +using Ombi.Api.Jellyfin.Models; using Ombi.Api.Plex; using Ombi.Api.Plex.Models.Status; using Ombi.Api.SickRage; diff --git a/src/Ombi.HealthChecks/HealthCheckExtensions.cs b/src/Ombi.HealthChecks/HealthCheckExtensions.cs index e608d5ec0..2f80378ff 100644 --- a/src/Ombi.HealthChecks/HealthCheckExtensions.cs +++ b/src/Ombi.HealthChecks/HealthCheckExtensions.cs @@ -12,6 +12,7 @@ namespace Ombi.HealthChecks { builder.AddCheck("Plex", tags: new string[] { "MediaServer" }); builder.AddCheck("Emby", tags: new string[] { "MediaServer" }); + builder.AddCheck("Jellyfin", tags: new string[] { "MediaServer" }); builder.AddCheck("Lidarr", tags: new string[] { "DVR" }); builder.AddCheck("Sonarr", tags: new string[] { "DVR" }); builder.AddCheck("Radarr", tags: new string[] { "DVR" }); diff --git a/src/Ombi.HealthChecks/Ombi.HealthChecks.csproj b/src/Ombi.HealthChecks/Ombi.HealthChecks.csproj index 5171c9c36..20cb07609 100644 --- a/src/Ombi.HealthChecks/Ombi.HealthChecks.csproj +++ b/src/Ombi.HealthChecks/Ombi.HealthChecks.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Ombi.Helpers.Tests/EmbyHelperTests.cs b/src/Ombi.Helpers.Tests/EmbyHelperTests.cs index 261ba87cc..50ac9c48f 100644 --- a/src/Ombi.Helpers.Tests/EmbyHelperTests.cs +++ b/src/Ombi.Helpers.Tests/EmbyHelperTests.cs @@ -15,13 +15,6 @@ namespace Ombi.Helpers.Tests return EmbyHelper.GetEmbyMediaUrl(mediaId, serverId, url); } - [TestCaseSource(nameof(JellyfinUrlData))] - public string TestJellyfinUrl(string mediaId, string url, string serverId) - { - // http://192.168.68.X:8097/web/index.html#!/details?id=7ffe222498445d5ebfddb31bc4fa9a6d&serverId=50cce67f0baa425093d189b3017331fb - return EmbyHelper.GetEmbyMediaUrl(mediaId, serverId, url, true); - } - public static IEnumerable UrlData { get @@ -33,16 +26,5 @@ namespace Ombi.Helpers.Tests yield return new TestCaseData(mediaId.ToString(), string.Empty, "1").Returns($"https://app.emby.media/web/index.html#!/item?id={mediaId}&serverId=1").SetName("EmbyHelper_GetMediaUrl_WithOutCustomDomain"); } } - - public static IEnumerable JellyfinUrlData - { - get - { - var mediaId = 1; - yield return new TestCaseData(mediaId.ToString(), "http://google.com", "1").Returns($"http://google.com/web/index.html#!/details?id={mediaId}&serverId=1").SetName("EmbyHelperJellyfin_GetMediaUrl_WithCustomDomain_WithoutTrailingSlash"); - yield return new TestCaseData(mediaId.ToString(), "http://google.com/", "1").Returns($"http://google.com/web/index.html#!/details?id={mediaId}&serverId=1").SetName("EmbyHelperJellyfin_GetMediaUrl_WithCustomDomain"); - yield return new TestCaseData(mediaId.ToString(), "https://google.com/", "1").Returns($"https://google.com/web/index.html#!/details?id={mediaId}&serverId=1").SetName("EmbyHelperJellyfin_GetMediaUrl_WithCustomDomain_Https"); - } - } } } diff --git a/src/Ombi.Helpers.Tests/JellyfinHelperTests.cs b/src/Ombi.Helpers.Tests/JellyfinHelperTests.cs new file mode 100644 index 000000000..df3f960d4 --- /dev/null +++ b/src/Ombi.Helpers.Tests/JellyfinHelperTests.cs @@ -0,0 +1,29 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ombi.Helpers.Tests +{ + [TestFixture] + public class JellyfinHelperTests + { + [TestCaseSource(nameof(UrlData))] + public string TestUrl(string mediaId, string url, string serverId) + { + // http://192.168.68.X:8097/web/index.html#!/details?id=7ffe222498445d5ebfddb31bc4fa9a6d&serverId=50cce67f0baa425093d189b3017331fb + return JellyfinHelper.GetJellyfinMediaUrl(mediaId, serverId, url); + } + + public static IEnumerable UrlData + { + get + { + var mediaId = 1; + yield return new TestCaseData(mediaId.ToString(), "http://google.com", "1").Returns($"http://google.com/web/index.html#!/details?id={mediaId}&serverId=1").SetName("JellyfinHelper_GetMediaUrl_WithCustomDomain_WithoutTrailingSlash"); + yield return new TestCaseData(mediaId.ToString(), "http://google.com/", "1").Returns($"http://google.com/web/index.html#!/details?id={mediaId}&serverId=1").SetName("JellyfinHelper_GetMediaUrl_WithCustomDomain"); + yield return new TestCaseData(mediaId.ToString(), "https://google.com/", "1").Returns($"https://google.com/web/index.html#!/details?id={mediaId}&serverId=1").SetName("JellyfinHelper_GetMediaUrl_WithCustomDomain_Https"); + } + } + } +} diff --git a/src/Ombi.Helpers/EmbyHelper.cs b/src/Ombi.Helpers/EmbyHelper.cs index 785ca47d4..db739b375 100644 --- a/src/Ombi.Helpers/EmbyHelper.cs +++ b/src/Ombi.Helpers/EmbyHelper.cs @@ -2,14 +2,10 @@ { public static class EmbyHelper { - public static string GetEmbyMediaUrl(string mediaId, string serverId, string customerServerUrl = null, bool isJellyfin = false) + public static string GetEmbyMediaUrl(string mediaId, string serverId, string customerServerUrl = null) { //web/index.html#!/details|item string path = "item"; - if (isJellyfin) - { - path = "details"; - } if (customerServerUrl.HasValue()) { if (!customerServerUrl.EndsWith("/")) diff --git a/src/Ombi.Helpers/JellyfinHelper.cs b/src/Ombi.Helpers/JellyfinHelper.cs new file mode 100644 index 000000000..506341d7d --- /dev/null +++ b/src/Ombi.Helpers/JellyfinHelper.cs @@ -0,0 +1,23 @@ +namespace Ombi.Helpers +{ + public static class JellyfinHelper + { + public static string GetJellyfinMediaUrl(string mediaId, string serverId, string customerServerUrl = null) + { + //web/index.html#!/details|item + string path = "details"; + if (customerServerUrl.HasValue()) + { + if (!customerServerUrl.EndsWith("/")) + { + return $"{customerServerUrl}/web/index.html#!/{path}?id={mediaId}&serverId={serverId}"; + } + return $"{customerServerUrl}web/index.html#!/{path}?id={mediaId}&serverId={serverId}"; + } + else + { + return $"http://localhost:8096/web/index.html#!/{path}?id={mediaId}&serverId={serverId}"; + } + } + } +} diff --git a/src/Ombi.Helpers/LoggingEvents.cs b/src/Ombi.Helpers/LoggingEvents.cs index a7c61d9d2..ed6a7bc2a 100644 --- a/src/Ombi.Helpers/LoggingEvents.cs +++ b/src/Ombi.Helpers/LoggingEvents.cs @@ -14,8 +14,10 @@ namespace Ombi.Helpers public static EventId RadarrCacher => new EventId(2001); public static EventId PlexEpisodeCacher => new EventId(2002); public static EventId EmbyContentCacher => new EventId(2003); + public static EventId JellyfinContentCacher => new EventId(2012); public static EventId PlexUserImporter => new EventId(2004); public static EventId EmbyUserImporter => new EventId(2005); + public static EventId JellyfinUserImporter => new EventId(2013); public static EventId SonarrCacher => new EventId(2006); public static EventId CouchPotatoCacher => new EventId(2007); public static EventId PlexContentCacher => new EventId(2008); @@ -43,4 +45,4 @@ namespace Ombi.Helpers public static EventId Updater => new EventId(6000); } -} \ No newline at end of file +} diff --git a/src/Ombi.Helpers/PlexHelper.cs b/src/Ombi.Helpers/PlexHelper.cs index 4dbde0c92..08548b8c9 100644 --- a/src/Ombi.Helpers/PlexHelper.cs +++ b/src/Ombi.Helpers/PlexHelper.cs @@ -42,7 +42,7 @@ namespace Ombi.Helpers //com.plexapp.agents.themoviedb://390043?lang=en //com.plexapp.agents.imdb://tt2543164?lang=en //plex://movie/5e1632df2d4d84003e48e54e - // https://github.com/tidusjar/Ombi/issues/3277 + // https://github.com/Ombi-app/Ombi/issues/3277 if (string.IsNullOrEmpty(guid)) { return new ProviderId(); diff --git a/src/Ombi.Helpers/StartupSingleton.cs b/src/Ombi.Helpers/StartupSingleton.cs index 41fd1a9ab..c9df7665a 100644 --- a/src/Ombi.Helpers/StartupSingleton.cs +++ b/src/Ombi.Helpers/StartupSingleton.cs @@ -11,5 +11,8 @@ public string StoragePath { get; set; } public string SecurityKey { get; set; } +#if DEBUG + = "test"; +#endif } } \ No newline at end of file diff --git a/src/Ombi.Mapping/Profiles/TvProfile.cs b/src/Ombi.Mapping/Profiles/TvProfile.cs index 8cb4e38b6..0e1378d95 100644 --- a/src/Ombi.Mapping/Profiles/TvProfile.cs +++ b/src/Ombi.Mapping/Profiles/TvProfile.cs @@ -47,7 +47,7 @@ namespace Ombi.Mapping.Profiles CreateMap() - .ForMember(dest => dest.Id, opts => opts.MapFrom(src => Convert.ToInt32(src.Ids.Tvdb.ToString()))) + .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.Ids.Tvdb.HasValue ? Convert.ToInt32(src.Ids.Tvdb.ToString()) : 0)) .ForMember(dest => dest.FirstAired, opts => opts.MapFrom(src => src.FirstAired.HasValue ? src.FirstAired.Value.ToString("yyyy-MM-ddTHH:mm:ss") : string.Empty)) .ForMember(dest => dest.ImdbId, opts => opts.MapFrom(src => src.Ids.Imdb)) .ForMember(dest => dest.Network, opts => opts.MapFrom(src => src.Network)) @@ -57,9 +57,9 @@ namespace Ombi.Mapping.Profiles .ForMember(dest => dest.Title, opts => opts.MapFrom(src => src.Title)) .ForMember(dest => dest.Status, opts => opts.MapFrom(src => TraktEnumHelper.GetDescription(src.Status))) .ForMember(dest => dest.Trailer, - opts => opts.MapFrom(src => src.Trailer.ToString().ToHttpsUrl())) + opts => opts.MapFrom(src => src.Trailer != null ? src.Trailer.ToString().ToHttpsUrl() : string.Empty)) .ForMember(dest => dest.Homepage, - opts => opts.MapFrom(src => src.Homepage.ToString().ToHttpsUrl())); + opts => opts.MapFrom(src => src.Homepage != null ? src.Homepage.ToString().ToHttpsUrl() : string.Empty)); } } } \ No newline at end of file diff --git a/src/Ombi.Notifications.Templates/Templates/BasicTemplate.html b/src/Ombi.Notifications.Templates/Templates/BasicTemplate.html index ac4c4a119..9f2564b99 100644 --- a/src/Ombi.Notifications.Templates/Templates/BasicTemplate.html +++ b/src/Ombi.Notifications.Templates/Templates/BasicTemplate.html @@ -166,7 +166,7 @@ - Powered by Ombi {@DATENOW} + Powered by Ombi {@DATENOW} diff --git a/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html b/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html index 5456743c9..10b76dd30 100644 --- a/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html +++ b/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html @@ -1,4 +1,4 @@ - + @@ -453,7 +453,7 @@ - Powered by Ombi + Powered by Ombi diff --git a/src/Ombi.Schedule/Jobs/Couchpotato/CouchPotatoSync.cs b/src/Ombi.Schedule/Jobs/Couchpotato/CouchPotatoSync.cs index 98a0c0910..cc05b711a 100644 --- a/src/Ombi.Schedule/Jobs/Couchpotato/CouchPotatoSync.cs +++ b/src/Ombi.Schedule/Jobs/Couchpotato/CouchPotatoSync.cs @@ -77,12 +77,16 @@ namespace Ombi.Schedule.Jobs.Couchpotato var movies = await _api.GetMovies(settings.FullUri, settings.ApiKey, new[] {"active"}); if (movies != null) { - // Let's remove the old cached data - using (var tran = await _ctx.Database.BeginTransactionAsync()) + var strat = _ctx.Database.CreateExecutionStrategy(); + await strat.ExecuteAsync(async () => { - await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM CouchPotatoCache"); - tran.Commit(); - } + // Let's remove the old cached data + using (var tran = await _ctx.Database.BeginTransactionAsync()) + { + await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM CouchPotatoCache"); + tran.Commit(); + } + }); // Save var movieIds = new List(); @@ -102,14 +106,17 @@ namespace Ombi.Schedule.Jobs.Couchpotato _log.LogError("TMDBId is not > 0 for movie {0}", m.title); } } - - using (var tran = await _ctx.Database.BeginTransactionAsync()) + strat = _ctx.Database.CreateExecutionStrategy(); + await strat.ExecuteAsync(async () => { - await _ctx.CouchPotatoCache.AddRangeAsync(movieIds); + using (var tran = await _ctx.Database.BeginTransactionAsync()) + { + await _ctx.CouchPotatoCache.AddRangeAsync(movieIds); - await _ctx.SaveChangesAsync(); - tran.Commit(); - } + await _ctx.SaveChangesAsync(); + tran.Commit(); + } + }); await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) .SendAsync(NotificationHub.NotificationEvent, "Couch Potato Sync Finished"); diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs index f8bde2755..866216fe4 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs @@ -58,7 +58,7 @@ namespace Ombi.Schedule.Jobs.Emby { await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) .SendAsync(NotificationHub.NotificationEvent, "Emby Content Sync Failed"); - _logger.LogError(e, "Exception when caching {1} for server {0}", server.Name, embySettings.IsJellyfin ? "Jellyfin" : "Emby"); + _logger.LogError(e, "Exception when caching Emby for server {0}", server.Name); } } @@ -145,7 +145,7 @@ namespace Ombi.Schedule.Jobs.Emby Title = tvShow.Name, Type = EmbyMediaType.Series, EmbyId = tvShow.Id, - Url = EmbyHelper.GetEmbyMediaUrl(tvShow.Id, server?.ServerId, server.ServerHostname, settings.IsJellyfin), + Url = EmbyHelper.GetEmbyMediaUrl(tvShow.Id, server?.ServerId, server.ServerHostname), AddedAt = DateTime.UtcNow }); } @@ -228,4 +228,4 @@ namespace Ombi.Schedule.Jobs.Emby } } -} \ No newline at end of file +} diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyUserImporter.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyUserImporter.cs index 1207e1f42..4b684b8ab 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyUserImporter.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyUserImporter.cs @@ -80,7 +80,7 @@ namespace Ombi.Schedule.Jobs.Emby Api = _apiFactory.CreateClient(settings); await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) - .SendAsync(NotificationHub.NotificationEvent, $"{(settings.IsJellyfin ? "Jellyfin" : "Emby")} User Importer Started"); + .SendAsync(NotificationHub.NotificationEvent, $"Emby User Importer Started"); var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.EmbyUser || x.UserType == UserType.EmbyConnectUser).ToListAsync(); foreach (var server in settings.Servers) { @@ -117,7 +117,8 @@ namespace Ombi.Schedule.Jobs.Emby ProviderUserId = embyUser.Id, Alias = isConnectUser ? embyUser.Name : string.Empty, MovieRequestLimit = userManagementSettings.MovieRequestLimit, - EpisodeRequestLimit = userManagementSettings.EpisodeRequestLimit + EpisodeRequestLimit = userManagementSettings.EpisodeRequestLimit, + StreamingCountry = userManagementSettings.DefaultStreamingCountry }; var result = await _userManager.CreateAsync(newUser); if (!result.Succeeded) @@ -180,4 +181,4 @@ namespace Ombi.Schedule.Jobs.Emby GC.SuppressFinalize(this); } } -} \ No newline at end of file +} diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinAvaliabilityChecker.cs b/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinAvaliabilityChecker.cs new file mode 100644 index 000000000..7b89fa3f2 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinAvaliabilityChecker.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Ombi.Schedule.Jobs.Jellyfin +{ + public interface IJellyfinAvaliabilityChecker : IBaseJob + { + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinContentSync.cs b/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinContentSync.cs new file mode 100644 index 000000000..e9ef28cc6 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinContentSync.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Ombi.Schedule.Jobs.Jellyfin +{ + public interface IJellyfinContentSync : IBaseJob + { + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinEpisodeSync.cs b/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinEpisodeSync.cs new file mode 100644 index 000000000..53392cc6a --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinEpisodeSync.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Ombi.Schedule.Jobs.Jellyfin +{ + public interface IJellyfinEpisodeSync : IBaseJob + { + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinUserImporter.cs b/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinUserImporter.cs new file mode 100644 index 000000000..06ff204f8 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Jellyfin/IJellyfinUserImporter.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Ombi.Schedule.Jobs.Jellyfin +{ + public interface IJellyfinUserImporter : IBaseJob + { + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinAvaliabilityChecker.cs b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinAvaliabilityChecker.cs new file mode 100644 index 000000000..60d017d93 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinAvaliabilityChecker.cs @@ -0,0 +1,235 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: JellyfinAvaliabilityCheker.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Ombi.Core; +using Ombi.Core.Notifications; +using Ombi.Helpers; +using Ombi.Hubs; +using Ombi.Notifications.Models; +using Ombi.Schedule.Jobs.Ombi; +using Ombi.Store.Entities; +using Ombi.Store.Repository; +using Ombi.Store.Repository.Requests; +using Quartz; + +namespace Ombi.Schedule.Jobs.Jellyfin +{ + public class JellyfinAvaliabilityChecker : IJellyfinAvaliabilityChecker + { + public JellyfinAvaliabilityChecker(IJellyfinContentRepository repo, ITvRequestRepository t, IMovieRequestRepository m, + INotificationHelper n, ILogger log, IHubContext notification) + { + _repo = repo; + _tvRepo = t; + _movieRepo = m; + _notificationService = n; + _log = log; + _notification = notification; + } + + private readonly ITvRequestRepository _tvRepo; + private readonly IMovieRequestRepository _movieRepo; + private readonly IJellyfinContentRepository _repo; + private readonly INotificationHelper _notificationService; + private readonly ILogger _log; + private readonly IHubContext _notification; + + public async Task Execute(IJobExecutionContext job) + { + _log.LogInformation("Starting Jellyfin Availability Check"); + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) + .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Availability Checker Started"); + await ProcessMovies(); + await ProcessTv(); + + _log.LogInformation("Finished Jellyfin Availability Check"); + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) + .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Availability Checker Finished"); + } + + private async Task ProcessMovies() + { + var movies = _movieRepo.GetAll().Include(x => x.RequestedUser).Where(x => !x.Available); + + foreach (var movie in movies) + { + JellyfinContent jellyfinContent = null; + if (movie.TheMovieDbId > 0) + { + jellyfinContent = await _repo.GetByTheMovieDbId(movie.TheMovieDbId.ToString()); + } + else if(movie.ImdbId.HasValue()) + { + jellyfinContent = await _repo.GetByImdbId(movie.ImdbId); + } + + if (jellyfinContent == null) + { + // We don't have this yet + continue; + } + + _log.LogInformation("We have found the request {0} on Jellyfin, sending the notification", movie?.Title ?? string.Empty); + + movie.Available = true; + movie.MarkedAsAvailable = DateTime.Now; + if (movie.Available) + { + var recipient = movie.RequestedUser.Email.HasValue() ? movie.RequestedUser.Email : string.Empty; + + _log.LogDebug("MovieId: {0}, RequestUser: {1}", movie.Id, recipient); + + await _notificationService.Notify(new NotificationOptions + { + DateTime = DateTime.Now, + NotificationType = NotificationType.RequestAvailable, + RequestId = movie.Id, + RequestType = RequestType.Movie, + Recipient = recipient, + }); + } + } + await _movieRepo.Save(); + } + + + + /// + /// TODO This is EXCATLY the same as the PlexAvailabilityChecker. Refactor Please future Jamie + /// + /// + private async Task ProcessTv() + { + var tv = _tvRepo.GetChild().Where(x => !x.Available); + var jellyfinEpisodes = _repo.GetAllEpisodes().Include(x => x.Series); + + foreach (var child in tv) + { + + var useImdb = false; + var useTvDb = false; + if (child.ParentRequest.ImdbId.HasValue()) + { + useImdb = true; + } + + if (child.ParentRequest.TvDbId.ToString().HasValue()) + { + useTvDb = true; + } + + var tvDbId = child.ParentRequest.TvDbId; + var imdbId = child.ParentRequest.ImdbId; + IQueryable seriesEpisodes = null; + if (useImdb) + { + seriesEpisodes = jellyfinEpisodes.Where(x => x.Series.ImdbId == imdbId.ToString()); + } + + if (useTvDb && (seriesEpisodes == null || !seriesEpisodes.Any())) + { + seriesEpisodes = jellyfinEpisodes.Where(x => x.Series.TvDbId == tvDbId.ToString()); + } + + if (seriesEpisodes == null) + { + continue; + } + + if (!seriesEpisodes.Any()) + { + // Let's try and match the series by name + seriesEpisodes = jellyfinEpisodes.Where(x => + x.Series.Title == child.Title); + } + + foreach (var season in child.SeasonRequests) + { + foreach (var episode in season.Episodes) + { + if (episode.Available) + { + continue; + } + + var foundEp = await seriesEpisodes.FirstOrDefaultAsync( + x => x.EpisodeNumber == episode.EpisodeNumber && + x.SeasonNumber == episode.Season.SeasonNumber); + + if (foundEp != null) + { + episode.Available = true; + } + } + } + + // Check to see if all of the episodes in all seasons are available for this request + var allAvailable = child.SeasonRequests.All(x => x.Episodes.All(c => c.Available)); + if (allAvailable) + { + // We have fulfulled this request! + child.Available = true; + child.MarkedAsAvailable = DateTime.Now; + await _notificationService.Notify(new NotificationOptions + { + DateTime = DateTime.Now, + NotificationType = NotificationType.RequestAvailable, + RequestId = child.Id, + RequestType = RequestType.TvShow, + Recipient = child.RequestedUser.Email + }); + } + } + + await _tvRepo.Save(); + } + + private bool _disposed; + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + } + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs new file mode 100644 index 000000000..ff96e2130 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; +using Ombi.Api.Jellyfin; +using Ombi.Api.Jellyfin.Models.Movie; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Helpers; +using Ombi.Hubs; +using Ombi.Schedule.Jobs.Ombi; +using Ombi.Store.Entities; +using Ombi.Store.Repository; +using Quartz; +using JellyfinMediaType = Ombi.Store.Entities.JellyfinMediaType; + +namespace Ombi.Schedule.Jobs.Jellyfin +{ + public class JellyfinContentSync : IJellyfinContentSync + { + public JellyfinContentSync(ISettingsService settings, IJellyfinApiFactory api, ILogger logger, + IJellyfinContentRepository repo, IHubContext notification) + { + _logger = logger; + _settings = settings; + _apiFactory = api; + _repo = repo; + _notification = notification; + } + + private readonly ILogger _logger; + private readonly ISettingsService _settings; + private readonly IJellyfinApiFactory _apiFactory; + private readonly IJellyfinContentRepository _repo; + private readonly IHubContext _notification; + private IJellyfinApi Api { get; set; } + + public async Task Execute(IJobExecutionContext job) + { + var jellyfinSettings = await _settings.GetSettingsAsync(); + if (!jellyfinSettings.Enable) + return; + + Api = _apiFactory.CreateClient(jellyfinSettings); + + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) + .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Content Sync Started"); + + foreach (var server in jellyfinSettings.Servers) + { + try + { + await StartServerCache(server, jellyfinSettings); + } + catch (Exception e) + { + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) + .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Content Sync Failed"); + _logger.LogError(e, "Exception when caching Jellyfin for server {0}", server.Name); + } + } + + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) + .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Content Sync Finished"); + // Episodes + + await OmbiQuartz.TriggerJob(nameof(IJellyfinEpisodeSync), "Jellyfin"); + } + + + private async Task StartServerCache(JellyfinServers server, JellyfinSettings settings) + { + if (!ValidateSettings(server)) + return; + + //await _repo.ExecuteSql("DELETE FROM JellyfinEpisode"); + //await _repo.ExecuteSql("DELETE FROM JellyfinContent"); + + var movies = await Api.GetAllMovies(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); + var totalCount = movies.TotalRecordCount; + var processed = 1; + + var mediaToAdd = new HashSet(); + + while (processed < totalCount) + { + foreach (var movie in movies.Items) + { + if (movie.Type.Equals("boxset", StringComparison.InvariantCultureIgnoreCase)) + { + var movieInfo = + await Api.GetCollection(movie.Id, server.ApiKey, server.AdministratorId, server.FullUri); + foreach (var item in movieInfo.Items) + { + await ProcessMovies(item, mediaToAdd, server); + } + + processed++; + } + else + { + processed++; + // Regular movie + await ProcessMovies(movie, mediaToAdd, server); + } + } + + // Get the next batch + movies = await Api.GetAllMovies(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); + await _repo.AddRange(mediaToAdd); + mediaToAdd.Clear(); + + } + + + // TV Time + var tv = await Api.GetAllShows(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); + var totalTv = tv.TotalRecordCount; + processed = 1; + while (processed < totalTv) + { + foreach (var tvShow in tv.Items) + { + try + { + + processed++; + if (string.IsNullOrEmpty(tvShow.ProviderIds?.Tvdb)) + { + _logger.LogInformation("Provider Id on tv {0} is null", tvShow.Name); + continue; + } + + var existingTv = await _repo.GetByJellyfinId(tvShow.Id); + if (existingTv == null) + { + _logger.LogDebug("Adding new TV Show {0}", tvShow.Name); + mediaToAdd.Add(new JellyfinContent + { + TvDbId = tvShow.ProviderIds?.Tvdb, + ImdbId = tvShow.ProviderIds?.Imdb, + TheMovieDbId = tvShow.ProviderIds?.Tmdb, + Title = tvShow.Name, + Type = JellyfinMediaType.Series, + JellyfinId = tvShow.Id, + Url = JellyfinHelper.GetJellyfinMediaUrl(tvShow.Id, server?.ServerId, server.ServerHostname), + AddedAt = DateTime.UtcNow + }); + } + else + { + _logger.LogDebug("We already have TV Show {0}", tvShow.Name); + } + + } + catch (Exception) + { + + throw; + } + } + // Get the next batch + tv = await Api.GetAllShows(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); + await _repo.AddRange(mediaToAdd); + mediaToAdd.Clear(); + } + + if (mediaToAdd.Any()) + await _repo.AddRange(mediaToAdd); + } + + private async Task ProcessMovies(JellyfinMovie movieInfo, ICollection content, JellyfinServers server) + { + // Check if it exists + var existingMovie = await _repo.GetByJellyfinId(movieInfo.Id); + var alreadyGoingToAdd = content.Any(x => x.JellyfinId == movieInfo.Id); + if (existingMovie == null && !alreadyGoingToAdd) + { + _logger.LogDebug("Adding new movie {0}", movieInfo.Name); + content.Add(new JellyfinContent + { + ImdbId = movieInfo.ProviderIds.Imdb, + TheMovieDbId = movieInfo.ProviderIds?.Tmdb, + Title = movieInfo.Name, + Type = JellyfinMediaType.Movie, + JellyfinId = movieInfo.Id, + Url = JellyfinHelper.GetJellyfinMediaUrl(movieInfo.Id, server?.ServerId, server.ServerHostname), + AddedAt = DateTime.UtcNow, + }); + } + else + { + // we have this + _logger.LogDebug("We already have movie {0}", movieInfo.Name); + } + } + + private bool ValidateSettings(JellyfinServers server) + { + if (server?.Ip == null || string.IsNullOrEmpty(server?.ApiKey)) + { + _logger.LogInformation(LoggingEvents.JellyfinContentCacher, $"Server {server?.Name} is not configured correctly"); + return false; + } + + return true; + } + + private bool _disposed; + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + //_settings?.Dispose(); + } + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } + +} diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs new file mode 100644 index 000000000..11c7ec7af --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs @@ -0,0 +1,181 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: JellyfinEpisodeCacher.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; +using Ombi.Api.Jellyfin; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Hubs; +using Ombi.Helpers; +using Ombi.Store.Entities; +using Ombi.Store.Repository; +using Quartz; +using Ombi.Schedule.Jobs.Ombi; + +namespace Ombi.Schedule.Jobs.Jellyfin +{ + public class JellyfinEpisodeSync : IJellyfinEpisodeSync + { + public JellyfinEpisodeSync(ISettingsService s, IJellyfinApiFactory api, ILogger l, IJellyfinContentRepository repo + , IHubContext