Merge pull request #3924 from joshuaboniface/jellyfin-redux

Add separate Jellyfin server type
pull/3954/head
Jamie 3 years ago committed by GitHub
commit 14c6170455
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

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

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

@ -13,38 +13,43 @@ 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;
public IEnumerable<RecentlyAddedMovieModel> GetRecentlyAddedMovies(DateTime from, DateTime to)
{
var plexMovies = _plex.GetAll().Where(x => x.Type == PlexMediaTypeEntity.Movie && x.AddedAt > from && x.AddedAt < to);
var embyMovies = _emby.GetAll().Where(x => x.Type == EmbyMediaType.Movie && x.AddedAt > from && x.AddedAt < to);
var jellyfinMovies = _jellyfin.GetAll().Where(x => x.Type == JellyfinMediaType.Movie && x.AddedAt > from && x.AddedAt < to);
return GetRecentlyAddedMovies(plexMovies, embyMovies).Take(30);
return GetRecentlyAddedMovies(plexMovies, embyMovies, jellyfinMovies).Take(30);
}
public IEnumerable<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);
}
@ -52,14 +57,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)
{
@ -136,17 +143,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)
{
@ -156,11 +202,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;
}
@ -181,6 +228,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)
@ -244,5 +307,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
});
}
}
}
}
}

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

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

@ -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,103 @@
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
{
obj.JellyfinUrl = JellyfinHelper.GetJellyfinMediaUrl(item.JellyfinId, server?.ServerId, null);
}
}
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;
@ -126,6 +128,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 +156,8 @@ 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>();
}
public static void RegisterStore(this IServiceCollection services) {
@ -169,6 +172,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 +217,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 +227,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>();

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

@ -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)
{
@ -180,4 +180,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,173 @@
#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
};
_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)
@ -379,6 +441,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 +496,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 +523,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 +532,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 +552,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 +564,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 +583,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 +743,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 +1139,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 +1321,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);
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);
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);
}
}
}
}

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

@ -3,6 +3,7 @@
public class JobSettings : Settings
{
public string EmbyContentSync { get; set; }
public string JellyfinContentSync { get; set; }
public string SonarrSync { get; set; }
public string RadarrSync { get; set; }
public string PlexContentSync { get; set; }
@ -18,4 +19,4 @@
public string MediaDatabaseRefresh { get; set; }
public string AutoDeleteRequests { get; set; }
}
}
}

@ -21,6 +21,11 @@ namespace Ombi.Settings.Settings.Models
return ValidateCron(Get(s.EmbyContentSync, Cron.Hourly(5)));
}
public static string JellyfinContent(JobSettings s)
{
return ValidateCron(Get(s.JellyfinContentSync, Cron.Hourly(5)));
}
public static string PlexContent(JobSettings s)
{
return ValidateCron(Get(s.PlexContentSync, Cron.Daily(2)));
@ -97,4 +102,4 @@ namespace Ombi.Settings.Settings.Models
return _defaultCron;
}
}
}
}

@ -7,10 +7,12 @@ namespace Ombi.Settings.Settings.Models
public bool ImportPlexAdmin { get; set; }
public bool ImportPlexUsers { get; set; }
public bool ImportEmbyUsers { get; set; }
public bool ImportJellyfinUsers { get; set; }
public int MovieRequestLimit { get; set; }
public int EpisodeRequestLimit { get; set; }
public List<string> DefaultRoles { get; set; } = new List<string>();
public List<string> BannedPlexUserIds { get; set; } = new List<string>();
public List<string> BannedEmbyUserIds { get; set; } = new List<string>();
public List<string> BannedJellyfinUserIds { get; set; } = new List<string>();
}
}
}

@ -30,6 +30,8 @@ namespace Ombi.Store.Context
public DbSet<CouchPotatoCache> CouchPotatoCache { get; set; }
public DbSet<EmbyContent> EmbyContent { get; set; }
public DbSet<EmbyEpisode> EmbyEpisode { get; set; }
public DbSet<JellyfinEpisode> JellyfinEpisode { get; set; }
public DbSet<JellyfinContent> JellyfinContent { get; set; }
public DbSet<SonarrCache> SonarrCache { get; set; }
public DbSet<LidarrArtistCache> LidarrArtistCache { get; set; }
@ -51,7 +53,13 @@ namespace Ombi.Store.Context
.HasPrincipalKey(x => x.EmbyId)
.HasForeignKey(p => p.ParentId);
builder.Entity<JellyfinEpisode>()
.HasOne(p => p.Series)
.WithMany(b => b.Episodes)
.HasPrincipalKey(x => x.JellyfinId)
.HasForeignKey(p => p.ParentId);
base.OnModelCreating(builder);
}
}
}
}

@ -0,0 +1,71 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: JellyfinContent.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.ComponentModel.DataAnnotations.Schema;
namespace Ombi.Store.Entities
{
[Table("JellyfinContent")]
public class JellyfinContent : Entity
{
public string Title { get; set; }
/// <summary>
/// OBSOLETE, Cannot delete due to DB migration issues with SQLite
/// </summary>
public string ProviderId { get; set; }
public string JellyfinId { get; set; }
public JellyfinMediaType Type { get; set; }
public DateTime AddedAt { get; set; }
public string ImdbId { get; set; }
public string TheMovieDbId { get; set; }
public string TvDbId { get; set; }
public string Url { get; set; }
public ICollection<JellyfinEpisode> Episodes { get; set; }
[NotMapped]
public bool HasImdb => !string.IsNullOrEmpty(ImdbId);
[NotMapped]
public bool HasTvDb => !string.IsNullOrEmpty(TvDbId);
[NotMapped]
public bool HasTheMovieDb => !string.IsNullOrEmpty(TheMovieDbId);
}
public enum JellyfinMediaType
{
Movie = 0,
Series = 1,
Music = 2
}
}

@ -0,0 +1,53 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: JellyfinEpisode.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.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore.Metadata;
namespace Ombi.Store.Entities
{
[Table("JellyfinEpisode")]
public class JellyfinEpisode : Entity
{
public string Title { get; set; }
public string JellyfinId { get; set; }
public int EpisodeNumber { get; set; }
public int SeasonNumber { get; set; }
public string ParentId { get; set; }
/// <summary>
/// NOT USED
/// </summary>
public string ProviderId { get; set; }
public DateTime AddedAt { get; set; }
public string TvDbId { get; set; }
public string ImdbId { get; set; }
public string TheMovieDbId { get; set; }
public JellyfinContent Series { get; set; }
}
}

@ -19,7 +19,8 @@ namespace Ombi.Store.Entities
{
Plex = 0,
Emby = 1,
Lidarr = 2
Lidarr = 2,
Jellyfin = 3
}
public enum ContentType
@ -28,4 +29,4 @@ namespace Ombi.Store.Entities
Episode = 1,
Album = 2,
}
}
}

@ -34,5 +34,6 @@ namespace Ombi.Store.Entities
PlexUser = 2,
EmbyUser = 3,
EmbyConnectUser = 4,
JellyfinUser = 5,
}
}
}

@ -0,0 +1,52 @@
```
dotnet ef migrations add Inital --context OmbiSqliteContext --startup-project ../Ombi/Ombi.csproj
```
If running migrations for any db provider other than Sqlite, then ensure the database.json is pointing at the correct DB type
## More detailed explanation
1. Install dotnet-ef, and include it in your $PATH if necessary:
```
dotnet tool install --global dotnet-ef
export PATH="$HOME/.dotnet/tools:$PATH"
```
1. In `src/Ombi`, install the `Microsoft.EntityFrameworkCore.Design` package:
```
cd src/Ombi
dotnet add package Microsoft.EntityFrameworkCore.Design
```
1. For some reason, the `StartupSingleton.Instance.SecurityKey` in `src/Ombi/Extensions/StartupExtensions.cs` is invalid when running `dotnet ef migrations add` so we must fix it; apply this patch which seems to do the job:
```
@@ -79,7 +79,7 @@ namespace Ombi
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
- IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(StartupSingleton.Instance.SecurityKey)),
+ IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(StartupSingleton.Instance.SecurityKey + "s")),
RequireExpirationTime = true,
ValidateLifetime = true,
ValidAudience = "Ombi",
```
*WARNING*: Don't forget to undo this before building Ombi, or things will be broken!
1. List the available `dbcontext`s, and select the one that matches the database your fields will go in:
```
cd src/Ombi.Store
dotnet ef dbcontext list
```
1. Run the migration using the command at the start of this document:
```
cd src/Ombi.Store
dotnet ef migrations add <name> --context <context> --startup-project ../Ombi/Ombi.csproj
```

@ -1,3 +0,0 @@
dotnet ef migrations add Inital --context OmbiSqliteContext --startup-project ../Ombi/Ombi.csproj
If running migrations for any db provider other than Sqlite, then ensure the database.json is pointing at the correct DB type

@ -0,0 +1,506 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Ombi.Store.Context.MySql;
namespace Ombi.Store.Migrations.ExternalMySql
{
[DbContext(typeof(ExternalMySqlContext))]
[Migration("20201212014227_Jellyfin")]
partial class Jellyfin
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.1");
modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("TheMovieDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("CouchPotatoCache");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<string>("EmbyId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<string>("ProviderId")
.HasColumnType("TEXT");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<string>("Url")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("EmbyContent");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<string>("EmbyId")
.HasColumnType("TEXT");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<string>("ParentId")
.HasColumnType("TEXT");
b.Property<string>("ProviderId")
.HasColumnType("TEXT");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("EmbyEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<string>("JellyfinId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ProviderId")
.HasColumnType("TEXT");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<string>("Url")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("JellyfinContent");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<string>("JellyfinId")
.HasColumnType("TEXT");
b.Property<string>("ParentId")
.HasColumnType("TEXT");
b.Property<string>("ProviderId")
.HasColumnType("TEXT");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("JellyfinEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<int>("ArtistId")
.HasColumnType("INTEGER");
b.Property<string>("ForeignAlbumId")
.HasColumnType("TEXT");
b.Property<bool>("Monitored")
.HasColumnType("INTEGER");
b.Property<decimal>("PercentOfTracks")
.HasColumnType("TEXT");
b.Property<DateTime>("ReleaseDate")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<int>("TrackCount")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("LidarrAlbumCache");
});
modelBuilder.Entity("Ombi.Store.Entities.LidarrArtistCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ArtistId")
.HasColumnType("INTEGER");
b.Property<string>("ArtistName")
.HasColumnType("TEXT");
b.Property<string>("ForeignArtistId")
.HasColumnType("TEXT");
b.Property<bool>("Monitored")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("LidarrArtistCache");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<int>("GrandparentKey")
.HasColumnType("INTEGER");
b.Property<int>("Key")
.HasColumnType("INTEGER");
b.Property<int>("ParentKey")
.HasColumnType("INTEGER");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("GrandparentKey");
b.ToTable("PlexEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ParentKey")
.HasColumnType("INTEGER");
b.Property<int>("PlexContentId")
.HasColumnType("INTEGER");
b.Property<int?>("PlexServerContentId")
.HasColumnType("INTEGER");
b.Property<int>("SeasonKey")
.HasColumnType("INTEGER");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("PlexServerContentId");
b.ToTable("PlexSeasonsContent");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<int>("Key")
.HasColumnType("INTEGER");
b.Property<string>("Quality")
.HasColumnType("TEXT");
b.Property<string>("ReleaseYear")
.HasColumnType("TEXT");
b.Property<int?>("RequestId")
.HasColumnType("INTEGER");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<string>("Url")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("PlexServerContent");
});
modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("HasFile")
.HasColumnType("INTEGER");
b.Property<int>("TheMovieDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("RadarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("TvDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("SickRageCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<int>("TvDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("SickRageEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("TvDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("SonarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<bool>("HasFile")
.HasColumnType("INTEGER");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<int>("TvDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("SonarrEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.EmbyContent", "Series")
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("EmbyId");
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.JellyfinContent", "Series")
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("JellyfinId");
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series")
.WithMany("Episodes")
.HasForeignKey("GrandparentKey")
.HasPrincipalKey("Key")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent", null)
.WithMany("Seasons")
.HasForeignKey("PlexServerContentId");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b =>
{
b.Navigation("Episodes");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b =>
{
b.Navigation("Episodes");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b =>
{
b.Navigation("Episodes");
b.Navigation("Seasons");
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,76 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Ombi.Store.Migrations.ExternalMySql
{
public partial class Jellyfin : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "JellyfinContent",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Title = table.Column<string>(nullable: true),
ProviderId = table.Column<string>(nullable: true),
JellyfinId = table.Column<string>(nullable: false),
Type = table.Column<int>(nullable: false),
AddedAt = table.Column<DateTime>(nullable: false),
ImdbId = table.Column<string>(nullable: true),
TheMovieDbId = table.Column<string>(nullable: true),
TvDbId = table.Column<string>(nullable: true),
Url = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_JellyfinContent", x => x.Id);
table.UniqueConstraint("AK_JellyfinContent_JellyfinId", x => x.JellyfinId);
});
migrationBuilder.CreateTable(
name: "JellyfinEpisode",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Title = table.Column<string>(nullable: true),
JellyfinId = table.Column<string>(nullable: true),
EpisodeNumber = table.Column<int>(nullable: false),
SeasonNumber = table.Column<int>(nullable: false),
ParentId = table.Column<string>(nullable: true),
ProviderId = table.Column<string>(nullable: true),
AddedAt = table.Column<DateTime>(nullable: false),
TvDbId = table.Column<string>(nullable: true),
ImdbId = table.Column<string>(nullable: true),
TheMovieDbId = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_JellyfinEpisode", x => x.Id);
table.ForeignKey(
name: "FK_JellyfinEpisode_JellyfinContent_ParentId",
column: x => x.ParentId,
principalTable: "JellyfinContent",
principalColumn: "JellyfinId",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_JellyfinEpisode_ParentId",
table: "JellyfinEpisode",
column: "ParentId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "JellyfinContent");
migrationBuilder.DropTable(
name: "JellyfinEpisode");
}
}
}

@ -113,6 +113,88 @@ namespace Ombi.Store.Migrations.ExternalMySql
b.ToTable("EmbyEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("AddedAt")
.HasColumnType("datetime(6)");
b.Property<string>("ImdbId")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<string>("JellyfinId")
.IsRequired()
.HasColumnType("varchar(255) CHARACTER SET utf8mb4");
b.Property<string>("ProviderId")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<string>("TheMovieDbId")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<string>("Title")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<string>("TvDbId")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<string>("Url")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.HasKey("Id");
b.ToTable("JellyfinContent");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("AddedAt")
.HasColumnType("dateime(6)");
b.Property<int>("EpisodeNumber")
.HasColumnType("int");
b.Property<string>("ImdbId")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<string>("JellyfinId")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<string>("ParentId")
.HasColumnType("varchar(255) CHARACTER SET utf8mb4");
b.Property<string>("ProviderId")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<int>("SeasonNumber")
.HasColumnType("int");
b.Property<string>("TheMovieDbId")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<string>("Title")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<string>("TvDbId")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("JellyfinEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b =>
{
b.Property<int>("Id")
@ -368,6 +450,18 @@ namespace Ombi.Store.Migrations.ExternalMySql
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("EmbyId");
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.JellyfinContent", "Series")
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("JellyfinId");
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
@ -378,6 +472,8 @@ namespace Ombi.Store.Migrations.ExternalMySql
.HasPrincipalKey("Key")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
@ -386,6 +482,23 @@ namespace Ombi.Store.Migrations.ExternalMySql
.WithMany("Seasons")
.HasForeignKey("PlexServerContentId");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b =>
{
b.Navigation("Episodes");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b =>
{
b.Navigation("Episodes");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b =>
{
b.Navigation("Episodes");
b.Navigation("Seasons");
});
#pragma warning restore 612, 618
}
}

@ -0,0 +1,506 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Ombi.Store.Context.Sqlite;
namespace Ombi.Store.Migrations.ExternalSqlite
{
[DbContext(typeof(ExternalSqliteContext))]
[Migration("20201212014227_Jellyfin")]
partial class Jellyfin
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.1");
modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("TheMovieDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("CouchPotatoCache");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<string>("EmbyId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<string>("ProviderId")
.HasColumnType("TEXT");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<string>("Url")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("EmbyContent");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<string>("EmbyId")
.HasColumnType("TEXT");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<string>("ParentId")
.HasColumnType("TEXT");
b.Property<string>("ProviderId")
.HasColumnType("TEXT");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("EmbyEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<string>("JellyfinId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ProviderId")
.HasColumnType("TEXT");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<string>("Url")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("JellyfinContent");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<string>("JellyfinId")
.HasColumnType("TEXT");
b.Property<string>("ParentId")
.HasColumnType("TEXT");
b.Property<string>("ProviderId")
.HasColumnType("TEXT");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("JellyfinEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<int>("ArtistId")
.HasColumnType("INTEGER");
b.Property<string>("ForeignAlbumId")
.HasColumnType("TEXT");
b.Property<bool>("Monitored")
.HasColumnType("INTEGER");
b.Property<decimal>("PercentOfTracks")
.HasColumnType("TEXT");
b.Property<DateTime>("ReleaseDate")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<int>("TrackCount")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("LidarrAlbumCache");
});
modelBuilder.Entity("Ombi.Store.Entities.LidarrArtistCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ArtistId")
.HasColumnType("INTEGER");
b.Property<string>("ArtistName")
.HasColumnType("TEXT");
b.Property<string>("ForeignArtistId")
.HasColumnType("TEXT");
b.Property<bool>("Monitored")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("LidarrArtistCache");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<int>("GrandparentKey")
.HasColumnType("INTEGER");
b.Property<int>("Key")
.HasColumnType("INTEGER");
b.Property<int>("ParentKey")
.HasColumnType("INTEGER");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("GrandparentKey");
b.ToTable("PlexEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ParentKey")
.HasColumnType("INTEGER");
b.Property<int>("PlexContentId")
.HasColumnType("INTEGER");
b.Property<int?>("PlexServerContentId")
.HasColumnType("INTEGER");
b.Property<int>("SeasonKey")
.HasColumnType("INTEGER");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("PlexServerContentId");
b.ToTable("PlexSeasonsContent");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<int>("Key")
.HasColumnType("INTEGER");
b.Property<string>("Quality")
.HasColumnType("TEXT");
b.Property<string>("ReleaseYear")
.HasColumnType("TEXT");
b.Property<int?>("RequestId")
.HasColumnType("INTEGER");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<string>("Url")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("PlexServerContent");
});
modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("HasFile")
.HasColumnType("INTEGER");
b.Property<int>("TheMovieDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("RadarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("TvDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("SickRageCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<int>("TvDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("SickRageEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("TvDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("SonarrCache");
});
modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<bool>("HasFile")
.HasColumnType("INTEGER");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<int>("TvDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("SonarrEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.EmbyContent", "Series")
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("EmbyId");
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.JellyfinContent", "Series")
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("JellyfinId");
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series")
.WithMany("Episodes")
.HasForeignKey("GrandparentKey")
.HasPrincipalKey("Key")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent", null)
.WithMany("Seasons")
.HasForeignKey("PlexServerContentId");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b =>
{
b.Navigation("Episodes");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b =>
{
b.Navigation("Episodes");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b =>
{
b.Navigation("Episodes");
b.Navigation("Seasons");
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,76 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace Ombi.Store.Migrations.ExternalSqlite
{
public partial class Jellyfin : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "JellyfinContent",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Title = table.Column<string>(type: "TEXT", nullable: true),
ProviderId = table.Column<string>(type: "TEXT", nullable: true),
JellyfinId = table.Column<string>(type: "TEXT", nullable: false),
Type = table.Column<int>(type: "INTEGER", nullable: false),
AddedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
ImdbId = table.Column<string>(type: "TEXT", nullable: true),
TheMovieDbId = table.Column<string>(type: "TEXT", nullable: true),
TvDbId = table.Column<string>(type: "TEXT", nullable: true),
Url = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_JellyfinContent", x => x.Id);
table.UniqueConstraint("AK_JellyfinContent_JellyfinId", x => x.JellyfinId);
});
migrationBuilder.CreateTable(
name: "JellyfinEpisode",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Title = table.Column<string>(type: "TEXT", nullable: true),
JellyfinId = table.Column<string>(type: "TEXT", nullable: true),
EpisodeNumber = table.Column<int>(type: "INTEGER", nullable: false),
SeasonNumber = table.Column<int>(type: "INTEGER", nullable: false),
ParentId = table.Column<string>(type: "TEXT", nullable: true),
ProviderId = table.Column<string>(type: "TEXT", nullable: true),
AddedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
TvDbId = table.Column<string>(type: "TEXT", nullable: true),
ImdbId = table.Column<string>(type: "TEXT", nullable: true),
TheMovieDbId = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_JellyfinEpisode", x => x.Id);
table.ForeignKey(
name: "FK_JellyfinEpisode_JellyfinContent_ParentId",
column: x => x.ParentId,
principalTable: "JellyfinContent",
principalColumn: "JellyfinId",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_JellyfinEpisode_ParentId",
table: "JellyfinEpisode",
column: "ParentId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "JellyfinContent");
migrationBuilder.DropTable(
name: "JellyfinEpisode");
}
}
}

@ -14,14 +14,16 @@ namespace Ombi.Store.Migrations.ExternalSqlite
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
.HasAnnotation("ProductVersion", "5.0.1");
modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("TheMovieDbId");
b.Property<int>("TheMovieDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
@ -31,26 +33,36 @@ namespace Ombi.Store.Migrations.ExternalSqlite
modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<string>("EmbyId")
.IsRequired();
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ImdbId");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<string>("ProviderId");
b.Property<string>("ProviderId")
.HasColumnType("TEXT");
b.Property<string>("TheMovieDbId");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.Property<int>("Type");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<string>("Url");
b.Property<string>("Url")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -60,27 +72,38 @@ namespace Ombi.Store.Migrations.ExternalSqlite
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<string>("EmbyId");
b.Property<string>("EmbyId")
.HasColumnType("TEXT");
b.Property<int>("EpisodeNumber");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<string>("ImdbId");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<string>("ParentId");
b.Property<string>("ParentId")
.HasColumnType("TEXT");
b.Property<string>("ProviderId");
b.Property<string>("ProviderId")
.HasColumnType("TEXT");
b.Property<int>("SeasonNumber");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<string>("TheMovieDbId");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -89,26 +112,117 @@ namespace Ombi.Store.Migrations.ExternalSqlite
b.ToTable("EmbyEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<string>("JellyfinId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ProviderId")
.HasColumnType("TEXT");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<string>("Url")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("JellyfinContent");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<string>("JellyfinId")
.HasColumnType("TEXT");
b.Property<string>("ParentId")
.HasColumnType("TEXT");
b.Property<string>("ProviderId")
.HasColumnType("TEXT");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ParentId");
b.ToTable("JellyfinEpisode");
});
modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<int>("ArtistId");
b.Property<int>("ArtistId")
.HasColumnType("INTEGER");
b.Property<string>("ForeignAlbumId");
b.Property<string>("ForeignAlbumId")
.HasColumnType("TEXT");
b.Property<bool>("Monitored");
b.Property<bool>("Monitored")
.HasColumnType("INTEGER");
b.Property<decimal>("PercentOfTracks");
b.Property<decimal>("PercentOfTracks")
.HasColumnType("TEXT");
b.Property<DateTime>("ReleaseDate");
b.Property<DateTime>("ReleaseDate")
.HasColumnType("TEXT");
b.Property<string>("Title");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<int>("TrackCount");
b.Property<int>("TrackCount")
.HasColumnType("INTEGER");
b.HasKey("Id");
@ -118,15 +232,20 @@ namespace Ombi.Store.Migrations.ExternalSqlite
modelBuilder.Entity("Ombi.Store.Entities.LidarrArtistCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ArtistId");
b.Property<int>("ArtistId")
.HasColumnType("INTEGER");
b.Property<string>("ArtistName");
b.Property<string>("ArtistName")
.HasColumnType("TEXT");
b.Property<string>("ForeignArtistId");
b.Property<string>("ForeignArtistId")
.HasColumnType("TEXT");
b.Property<bool>("Monitored");
b.Property<bool>("Monitored")
.HasColumnType("INTEGER");
b.HasKey("Id");
@ -136,19 +255,26 @@ namespace Ombi.Store.Migrations.ExternalSqlite
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("EpisodeNumber");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<int>("GrandparentKey");
b.Property<int>("GrandparentKey")
.HasColumnType("INTEGER");
b.Property<int>("Key");
b.Property<int>("Key")
.HasColumnType("INTEGER");
b.Property<int>("ParentKey");
b.Property<int>("ParentKey")
.HasColumnType("INTEGER");
b.Property<int>("SeasonNumber");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<string>("Title");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -160,17 +286,23 @@ namespace Ombi.Store.Migrations.ExternalSqlite
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ParentKey");
b.Property<int>("ParentKey")
.HasColumnType("INTEGER");
b.Property<int>("PlexContentId");
b.Property<int>("PlexContentId")
.HasColumnType("INTEGER");
b.Property<int?>("PlexServerContentId");
b.Property<int?>("PlexServerContentId")
.HasColumnType("INTEGER");
b.Property<int>("SeasonKey");
b.Property<int>("SeasonKey")
.HasColumnType("INTEGER");
b.Property<int>("SeasonNumber");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.HasKey("Id");
@ -182,29 +314,41 @@ namespace Ombi.Store.Migrations.ExternalSqlite
modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("AddedAt");
b.Property<DateTime>("AddedAt")
.HasColumnType("TEXT");
b.Property<string>("ImdbId");
b.Property<string>("ImdbId")
.HasColumnType("TEXT");
b.Property<int>("Key");
b.Property<int>("Key")
.HasColumnType("INTEGER");
b.Property<string>("Quality");
b.Property<string>("Quality")
.HasColumnType("TEXT");
b.Property<string>("ReleaseYear");
b.Property<string>("ReleaseYear")
.HasColumnType("TEXT");
b.Property<int?>("RequestId");
b.Property<int?>("RequestId")
.HasColumnType("INTEGER");
b.Property<string>("TheMovieDbId");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("Title");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<string>("TvDbId");
b.Property<string>("TvDbId")
.HasColumnType("TEXT");
b.Property<int>("Type");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<string>("Url");
b.Property<string>("Url")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -214,11 +358,14 @@ namespace Ombi.Store.Migrations.ExternalSqlite
modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("HasFile");
b.Property<bool>("HasFile")
.HasColumnType("INTEGER");
b.Property<int>("TheMovieDbId");
b.Property<int>("TheMovieDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
@ -228,9 +375,11 @@ namespace Ombi.Store.Migrations.ExternalSqlite
modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("TvDbId");
b.Property<int>("TvDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
@ -240,13 +389,17 @@ namespace Ombi.Store.Migrations.ExternalSqlite
modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("EpisodeNumber");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<int>("SeasonNumber");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<int>("TvDbId");
b.Property<int>("TvDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
@ -256,9 +409,11 @@ namespace Ombi.Store.Migrations.ExternalSqlite
modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("TvDbId");
b.Property<int>("TvDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
@ -268,15 +423,20 @@ namespace Ombi.Store.Migrations.ExternalSqlite
modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("EpisodeNumber");
b.Property<int>("EpisodeNumber")
.HasColumnType("INTEGER");
b.Property<bool>("HasFile");
b.Property<bool>("HasFile")
.HasColumnType("INTEGER");
b.Property<int>("SeasonNumber");
b.Property<int>("SeasonNumber")
.HasColumnType("INTEGER");
b.Property<int>("TvDbId");
b.Property<int>("TvDbId")
.HasColumnType("INTEGER");
b.HasKey("Id");
@ -289,6 +449,18 @@ namespace Ombi.Store.Migrations.ExternalSqlite
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("EmbyId");
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.JellyfinContent", "Series")
.WithMany("Episodes")
.HasForeignKey("ParentId")
.HasPrincipalKey("JellyfinId");
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b =>
@ -297,15 +469,35 @@ namespace Ombi.Store.Migrations.ExternalSqlite
.WithMany("Episodes")
.HasForeignKey("GrandparentKey")
.HasPrincipalKey("Key")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Series");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b =>
{
b.HasOne("Ombi.Store.Entities.PlexServerContent")
b.HasOne("Ombi.Store.Entities.PlexServerContent", null)
.WithMany("Seasons")
.HasForeignKey("PlexServerContentId");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b =>
{
b.Navigation("Episodes");
});
modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b =>
{
b.Navigation("Episodes");
});
modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b =>
{
b.Navigation("Episodes");
b.Navigation("Seasons");
});
#pragma warning restore 612, 618
}
}

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Ombi.Store.Entities;
namespace Ombi.Store.Repository
{
public interface IJellyfinContentRepository : IRepository<JellyfinContent>
{
IQueryable<JellyfinContent> Get();
Task<JellyfinContent> GetByTheMovieDbId(string mov);
Task<JellyfinContent> GetByTvDbId(string tv);
Task<JellyfinContent> GetByImdbId(string imdbid);
Task<JellyfinContent> GetByJellyfinId(string jellyfinId);
Task Update(JellyfinContent existingContent);
IQueryable<JellyfinEpisode> GetAllEpisodes();
Task<JellyfinEpisode> Add(JellyfinEpisode content);
Task<JellyfinEpisode> GetEpisodeByJellyfinId(string key);
Task AddRange(IEnumerable<JellyfinEpisode> content);
void UpdateWithoutSave(JellyfinContent existingContent);
}
}

@ -0,0 +1,106 @@
#region Copyright
// /************************************************************************
// Copyright (c) 2017 Jamie Rees
// File: PlexContentRepository.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.EntityFrameworkCore;
using Ombi.Store.Context;
using Ombi.Store.Entities;
namespace Ombi.Store.Repository
{
public class JellyfinContentRepository : ExternalRepository<JellyfinContent>, IJellyfinContentRepository
{
public JellyfinContentRepository(ExternalContext db):base(db)
{
Db = db;
}
private ExternalContext Db { get; }
public async Task<JellyfinContent> GetByImdbId(string imdbid)
{
return await Db.JellyfinContent.FirstOrDefaultAsync(x => x.ImdbId == imdbid);
}
public async Task<JellyfinContent> GetByTvDbId(string tv)
{
return await Db.JellyfinContent.FirstOrDefaultAsync(x => x.TvDbId == tv);
}
public async Task<JellyfinContent> GetByTheMovieDbId(string mov)
{
return await Db.JellyfinContent.FirstOrDefaultAsync(x => x.TheMovieDbId == mov);
}
public IQueryable<JellyfinContent> Get()
{
return Db.JellyfinContent.AsQueryable();
}
public async Task<JellyfinContent> GetByJellyfinId(string jellyfinId)
{
return await Db.JellyfinContent./*Include(x => x.Seasons).*/FirstOrDefaultAsync(x => x.JellyfinId == jellyfinId);
}
public async Task Update(JellyfinContent existingContent)
{
Db.JellyfinContent.Update(existingContent);
await InternalSaveChanges();
}
public IQueryable<JellyfinEpisode> GetAllEpisodes()
{
return Db.JellyfinEpisode.AsQueryable();
}
public async Task<JellyfinEpisode> Add(JellyfinEpisode content)
{
await Db.JellyfinEpisode.AddAsync(content);
await InternalSaveChanges();
return content;
}
public async Task<JellyfinEpisode> GetEpisodeByJellyfinId(string key)
{
return await Db.JellyfinEpisode.FirstOrDefaultAsync(x => x.JellyfinId == key);
}
public async Task AddRange(IEnumerable<JellyfinEpisode> content)
{
Db.JellyfinEpisode.AddRange(content);
await InternalSaveChanges();
}
public void UpdateWithoutSave(JellyfinContent existingContent)
{
Db.JellyfinContent.Update(existingContent);
}
}
}

@ -14,7 +14,7 @@ namespace Ombi.Test.Common
{
var store = new Mock<IUserStore<OmbiUser>>();
//var u = new OmbiUserManager(store.Object, null, null, null, null, null, null, null, null,null,null,null,null)
var mgr = new Mock<OmbiUserManager>(store.Object, null, null, null, null, null, null, null, null, null, null, null, null);
var mgr = new Mock<OmbiUserManager>(store.Object, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
mgr.Object.UserValidators.Add(new UserValidator<OmbiUser>());
mgr.Object.PasswordValidators.Add(new PasswordValidator<OmbiUser>());

@ -39,6 +39,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Schedule", "Ombi.Sched
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Emby", "Ombi.Api.Emby\Ombi.Api.Emby.csproj", "{08FF107D-31E1-470D-AF86-E09B015CEE06}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Jellyfin", "Ombi.Api.Jellyfin\Ombi.Api.Jellyfin.csproj", "{08FF107D-31E1-470D-AF86-E09B015CEE06}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Sonarr", "Ombi.Api.Sonarr\Ombi.Api.Sonarr.csproj", "{CFB5E008-D0D0-43C0-AA06-89E49D17F384}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{6F42AB98-9196-44C4-B888-D5E409F415A1}"

@ -22,6 +22,10 @@
<i matTooltip=" {{'Search.ViewOnEmby' | translate}}"
class="fa fa-play-circle fa-2x grow"></i>
</a>
<a *ngIf="movie.jellyfinUrl" class="media-icons" href="{{movie.jellyfinUrl}}" target="_blank">
<i matTooltip=" {{'Search.ViewOnJellyfin' | translate}}"
class="fa fa-play-circle fa-2x grow"></i>
</a>
</span>
<span *ngIf="tv">
@ -33,6 +37,10 @@
<i matTooltip=" {{'Search.ViewOnEmby' | translate}}"
class="fa fa-play-circle fa-2x grow"></i>
</a>
<a *ngIf="tv.jellyfinUrl" class="media-icons" href="{{tv.jellyfinUrl}}" target="_blank">
<i matTooltip=" {{'Search.ViewOnJellyfin' | translate}}"
class="fa fa-play-circle fa-2x grow"></i>
</a>
</span>
<a class="media-icons" (click)="close()">
@ -151,10 +159,13 @@
<a *ngIf="tv.embyUrl" mat-raised-button class="btn-green btn-spacing" href="{{tv.embyUrl}}"
target="_blank"><i class="fa fa-eye"></i> {{'Search.ViewOnEmby' |
translate}}</a>
<a *ngIf="tv.jellyfinUrl" mat-raised-button class="btn-green btn-spacing" href="{{tv.jellyfinUrl}}"
target="_blank"><i class="fa fa-eye"></i> {{'Search.ViewOnJellyfin' |
translate}}</a>
</span>
<button mat-raised-button class="btn-green btn-spacing" (click)="openDetails()"> {{
'Common.ViewDetails' | translate }}</button>
</div>
</div>
</div>
</div>

@ -50,6 +50,10 @@
<mat-icon style="color:white" matTooltip=" {{'Search.ViewOnEmby' | translate}}">
play_circle_outline</mat-icon>
</a></mat-chip>
<mat-chip *ngIf="movie && movie.jellyfinUrl"> <a href="{{movie.jellyfinUrl}}" target="_blank">
<mat-icon style="color:white" matTooltip=" {{'Search.ViewOnJellyfin' | translate}}">
play_circle_outline</mat-icon>
</a></mat-chip>
<mat-chip *ngIf="tv && tv.plexUrl"> <a href="{{tv.plexUrl}}" target="_blank">
<mat-icon style="color:white" matTooltip=" {{'Search.ViewOnPlex' | translate}}">
@ -59,6 +63,10 @@
<mat-icon style="color:white" matTooltip=" {{'Search.ViewOnEmby' | translate}}">
play_circle_outline</mat-icon>
</a></mat-chip>
<mat-chip *ngIf="tv &&tv.jellyfinUrl"> <a href="{{movie.jellyfinUrl}}" target="_blank">
<mat-icon style="color:white" matTooltip=" {{'Search.ViewOnJellyfin' | translate}}">
play_circle_outline</mat-icon>
</a></mat-chip>
</mat-chip-list>
</div>
<div class="row">
@ -148,10 +156,13 @@
<a *ngIf="tv.embyUrl" mat-raised-button class="btn-green btn-spacing" href="{{tv.embyUrl}}"
target="_blank"><i class="fa fa-eye"></i> {{'Search.ViewOnEmby' |
translate}}</a>
<a *ngIf="tv.jellyfinUrl" mat-raised-button class="btn-green btn-spacing" href="{{tv.jellyfinUrl}}"
target="_blank"><i class="fa fa-eye"></i> {{'Search.ViewOnJellyfin' |
translate}}</a>
</span>
</div>
</div>
</div>
</div>
</mat-card>
</div>
</div>

@ -26,4 +26,5 @@ export interface IRecentlyAddedRangeModel {
export enum RecentlyAddedType {
Plex,
Emby,
Jellyfin,
}

@ -23,6 +23,7 @@
available: boolean;
plexUrl: string;
embyUrl: string;
jellyfinUrl: string;
quality: string;
digitalReleaseDate: Date;
subscribed: boolean;

@ -34,6 +34,7 @@
recommendations: IOtherMovies;
plexUrl: string;
embyUrl: string;
jellyfinUrl: string;
quality: string;
digitalReleaseDate: Date;
subscribed: boolean;
@ -166,4 +167,4 @@ export interface IOtherMoviesViewModel {
vote_average: number;
vote_count: number;
popularity: number;
}
}

@ -28,6 +28,7 @@ export interface ISearchTvResult {
available: boolean;
plexUrl: string;
embyUrl: string;
jellyfinUrl: string;
quality: string;
firstSeason: boolean;
latestSeason: boolean;

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

Loading…
Cancel
Save