First step towards played sync

pull/4881/head
sephrat 1 year ago
parent 605e746fce
commit 9c84acb6fa

@ -248,5 +248,36 @@ namespace Ombi.Api.Emby
req.AddContentHeader("Content-Type", "application/json");
req.AddHeader("Device", "Ombi");
}
public async Task<EmbyItemContainer<EmbyMovie>> GetMoviesPlayed(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri)
{
return await GetPlayed<EmbyMovie>("Movie", apiKey, userId, baseUri, startIndex, count, parentIdFilder);
}
private async Task<EmbyItemContainer<T>> GetPlayed<T>(string type, string apiKey, string userId, string baseUri, int startIndex, int count, string parentIdFilder = default)
{
var request = new Request($"emby/items", baseUri, HttpMethod.Get);
request.AddQueryString("Recursive", true.ToString());
request.AddQueryString("IncludeItemTypes", type);
request.AddQueryString("Fields", "ProviderIds");
request.AddQueryString("UserId", userId);
// paginate and display recently played items first
request.AddQueryString("sortBy", "DatePlayed");
request.AddQueryString("SortOrder", "Descending");
request.AddQueryString("startIndex", startIndex.ToString());
request.AddQueryString("limit", count.ToString());
if (!string.IsNullOrEmpty(parentIdFilder))
{
request.AddQueryString("ParentId", parentIdFilder);
}
AddHeaders(request, apiKey);
var obj = await Api.Request<EmbyItemContainer<T>>(request);
return obj;
}
}
}

@ -32,5 +32,7 @@ namespace Ombi.Api.Emby
Task<EmbyItemContainer<EmbyMovie>> RecentlyAddedMovies(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri);
Task<EmbyItemContainer<EmbyEpisodes>> RecentlyAddedEpisodes(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri);
Task<EmbyItemContainer<EmbySeries>> RecentlyAddedShows(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri);
Task<EmbyItemContainer<EmbyMovie>> GetMoviesPlayed(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri);
}
}

@ -197,6 +197,7 @@ namespace Ombi.DependencyInjection
services.AddScoped<IEmbyContentRepository, EmbyContentRepository>();
services.AddScoped<IJellyfinContentRepository, JellyfinContentRepository>();
services.AddScoped<INotificationTemplatesRepository, NotificationTemplatesRepository>();
services.AddScoped<IUserPlayedMovieRepository, UserPlayedMovieRepository>();
services.AddScoped<ITvRequestRepository, TvRequestRepository>();
services.AddScoped<IMovieRequestRepository, MovieRequestRepository>();
@ -244,6 +245,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IPlexContentSync, PlexContentSync>();
services.AddTransient<IPlexWatchlistImport, PlexWatchlistImport>();
services.AddTransient<IEmbyContentSync, EmbyContentSync>();
services.AddTransient<IEmbyPlayedSync, EmbyPlayedSync>();
services.AddTransient<IEmbyEpisodeSync, EmbyEpisodeSync>();
services.AddTransient<IEmbyAvaliabilityChecker, EmbyAvaliabilityChecker>();
services.AddTransient<IJellyfinContentSync, JellyfinContentSync>();

@ -19,108 +19,29 @@ using MediaType = Ombi.Store.Entities.MediaType;
namespace Ombi.Schedule.Jobs.Emby
{
public class EmbyContentSync : IEmbyContentSync
public class EmbyContentSync : EmbyLibrarySync, IEmbyContentSync
{
public EmbyContentSync(ISettingsService<EmbySettings> settings, IEmbyApiFactory api, ILogger<EmbyContentSync> logger,
IEmbyContentRepository repo, INotificationHubService notification)
IEmbyContentRepository repo, INotificationHubService notification):
base(settings, api, logger, notification)
{
_logger = logger;
_settings = settings;
_apiFactory = api;
_repo = repo;
_notification = notification;
}
private readonly ILogger<EmbyContentSync> _logger;
private readonly ISettingsService<EmbySettings> _settings;
private readonly IEmbyApiFactory _apiFactory;
private readonly IEmbyContentRepository _repo;
private readonly INotificationHubService _notification;
private const int AmountToTake = 100;
private IEmbyApi Api { get; set; }
public async Task Execute(IJobExecutionContext context)
public async override Task Execute(IJobExecutionContext context)
{
JobDataMap dataMap = context.JobDetail.JobDataMap;
var recentlyAddedSearch = false;
if (dataMap.TryGetValue(JobDataKeys.EmbyRecentlyAddedSearch, out var recentlyAddedObj))
{
recentlyAddedSearch = Convert.ToBoolean(recentlyAddedObj);
}
var embySettings = await _settings.GetSettingsAsync();
if (!embySettings.Enable)
return;
Api = _apiFactory.CreateClient(embySettings);
await base.Execute(context);
await _notification.SendNotificationToAdmins(recentlyAddedSearch ? "Emby Recently Added Started" : "Emby Content Sync Started");
foreach (var server in embySettings.Servers)
{
try
{
await StartServerCache(server, recentlyAddedSearch);
}
catch (Exception e)
{
await _notification.SendNotificationToAdmins("Emby Content Sync Failed");
_logger.LogError(e, "Exception when caching Emby for server {0}", server.Name);
}
}
await _notification.SendNotificationToAdmins("Emby Content Sync Finished");
// Episodes
await OmbiQuartz.Scheduler.TriggerJob(new JobKey(nameof(IEmbyEpisodeSync), "Emby"), new JobDataMap(new Dictionary<string, string> { { JobDataKeys.EmbyRecentlyAddedSearch, recentlyAddedSearch.ToString() } }));
await OmbiQuartz.Scheduler.TriggerJob(new JobKey(nameof(IEmbyEpisodeSync), "Emby"), new JobDataMap(new Dictionary<string, string> { { JobDataKeys.EmbyRecentlyAddedSearch, recentlyAdded.ToString() } }));
}
private async Task StartServerCache(EmbyServers server, bool recentlyAdded)
{
if (!ValidateSettings(server))
{
return;
}
if (server.EmbySelectedLibraries.Any() && server.EmbySelectedLibraries.Any(x => x.Enabled))
{
var movieLibsToFilter = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "movies");
foreach (var movieParentIdFilder in movieLibsToFilter)
{
_logger.LogInformation($"Scanning Lib '{movieParentIdFilder.Title}'");
await ProcessMovies(server, recentlyAdded, movieParentIdFilder.Key);
}
var tvLibsToFilter = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "tvshows");
foreach (var tvParentIdFilter in tvLibsToFilter)
{
_logger.LogInformation($"Scanning Lib '{tvParentIdFilter.Title}'");
await ProcessTv(server, recentlyAdded, tvParentIdFilter.Key);
}
var mixedLibs = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "mixed");
foreach (var m in mixedLibs)
{
_logger.LogInformation($"Scanning Lib '{m.Title}'");
await ProcessTv(server, recentlyAdded, m.Key);
await ProcessMovies(server, recentlyAdded, m.Key);
}
}
else
{
await ProcessMovies(server, recentlyAdded);
await ProcessTv(server, recentlyAdded);
}
}
private async Task ProcessTv(EmbyServers server, bool recentlyAdded, string parentId = default)
protected async override Task ProcessTv(EmbyServers server, string parentId = default)
{
// TV Time
var mediaToAdd = new HashSet<EmbyContent>();
@ -196,7 +117,7 @@ namespace Ombi.Schedule.Jobs.Emby
await _repo.AddRange(mediaToAdd);
}
private async Task ProcessMovies(EmbyServers server, bool recentlyAdded, string parentId = default)
protected override async Task ProcessMovies(EmbyServers server, string parentId = default)
{
EmbyItemContainer<EmbyMovie> movies;
if (recentlyAdded)
@ -319,36 +240,6 @@ namespace Ombi.Schedule.Jobs.Emby
content.Quality = has4K ? null : quality;
content.Has4K = has4K;
}
private bool ValidateSettings(EmbyServers server)
{
if (server?.Ip == null || string.IsNullOrEmpty(server?.ApiKey))
{
_logger.LogInformation(LoggingEvents.EmbyContentCacher, $"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,146 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Api.Emby;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Hubs;
using Quartz;
namespace Ombi.Schedule.Jobs.Emby
{
public abstract class EmbyLibrarySync
{
public EmbyLibrarySync(ISettingsService<EmbySettings> settings, IEmbyApiFactory api, ILogger<EmbyContentSync> logger,
INotificationHubService notification)
{
_logger = logger;
_settings = settings;
_apiFactory = api;
_notification = notification;
}
protected readonly ILogger<EmbyContentSync> _logger;
protected readonly ISettingsService<EmbySettings> _settings;
protected readonly IEmbyApiFactory _apiFactory;
protected bool recentlyAdded;
protected readonly INotificationHubService _notification;
protected const int AmountToTake = 100;
protected IEmbyApi Api { get; set; }
public virtual async Task Execute(IJobExecutionContext context)
{
JobDataMap dataMap = context.JobDetail.JobDataMap;
if (dataMap.TryGetValue(JobDataKeys.EmbyRecentlyAddedSearch, out var recentlyAddedObj))
{
recentlyAdded = Convert.ToBoolean(recentlyAddedObj);
}
await _notification.SendNotificationToAdmins(recentlyAdded ? "Emby Recently Added Started" : "Emby Content Sync Started");
var embySettings = await _settings.GetSettingsAsync();
if (!embySettings.Enable)
return;
Api = _apiFactory.CreateClient(embySettings);
foreach (var server in embySettings.Servers)
{
try
{
await StartServerCache(server);
}
catch (Exception e)
{
await _notification.SendNotificationToAdmins("Emby Content Sync Failed");
_logger.LogError(e, "Exception when caching Emby for server {0}", server.Name);
}
}
await _notification.SendNotificationToAdmins("Emby Content Sync Finished");
}
private async Task StartServerCache(EmbyServers server)
{
if (!ValidateSettings(server))
{
return;
}
if (server.EmbySelectedLibraries.Any() && server.EmbySelectedLibraries.Any(x => x.Enabled))
{
var movieLibsToFilter = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "movies");
foreach (var movieParentIdFilder in movieLibsToFilter)
{
_logger.LogInformation($"Scanning Lib '{movieParentIdFilder.Title}'");
await ProcessMovies(server, movieParentIdFilder.Key);
}
var tvLibsToFilter = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "tvshows");
foreach (var tvParentIdFilter in tvLibsToFilter)
{
_logger.LogInformation($"Scanning Lib '{tvParentIdFilter.Title}'");
await ProcessTv(server, tvParentIdFilter.Key);
}
var mixedLibs = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "mixed");
foreach (var m in mixedLibs)
{
_logger.LogInformation($"Scanning Lib '{m.Title}'");
await ProcessTv(server, m.Key);
await ProcessMovies(server, m.Key);
}
}
else
{
await ProcessMovies(server);
await ProcessTv(server);
}
}
protected abstract Task ProcessTv(EmbyServers server, string parentId = default);
protected abstract Task ProcessMovies(EmbyServers server, string parentId = default);
private bool ValidateSettings(EmbyServers server)
{
if (server?.Ip == null || string.IsNullOrEmpty(server?.ApiKey))
{
_logger.LogInformation(LoggingEvents.EmbyContentCacher, $"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,110 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Api.Emby;
using Ombi.Api.Emby.Models;
using Ombi.Api.Emby.Models.Movie;
using Ombi.Core.Authentication;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Hubs;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
namespace Ombi.Schedule.Jobs.Emby
{
public class EmbyPlayedSync : EmbyLibrarySync, IEmbyPlayedSync
{
public EmbyPlayedSync(ISettingsService<EmbySettings> settings, IEmbyApiFactory api, ILogger<EmbyContentSync> logger,
IUserPlayedMovieRepository repo, INotificationHubService notification, OmbiUserManager user) : base(settings, api, logger, notification)
{
_userManager = user;
_repo = repo;
}
private OmbiUserManager _userManager { get; }
private readonly IUserPlayedMovieRepository _repo;
protected override Task ProcessTv(EmbyServers server, string parentId = default)
{
// TODO
return Task.CompletedTask;
}
protected async override Task ProcessMovies(EmbyServers server, string parentId = default)
{
var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.EmbyUser || x.UserType == UserType.EmbyConnectUser).ToListAsync();
foreach (var user in allUsers)
{
await ProcessMoviesUser(server, user, parentId);
}
}
private async Task ProcessMoviesUser(EmbyServers server, OmbiUser user, string parentId = default)
{
EmbyItemContainer<EmbyMovie> movies;
if (recentlyAdded)
{
var recentlyAddedAmountToTake = 5; // to be adjusted?
movies = await Api.GetMoviesPlayed(server.ApiKey, parentId, 0, recentlyAddedAmountToTake, user.ProviderUserId, server.FullUri);
// Setting this so we don't attempt to grab more than we need
if (movies.TotalRecordCount > recentlyAddedAmountToTake)
{
movies.TotalRecordCount = recentlyAddedAmountToTake;
}
}
else
{
movies = await Api.GetMoviesPlayed(server.ApiKey, parentId, 0, AmountToTake, user.ProviderUserId, server.FullUri);
}
var totalCount = movies.TotalRecordCount;
var processed = 0;
var mediaToAdd = new HashSet<UserPlayedMovie>();
_logger.LogCritical($"Adding {totalCount.ToString()} for {user.UserName}");
while (processed < totalCount)
{
foreach (var movie in movies.Items)
{
await ProcessMovie(movie, user, mediaToAdd, server);
processed++;
}
// Get the next batch
// Recently Added should never be checked as the TotalRecords should equal the amount to take
if (!recentlyAdded)
{
movies = await Api.GetMoviesPlayed(server.ApiKey, parentId, processed, AmountToTake, user.ProviderUserId, server.FullUri);
}
await _repo.AddRange(mediaToAdd);
mediaToAdd.Clear();
}
}
private async Task ProcessMovie(EmbyMovie movieInfo, OmbiUser user, ICollection<UserPlayedMovie> content, EmbyServers server)
{
if (movieInfo.ProviderIds.Tmdb.IsNullOrEmpty())
{
_logger.LogWarning($"Movie {movieInfo.Name} has no relevant metadata. Skipping.");
return;
}
var userPlayedMovie = new UserPlayedMovie()
{
TheMovieDbId = movieInfo.ProviderIds.Tmdb,
UserId = user.Id
};
// Check if it exists
var existingMovie = await _repo.Get(userPlayedMovie.TheMovieDbId, userPlayedMovie.UserId);
var alreadyGoingToAdd = content.Any(x => x.TheMovieDbId == userPlayedMovie.TheMovieDbId && x.UserId == userPlayedMovie.UserId);
if (existingMovie == null && !alreadyGoingToAdd)
{
content.Add(userPlayedMovie);
}
}
}
}

@ -0,0 +1,6 @@
namespace Ombi.Schedule.Jobs.Emby
{
public interface IEmbyPlayedSync : IBaseJob
{
}
}

@ -98,6 +98,8 @@ namespace Ombi.Schedule
{
await OmbiQuartz.Instance.AddJob<IEmbyContentSync>(nameof(IEmbyContentSync), "Emby", JobSettingsHelper.EmbyContent(s));
await OmbiQuartz.Instance.AddJob<IEmbyContentSync>(nameof(IEmbyContentSync) + "RecentlyAdded", "Emby", JobSettingsHelper.EmbyRecentlyAddedSync(s), new Dictionary<string, string> { { JobDataKeys.EmbyRecentlyAddedSearch, "true" } });
await OmbiQuartz.Instance.AddJob<IEmbyPlayedSync>(nameof(IEmbyPlayedSync), "Emby", JobSettingsHelper.EmbyContent(s));
await OmbiQuartz.Instance.AddJob<IEmbyPlayedSync>(nameof(IEmbyPlayedSync) + "RecentlyAdded", "Emby", JobSettingsHelper.EmbyRecentlyAddedSync(s), new Dictionary<string, string> { { JobDataKeys.EmbyRecentlyAddedSearch, "true" } });
await OmbiQuartz.Instance.AddJob<IEmbyEpisodeSync>(nameof(IEmbyEpisodeSync), "Emby", null);
await OmbiQuartz.Instance.AddJob<IEmbyAvaliabilityChecker>(nameof(IEmbyAvaliabilityChecker), "Emby", null);
await OmbiQuartz.Instance.AddJob<IEmbyUserImporter>(nameof(IEmbyUserImporter), "Emby", JobSettingsHelper.UserImporter(s));

@ -41,6 +41,7 @@ namespace Ombi.Store.Context
public DbSet<SonarrEpisodeCache> SonarrEpisodeCache { get; set; }
public DbSet<SickRageCache> SickRageCache { get; set; }
public DbSet<SickRageEpisodeCache> SickRageEpisodeCache { get; set; }
public DbSet<UserPlayedMovie> UserPlayedMovie { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{

@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Ombi.Store.Entities
{
public class UserPlayedMovie : Entity
{
public string TheMovieDbId { get; set; }
public string UserId { get; set; }
}
}

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

@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Ombi.Store.Migrations.ExternalSqlite
{
public partial class MovieUserPlayed : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "UserPlayedMovie",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
TheMovieDbId = table.Column<string>(type: "TEXT", nullable: true),
UserId = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_UserPlayedMovie", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "UserPlayedMovie");
}
}
}

@ -15,7 +15,7 @@ namespace Ombi.Store.Migrations.ExternalSqlite
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.0");
modelBuilder.HasAnnotation("ProductVersion", "6.0.9");
modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b =>
{
@ -486,6 +486,23 @@ namespace Ombi.Store.Migrations.ExternalSqlite
b.ToTable("SonarrEpisodeCache");
});
modelBuilder.Entity("Ombi.Store.Entities.UserPlayedMovie", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("TheMovieDbId")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("UserPlayedMovie");
});
modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b =>
{
b.HasOne("Ombi.Store.Entities.EmbyContent", "Series")

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Ombi.Store.Entities;
namespace Ombi.Store.Repository
{
public interface IUserPlayedMovieRepository : IExternalRepository<UserPlayedMovie>
{
Task<UserPlayedMovie> Get(string theMovieDbId, string userId);
}
}

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Ombi.Store.Context;
using Ombi.Store.Entities;
namespace Ombi.Store.Repository
{
public class UserPlayedMovieRepository : ExternalRepository<UserPlayedMovie>, IUserPlayedMovieRepository
{
protected ExternalContext Db { get; }
public UserPlayedMovieRepository(ExternalContext db) : base(db)
{
Db = db;
}
public async Task<UserPlayedMovie> Get(string theMovieDbId, string userId)
{
return await Db.UserPlayedMovie.FirstOrDefaultAsync(x => x.TheMovieDbId == theMovieDbId && x.UserId == userId);
}
}
}

@ -51,6 +51,10 @@ export class JobService extends ServiceHelpers {
return this.http.post<boolean>(`${this.url}embyrecentlyadded/`, {headers: this.headers});
}
public runEmbyRecentlyPlayedCacher(): Observable<boolean> {
return this.http.post<boolean>(`${this.url}embyrecentlyplayed/`, {headers: this.headers});
}
public clearMediaserverData(): Observable<boolean> {
return this.http.post<boolean>(`${this.url}clearmediaserverdata/`, {headers: this.headers});
}
@ -59,6 +63,10 @@ export class JobService extends ServiceHelpers {
return this.http.post<boolean>(`${this.url}embycontentcacher/`, {headers: this.headers});
}
public runEmbyPlayedCacher(): Observable<boolean> {
return this.http.post<boolean>(`${this.url}embyplayedcacher/`, {headers: this.headers});
}
public runJellyfinCacher(): Observable<boolean> {
return this.http.post<boolean>(`${this.url}jellyfincontentcacher/`, {headers: this.headers});
}

@ -115,6 +115,15 @@
class="mat-focus-indicator mat-stroked-button mat-button-base">Manually Run Recently
Added Sync</button>
</div>
<div class="form-group">
<div>
<button mat-raised-button (click)="runPlayedCacher()" type="button" id="playedSync" class="mat-focus-indicator mat-stroked-button mat-button-base">Manually Run Played Sync</button>
</div>
</div>
<div class="form-group">
<button mat-raised-button (click)="runRecentlyPlayedCacher()" type="button" id="recentlyPlayedSync"
class="mat-focus-indicator mat-stroked-button mat-button-base">Manually Run Recently Played Sync</button>
</div>
<div class="form-group">
<button mat-raised-button (click)="clearDataAndResync()" type="button" id="clearData"
class="mat-focus-indicator mat-stroked-button mat-button-base">

@ -102,6 +102,22 @@ export class EmbyComponent implements OnInit {
});
}
public runPlayedCacher(): void {
this.jobService.runEmbyPlayedCacher().subscribe(x => {
if(x) {
this.notificationService.success("Triggered the Emby Played Cacher");
}
});
}
public runRecentlyPlayedCacher(): void {
this.jobService.runEmbyRecentlyPlayedCacher().subscribe(x => {
if (x) {
this.notificationService.success("Triggered the Emby Recently Played Sync");
}
});
}
public clearDataAndResync(): void {
this.jobService.clearMediaserverData().subscribe(x => {
if (x) {

@ -167,6 +167,19 @@ namespace Ombi.Controllers.V1
await OmbiQuartz.Scheduler.TriggerJob(new JobKey(nameof(IEmbyContentSync), "Emby"), new JobDataMap(new Dictionary<string, string> { { JobDataKeys.EmbyRecentlyAddedSearch, "false" } }));
return true;
}
[HttpPost("embyplayedcacher")]
public async Task<bool> StartEmbyPlayedCacher()
{
await OmbiQuartz.Scheduler.TriggerJob(new JobKey(nameof(IEmbyPlayedSync), "Emby"), new JobDataMap(new Dictionary<string, string> { { JobDataKeys.EmbyRecentlyAddedSearch, "false" } }));
return true;
}
[HttpPost("embyrecentlyplayed")]
public async Task<bool> StartEmbyRecentlyPlayedCacher()
{
await OmbiQuartz.Scheduler.TriggerJob(new JobKey(nameof(IEmbyPlayedSync), "Emby"), new JobDataMap(new Dictionary<string, string> { { JobDataKeys.EmbyRecentlyAddedSearch, "true" } }));
return true;
}
/// <summary>
/// Runs a smaller version of the content cacher

Loading…
Cancel
Save