pull/3996/head
tidusjar 4 years ago
commit eedd67b205

@ -25,10 +25,6 @@ namespace Ombi.Api.Emby
public IEmbyApi CreateClient(EmbySettings settings)
{
if (settings.IsJellyfin)
{
return new JellyfinApi(_api);
}
return new EmbyApi(_api);
}
}

@ -5,15 +5,8 @@
public string LocalAddress { get; set; }
public string ServerName { get; set; }
public string Version { get; set; }
/// <summary>
/// Only populated for Jellyfin
/// </summary>
public string ProductName { get; set; }
public bool IsJellyfin => !string.IsNullOrEmpty(ProductName) && ProductName.Contains("Jellyfin");
public string OperatingSystem { get; set; }
public string Id { get; set; }
}
}
}

@ -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<JellyfinSystemInfo> GetSystemInformation(string apiKey, string baseUrl);
Task<List<JellyfinUser>> GetUsers(string baseUri, string apiKey);
Task<JellyfinUser> LogIn(string username, string password, string apiKey, string baseUri);
Task<JellyfinItemContainer<JellyfinMovie>> GetAllMovies(string apiKey, int startIndex, int count, string userId,
string baseUri);
Task<JellyfinItemContainer<JellyfinEpisodes>> GetAllEpisodes(string apiKey, int startIndex, int count, string userId,
string baseUri);
Task<JellyfinItemContainer<JellyfinSeries>> GetAllShows(string apiKey, int startIndex, int count, string userId,
string baseUri);
Task<JellyfinItemContainer<JellyfinMovie>> GetCollection(string mediaId,
string apiKey, string userId, string baseUrl);
Task<SeriesInformation> GetSeriesInformation(string mediaId, string apiKey, string userId, string baseUrl);
Task<MovieInformation> GetMovieInformation(string mediaId, string apiKey, string userId, string baseUrl);
Task<EpisodeInformation> GetEpisodeInformation(string mediaId, string apiKey, string userId, string baseUrl);
Task<PublicInfo> GetPublicInformation(string baseUrl);
}
}

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Ombi.Api.Jellyfin.Models;
namespace Ombi.Api.Jellyfin
{
public interface IJellyfinApi : IBaseJellyfinApi
{
Task<JellyfinConnectUser> LoginConnectUser(string username, string password);
}
}

@ -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; }
/// <summary>
/// Returns all users from the Emby Instance
/// Returns all users from the Jellyfin Instance
/// </summary>
/// <param name="baseUri"></param>
/// <param name="apiKey"></param>
public async Task<List<EmbyUser>> GetUsers(string baseUri, string apiKey)
public async Task<List<JellyfinUser>> GetUsers(string baseUri, string apiKey)
{
var request = new Request("users", baseUri, HttpMethod.Get);
AddHeaders(request, apiKey);
var obj = await Api.Request<List<EmbyUser>>(request);
var obj = await Api.Request<List<JellyfinUser>>(request);
return obj;
}
public async Task<EmbySystemInfo> GetSystemInformation(string apiKey, string baseUrl)
public async Task<JellyfinSystemInfo> GetSystemInformation(string apiKey, string baseUrl)
{
var request = new Request("System/Info", baseUrl, HttpMethod.Get);
AddHeaders(request, apiKey);
var obj = await Api.Request<EmbySystemInfo>(request);
var obj = await Api.Request<JellyfinSystemInfo>(request);
return obj;
}
@ -56,7 +56,7 @@ namespace Ombi.Api.Emby
return obj;
}
public async Task<EmbyUser> LogIn(string username, string password, string apiKey, string baseUri)
public async Task<JellyfinUser> 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<EmbyUser>(request);
var obj = await Api.Request<JellyfinUser>(request);
return obj;
}
public async Task<EmbyItemContainer<EmbyMovie>> GetCollection(string mediaId, string apiKey, string userId, string baseUrl)
public async Task<JellyfinItemContainer<JellyfinMovie>> 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<EmbyItemContainer<EmbyMovie>>(request);
return await Api.Request<JellyfinItemContainer<JellyfinMovie>>(request);
}
public async Task<EmbyItemContainer<EmbyMovie>> GetAllMovies(string apiKey, int startIndex, int count, string userId, string baseUri)
public async Task<JellyfinItemContainer<JellyfinMovie>> GetAllMovies(string apiKey, int startIndex, int count, string userId, string baseUri)
{
return await GetAll<EmbyMovie>("Movie", apiKey, userId, baseUri, true, startIndex, count);
return await GetAll<JellyfinMovie>("Movie", apiKey, userId, baseUri, true, startIndex, count);
}
public async Task<EmbyItemContainer<EmbyEpisodes>> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, string baseUri)
public async Task<JellyfinItemContainer<JellyfinEpisodes>> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, string baseUri)
{
return await GetAll<EmbyEpisodes>("Episode", apiKey, userId, baseUri, false, startIndex, count);
return await GetAll<JellyfinEpisodes>("Episode", apiKey, userId, baseUri, false, startIndex, count);
}
public async Task<EmbyItemContainer<EmbySeries>> GetAllShows(string apiKey, int startIndex, int count, string userId, string baseUri)
public async Task<JellyfinItemContainer<JellyfinSeries>> GetAllShows(string apiKey, int startIndex, int count, string userId, string baseUri)
{
return await GetAll<EmbySeries>("Series", apiKey, userId, baseUri, false, startIndex, count);
return await GetAll<JellyfinSeries>("Series", apiKey, userId, baseUri, false, startIndex, count);
}
public async Task<SeriesInformation> GetSeriesInformation(string mediaId, string apiKey, string userId, string baseUrl)
@ -126,7 +126,7 @@ namespace Ombi.Api.Emby
return JsonConvert.DeserializeObject<T>(response);
}
private async Task<EmbyItemContainer<T>> GetAll<T>(string type, string apiKey, string userId, string baseUri, bool includeOverview = false)
private async Task<JellyfinItemContainer<T>> GetAll<T>(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<EmbyItemContainer<T>>(request);
var obj = await Api.Request<JellyfinItemContainer<T>>(request);
return obj;
}
private async Task<EmbyItemContainer<T>> GetAll<T>(string type, string apiKey, string userId, string baseUri, bool includeOverview, int startIndex, int count)
private async Task<JellyfinItemContainer<T>> GetAll<T>(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<EmbyItemContainer<T>>(request);
var obj = await Api.Request<JellyfinItemContainer<T>>(request);
return obj;
}
@ -172,7 +172,7 @@ namespace Ombi.Api.Emby
req.AddHeader("Device", "Ombi");
}
public Task<EmbyConnectUser> LoginConnectUser(string username, string password)
public Task<JellyfinConnectUser> LoginConnectUser(string username, string password)
{
throw new System.NotImplementedException();
}

@ -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> _jellyfinSettings;
private readonly IApi _api;
// TODO, if we need to derive futher, need to rework
public JellyfinApiFactory(ISettingsService<JellyfinSettings> jellyfinSettings, IApi api)
{
_jellyfinSettings = jellyfinSettings;
_api = api;
}
public async Task<IJellyfinApi> CreateClient()
{
var settings = await _jellyfinSettings.GetSettingsAsync();
return CreateClient(settings);
}
public IJellyfinApi CreateClient(JellyfinSettings settings)
{
return new JellyfinApi(_api);
}
}
public interface IJellyfinApiFactory
{
Task<IJellyfinApi> CreateClient();
IJellyfinApi CreateClient(JellyfinSettings settings);
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace Ombi.Api.Jellyfin.Models
{
public class JellyfinItemContainer<T>
{
public List<T> Items { get; set; }
public int TotalRecordCount { get; set; }
}
}

@ -0,0 +1,10 @@
namespace Ombi.Api.Jellyfin.Models
{
public enum JellyfinMediaType
{
Movie = 0,
Series = 1,
Music = 2,
Episode = 3
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -0,0 +1,7 @@
namespace Ombi.Api.Jellyfin.Models
{
public class JellyfinUserLogin
{
public JellyfinUser User { get; set; }
}
}

@ -0,0 +1,8 @@
namespace Ombi.Api.Jellyfin.Models.Movie
{
public class JellyfinChapter
{
public long StartPositionTicks { get; set; }
public string Name { get; set; }
}
}

@ -0,0 +1,8 @@
namespace Ombi.Api.Jellyfin.Models.Movie
{
public class JellyfinExternalurl
{
public string Name { get; set; }
public string Url { get; set; }
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -0,0 +1,8 @@
namespace Ombi.Api.Jellyfin.Models.Movie
{
public class JellyfinRemotetrailer
{
public string Url { get; set; }
public string Name { get; set; }
}
}

@ -0,0 +1,8 @@
namespace Ombi.Api.Jellyfin.Models.Movie
{
public class JellyfinStudio
{
public string Name { get; set; }
public string Id { get; set; }
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -0,0 +1,8 @@
namespace Ombi.Api.Jellyfin.Models.Media.Tv
{
public class JellyfinRemotetrailer
{
public string Url { get; set; }
public string Name { get; set; }
}
}

@ -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; }
}
}

@ -0,0 +1,8 @@
namespace Ombi.Api.Jellyfin.Models.Media.Tv
{
public class JellyfinSeriesstudioinfo
{
public string Name { get; set; }
public string Id { get; set; }
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<AssemblyVersion>3.0.0.0</AssemblyVersion>
<FileVersion>3.0.0.0</FileVersion>
<Version></Version>
<PackageVersion></PackageVersion>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ombi.Api\Ombi.Api.csproj" />
<ProjectReference Include="..\Ombi.Helpers\Ombi.Helpers.csproj" />
</ItemGroup>
</Project>

@ -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<IEnumerable<Artist>> SearchArtist(string artistQuery);
Task<IEnumerable<Release>> GetReleaseForArtist(string artistId);
Task<Artist> GetArtistInformation(string artistId);
Task<Release> GetAlbumInformation(string albumId);
Task<ReleaseGroupArt> GetCoverArtForReleaseGroup(string musicBrainzId, CancellationToken token);
}
}

@ -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<Release> GetAlbumInformation(string albumId)
{
var album = Release.GetAsync(albumId);
return album;
}
public async Task<IEnumerable<Artist>> SearchArtist(string artistQuery)
{
var artist = await Artist.SearchAsync(artistQuery, 10);

@ -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<MovieRatings> GetMovieRatings(string movieName, int movieYear);
Task<TvRatings> GetTvRatings(string showName, int showYear);
}
}

@ -0,0 +1,35 @@
using System.Collections.Generic;
namespace Ombi.Api.RottenTomatoes.Models
{
public class RottenTomatoesMovieResponse
{
public int total { get; set; }
public List<Movie> 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; }
}
}

@ -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; }
}
}

@ -0,0 +1,8 @@
namespace Ombi.Api.RottenTomatoes.Models
{
public class TvRatings
{
public string Class { get; set; }
public int Score { get; set; }
}
}

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ombi.Api\Ombi.Api.csproj" />
</ItemGroup>
</Project>

@ -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<MovieRatings> 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<RottenTomatoesMovieResponse>(request);
var movieFound = result.movies.FirstOrDefault(x => x.year == movieYear);
if (movieFound == null)
{
return null;
}
return movieFound.ratings;
}
public async Task<TvRatings> 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<RottenTomatoesTvResponse>(request);
var showFound = result.tvSeries.FirstOrDefault(x => x.startYear == showYear);
if (showFound == null)
{
return null;
}
return new TvRatings
{
Class = showFound.meterClass,
Score = showFound.meterScore
};
}
}
}

@ -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; }

@ -115,4 +115,4 @@ namespace Ombi.Core.Tests.Rule.Search
Assert.False(search.Available);
}
}
}
}

@ -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<IJellyfinContentRepository>();
SettingsMock = new Mock<ISettingsService<JellyfinSettings>>();
Rule = new JellyfinAvailabilityRule(ContextMock.Object, SettingsMock.Object);
}
private JellyfinAvailabilityRule Rule { get; set; }
private Mock<IJellyfinContentRepository> ContextMock { get; set; }
private Mock<ISettingsService<JellyfinSettings>> 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<string>())).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<JellyfinServers>
{
new JellyfinServers
{
ServerHostname = "http://test.com/",
ServerId = "8"
}
}
});
ContextMock.Setup(x => x.GetByTheMovieDbId(It.IsAny<string>())).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<JellyfinServers>
{
new JellyfinServers
{
Ip = "8080",
Port = 9090,
ServerHostname = string.Empty,
ServerId = "8"
}
}
});
ContextMock.Setup(x => x.GetByTheMovieDbId(It.IsAny<string>())).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<string>())).Returns(Task.FromResult(default(JellyfinContent)));
var search = new SearchMovieViewModel();
var result = await Rule.Execute(search);
Assert.True(result.Success);
Assert.False(search.Available);
}
}
}

@ -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<StreamData>
{
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<StreamData>
{
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<StreamData>
{
new StreamData
{
provider_name = "Netflix",
display_priority = 0,
logo_path = "logo",
provider_id = 8
}
}
}
}
}, new OmbiUser { StreamingCountry = "BLAH" });
Assert.That(result, Is.Empty);
}
}
}

@ -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<OmbiUser> passwordHasher, IEnumerable<IUserValidator<OmbiUser>> userValidators,
IEnumerable<IPasswordValidator<OmbiUser>> passwordValidators, ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<OmbiUser>> logger, IPlexApi plexApi,
IEmbyApiFactory embyApi, ISettingsService<EmbySettings> embySettings, ISettingsService<AuthenticationSettings> auth)
IEmbyApiFactory embyApi, ISettingsService<EmbySettings> embySettings,
IJellyfinApiFactory jellyfinApi, ISettingsService<JellyfinSettings> jellyfinSettings,
ISettingsService<AuthenticationSettings> 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> _embySettings;
private readonly ISettingsService<JellyfinSettings> _jellyfinSettings;
private readonly ISettingsService<AuthenticationSettings> _authSettings;
public override async Task<bool> 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;
}
/// <summary>
/// Sign the user into Jellyfin
/// <remarks>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</remarks>
/// </summary>
/// <param name="user"></param>
/// <param name="password"></param>
/// <returns></returns>
private async Task<bool> 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;
}
}
}
}

@ -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<List<StreamData>> GetUserWatchProvider(WatchProviders providers)
{
var user = await GetUser();
return WatchProviderParser.GetUserWatchProviders(providers, user);
}
private OmbiSettings ombiSettings;
protected async Task<OmbiSettings> GetOmbiSettings()
{

@ -26,5 +26,6 @@ namespace Ombi.Core.Engine.Interfaces
int ResultLimit { get; set; }
Task<MovieFullInfoViewModel> GetMovieInfoByImdbId(string imdbId, CancellationToken requestAborted);
Task<IEnumerable<StreamingData>> GetStreamInformation(int movieDbId, CancellationToken cancellationToken);
}
}

@ -9,5 +9,6 @@ namespace Ombi.Core.Engine.Interfaces
Task<ArtistInformation> GetArtistInformation(string artistId);
Task<ArtistInformation> GetArtistInformationByRequestId(int requestId);
Task<AlbumArt> GetReleaseGroupArt(string musicBrainzId, CancellationToken token);
Task<ReleaseGroup> GetAlbum(string albumId);
}
}

@ -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<SearchFullInfoTvShowViewModel> GetShowInformation(int tvdbid);
Task<SearchFullInfoTvShowViewModel> GetShowByRequest(int requestId);
Task<IEnumerable<StreamingData>> GetStreamInformation(int tvDbId, int tvMazeId, CancellationToken cancellationToken);
}
}

@ -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<MovieRequests> GetRequest(int requestId)
{
var request = await MovieRepository.GetWithUser().Where(x => x.Id == requestId).FirstOrDefaultAsync();
await CheckForSubscription(new HideResult(), new List<MovieRequests>{request });
await CheckForSubscription(new HideResult(), new List<MovieRequests> { request });
return request;
}
@ -654,19 +670,19 @@ namespace Ombi.Core.Engine
};
}
private async Task<RequestEngineResult> AddMovieRequest(MovieRequests model, string movieName)
private async Task<RequestEngineResult> 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,

@ -13,15 +13,17 @@ namespace Ombi.Core.Engine
{
public class RecentlyAddedEngine : IRecentlyAddedEngine
{
public RecentlyAddedEngine(IPlexContentRepository plex, IEmbyContentRepository emby, IRepository<RecentlyAddedLog> recentlyAdded)
public RecentlyAddedEngine(IPlexContentRepository plex, IEmbyContentRepository emby, IJellyfinContentRepository jellyfin, IRepository<RecentlyAddedLog> 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> _recentlyAddedLog;
@ -30,23 +32,26 @@ namespace Ombi.Core.Engine
{
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<RecentlyAddedMovieModel> 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<RecentlyAddedTvModel> 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);
}
@ -54,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<bool> 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<RecentlyAddedLog>();
foreach (var p in plexContent)
{
@ -138,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<RecentlyAddedTvModel> GetRecentlyAddedTv(IQueryable<PlexServerContent> plexTv, IQueryable<EmbyContent> embyTv,
private IEnumerable<RecentlyAddedTvModel> GetRecentlyAddedTv(IQueryable<PlexServerContent> plexTv, IQueryable<EmbyContent> embyTv, IQueryable<JellyfinContent> jellyfinTv,
bool groupBySeason)
{
var model = new HashSet<RecentlyAddedTvModel>();
TransformPlexShows(plexTv, model);
TransformEmbyShows(embyTv, model);
TransformJellyfinShows(jellyfinTv, model);
if (groupBySeason)
{
@ -158,11 +204,12 @@ namespace Ombi.Core.Engine
return model;
}
private IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies(IQueryable<PlexServerContent> plexMovies, IQueryable<EmbyContent> embyMovies)
private IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies(IQueryable<PlexServerContent> plexMovies, IQueryable<EmbyContent> embyMovies, IQueryable<JellyfinContent> jellyfinMovies)
{
var model = new HashSet<RecentlyAddedMovieModel>();
TransformPlexMovies(plexMovies, model);
TransformEmbyMovies(embyMovies, model);
TransformJellyfinMovies(jellyfinMovies, model);
return model;
}
@ -183,6 +230,22 @@ namespace Ombi.Core.Engine
}
}
private static void TransformJellyfinMovies(IQueryable<JellyfinContent> jellyfinMovies, HashSet<RecentlyAddedMovieModel> 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<PlexServerContent> plexMovies, HashSet<RecentlyAddedMovieModel> model)
{
foreach (var plex in plexMovies)
@ -246,5 +309,26 @@ namespace Ombi.Core.Engine
}
}
}
private static void TransformJellyfinShows(IQueryable<JellyfinContent> jellyfinShows, HashSet<RecentlyAddedTvModel> 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
});
}
}
}
}
}

@ -51,12 +51,28 @@ namespace Ombi.Core.Engine
public async Task<RequestEngineResult> 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<RequestsViewModel<TvRequests>> GetRequests(int count, int position, OrderFilterModel type)
@ -736,21 +752,21 @@ namespace Ombi.Core.Engine
}
}
private async Task<RequestEngineResult> AddExistingRequest(ChildRequests newRequest, TvRequests existingRequest)
private async Task<RequestEngineResult> 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<RequestEngineResult> AddRequest(TvRequests model)
private async Task<RequestEngineResult> 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<ChildRequests> SortEpisodes(List<ChildRequests> items)
@ -766,7 +782,7 @@ namespace Ombi.Core.Engine
}
private async Task<RequestEngineResult> AfterRequest(ChildRequests model)
private async Task<RequestEngineResult> 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,

@ -249,6 +249,26 @@ namespace Ombi.Core.Engine.V2
return result;
}
public async Task<IEnumerable<StreamingData>> GetStreamInformation(int movieDbId, CancellationToken cancellationToken)
{
var providers = await MovieApi.GetMovieWatchProviders(movieDbId, cancellationToken);
var results = await GetUserWatchProvider(providers);
var data = new List<StreamingData>();
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<List<SearchMovieViewModel>> TransformMovieResultsToResponse(
IEnumerable<MovieSearchResult> 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;

@ -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<ReleaseGroup> 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<ArtistInformation> 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<ArtistInformation> GetArtistInformationByRequestId(int requestId)
{
var request = await RequestService.MusicRequestRepository.Find(requestId);

@ -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<OmbiSettings> s,
IRepository<RequestSubscription> sub)
IRepository<RequestSubscription> 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<IEnumerable<StreamingData>> 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<StreamingData>();
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<SearchTvShowViewModel> ProcessResults<T>(IEnumerable<T> items)
{
var retVal = new List<SearchTvShowViewModel>();
@ -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)

@ -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<StreamData> GetUserWatchProviders(WatchProviders providers, OmbiUser user)
{
var data = new List<StreamData>();
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;
}
}
}

@ -18,6 +18,7 @@ namespace Ombi.Core.Models
public enum RecentlyAddedType
{
Plex,
Emby
Emby,
Jellyfin
}
}
}

@ -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; }
/// <summary>
/// This is only set from a HTTP Header

@ -12,6 +12,8 @@ namespace Ombi.Core.Models.Requests
public List<SeasonsViewModel> Seasons { get; set; } = new List<SeasonsViewModel>();
[JsonIgnore]
public string RequestedByAlias { get; set; }
public string RequestOnBehalf { get; set; }
}
public class SeasonsViewModel

@ -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; }
/// <summary>
/// This is used for the PlexAvailabilityCheck/EmbyAvailabilityRule rule
/// This is used for the PlexAvailabilityCheck/EmbyAvailabilityRule/JellyfinAvailabilityRule rule
/// </summary>
/// <value>
/// The custom identifier.
@ -35,4 +36,4 @@ namespace Ombi.Core.Models.Search
[NotMapped]
public bool ShowSubscribe { get; set; }
}
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -19,6 +19,7 @@ namespace Ombi.Core.Models
{
LocalUser = 1,
PlexUser = 2,
EmbyUser = 3
EmbyUser = 3,
JellyfinUser = 5
}
}
}

@ -24,6 +24,7 @@
<ProjectReference Include="..\Ombi.Api.CouchPotato\Ombi.Api.CouchPotato.csproj" />
<ProjectReference Include="..\Ombi.Api.DogNzb\Ombi.Api.DogNzb.csproj" />
<ProjectReference Include="..\Ombi.Api.Emby\Ombi.Api.Emby.csproj" />
<ProjectReference Include="..\Ombi.Api.Jellyfin\Ombi.Api.Jellyfin.csproj" />
<ProjectReference Include="..\Ombi.Api.FanartTv\Ombi.Api.FanartTv.csproj" />
<ProjectReference Include="..\Ombi.Api.Lidarr\Ombi.Api.Lidarr.csproj" />
<ProjectReference Include="..\Ombi.Api.MusicBrainz\Ombi.Api.MusicBrainz.csproj" />
@ -40,4 +41,4 @@
<ProjectReference Include="..\Ombi.TheMovieDbApi\Ombi.Api.TheMovieDb.csproj" />
</ItemGroup>
</Project>
</Project>

@ -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<JellyfinEpisode> 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;
}
}
}
}
}

@ -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();
}
}
}
}

@ -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<SearchViewModel>
{
public JellyfinAvailabilityRule(IJellyfinContentRepository repo, ISettingsService<JellyfinSettings> s)
{
JellyfinContentRepository = repo;
JellyfinSettings = s;
}
private IJellyfinContentRepository JellyfinContentRepository { get; }
private ISettingsService<JellyfinSettings> JellyfinSettings { get; }
public async Task<RuleResult> 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();
}
}
}

@ -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<IMovieDbApi, Api.TheMovieDb.TheMovieDbApi>();
services.AddTransient<IPlexApi, PlexApi>();
services.AddTransient<IEmbyApi, EmbyApi>();
services.AddTransient<IJellyfinApi, JellyfinApi>();
services.AddTransient<ISonarrApi, SonarrApi>();
services.AddTransient<ISonarrV3Api, SonarrV3Api>();
services.AddTransient<ISlackApi, SlackApi>();
@ -153,8 +157,9 @@ namespace Ombi.DependencyInjection
services.AddTransient<IMusicBrainzApi, MusicBrainzApi>();
services.AddTransient<IWhatsAppApi, WhatsAppApi>();
services.AddTransient<ICloudMobileNotification, CloudMobileNotification>();
services.AddTransient<IBaseEmbyApi, JellyfinApi>();
services.AddTransient<IEmbyApiFactory, EmbyApiFactory>();
services.AddTransient<IJellyfinApiFactory, JellyfinApiFactory>();
services.AddTransient<IRottenTomatoesApi, RottenTomatoesApi>();
}
public static void RegisterStore(this IServiceCollection services) {
@ -169,6 +174,7 @@ namespace Ombi.DependencyInjection
services.AddScoped<ISettingsResolver, SettingsResolver>();
services.AddScoped<IPlexContentRepository, PlexServerContentRepository>();
services.AddScoped<IEmbyContentRepository, EmbyContentRepository>();
services.AddScoped<IJellyfinContentRepository, JellyfinContentRepository>();
services.AddScoped<INotificationTemplatesRepository, NotificationTemplatesRepository>();
services.AddScoped<ITvRequestRepository, TvRequestRepository>();
@ -213,6 +219,9 @@ namespace Ombi.DependencyInjection
services.AddTransient<IEmbyContentSync, EmbyContentSync>();
services.AddTransient<IEmbyEpisodeSync, EmbyEpisodeSync>();
services.AddTransient<IEmbyAvaliabilityChecker, EmbyAvaliabilityChecker>();
services.AddTransient<IJellyfinContentSync, JellyfinContentSync>();
services.AddTransient<IJellyfinEpisodeSync, JellyfinEpisodeSync>();
services.AddTransient<IJellyfinAvaliabilityChecker, JellyfinAvaliabilityChecker>();
services.AddTransient<IPlexEpisodeSync, PlexEpisodeSync>();
services.AddTransient<IPlexAvailabilityChecker, PlexAvailabilityChecker>();
services.AddTransient<IRadarrSync, RadarrSync>();
@ -220,6 +229,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IOmbiAutomaticUpdater, OmbiAutomaticUpdater>();
services.AddTransient<IPlexUserImporter, PlexUserImporter>();
services.AddTransient<IEmbyUserImporter, EmbyUserImporter>();
services.AddTransient<IJellyfinUserImporter, JellyfinUserImporter>();
services.AddTransient<IWelcomeEmail, WelcomeEmail>();
services.AddTransient<ICouchPotatoSync, CouchPotatoSync>();
services.AddTransient<IProcessProvider, ProcessProvider>();

@ -32,6 +32,7 @@
<ProjectReference Include="..\Ombi.Api.Pushbullet\Ombi.Api.Pushbullet.csproj" />
<ProjectReference Include="..\Ombi.Api.Pushover\Ombi.Api.Pushover.csproj" />
<ProjectReference Include="..\Ombi.Api.Radarr\Ombi.Api.Radarr.csproj" />
<ProjectReference Include="..\Ombi.Api.RottenTomatoes\Ombi.Api.RottenTomatoes.csproj" />
<ProjectReference Include="..\Ombi.Api.Service\Ombi.Api.Service.csproj" />
<ProjectReference Include="..\Ombi.Api.SickRage\Ombi.Api.SickRage.csproj" />
<ProjectReference Include="..\Ombi.Api.Slack\Ombi.Api.Slack.csproj" />

@ -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;

@ -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<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
using (var scope = CreateScope())
{
var settingsProvider = scope.ServiceProvider.GetRequiredService<ISettingsService<JellyfinSettings>>();
var api = scope.ServiceProvider.GetRequiredService<IJellyfinApiFactory>();
var settings = await settingsProvider.GetSettingsAsync();
if (settings == null)
{
return HealthCheckResult.Healthy("Jellyfin is not configured.");
}
var client = api.CreateClient(settings);
var taskResult = new List<Task<JellyfinSystemInfo>>();
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);
}
}
}
}
}

@ -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;

@ -12,6 +12,7 @@ namespace Ombi.HealthChecks
{
builder.AddCheck<PlexHealthCheck>("Plex", tags: new string[] { "MediaServer" });
builder.AddCheck<EmbyHealthCheck>("Emby", tags: new string[] { "MediaServer" });
builder.AddCheck<JellyfinHealthCheck>("Jellyfin", tags: new string[] { "MediaServer" });
builder.AddCheck<LidarrHealthCheck>("Lidarr", tags: new string[] { "DVR" });
builder.AddCheck<SonarrHealthCheck>("Sonarr", tags: new string[] { "DVR" });
builder.AddCheck<RadarrHealthCheck>("Radarr", tags: new string[] { "DVR" });

@ -13,6 +13,7 @@
<ItemGroup>
<ProjectReference Include="..\Ombi.Api.CouchPotato\Ombi.Api.CouchPotato.csproj" />
<ProjectReference Include="..\Ombi.Api.Emby\Ombi.Api.Emby.csproj" />
<ProjectReference Include="..\Ombi.Api.Jellyfin\Ombi.Api.Jellyfin.csproj" />
<ProjectReference Include="..\Ombi.Api.Lidarr\Ombi.Api.Lidarr.csproj" />
<ProjectReference Include="..\Ombi.Api.Plex\Ombi.Api.Plex.csproj" />
<ProjectReference Include="..\Ombi.Api.Radarr\Ombi.Api.Radarr.csproj" />

@ -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<TestCaseData> 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<TestCaseData> 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");
}
}
}
}

@ -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<TestCaseData> 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");
}
}
}
}

@ -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("/"))

@ -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}";
}
}
}
}

@ -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);
}
}
}

@ -11,5 +11,8 @@
public string StoragePath { get; set; }
public string SecurityKey { get; set; }
#if DEBUG
= "test";
#endif
}
}

@ -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
}
}
}
}

@ -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);
}
}
}
}

@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace Ombi.Schedule.Jobs.Jellyfin
{
public interface IJellyfinAvaliabilityChecker : IBaseJob
{
}
}

@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace Ombi.Schedule.Jobs.Jellyfin
{
public interface IJellyfinContentSync : IBaseJob
{
}
}

@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace Ombi.Schedule.Jobs.Jellyfin
{
public interface IJellyfinEpisodeSync : IBaseJob
{
}
}

@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace Ombi.Schedule.Jobs.Jellyfin
{
public interface IJellyfinUserImporter : IBaseJob
{
}
}

@ -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<JellyfinAvaliabilityChecker> log, IHubContext<NotificationHub> 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<JellyfinAvaliabilityChecker> _log;
private readonly IHubContext<NotificationHub> _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();
}
/// <summary>
/// TODO This is EXCATLY the same as the PlexAvailabilityChecker. Refactor Please future Jamie
/// </summary>
/// <returns></returns>
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<JellyfinEpisode> 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);
}
}
}

@ -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<JellyfinSettings> settings, IJellyfinApiFactory api, ILogger<JellyfinContentSync> logger,
IJellyfinContentRepository repo, IHubContext<NotificationHub> notification)
{
_logger = logger;
_settings = settings;
_apiFactory = api;
_repo = repo;
_notification = notification;
}
private readonly ILogger<JellyfinContentSync> _logger;
private readonly ISettingsService<JellyfinSettings> _settings;
private readonly IJellyfinApiFactory _apiFactory;
private readonly IJellyfinContentRepository _repo;
private readonly IHubContext<NotificationHub> _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<JellyfinContent>();
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<JellyfinContent> 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);
}
}
}

@ -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<JellyfinSettings> s, IJellyfinApiFactory api, ILogger<JellyfinEpisodeSync> l, IJellyfinContentRepository repo
, IHubContext<NotificationHub> notification)
{
_apiFactory = api;
_logger = l;
_settings = s;
_repo = repo;
_notification = notification;
}
private readonly ISettingsService<JellyfinSettings> _settings;
private readonly IJellyfinApiFactory _apiFactory;
private readonly ILogger<JellyfinEpisodeSync> _logger;
private readonly IJellyfinContentRepository _repo;
private readonly IHubContext<NotificationHub> _notification;
private IJellyfinApi Api { get; set; }
public async Task Execute(IJobExecutionContext job)
{
var settings = await _settings.GetSettingsAsync();
Api = _apiFactory.CreateClient(settings);
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin Episode Sync Started");
foreach (var server in settings.Servers)
{
await CacheEpisodes(server);
}
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin Episode Sync Finished");
_logger.LogInformation("Jellyfin Episode Sync Finished - Triggering Metadata refresh");
await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System");
}
private async Task CacheEpisodes(JellyfinServers server)
{
var allEpisodes = await Api.GetAllEpisodes(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri);
var total = allEpisodes.TotalRecordCount;
var processed = 1;
var epToAdd = new HashSet<JellyfinEpisode>();
while (processed < total)
{
foreach (var ep in allEpisodes.Items)
{
processed++;
if (ep.LocationType?.Equals("Virtual", StringComparison.InvariantCultureIgnoreCase) ?? false)
{
// For some reason Jellyfin is not respecting the `IsVirtualItem` field.
continue;
}
// Let's make sure we have the parent request, stop those pesky forign key errors,
// Damn me having data integrity
var parent = await _repo.GetByJellyfinId(ep.SeriesId);
if (parent == null)
{
_logger.LogInformation("The episode {0} does not relate to a series, so we cannot save this",
ep.Name);
continue;
}
var existingEpisode = await _repo.GetEpisodeByJellyfinId(ep.Id);
// Make sure it's not in the hashset too
var existingInList = epToAdd.Any(x => x.JellyfinId == ep.Id);
if (existingEpisode == null && !existingInList)
{
_logger.LogDebug("Adding new episode {0} to parent {1}", ep.Name, ep.SeriesName);
// add it
epToAdd.Add(new JellyfinEpisode
{
JellyfinId = ep.Id,
EpisodeNumber = ep.IndexNumber,
SeasonNumber = ep.ParentIndexNumber,
ParentId = ep.SeriesId,
TvDbId = ep.ProviderIds.Tvdb,
TheMovieDbId = ep.ProviderIds.Tmdb,
ImdbId = ep.ProviderIds.Imdb,
Title = ep.Name,
AddedAt = DateTime.UtcNow
});
if (ep.IndexNumberEnd.HasValue && ep.IndexNumberEnd.Value != ep.IndexNumber)
{
epToAdd.Add(new JellyfinEpisode
{
JellyfinId = ep.Id,
EpisodeNumber = ep.IndexNumberEnd.Value,
SeasonNumber = ep.ParentIndexNumber,
ParentId = ep.SeriesId,
TvDbId = ep.ProviderIds.Tvdb,
TheMovieDbId = ep.ProviderIds.Tmdb,
ImdbId = ep.ProviderIds.Imdb,
Title = ep.Name,
AddedAt = DateTime.UtcNow
});
}
}
}
await _repo.AddRange(epToAdd);
epToAdd.Clear();
allEpisodes = await Api.GetAllEpisodes(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri);
}
if (epToAdd.Any())
{
await _repo.AddRange(epToAdd);
}
}
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);
}
}
}

@ -0,0 +1,174 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: JellyfinUserImporter.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.Identity;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Api.Jellyfin;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Hubs;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Quartz;
namespace Ombi.Schedule.Jobs.Jellyfin
{
public class JellyfinUserImporter : IJellyfinUserImporter
{
public JellyfinUserImporter(IJellyfinApiFactory api, UserManager<OmbiUser> um, ILogger<JellyfinUserImporter> log,
ISettingsService<JellyfinSettings> jellyfinSettings, ISettingsService<UserManagementSettings> ums, IHubContext<NotificationHub> notification)
{
_apiFactory = api;
_userManager = um;
_log = log;
_jellyfinSettings = jellyfinSettings;
_userManagementSettings = ums;
_notification = notification;
}
private readonly IJellyfinApiFactory _apiFactory;
private readonly UserManager<OmbiUser> _userManager;
private readonly ILogger<JellyfinUserImporter> _log;
private readonly ISettingsService<JellyfinSettings> _jellyfinSettings;
private readonly ISettingsService<UserManagementSettings> _userManagementSettings;
private readonly IHubContext<NotificationHub> _notification;
private IJellyfinApi Api { get; set; }
public async Task Execute(IJobExecutionContext job)
{
var userManagementSettings = await _userManagementSettings.GetSettingsAsync();
if (!userManagementSettings.ImportJellyfinUsers)
{
return;
}
var settings = await _jellyfinSettings.GetSettingsAsync();
if (!settings.Enable)
{
return;
}
Api = _apiFactory.CreateClient(settings);
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, $"Jellyfin User Importer Started");
var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.JellyfinUser).ToListAsync();
foreach (var server in settings.Servers)
{
if (string.IsNullOrEmpty(server.ApiKey))
{
continue;
}
var jellyfinUsers = await Api.GetUsers(server.FullUri, server.ApiKey);
foreach (var jellyfinUser in jellyfinUsers)
{
// Check if we should import this user
if (userManagementSettings.BannedJellyfinUserIds.Contains(jellyfinUser.Id))
{
// Do not import these, they are not allowed into the country.
continue;
}
// Check if this Jellyfin User already exists
var existingJellyfinUser = allUsers.FirstOrDefault(x => x.ProviderUserId == jellyfinUser.Id);
if (existingJellyfinUser == null)
{
if (!jellyfinUser.Name.HasValue())
{
_log.LogInformation("Could not create Jellyfin user since the have no username, JellyfinUserId: {0}", jellyfinUser.Id);
continue;
}
// Create this users
var newUser = new OmbiUser
{
UserName = jellyfinUser.Name,
UserType = UserType.JellyfinUser,
ProviderUserId = jellyfinUser.Id,
MovieRequestLimit = userManagementSettings.MovieRequestLimit,
EpisodeRequestLimit = userManagementSettings.EpisodeRequestLimit,
StreamingCountry = userManagementSettings.DefaultStreamingCountry
};
_log.LogInformation("Creating Jellyfin user {0}", newUser.UserName);
var result = await _userManager.CreateAsync(newUser);
if (!result.Succeeded)
{
foreach (var identityError in result.Errors)
{
_log.LogError(LoggingEvents.JellyfinUserImporter, identityError.Description);
}
continue;
}
if (userManagementSettings.DefaultRoles.Any())
{
foreach (var defaultRole in userManagementSettings.DefaultRoles)
{
await _userManager.AddToRoleAsync(newUser, defaultRole);
}
}
}
else
{
// Do we need to update this user?
existingJellyfinUser.UserName = jellyfinUser.Name;
await _userManager.UpdateAsync(existingJellyfinUser);
}
}
}
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin User Importer Finished");
}
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
_userManager?.Dispose();
//_jellyfinSettings?.Dispose();
//_userManagementSettings?.Dispose();
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

@ -5,6 +5,7 @@ using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Schedule.Jobs.Emby;
using Ombi.Schedule.Jobs.Jellyfin;
using Ombi.Schedule.Jobs.Plex.Interfaces;
using Ombi.Store.Repository;
using Quartz;
@ -14,12 +15,13 @@ namespace Ombi.Schedule.Jobs.Ombi
public class MediaDatabaseRefresh : IMediaDatabaseRefresh
{
public MediaDatabaseRefresh(ISettingsService<PlexSettings> s, ILogger<MediaDatabaseRefresh> log,
IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo)
IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, IJellyfinContentRepository jellyfinRepo)
{
_settings = s;
_log = log;
_plexRepo = plexRepo;
_embyRepo = embyRepo;
_jellyfinRepo = jellyfinRepo;
_settings.ClearCache();
}
@ -27,6 +29,7 @@ namespace Ombi.Schedule.Jobs.Ombi
private readonly ILogger _log;
private readonly IPlexContentRepository _plexRepo;
private readonly IEmbyContentRepository _embyRepo;
private readonly IJellyfinContentRepository _jellyfinRepo;
public async Task Execute(IJobExecutionContext job)
{
@ -34,6 +37,7 @@ namespace Ombi.Schedule.Jobs.Ombi
{
await RemovePlexData();
await RemoveEmbyData();
await RemoveJellyfinData();
}
catch (Exception e)
{
@ -64,6 +68,28 @@ namespace Ombi.Schedule.Jobs.Ombi
}
}
private async Task RemoveJellyfinData()
{
try
{
var s = await _settings.GetSettingsAsync();
if (!s.Enable)
{
return;
}
const string episodeSQL = "DELETE FROM JellyfinEpisode";
const string mainSql = "DELETE FROM JellyfinContent";
await _jellyfinRepo.ExecuteSql(episodeSQL);
await _jellyfinRepo.ExecuteSql(mainSql);
await OmbiQuartz.TriggerJob(nameof(IJellyfinContentSync), "Jellyfin");
}
catch (Exception e)
{
_log.LogError(LoggingEvents.MediaReferesh, e, "Refreshing Jellyfin Data Failed");
}
}
private async Task RemovePlexData()
{
try
@ -108,4 +134,4 @@ namespace Ombi.Schedule.Jobs.Ombi
GC.SuppressFinalize(this);
}
}
}
}

@ -38,16 +38,17 @@ namespace Ombi.Schedule.Jobs.Ombi
{
public class NewsletterJob : HtmlTemplateGenerator, INewsletterJob
{
public NewsletterJob(IPlexContentRepository plex, IEmbyContentRepository emby, IRepository<RecentlyAddedLog> addedLog,
public NewsletterJob(IPlexContentRepository plex, IEmbyContentRepository emby, IJellyfinContentRepository jellyfin, IRepository<RecentlyAddedLog> addedLog,
IMovieDbApi movieApi, ITvMazeApi tvApi, IEmailProvider email, ISettingsService<CustomizationSettings> custom,
ISettingsService<EmailNotificationSettings> emailSettings, INotificationTemplatesRepository templateRepo,
UserManager<OmbiUser> um, ISettingsService<NewsletterSettings> newsletter, ILogger<NewsletterJob> log,
ILidarrApi lidarrApi, IExternalRepository<LidarrAlbumCache> albumCache, ISettingsService<LidarrSettings> lidarrSettings,
ISettingsService<OmbiSettings> ombiSettings, ISettingsService<PlexSettings> plexSettings, ISettingsService<EmbySettings> embySettings
, IHubContext<NotificationHub> notification, IRefreshMetadata refreshMetadata)
ISettingsService<OmbiSettings> ombiSettings, ISettingsService<PlexSettings> plexSettings, ISettingsService<EmbySettings> embySettings, ISettingsService<JellyfinSettings> jellyfinSettings,
IHubContext<NotificationHub> notification, IRefreshMetadata refreshMetadata)
{
_plex = plex;
_emby = emby;
_jellyfin = jellyfin;
_recentlyAddedLog = addedLog;
_movieApi = movieApi;
_tvApi = tvApi;
@ -64,6 +65,7 @@ namespace Ombi.Schedule.Jobs.Ombi
_ombiSettings = ombiSettings;
_plexSettings = plexSettings;
_embySettings = embySettings;
_jellyfinSettings = jellyfinSettings;
_notification = notification;
_ombiSettings.ClearCache();
_plexSettings.ClearCache();
@ -74,6 +76,7 @@ namespace Ombi.Schedule.Jobs.Ombi
private readonly IPlexContentRepository _plex;
private readonly IEmbyContentRepository _emby;
private readonly IJellyfinContentRepository _jellyfin;
private readonly IRepository<RecentlyAddedLog> _recentlyAddedLog;
private readonly IMovieDbApi _movieApi;
private readonly ITvMazeApi _tvApi;
@ -90,6 +93,7 @@ namespace Ombi.Schedule.Jobs.Ombi
private readonly ISettingsService<LidarrSettings> _lidarrSettings;
private readonly ISettingsService<PlexSettings> _plexSettings;
private readonly ISettingsService<EmbySettings> _embySettings;
private readonly ISettingsService<JellyfinSettings> _jellyfinSettings;
private readonly IHubContext<NotificationHub> _notification;
private readonly IRefreshMetadata _refreshMetadata;
@ -123,36 +127,46 @@ namespace Ombi.Schedule.Jobs.Ombi
// Get the Content
var plexContent = _plex.GetAll().Include(x => x.Episodes).AsNoTracking();
var embyContent = _emby.GetAll().Include(x => x.Episodes).AsNoTracking();
var jellyfinContent = _jellyfin.GetAll().Include(x => x.Episodes).AsNoTracking();
var lidarrContent = _lidarrAlbumRepository.GetAll().AsNoTracking().ToList().Where(x => x.FullyAvailable);
var addedLog = _recentlyAddedLog.GetAll();
var addedPlexMovieLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet();
var addedEmbyMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet();
var addedJellyfinMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Jellyfin && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet();
var addedAlbumLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Lidarr && x.ContentType == ContentType.Album).Select(x => x.AlbumId).ToHashSet();
var addedPlexEpisodesLogIds =
addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Episode);
var addedEmbyEpisodesLogIds =
addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Episode);
var addedJellyfinEpisodesLogIds =
addedLog.Where(x => x.Type == RecentlyAddedType.Jellyfin && x.ContentType == ContentType.Episode);
// Filter out the ones that we haven't sent yet
var plexContentLocalDataset = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet();
var embyContentLocalDataset = embyContent.Where(x => x.Type == EmbyMediaType.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet();
var jellyfinContentLocalDataset = jellyfinContent.Where(x => x.Type == JellyfinMediaType.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet();
var plexContentMoviesToSend = plexContentLocalDataset.Where(x => !addedPlexMovieLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId))).ToHashSet();
var embyContentMoviesToSend = embyContentLocalDataset.Where(x => !addedEmbyMoviesLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId))).ToHashSet();
var jellyfinContentMoviesToSend = jellyfinContentLocalDataset.Where(x => !addedJellyfinMoviesLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId))).ToHashSet();
var lidarrContentAlbumsToSend = lidarrContent.Where(x => !addedAlbumLogIds.Contains(x.ForeignAlbumId)).ToHashSet();
_log.LogInformation("Plex Movies to send: {0}", plexContentMoviesToSend.Count());
_log.LogInformation("Emby Movies to send: {0}", embyContentMoviesToSend.Count());
_log.LogInformation("Jellyfin Movies to send: {0}", jellyfinContentMoviesToSend.Count());
_log.LogInformation("Albums to send: {0}", lidarrContentAlbumsToSend.Count());
// Find the movies that do not yet have MovieDbIds
var needsMovieDbPlex = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet();
var needsMovieDbEmby = embyContent.Where(x => x.Type == EmbyMediaType.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet();
var needsMovieDbJellyfin = jellyfinContent.Where(x => x.Type == JellyfinMediaType.Movie && !string.IsNullOrEmpty(x.TheMovieDbId)).ToHashSet();
var newPlexMovies = await GetMoviesWithoutId(addedPlexMovieLogIds, needsMovieDbPlex);
var newEmbyMovies = await GetMoviesWithoutId(addedEmbyMoviesLogIds, needsMovieDbEmby);
var newJellyfinMovies = await GetMoviesWithoutId(addedJellyfinMoviesLogIds, needsMovieDbJellyfin);
plexContentMoviesToSend = plexContentMoviesToSend.Union(newPlexMovies).ToHashSet();
embyContentMoviesToSend = embyContentMoviesToSend.Union(newEmbyMovies).ToHashSet();
jellyfinContentMoviesToSend = jellyfinContentMoviesToSend.Union(newJellyfinMovies).ToHashSet();
plexContentMoviesToSend = plexContentMoviesToSend.DistinctBy(x => x.Id).ToHashSet();
embyContentMoviesToSend = embyContentMoviesToSend.DistinctBy(x => x.Id).ToHashSet();
@ -161,24 +175,30 @@ namespace Ombi.Schedule.Jobs.Ombi
FilterPlexEpisodes(_plex.GetAllEpisodes().Include(x => x.Series).AsNoTracking(), addedPlexEpisodesLogIds);
var embyEpisodesToSend = FilterEmbyEpisodes(_emby.GetAllEpisodes().Include(x => x.Series).AsNoTracking(),
addedEmbyEpisodesLogIds);
var jellyfinEpisodesToSend = FilterJellyfinEpisodes(_jellyfin.GetAllEpisodes().Include(x => x.Series).AsNoTracking(),
addedJellyfinEpisodesLogIds);
_log.LogInformation("Plex Episodes to send: {0}", plexEpisodesToSend.Count());
_log.LogInformation("Emby Episodes to send: {0}", embyEpisodesToSend.Count());
_log.LogInformation("Jellyfin Episodes to send: {0}", jellyfinEpisodesToSend.Count());
var plexSettings = await _plexSettings.GetSettingsAsync();
var embySettings = await _embySettings.GetSettingsAsync();
var jellyfinSettings = await _jellyfinSettings.GetSettingsAsync();
var body = string.Empty;
if (test)
{
var plexm = plexContent.Where(x => x.Type == PlexMediaTypeEntity.Movie).OrderByDescending(x => x.AddedAt).Take(10);
var embym = embyContent.Where(x => x.Type == EmbyMediaType.Movie).OrderByDescending(x => x.AddedAt).Take(10);
var jellyfinm = jellyfinContent.Where(x => x.Type == JellyfinMediaType.Movie).OrderByDescending(x => x.AddedAt).Take(10);
var plext = _plex.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.Series.AddedAt).Take(10).ToHashSet();
var embyt = _emby.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.AddedAt).Take(10).ToHashSet();
var jellyfint = _jellyfin.GetAllEpisodes().Include(x => x.Series).OrderByDescending(x => x.AddedAt).Take(10).ToHashSet();
var lidarr = lidarrContent.OrderByDescending(x => x.AddedAt).Take(10).ToHashSet();
body = await BuildHtml(plexm, embym, plext, embyt, lidarr, settings, embySettings, plexSettings);
body = await BuildHtml(plexm, embym, jellyfinm, plext, embyt, jellyfint, lidarr, settings, embySettings, jellyfinSettings, plexSettings);
}
else
{
body = await BuildHtml(plexContentMoviesToSend.AsQueryable(), embyContentMoviesToSend.AsQueryable(), plexEpisodesToSend, embyEpisodesToSend, lidarrContentAlbumsToSend, settings, embySettings, plexSettings);
body = await BuildHtml(plexContentMoviesToSend.AsQueryable(), embyContentMoviesToSend.AsQueryable(), jellyfinContentMoviesToSend.AsQueryable(), plexEpisodesToSend, embyEpisodesToSend, jellyfinEpisodesToSend, lidarrContentAlbumsToSend, settings, embySettings, jellyfinSettings, plexSettings);
if (body.IsNullOrEmpty())
{
return;
@ -285,6 +305,34 @@ namespace Ombi.Schedule.Jobs.Ombi
SeasonNumber = p.SeasonNumber
});
}
foreach (var e in jellyfinContentMoviesToSend)
{
if (e.Type == JellyfinMediaType.Movie)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Jellyfin,
ContentType = ContentType.Parent,
ContentId = StringHelper.IntParseLinq(e.TheMovieDbId),
});
}
}
foreach (var p in jellyfinEpisodesToSend)
{
recentlyAddedLog.Add(new RecentlyAddedLog
{
AddedAt = DateTime.Now,
Type = RecentlyAddedType.Jellyfin,
ContentType = ContentType.Episode,
ContentId = StringHelper.IntParseLinq(p.Series.TvDbId),
EpisodeNumber = p.EpisodeNumber,
SeasonNumber = p.SeasonNumber
});
}
await _recentlyAddedLog.AddRange(recentlyAddedLog);
}
else
@ -349,6 +397,20 @@ namespace Ombi.Schedule.Jobs.Ombi
return result.ToHashSet();
}
private async Task<HashSet<JellyfinContent>> GetMoviesWithoutId(HashSet<int> addedMovieLogIds, HashSet<JellyfinContent> needsMovieDbJellyfin)
{
foreach (var movie in needsMovieDbJellyfin)
{
var id = await _refreshMetadata.GetTheMovieDbId(false, true, null, movie.ImdbId, movie.Title, true);
movie.TheMovieDbId = id.ToString();
}
var result = needsMovieDbJellyfin.Where(x => x.HasTheMovieDb && !addedMovieLogIds.Contains(StringHelper.IntParseLinq(x.TheMovieDbId)));
await UpdateTheMovieDbId(result);
// Filter them out now
return result.ToHashSet();
}
private async Task UpdateTheMovieDbId(IEnumerable<PlexServerContent> content)
{
foreach (var movie in content)
@ -358,6 +420,10 @@ namespace Ombi.Schedule.Jobs.Ombi
continue;
}
var entity = await _plex.Find(movie.Id);
if (entity == null)
{
return;
}
entity.TheMovieDbId = movie.TheMovieDbId;
_plex.UpdateWithoutSave(entity);
}
@ -379,6 +445,21 @@ namespace Ombi.Schedule.Jobs.Ombi
await _plex.SaveChangesAsync();
}
private async Task UpdateTheMovieDbId(IEnumerable<JellyfinContent> content)
{
foreach (var movie in content)
{
if (!movie.HasTheMovieDb)
{
continue;
}
var entity = await _jellyfin.Find(movie.Id);
entity.TheMovieDbId = movie.TheMovieDbId;
_jellyfin.UpdateWithoutSave(entity);
}
await _plex.SaveChangesAsync();
}
public async Task Execute(IJobExecutionContext job)
{
var newsletterSettings = await _newsletterSettings.GetSettingsAsync();
@ -419,6 +500,23 @@ namespace Ombi.Schedule.Jobs.Ombi
return itemsToReturn;
}
private HashSet<JellyfinEpisode> FilterJellyfinEpisodes(IEnumerable<JellyfinEpisode> source, IQueryable<RecentlyAddedLog> recentlyAdded)
{
var itemsToReturn = new HashSet<JellyfinEpisode>();
foreach (var ep in source.Where(x => x.Series.HasTvDb))
{
var tvDbId = StringHelper.IntParseLinq(ep.Series.TvDbId);
if (recentlyAdded.Any(x => x.ContentId == tvDbId && x.EpisodeNumber == ep.EpisodeNumber && x.SeasonNumber == ep.SeasonNumber))
{
continue;
}
itemsToReturn.Add(ep);
}
return itemsToReturn;
}
private NotificationMessageContent ParseTemplate(NotificationTemplates template, CustomizationSettings settings)
{
var resolver = new NotificationMessageResolver();
@ -429,8 +527,8 @@ namespace Ombi.Schedule.Jobs.Ombi
return resolver.ParseMessage(template, curlys);
}
private async Task<string> BuildHtml(IQueryable<PlexServerContent> plexContentToSend, IQueryable<EmbyContent> embyContentToSend,
HashSet<PlexEpisode> plexEpisodes, HashSet<EmbyEpisode> embyEp, HashSet<LidarrAlbumCache> albums, NewsletterSettings settings, EmbySettings embySettings,
private async Task<string> BuildHtml(IQueryable<PlexServerContent> plexContentToSend, IQueryable<EmbyContent> embyContentToSend, IQueryable<JellyfinContent> jellyfinContentToSend,
HashSet<PlexEpisode> plexEpisodes, HashSet<EmbyEpisode> embyEp, HashSet<JellyfinEpisode> jellyfinEp, HashSet<LidarrAlbumCache> albums, NewsletterSettings settings, EmbySettings embySettings, JellyfinSettings jellyfinSettings,
PlexSettings plexSettings)
{
var ombiSettings = await _ombiSettings.GetSettingsAsync();
@ -438,6 +536,7 @@ namespace Ombi.Schedule.Jobs.Ombi
var plexMovies = plexContentToSend.Where(x => x.Type == PlexMediaTypeEntity.Movie);
var embyMovies = embyContentToSend.Where(x => x.Type == EmbyMediaType.Movie);
var jellyfinMovies = jellyfinContentToSend.Where(x => x.Type == JellyfinMediaType.Movie);
if ((plexMovies.Any() || embyMovies.Any()) && !settings.DisableMovies)
{
sb.Append("<h1 style=\"text-align: center; max-width: 1042px;\">New Movies</h1><br /><br />");
@ -457,6 +556,11 @@ namespace Ombi.Schedule.Jobs.Ombi
await ProcessEmbyMovies(embyMovies, sb, ombiSettings.DefaultLanguageCode, embySettings.Servers.FirstOrDefault()?.ServerHostname ?? string.Empty);
}
if (jellyfinSettings.Enable)
{
await ProcessJellyfinMovies(jellyfinMovies, sb, ombiSettings.DefaultLanguageCode, jellyfinSettings.Servers.FirstOrDefault()?.ServerHostname ?? string.Empty);
}
sb.Append("</tr>");
sb.Append("</table>");
sb.Append("</td>");
@ -464,7 +568,7 @@ namespace Ombi.Schedule.Jobs.Ombi
sb.Append("</table>");
}
if ((plexEpisodes.Any() || embyEp.Any()) && !settings.DisableTv)
if ((plexEpisodes.Any() || embyEp.Any()) || jellyfinEp.Any() && !settings.DisableTv)
{
sb.Append("<br /><br /><h1 style=\"text-align: center; max-width: 1042px;\">New TV</h1><br /><br />");
sb.Append(
@ -483,6 +587,11 @@ namespace Ombi.Schedule.Jobs.Ombi
await ProcessEmbyTv(embyEp, sb, embySettings.Servers.FirstOrDefault()?.ServerHostname ?? string.Empty);
}
if (jellyfinSettings.Enable)
{
await ProcessJellyfinTv(jellyfinEp, sb, jellyfinSettings.Servers.FirstOrDefault()?.ServerHostname ?? string.Empty);
}
sb.Append("</tr>");
sb.Append("</table>");
sb.Append("</td>");
@ -638,6 +747,59 @@ namespace Ombi.Schedule.Jobs.Ombi
}
}
private async Task ProcessJellyfinMovies(IQueryable<JellyfinContent> embyContent, StringBuilder sb, string defaultLangaugeCode, string customUrl)
{
int count = 0;
var ordered = embyContent.OrderByDescending(x => x.AddedAt);
foreach (var content in ordered)
{
var theMovieDbId = content.TheMovieDbId;
if (!content.TheMovieDbId.HasValue())
{
var imdbId = content.ImdbId;
var findResult = await _movieApi.Find(imdbId, ExternalSource.imdb_id);
var result = findResult.movie_results?.FirstOrDefault();
if (result == null)
{
continue;
}
theMovieDbId = result.id.ToString();
}
var mediaurl = content.Url;
if (customUrl.HasValue())
{
mediaurl = customUrl;
}
var info = await _movieApi.GetMovieInformationWithExtraInfo(StringHelper.IntParseLinq(theMovieDbId), defaultLangaugeCode);
if (info == null)
{
continue;
}
try
{
CreateMovieHtmlContent(sb, info, mediaurl);
count += 1;
}
catch (Exception e)
{
_log.LogError(e, "Error when processing Jellyfin Movies {0}", info.Title);
}
finally
{
EndLoopHtml(sb);
}
if (count == 2)
{
count = 0;
sb.Append("</tr>");
sb.Append("<tr>");
}
}
}
private void CreateMovieHtmlContent(StringBuilder sb, MovieResponseDto info, string mediaurl)
{
AddBackgroundInsideTable(sb, $"https://image.tmdb.org/t/p/w1280/{info.BackdropPath}");
@ -981,6 +1143,129 @@ namespace Ombi.Schedule.Jobs.Ombi
}
}
private async Task ProcessJellyfinTv(HashSet<JellyfinEpisode> embyContent, StringBuilder sb, string serverUrl)
{
var series = new List<JellyfinContent>();
foreach (var episode in embyContent)
{
var alreadyAdded = series.FirstOrDefault(x => x.JellyfinId == episode.Series.JellyfinId);
if (alreadyAdded != null)
{
alreadyAdded.Episodes.Add(episode);
}
else
{
episode.Series.Episodes = new List<JellyfinEpisode>
{
episode
};
series.Add(episode.Series);
}
}
int count = 0;
var orderedTv = series.OrderByDescending(x => x.AddedAt);
foreach (var t in orderedTv)
{
if (!t.TvDbId.HasValue())
{
continue;
}
int.TryParse(t.TvDbId, out var tvdbId);
var info = await _tvApi.ShowLookupByTheTvDbId(tvdbId);
if (info == null)
{
continue;
}
try
{
var banner = info.image?.original;
if (!string.IsNullOrEmpty(banner))
{
banner = banner.ToHttpsUrl(); // Always use the Https banners
}
var tvInfo = await _movieApi.GetTVInfo(t.TheMovieDbId);
if (tvInfo != null && tvInfo.backdrop_path.HasValue())
{
AddBackgroundInsideTable(sb, $"https://image.tmdb.org/t/p/w500{tvInfo.backdrop_path}");
}
else
{
AddBackgroundInsideTable(sb, $"https://image.tmdb.org/t/p/w1280/");
}
AddPosterInsideTable(sb, banner);
AddMediaServerUrl(sb, serverUrl.HasValue() ? serverUrl : t.Url, banner);
AddInfoTable(sb);
var title = "";
if (!String.IsNullOrEmpty(info.premiered) && info.premiered.Length > 4)
{
title = $"{t.Title} ({info.premiered.Remove(4)})";
}
else
{
title = $"{t.Title}";
}
AddTitle(sb, $"https://www.imdb.com/title/{info.externals.imdb}/", title);
// Group by the season number
var results = t.Episodes?.GroupBy(p => p.SeasonNumber,
(key, g) => new
{
SeasonNumber = key,
Episodes = g.ToList(),
EpisodeAirDate = tvInfo?.seasons?.Where(x => x.season_number == key)?.Select(x => x.air_date).FirstOrDefault()
}
);
// Group the episodes
var finalsb = new StringBuilder();
foreach (var epInformation in results.OrderBy(x => x.SeasonNumber))
{
var orderedEpisodes = epInformation.Episodes.OrderBy(x => x.EpisodeNumber).ToList();
var episodeString = StringHelper.BuildEpisodeList(orderedEpisodes.Select(x => x.EpisodeNumber));
var episodeAirDate = epInformation.EpisodeAirDate;
finalsb.Append($"Season: {epInformation.SeasonNumber} - Episodes: {episodeString} {episodeAirDate}");
finalsb.Append("<br />");
}
var summary = info.summary;
if (summary.Length > 280)
{
summary = summary.Remove(280);
summary = summary + "...</p>";
}
AddTvParagraph(sb, finalsb.ToString(), summary);
if (info.genres.Any())
{
AddGenres(sb, $"Genres: {string.Join(", ", info.genres.Select(x => x.ToString()).ToArray())}");
}
}
catch (Exception e)
{
_log.LogError(e, "Error when processing Jellyfin TV {0}", t.Title);
}
finally
{
EndLoopHtml(sb);
count += 1;
}
if (count == 2)
{
count = 0;
sb.Append("</tr>");
sb.Append("<tr>");
}
}
}
private void EndLoopHtml(StringBuilder sb)
{
//NOTE: BR have to be in TD's as per html spec or it will be put outside of the table...
@ -1040,4 +1325,4 @@ namespace Ombi.Schedule.Jobs.Ombi
GC.SuppressFinalize(this);
}
}
}
}

@ -6,6 +6,7 @@ using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Api.Emby;
using Ombi.Api.Jellyfin;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Api.TvMaze;
@ -14,6 +15,7 @@ using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Hubs;
using Ombi.Schedule.Jobs.Emby;
using Ombi.Schedule.Jobs.Jellyfin;
using Ombi.Schedule.Jobs.Plex;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
@ -23,31 +25,41 @@ namespace Ombi.Schedule.Jobs.Ombi
{
public class RefreshMetadata : IRefreshMetadata
{
public RefreshMetadata(IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo,
public RefreshMetadata(IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, IJellyfinContentRepository jellyfinRepo,
ILogger<RefreshMetadata> log, ITvMazeApi tvApi, ISettingsService<PlexSettings> plexSettings,
IMovieDbApi movieApi, ISettingsService<EmbySettings> embySettings, IEmbyApiFactory embyApi, IHubContext<NotificationHub> notification)
IMovieDbApi movieApi,
ISettingsService<EmbySettings> embySettings, IEmbyApiFactory embyApi,
ISettingsService<JellyfinSettings> jellyfinSettings, IJellyfinApiFactory jellyfinApi,
IHubContext<NotificationHub> notification)
{
_plexRepo = plexRepo;
_embyRepo = embyRepo;
_jellyfinRepo = jellyfinRepo;
_log = log;
_movieApi = movieApi;
_tvApi = tvApi;
_plexSettings = plexSettings;
_embySettings = embySettings;
_embyApiFactory = embyApi;
_jellyfinSettings = jellyfinSettings;
_jellyfinApiFactory = jellyfinApi;
_notification = notification;
}
private readonly IPlexContentRepository _plexRepo;
private readonly IEmbyContentRepository _embyRepo;
private readonly IJellyfinContentRepository _jellyfinRepo;
private readonly ILogger _log;
private readonly IMovieDbApi _movieApi;
private readonly ITvMazeApi _tvApi;
private readonly ISettingsService<PlexSettings> _plexSettings;
private readonly ISettingsService<EmbySettings> _embySettings;
private readonly ISettingsService<JellyfinSettings> _jellyfinSettings;
private readonly IEmbyApiFactory _embyApiFactory;
private readonly IJellyfinApiFactory _jellyfinApiFactory;
private readonly IHubContext<NotificationHub> _notification;
private IEmbyApi EmbyApi { get; set; }
private IJellyfinApi JellyfinApi { get; set; }
public async Task Execute(IJobExecutionContext job)
{
@ -72,6 +84,14 @@ namespace Ombi.Schedule.Jobs.Ombi
await OmbiQuartz.TriggerJob(nameof(IEmbyAvaliabilityChecker), "Emby");
}
var jellyfinSettings = await _jellyfinSettings.GetSettingsAsync();
if (jellyfinSettings.Enable)
{
await StartJellyfin(jellyfinSettings);
await OmbiQuartz.TriggerJob(nameof(IJellyfinAvaliabilityChecker), "Jellyfin");
}
}
catch (Exception e)
{
@ -107,6 +127,13 @@ namespace Ombi.Schedule.Jobs.Ombi
await StartEmbyTv();
}
private async Task StartJellyfin(JellyfinSettings s)
{
JellyfinApi = _jellyfinApiFactory.CreateClient(s);
await StartJellyfinMovies(s);
await StartJellyfinTv();
}
private async Task StartPlexTv(List<PlexServerContent> allTv)
{
foreach (var show in allTv)
@ -178,6 +205,41 @@ namespace Ombi.Schedule.Jobs.Ombi
}
}
private async Task StartJellyfinTv()
{
var allTv = await _jellyfinRepo.GetAll().Where(x =>
x.Type == JellyfinMediaType.Series && (x.TheMovieDbId == null || x.ImdbId == null || x.TvDbId == null)).ToListAsync();
foreach (var show in allTv)
{
var hasImdb = show.ImdbId.HasValue();
var hasTheMovieDb = show.TheMovieDbId.HasValue();
var hasTvDbId = show.TvDbId.HasValue();
if (!hasTheMovieDb)
{
var id = await GetTheMovieDbId(hasTvDbId, hasImdb, show.TvDbId, show.ImdbId, show.Title, false);
show.TheMovieDbId = id;
}
if (!hasImdb)
{
var id = await GetImdbId(hasTheMovieDb, hasTvDbId, show.Title, show.TheMovieDbId, show.TvDbId, RequestType.TvShow);
show.ImdbId = id;
_jellyfinRepo.UpdateWithoutSave(show);
}
if (!hasTvDbId)
{
var id = await GetTvDbId(hasTheMovieDb, hasImdb, show.TheMovieDbId, show.ImdbId, show.Title);
show.TvDbId = id;
_jellyfinRepo.UpdateWithoutSave(show);
}
await _jellyfinRepo.SaveChangesAsync();
}
}
private async Task StartPlexMovies(List<PlexServerContent> allMovies)
{
foreach (var movie in allMovies)
@ -263,6 +325,61 @@ namespace Ombi.Schedule.Jobs.Ombi
}
}
private async Task StartJellyfinMovies(JellyfinSettings settings)
{
var allMovies = await _jellyfinRepo.GetAll().Where(x =>
x.Type == JellyfinMediaType.Movie && (x.TheMovieDbId == null || x.ImdbId == null)).ToListAsync();
foreach (var movie in allMovies)
{
movie.ImdbId.HasValue();
movie.TheMovieDbId.HasValue();
// Movies don't really use TheTvDb
// Check if it even has 1 ID
if (!movie.HasImdb && !movie.HasTheMovieDb)
{
// Ok this sucks,
// The only think I can think that has happened is that we scanned Jellyfin before Jellyfin has got the metadata
// So let's recheck jellyfin to see if they have got the metadata now
//
// Yeah your right that does suck - Future Jamie
_log.LogInformation($"Movie {movie.Title} does not have a ImdbId or TheMovieDbId, so rechecking jellyfin");
foreach (var server in settings.Servers)
{
_log.LogInformation($"Checking server {server.Name} for upto date metadata");
var movieInfo = await JellyfinApi.GetMovieInformation(movie.JellyfinId, server.ApiKey, server.AdministratorId,
server.FullUri);
if (movieInfo.ProviderIds?.Imdb.HasValue() ?? false)
{
movie.ImdbId = movieInfo.ProviderIds.Imdb;
}
if (movieInfo.ProviderIds?.Tmdb.HasValue() ?? false)
{
movie.TheMovieDbId = movieInfo.ProviderIds.Tmdb;
}
}
}
if (!movie.HasImdb)
{
var imdbId = await GetImdbId(movie.HasTheMovieDb, false, movie.Title, movie.TheMovieDbId, string.Empty, RequestType.Movie);
movie.ImdbId = imdbId;
_jellyfinRepo.UpdateWithoutSave(movie);
}
if (!movie.HasTheMovieDb)
{
var id = await GetTheMovieDbId(false, movie.HasImdb, string.Empty, movie.ImdbId, movie.Title, true);
movie.TheMovieDbId = id;
_jellyfinRepo.UpdateWithoutSave(movie);
}
await _jellyfinRepo.SaveChangesAsync();
}
}
public async Task<string> GetTheMovieDbId(bool hasTvDbId, bool hasImdb, string tvdbID, string imdbId, string title, bool movie)
{
_log.LogInformation("The Media item {0} does not have a TheMovieDbId, searching for TheMovieDbId", title);
@ -392,4 +509,4 @@ namespace Ombi.Schedule.Jobs.Ombi
GC.SuppressFinalize(this);
}
}
}
}

@ -88,6 +88,12 @@ namespace Ombi.Schedule.Jobs.Plex
_log.LogInformation("Could not create Plex user since the have no username, PlexUserId: {0}", plexUser.Id);
continue;
}
if ((plexUser.Email.HasValue()) && await _userManager.FindByEmailAsync(plexUser.Email) != null)
{
_log.LogWarning($"Cannot add user {plexUser.Username} because their email address is already in Ombi, skipping this user");
continue;
}
// Create this users
// We do not store a password against the user since they will authenticate via Plex
var newUser = new OmbiUser
@ -98,7 +104,8 @@ namespace Ombi.Schedule.Jobs.Plex
Email = plexUser?.Email ?? string.Empty,
Alias = string.Empty,
MovieRequestLimit = userManagementSettings.MovieRequestLimit,
EpisodeRequestLimit = userManagementSettings.EpisodeRequestLimit
EpisodeRequestLimit = userManagementSettings.EpisodeRequestLimit,
StreamingCountry = userManagementSettings.DefaultStreamingCountry
};
_log.LogInformation("Creating Plex user {0}", newUser.UserName);
var result = await _userManager.CreateAsync(newUser);
@ -161,7 +168,8 @@ namespace Ombi.Schedule.Jobs.Plex
UserName = plexAdmin.username ?? plexAdmin.id,
ProviderUserId = plexAdmin.id,
Email = plexAdmin.email ?? string.Empty,
Alias = string.Empty
Alias = string.Empty,
StreamingCountry = settings.DefaultStreamingCountry
};
var result = await _userManager.CreateAsync(newUser);

@ -23,6 +23,7 @@
<ItemGroup>
<ProjectReference Include="..\Ombi.Api.CouchPotato\Ombi.Api.CouchPotato.csproj" />
<ProjectReference Include="..\Ombi.Api.Emby\Ombi.Api.Emby.csproj" />
<ProjectReference Include="..\Ombi.Api.Jellyfin\Ombi.Api.Jellyfin.csproj" />
<ProjectReference Include="..\Ombi.Api.Lidarr\Ombi.Api.Lidarr.csproj" />
<ProjectReference Include="..\Ombi.Api.Plex\Ombi.Api.Plex.csproj" />
<ProjectReference Include="..\Ombi.Api.Radarr\Ombi.Api.Radarr.csproj" />
@ -37,4 +38,4 @@
<ProjectReference Include="..\Ombi.TheMovieDbApi\Ombi.Api.TheMovieDb.csproj" />
</ItemGroup>
</Project>
</Project>

@ -7,6 +7,7 @@ using Ombi.Helpers;
using Ombi.Schedule.Jobs;
using Ombi.Schedule.Jobs.Couchpotato;
using Ombi.Schedule.Jobs.Emby;
using Ombi.Schedule.Jobs.Jellyfin;
using Ombi.Schedule.Jobs.Lidarr;
using Ombi.Schedule.Jobs.Ombi;
using Ombi.Schedule.Jobs.Plex;
@ -51,6 +52,7 @@ namespace Ombi.Schedule
// Run configuration
await AddPlex(s);
await AddEmby(s);
await AddJellyfin(s);
await AddDvrApps(s);
await AddSystem(s);
await AddNotifications(s);
@ -98,9 +100,18 @@ namespace Ombi.Schedule
await OmbiQuartz.Instance.AddJob<IEmbyAvaliabilityChecker>(nameof(IEmbyAvaliabilityChecker), "Emby", null);
await OmbiQuartz.Instance.AddJob<IEmbyUserImporter>(nameof(IEmbyUserImporter), "Emby", JobSettingsHelper.UserImporter(s));
}
private static async Task AddJellyfin(JobSettings s)
{
await OmbiQuartz.Instance.AddJob<IJellyfinContentSync>(nameof(IJellyfinContentSync), "Jellyfin", JobSettingsHelper.JellyfinContent(s));
await OmbiQuartz.Instance.AddJob<IJellyfinEpisodeSync>(nameof(IJellyfinEpisodeSync), "Jellyfin", null);
await OmbiQuartz.Instance.AddJob<IJellyfinAvaliabilityChecker>(nameof(IJellyfinAvaliabilityChecker), "Jellyfin", null);
await OmbiQuartz.Instance.AddJob<IJellyfinUserImporter>(nameof(IJellyfinUserImporter), "Jellyfin", JobSettingsHelper.UserImporter(s));
}
private static async Task AddNotifications(JobSettings s)
{
await OmbiQuartz.Instance.AddJob<INotificationService>(nameof(INotificationService), "Notifications", null);
}
}
}
}

@ -6,7 +6,6 @@ namespace Ombi.Core.Settings.Models.External
public sealed class EmbySettings : Ombi.Settings.Settings.Models.Settings
{
public bool Enable { get; set; }
public bool IsJellyfin { get; set; }
public List<EmbyServers> Servers { get; set; } = new List<EmbyServers>();
}
@ -19,4 +18,4 @@ namespace Ombi.Core.Settings.Models.External
public string ServerHostname { get; set; }
public bool EnableEpisodeSearching { get; set; }
}
}
}

@ -0,0 +1,21 @@
using System.Collections.Generic;
using Ombi.Settings.Settings.Models.External;
namespace Ombi.Core.Settings.Models.External
{
public sealed class JellyfinSettings : Ombi.Settings.Settings.Models.Settings
{
public bool Enable { get; set; }
public List<JellyfinServers> Servers { get; set; } = new List<JellyfinServers>();
}
public class JellyfinServers : ExternalSettings
{
public string ServerId { get; set; }
public string Name { get; set; }
public string ApiKey { get; set; }
public string AdministratorId { get; set; }
public string ServerHostname { get; set; }
public bool EnableEpisodeSearching { get; set; }
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save