Add separate Jellyfin server type

Due to forthcoming changes to the Jellyfin API, this adds support for
Jellyfin as server type completely independent from Emby. It also undoes
the workarounds that treated Jellyfin as a subset of Emby.
pull/3924/head
Joshua M. Boniface 3 years ago
parent ce39f205f3
commit 9fe644f3db

@ -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,9 @@
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
@ -67,15 +67,15 @@ namespace Ombi.Api.Emby
request.AddJsonBody(body);
request.AddHeader("X-Emby-Authorization",
request.AddHeader("X-Jellyfin-Authorization",
$"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,11 @@
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,19 @@
namespace Ombi.Api.Jellyfin.Models
{
public class PublicInfo
{
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,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>

@ -115,4 +115,107 @@ namespace Ombi.Core.Tests.Rule.Search
Assert.False(search.Available);
}
}
}
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#!/item?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 = 4
}
}
}

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

@ -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, s.IsJellyfin);
}
else
{
obj.JellyfinUrl = JellyfinHelper.GetJellyfinMediaUrl(item.JellyfinId, server?.ServerId, null, s.IsJellyfin);
}
}
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" />

@ -0,0 +1,27 @@
namespace Ombi.Helpers
{
public static class JellyfinHelper
{
public static string GetJellyfinMediaUrl(string mediaId, string serverId, string customerServerUrl = null, bool isJellyfin = false)
{
//web/index.html#!/details|item
string path = "item";
if (isJellyfin)
{
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 $"https://app.jellyfin.media/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);
}
}
}

@ -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 {1} for server {0}", server.Name, jellyfinSettings.IsJellyfin ? "Jellyfin" : "Jellyfin");
}
}
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, settings.IsJellyfin),
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, $"{(settings.IsJellyfin ? "Jellyfin" : "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.ConnectUserName.HasValue() && !jellyfinUser.Name.HasValue())
{
_log.LogInformation("Could not create Jellyfin user since the have no username, PlexUserId: {0}", jellyfinUser.Id);
continue;
}
var isConnectUser = jellyfinUser.ConnectUserName.HasValue();
// Create this users
var newUser = new OmbiUser
{
UserName = jellyfinUser.Name,
ProviderUserId = jellyfinUser.Id,
Alias = isConnectUser ? jellyfinUser.Name : string.Empty,
MovieRequestLimit = userManagementSettings.MovieRequestLimit,
EpisodeRequestLimit = userManagementSettings.EpisodeRequestLimit
};
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);
@ -383,4 +500,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);
}
}
}
}

@ -0,0 +1,22 @@
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 bool IsJellyfin { 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,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);
}
}
}

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

@ -29,6 +29,7 @@ export interface ISearchTvResultV2 {
available: boolean;
plexUrl: string;
embyUrl: string;
jellyfinUrl: string;
quality: string;
firstSeason: boolean;
latestSeason: boolean;
@ -62,6 +63,7 @@ export interface IMovieCollection {
available: boolean;
plexUrl: string;
embyUrl: string;
jellyfinUrl: string;
imdbId: string;
}

@ -52,7 +52,26 @@ export interface IEmbyServer extends IExternalSettings {
export interface IPublicInfo {
id: string;
serverName: string;
}
export interface IJellyfinSettings extends ISettings {
enable: boolean;
isJellyfin: boolean;
servers: IJellyfinServer[];
}
export interface IJellyfinServer extends IExternalSettings {
serverId: string;
name: string;
apiKey: string;
administratorId: string;
enableEpisodeSearching: boolean;
serverHostname: string;
}
export interface IPublicInfo {
id: string;
serverName: string;
}
export interface IPlexSettings extends ISettings {
@ -141,6 +160,7 @@ export interface ICustomizationSettings extends ISettings {
export interface IJobSettings {
embyContentSync: string;
jellyfinContentSync: string;
sonarrSync: string;
radarrSync: string;
plexContentSync: string;
@ -187,11 +207,13 @@ export interface IUserManagementSettings extends ISettings {
importPlexUsers: boolean;
importPlexAdmin: boolean;
importEmbyUsers: boolean;
importJellyfinUsers: boolean;
defaultRoles: string[];
movieRequestLimit: number;
episodeRequestLimit: number;
bannedPlexUserIds: string[];
bannedEmbyUserIds: string[];
bannedJellyfinUserIds: string[];
}
export interface IAbout {

@ -48,6 +48,7 @@ export enum UserType {
LocalUser = 1,
PlexUser = 2,
EmbyUser = 3,
JellyfinUser = 4,
}
export interface IIdentityResult {

@ -17,7 +17,7 @@
<div class="col-12 col-lg-3 col-xl-3 media-row">
<social-icons [homepage]="movie.homepage" [theMoviedbId]="movie.id" [hasTrailer]="movie.videos?.results?.length > 0" (openTrailer)="openDialog()" [imdbId]="movie.imdbId" [twitter]="movie.externalIds.twitterId" [facebook]="movie.externalIds.facebookId"
[instagram]="movie.externalIds.instagramId" [available]="movie.available" [plexUrl]="movie.plexUrl" [embyUrl]="movie.embyUrl"></social-icons>
[instagram]="movie.externalIds.instagramId" [available]="movie.available" [plexUrl]="movie.plexUrl" [embyUrl]="movie.embyUrl" [jellyfinUrl]="movie.jellyfinUrl"></social-icons>
</div>
@ -198,4 +198,4 @@
</div>
</div>

@ -40,4 +40,9 @@
class="fa fa-play-circle fa-2x grow-social"></i>
</a>
</span>
<a *ngIf="jellyfinUrl" class="media-icons jellyfin" href="{{jellyfinUrl}}" target="_blank">
<i matTooltip=" {{'Search.ViewOnJellyfin' | translate}}"
class="fa fa-play-circle fa-2x grow-social"></i>
</a>
</span>

@ -6,4 +6,8 @@
.media-icons.emby {
color: #52b54a !important;
}
}
.media-icons.jellyfin {
color: #00a4dc !important;
}

@ -18,6 +18,7 @@ export class SocialIconsComponent {
@Input() available: boolean;
@Input() plexUrl: string;
@Input() embyUrl: string;
@Input() jellyfinUrl: string;
@Input() doNotAppend: boolean;
@Output() openTrailer: EventEmitter<any> = new EventEmitter();

@ -24,7 +24,7 @@
<!--Next to poster-->
<div class="col-12 col-lg-3 col-xl-3 media-row">
<social-icons [homepage]="tv.homepage" [tvdbId]="tv.id" [hasTrailer]="tv.trailer" (openTrailer)="openDialog()" [imdbId]="tv.imdbId" [available]="tv.available" [plexUrl]="tv.plexUrl" [embyUrl]="tv.embyUrl">
<social-icons [homepage]="tv.homepage" [tvdbId]="tv.id" [hasTrailer]="tv.trailer" (openTrailer)="openDialog()" [imdbId]="tv.imdbId" [available]="tv.available" [plexUrl]="tv.plexUrl" [embyUrl]="tv.embyUrl" [jellyfinUrl]="tv.jellyfinUrl">
</social-icons>
</div>
@ -125,4 +125,4 @@
</div>
</ng-template>
</div>
</div>

@ -168,6 +168,9 @@
<a *ngIf="result.embyUrl" style="text-align: right" id="embybtn" class="btn btn-sm btn-success-outline"
href="{{result.embyUrl}}" target="_blank"><i class="fa fa-eye"></i> {{'Search.ViewOnEmby' |
translate}}</a>
<a *ngIf="result.jellyfinUrl" style="text-align: right" id="jellyfinbtn" class="btn btn-sm btn-success-outline"
href="{{result.jellyfinUrl}}" target="_blank"><i class="fa fa-eye"></i> {{'Search.ViewOnJellyfin' |
translate}}</a>
</div>
<div class="dropdown" *ngIf="result.available && issueCategories && issuesEnabled">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown"

@ -142,6 +142,11 @@
<i class="fa fa-eye"></i> {{ 'Search.ViewOnEmby' | translate }}
</a>
</div>
<div *ngIf="node.jellyfinUrl && node.available">
<a style="text-align: right" id="jellyfinbtn" class="btn btn-sm btn-success-outline" href="{{node.jellyfinUrl}}" target="_blank">
<i class="fa fa-eye"></i> {{ 'Search.ViewOnJellyfin' | translate }}
</a>
</div>
<div class="dropdown" *ngIf="issueCategories && issuesEnabled">
<button class="btn btn-sm btn-primary-outline dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true">

@ -1,5 +1,6 @@
export * from "./couchpotato.service";
export * from "./emby.service";
export * from "./jellyfin.service";
export * from "./plex.service";
export * from "./radarr.service";
export * from "./sonarr.service";

@ -0,0 +1,28 @@
import { PlatformLocation, APP_BASE_HREF } from "@angular/common";
import { HttpClient } from "@angular/common/http";
import { Injectable, Inject } from "@angular/core";
import { Observable } from "rxjs";
import { ServiceHelpers } from "../service.helpers";
import { IJellyfinServer, IJellyfinSettings, IPublicInfo, IUsersModel } from "../../interfaces";
@Injectable()
export class JellyfinService extends ServiceHelpers {
constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) {
super(http, "/api/v1/Jellyfin/", href);
}
public logIn(settings: IJellyfinSettings): Observable<IJellyfinSettings> {
return this.http.post<IJellyfinSettings>(`${this.url}`, JSON.stringify(settings), {headers: this.headers});
}
public getUsers(): Observable<IUsersModel[]> {
return this.http.get<IUsersModel[]>(`${this.url}users`, {headers: this.headers});
}
public getPublicInfo(server: IJellyfinServer): Observable<IPublicInfo> {
return this.http.post<IPublicInfo>(`${this.url}info`, JSON.stringify(server), {headers: this.headers});
}
}

@ -11,6 +11,7 @@ import {
IDiscordNotifcationSettings,
IEmailNotificationSettings,
IEmbyServer,
IJellyfinServer,
IGotifyNotificationSettings,
ILidarrSettings,
IMattermostNotifcationSettings,
@ -78,6 +79,10 @@ export class TesterService extends ServiceHelpers {
return this.http.post<boolean>(`${this.url}emby`, JSON.stringify(settings), {headers: this.headers});
}
public jellyfinTest(settings: IJellyfinServer): Observable<boolean> {
return this.http.post<boolean>(`${this.url}jellyfin`, JSON.stringify(settings), {headers: this.headers});
}
public radarrTest(settings: IRadarrSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}radarr`, JSON.stringify(settings), {headers: this.headers});
}

@ -31,6 +31,10 @@ export class JobService extends ServiceHelpers {
return this.http.post<boolean>(`${this.url}embyUserImporter/`, {headers: this.headers});
}
public runJellyfinImporter(): Observable<boolean> {
return this.http.post<boolean>(`${this.url}jellyfinUserImporter/`, {headers: this.headers});
}
public runPlexCacher(): Observable<boolean> {
return this.http.post<boolean>(`${this.url}plexcontentcacher/`, {headers: this.headers});
}
@ -43,6 +47,10 @@ export class JobService extends ServiceHelpers {
return this.http.post<boolean>(`${this.url}embycontentcacher/`, {headers: this.headers});
}
public runJellyfinCacher(): Observable<boolean> {
return this.http.post<boolean>(`${this.url}jellyfincontentcacher/`, {headers: this.headers});
}
public runNewsletter(): Observable<boolean> {
return this.http.post<boolean>(`${this.url}newsletter/`, {headers: this.headers});
}

@ -14,6 +14,7 @@ import {
IDogNzbSettings,
IEmailNotificationSettings,
IEmbySettings,
IJellyfinSettings,
IGotifyNotificationSettings,
IIssueSettings,
IJobSettings,
@ -76,6 +77,14 @@ export class SettingsService extends ServiceHelpers {
return this.http.post<boolean>(`${this.url}/Emby/`, JSON.stringify(settings), {headers: this.headers});
}
public getJellyfin(): Observable<IJellyfinSettings> {
return this.http.get<IJellyfinSettings>(`${this.url}/Jellyfin/`);
}
public saveJellyfin(settings: IJellyfinSettings): Observable<boolean> {
return this.http.post<boolean>(`${this.url}/Jellyfin/`, JSON.stringify(settings), {headers: this.headers});
}
public getPlex(): Observable<IPlexSettings> {
return this.http.get<IPlexSettings>(`${this.url}/Plex/`, {headers: this.headers});
}

@ -27,7 +27,6 @@ export class EmbyComponent implements OnInit {
public async discoverServerInfo(server: IEmbyServer) {
const result = await this.embyService.getPublicInfo(server).toPromise();
this.settings.isJellyfin = result.isJellyfin;
server.name = result.serverName;
server.serverId = result.id;
this.hasDiscoveredOrDirty = true;

@ -0,0 +1,111 @@

<settings-menu></settings-menu>
<div class="small-middle-container">
<div *ngIf="settings">
<fieldset>
<legend>
Jellyfin/Jellyfin Configuration
</legend>
<div class="row">
<div class="col-md-6 col-6 col-sm-6">
<div style="float:right;text-align:left;">
<div class="md-form-field">
<mat-slide-toggle [(ngModel)]="settings.enable" (change)="toggle()" [checked]="settings.enable">Enable</mat-slide-toggle>
</div>
</div>
</div>
</div>
<mat-tab-group #tabGroup [selectedIndex]="selected.value" (selectedTabChange)="addTab($event)" (selectedIndexChange)="selected.setValue($event)" animationDuration="0ms" style="display:block;">
<mat-tab *ngFor="let server of settings.servers" [label]="server.name">
<div class="col-md-6 col-6 col-sm-6" style="float: right; width:100%; text-align:right;">
<button type="button" (click)="removeServer(server)" class="mat-focus-indicator mat-flat-button mat-button-base mat-warn">Remove Server</button>
</div>
<br />
<br />
<div class="col-md-10 col-10 col-sm-12">
<div class="md-form-field">
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Server Name</mat-label>
<input matInput placeholder="Server Name" [(ngModel)]="server.name" value="{{server.name}}">
</mat-form-field>
</div>
<div class="md-form-field">
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Hostname / IP</mat-label>
<input matInput placeholder="Hostname or IP" [(ngModel)]="server.ip" value="{{server.ip}}">
</mat-form-field>
<div class="md-form-field">
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Server ID</mat-label>
<input disabled matInput placeholder="Server Id" [(ngModel)]="server.serverId" value="{{server.serverId}}">
</mat-form-field>
</div>
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Port</mat-label>
<input matInput placeholder="Port" [(ngModel)]="server.port" value="{{server.port}}">
</mat-form-field>
<mat-slide-toggle id="ssl" [(ngModel)]="server.ssl" [checked]="server.ssl">
SSL
</mat-slide-toggle>
</div>
<div class="md-form-field">
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>API Key</mat-label>
<input matInput placeholder="Api Key" [(ngModel)]="server.apiKey" value="{{server.apiKey}}">
</mat-form-field>
</div>
<div class="md-form-field">
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Base URL</mat-label>
<input matInput placeholder="Base Url" [(ngModel)]="server.subDir" value="{{server.subDir}}">
</mat-form-field>
</div>
<div class="md-form-field">
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Externally Facing Hostname</mat-label>
<input matInput placeholder="e.g. https://jellyfin.server.com/" [(ngModel)]="server.serverHostname" value="{{server.serverHostname}}" matTooltip="This will be the external address that users will navigate to when they press the 'View On Jellyfin' button">
</mat-form-field>
<small>
<span *ngIf="server.serverHostname">Current URL: "{{server.serverHostname}}/#!/{{settings.isJellyfin ? ("itemdetails"): ("item/item")}}.html?id=1"</span>
<span *ngIf="!server.serverHostname">Current URL: "https://app.jellyfin.media/#!/{{settings.isJellyfin ? ("itemdetails"): ("item/item")}}.html?id=1</span>
</small>
</div>
<div class="form-group">
<div>
<button mat-raised-button id="testJellyfin" type="button" (click)="test(server)" class="mat-focus-indicator mat-stroked-button mat-button-base">Test Connectivity <div id="spinner"></div></button>
</div>
</div>
<div class="form-group">
<div>
<button mat-raised-button id="discover" type="button" (click)="discoverServerInfo(server)" class="mat-focus-indicator mat-stroked-button mat-button-base">Discover Server Information <div id="spinner"></div></button>
</div>
</div>
</div>
</mat-tab>
<mat-tab label="" disabled=true> </mat-tab>
<mat-tab label="Add Server" position=100> </mat-tab>
</mat-tab-group>
<div class="col-md-2">
<div class="form-group">
<div>
<button mat-raised-button [disabled]="!hasDiscoveredOrDirty" (click)="save()" type="submit" id="save" class="mat-focus-indicator mat-raised-button mat-button-base mat-accent">Submit</button>
</div>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<div>
<button mat-raised-button (click)="runCacher()" type="button" id="save" class="mat-focus-indicator mat-stroked-button mat-button-base">Manually Run Cacher</button>
</div>
</div>
</div>
</fieldset>
</div>
</div>

@ -0,0 +1,41 @@
@import "~styles/shared.scss";
.small-middle-container {
margin: auto;
width: 95%;
margin-top: 10px;
}
.col-md-10 {
display: grid;
}
.col-md-2 {
display: contents;
}
.control-label {
font-weight: 400;
}
.row {
display: block;
}
.btn-danger-outline {
background-color: #E84C3D;
}
.btn-success-outline {
background-color: #1b9d1b;
}
::ng-deep .dark .btn:hover {
box-shadow: 0 5px 11px 0 rgba(255, 255, 255, 0.18), 0 4px 15px 0 rgba(255, 255, 255, 0.15);
color: inherit;
}
@media (min-width:1440px) {
.col-md-2 {
display: inline-table;
}
}

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

Loading…
Cancel
Save