mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-07-16 02:02:55 -07:00
parent
15fe04d4a6
commit
c222f1a945
31 changed files with 3289 additions and 16 deletions
|
@ -1,9 +1,12 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Ombi.Api.Plex.Models
|
namespace Ombi.Api.Plex.Models
|
||||||
{
|
{
|
||||||
public class PlexWatchlistContainer
|
public class PlexWatchlistContainer
|
||||||
{
|
{
|
||||||
public PlexWatchlist MediaContainer { get; set; }
|
public PlexWatchlist MediaContainer { get; set; }
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool AuthError { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -295,9 +296,18 @@ namespace Ombi.Api.Plex
|
||||||
var request = new Request("library/sections/watchlist/all", WatchlistUri, HttpMethod.Get);
|
var request = new Request("library/sections/watchlist/all", WatchlistUri, HttpMethod.Get);
|
||||||
await AddHeaders(request, plexToken);
|
await AddHeaders(request, plexToken);
|
||||||
|
|
||||||
var result = await Api.Request<PlexWatchlistContainer>(request, cancellationToken);
|
var result = await Api.Request(request, cancellationToken);
|
||||||
|
|
||||||
return result;
|
if (result.StatusCode.Equals(HttpStatusCode.Unauthorized))
|
||||||
|
{
|
||||||
|
return new PlexWatchlistContainer
|
||||||
|
{
|
||||||
|
AuthError = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var receivedString = await result.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
return JsonConvert.DeserializeObject<PlexWatchlistContainer>(receivedString);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PlexWatchlistMetadataContainer> GetWatchlistMetadata(string ratingKey, string plexToken, CancellationToken cancellationToken)
|
public async Task<PlexWatchlistMetadataContainer> GetWatchlistMetadata(string ratingKey, string plexToken, CancellationToken cancellationToken)
|
||||||
|
|
146
src/Ombi.Core.Tests/Services/PlexServiceTests.cs
Normal file
146
src/Ombi.Core.Tests/Services/PlexServiceTests.cs
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
using MockQueryable.Moq;
|
||||||
|
using Moq.AutoMock;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using Ombi.Core.Models;
|
||||||
|
using Ombi.Core.Services;
|
||||||
|
using Ombi.Store.Entities;
|
||||||
|
using Ombi.Store.Repository;
|
||||||
|
using Ombi.Test.Common;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using UserType = Ombi.Store.Entities.UserType;
|
||||||
|
|
||||||
|
namespace Ombi.Core.Tests.Services
|
||||||
|
{
|
||||||
|
public class PlexServiceTests
|
||||||
|
{
|
||||||
|
|
||||||
|
private PlexService _subject;
|
||||||
|
private AutoMocker _mocker;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_mocker = new AutoMocker();
|
||||||
|
_subject = _mocker.CreateInstance<PlexService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetWatchListUsers_AllUsersSynced()
|
||||||
|
{
|
||||||
|
var userMock = MockHelper.MockUserManager(new List<OmbiUser>
|
||||||
|
{
|
||||||
|
new OmbiUser
|
||||||
|
{
|
||||||
|
MediaServerToken = "token",
|
||||||
|
Id = "1",
|
||||||
|
UserName = "user1",
|
||||||
|
UserType = UserType.PlexUser,
|
||||||
|
},
|
||||||
|
new OmbiUser
|
||||||
|
{
|
||||||
|
MediaServerToken = "token",
|
||||||
|
Id = "2",
|
||||||
|
UserName = "user2",
|
||||||
|
UserType = UserType.PlexUser,
|
||||||
|
},
|
||||||
|
new OmbiUser
|
||||||
|
{
|
||||||
|
MediaServerToken = "token",
|
||||||
|
Id = "2",
|
||||||
|
UserName = "user2",
|
||||||
|
UserType = UserType.LocalUser,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_mocker.Use(userMock.Object);
|
||||||
|
_subject = _mocker.CreateInstance<PlexService>();
|
||||||
|
|
||||||
|
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll())
|
||||||
|
.Returns(new List<PlexWatchlistUserError>().AsQueryable().BuildMock().Object);
|
||||||
|
|
||||||
|
var result = await _subject.GetWatchlistUsers(CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(result.All(x => x.SyncStatus == WatchlistSyncStatus.Successful));
|
||||||
|
Assert.That(result.Count, Is.EqualTo(2));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetWatchListUsers_NotEnabled()
|
||||||
|
{
|
||||||
|
var userMock = MockHelper.MockUserManager(new List<OmbiUser>
|
||||||
|
{
|
||||||
|
new OmbiUser
|
||||||
|
{
|
||||||
|
MediaServerToken = "",
|
||||||
|
Id = "1",
|
||||||
|
UserName = "user1",
|
||||||
|
UserType = UserType.PlexUser,
|
||||||
|
},
|
||||||
|
new OmbiUser
|
||||||
|
{
|
||||||
|
MediaServerToken = null,
|
||||||
|
Id = "2",
|
||||||
|
UserName = "user2",
|
||||||
|
UserType = UserType.PlexUser,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
_mocker.Use(userMock.Object);
|
||||||
|
_subject = _mocker.CreateInstance<PlexService>();
|
||||||
|
|
||||||
|
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll())
|
||||||
|
.Returns(new List<PlexWatchlistUserError>().AsQueryable().BuildMock().Object);
|
||||||
|
|
||||||
|
var result = await _subject.GetWatchlistUsers(CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(result.All(x => x.SyncStatus == WatchlistSyncStatus.NotEnabled));
|
||||||
|
Assert.That(result.Count, Is.EqualTo(2));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetWatchListUsers_Failed()
|
||||||
|
{
|
||||||
|
var userMock = MockHelper.MockUserManager(new List<OmbiUser>
|
||||||
|
{
|
||||||
|
new OmbiUser
|
||||||
|
{
|
||||||
|
MediaServerToken = "test",
|
||||||
|
Id = "1",
|
||||||
|
UserName = "user1",
|
||||||
|
UserType = UserType.PlexUser,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
_mocker.Use(userMock.Object);
|
||||||
|
_subject = _mocker.CreateInstance<PlexService>();
|
||||||
|
|
||||||
|
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll())
|
||||||
|
.Returns(new List<PlexWatchlistUserError>
|
||||||
|
{
|
||||||
|
new PlexWatchlistUserError
|
||||||
|
{
|
||||||
|
UserId = "1",
|
||||||
|
MediaServerToken = "test",
|
||||||
|
}
|
||||||
|
}.AsQueryable().BuildMock().Object);
|
||||||
|
|
||||||
|
var result = await _subject.GetWatchlistUsers(CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(result.All(x => x.SyncStatus == WatchlistSyncStatus.Failed));
|
||||||
|
Assert.That(result.Count, Is.EqualTo(1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ namespace Ombi.Core.Engine
|
||||||
private readonly IMusicRequestRepository _musicRepository;
|
private readonly IMusicRequestRepository _musicRepository;
|
||||||
private readonly IRepository<Votes> _voteRepository;
|
private readonly IRepository<Votes> _voteRepository;
|
||||||
private readonly IRepository<MobileDevices> _mobileDevicesRepository;
|
private readonly IRepository<MobileDevices> _mobileDevicesRepository;
|
||||||
|
private readonly IRepository<PlexWatchlistUserError> _watchlistUserError;
|
||||||
|
|
||||||
public UserDeletionEngine(IMovieRequestRepository movieRepository,
|
public UserDeletionEngine(IMovieRequestRepository movieRepository,
|
||||||
OmbiUserManager userManager,
|
OmbiUserManager userManager,
|
||||||
|
@ -39,7 +40,8 @@ namespace Ombi.Core.Engine
|
||||||
IRepository<UserNotificationPreferences> notificationPreferencesRepo,
|
IRepository<UserNotificationPreferences> notificationPreferencesRepo,
|
||||||
IRepository<UserQualityProfiles> qualityProfilesRepo,
|
IRepository<UserQualityProfiles> qualityProfilesRepo,
|
||||||
IRepository<Votes> voteRepository,
|
IRepository<Votes> voteRepository,
|
||||||
IRepository<MobileDevices> mobileDevicesRepository
|
IRepository<MobileDevices> mobileDevicesRepository,
|
||||||
|
IRepository<PlexWatchlistUserError> watchlistUserError
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_movieRepository = movieRepository;
|
_movieRepository = movieRepository;
|
||||||
|
@ -56,6 +58,7 @@ namespace Ombi.Core.Engine
|
||||||
_userQualityProfiles = qualityProfilesRepo;
|
_userQualityProfiles = qualityProfilesRepo;
|
||||||
_voteRepository = voteRepository;
|
_voteRepository = voteRepository;
|
||||||
_mobileDevicesRepository = mobileDevicesRepository;
|
_mobileDevicesRepository = mobileDevicesRepository;
|
||||||
|
_watchlistUserError = watchlistUserError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,6 +71,7 @@ namespace Ombi.Core.Engine
|
||||||
var musicRequested = _musicRepository.GetAll().Where(x => x.RequestedUserId == userId);
|
var musicRequested = _musicRepository.GetAll().Where(x => x.RequestedUserId == userId);
|
||||||
var notificationPreferences = _userNotificationPreferences.GetAll().Where(x => x.UserId == userId);
|
var notificationPreferences = _userNotificationPreferences.GetAll().Where(x => x.UserId == userId);
|
||||||
var userQuality = await _userQualityProfiles.GetAll().FirstOrDefaultAsync(x => x.UserId == userId);
|
var userQuality = await _userQualityProfiles.GetAll().FirstOrDefaultAsync(x => x.UserId == userId);
|
||||||
|
var watchlistError = await _watchlistUserError.GetAll().FirstOrDefaultAsync(x => x.UserId == userId);
|
||||||
|
|
||||||
if (moviesUserRequested.Any())
|
if (moviesUserRequested.Any())
|
||||||
{
|
{
|
||||||
|
@ -89,6 +93,10 @@ namespace Ombi.Core.Engine
|
||||||
{
|
{
|
||||||
await _userQualityProfiles.Delete(userQuality);
|
await _userQualityProfiles.Delete(userQuality);
|
||||||
}
|
}
|
||||||
|
if (watchlistError != null)
|
||||||
|
{
|
||||||
|
await _watchlistUserError.Delete(watchlistError);
|
||||||
|
}
|
||||||
|
|
||||||
// Delete any issues and request logs
|
// Delete any issues and request logs
|
||||||
var issues = _issuesRepository.GetAll().Where(x => x.UserReportedId == userId);
|
var issues = _issuesRepository.GetAll().Where(x => x.UserReportedId == userId);
|
||||||
|
|
16
src/Ombi.Core/Models/PlexUserWatchlistModel.cs
Normal file
16
src/Ombi.Core/Models/PlexUserWatchlistModel.cs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
namespace Ombi.Core.Models
|
||||||
|
{
|
||||||
|
public class PlexUserWatchlistModel
|
||||||
|
{
|
||||||
|
public string UserId { get; set; }
|
||||||
|
public WatchlistSyncStatus SyncStatus { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum WatchlistSyncStatus
|
||||||
|
{
|
||||||
|
Successful,
|
||||||
|
Failed,
|
||||||
|
NotEnabled
|
||||||
|
}
|
||||||
|
}
|
12
src/Ombi.Core/Services/IPlexService.cs
Normal file
12
src/Ombi.Core/Services/IPlexService.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
using Ombi.Core.Models;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ombi.Core.Services
|
||||||
|
{
|
||||||
|
public interface IPlexService
|
||||||
|
{
|
||||||
|
Task<List<PlexUserWatchlistModel>> GetWatchlistUsers(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
55
src/Ombi.Core/Services/PlexService.cs
Normal file
55
src/Ombi.Core/Services/PlexService.cs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Ombi.Core.Authentication;
|
||||||
|
using Ombi.Core.Models;
|
||||||
|
using Ombi.Store.Entities;
|
||||||
|
using Ombi.Store.Repository;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using UserType = Ombi.Store.Entities.UserType;
|
||||||
|
|
||||||
|
namespace Ombi.Core.Services
|
||||||
|
{
|
||||||
|
public class PlexService : IPlexService
|
||||||
|
{
|
||||||
|
private readonly IRepository<PlexWatchlistUserError> _watchlistUserErrors;
|
||||||
|
private readonly OmbiUserManager _userManager;
|
||||||
|
|
||||||
|
public PlexService(IRepository<PlexWatchlistUserError> watchlistUserErrors, OmbiUserManager userManager)
|
||||||
|
{
|
||||||
|
_watchlistUserErrors = watchlistUserErrors;
|
||||||
|
_userManager = userManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<PlexUserWatchlistModel>> GetWatchlistUsers(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var plexUsers = _userManager.Users.Where(x => x.UserType == UserType.PlexUser);
|
||||||
|
var userErrors = await _watchlistUserErrors.GetAll().ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var model = new List<PlexUserWatchlistModel>();
|
||||||
|
|
||||||
|
|
||||||
|
foreach(var plexUser in plexUsers)
|
||||||
|
{
|
||||||
|
model.Add(new PlexUserWatchlistModel
|
||||||
|
{
|
||||||
|
UserId = plexUser.Id,
|
||||||
|
UserName = plexUser.UserName,
|
||||||
|
SyncStatus = GetWatchlistSyncStatus(plexUser, userErrors)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WatchlistSyncStatus GetWatchlistSyncStatus(OmbiUser user, List<PlexWatchlistUserError> userErrors)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(user.MediaServerToken))
|
||||||
|
{
|
||||||
|
return WatchlistSyncStatus.NotEnabled;
|
||||||
|
}
|
||||||
|
return userErrors.Any(x => x.UserId == user.Id) ? WatchlistSyncStatus.Failed : WatchlistSyncStatus.Successful;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -229,6 +229,7 @@ namespace Ombi.DependencyInjection
|
||||||
services.AddTransient<IChangeLogProcessor, ChangeLogProcessor>();
|
services.AddTransient<IChangeLogProcessor, ChangeLogProcessor>();
|
||||||
services.AddScoped<IFeatureService, FeatureService>();
|
services.AddScoped<IFeatureService, FeatureService>();
|
||||||
services.AddTransient<IRecentlyRequestedService, RecentlyRequestedService>();
|
services.AddTransient<IRecentlyRequestedService, RecentlyRequestedService>();
|
||||||
|
services.AddTransient<IPlexService, PlexService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RegisterJobs(this IServiceCollection services)
|
public static void RegisterJobs(this IServiceCollection services)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Moq;
|
using MockQueryable.Moq;
|
||||||
|
using Moq;
|
||||||
using Moq.AutoMock;
|
using Moq.AutoMock;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using Ombi.Api.Plex;
|
using Ombi.Api.Plex;
|
||||||
|
@ -8,6 +9,7 @@ using Ombi.Core.Engine.Interfaces;
|
||||||
using Ombi.Core.Models.Requests;
|
using Ombi.Core.Models.Requests;
|
||||||
using Ombi.Core.Settings;
|
using Ombi.Core.Settings;
|
||||||
using Ombi.Core.Settings.Models.External;
|
using Ombi.Core.Settings.Models.External;
|
||||||
|
using Ombi.Core.Tests;
|
||||||
using Ombi.Schedule.Jobs.Plex;
|
using Ombi.Schedule.Jobs.Plex;
|
||||||
using Ombi.Store.Entities;
|
using Ombi.Store.Entities;
|
||||||
using Ombi.Store.Repository;
|
using Ombi.Store.Repository;
|
||||||
|
@ -32,11 +34,12 @@ namespace Ombi.Schedule.Tests
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
_mocker = new AutoMocker();
|
_mocker = new AutoMocker();
|
||||||
var um = MockHelper.MockUserManager(new List<OmbiUser> { new OmbiUser { Id = "abc", UserType = UserType.PlexUser, MediaServerToken = "abc", UserName = "abc", NormalizedUserName = "ABC" } });
|
var um = MockHelper.MockUserManager(new List<OmbiUser> { new OmbiUser { Id = "abc", UserType = UserType.PlexUser, MediaServerToken = "token1", UserName = "abc", NormalizedUserName = "ABC" } });
|
||||||
_mocker.Use(um);
|
_mocker.Use(um);
|
||||||
_context = _mocker.GetMock<IJobExecutionContext>();
|
_context = _mocker.GetMock<IJobExecutionContext>();
|
||||||
_context.Setup(x => x.CancellationToken).Returns(CancellationToken.None);
|
_context.Setup(x => x.CancellationToken).Returns(CancellationToken.None);
|
||||||
_subject = _mocker.CreateInstance<PlexWatchlistImport>();
|
_subject = _mocker.CreateInstance<PlexWatchlistImport>();
|
||||||
|
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll()).Returns(new List<PlexWatchlistUserError>().AsQueryable().BuildMock().Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -53,6 +56,7 @@ namespace Ombi.Schedule.Tests
|
||||||
[Test]
|
[Test]
|
||||||
public async Task TerminatesWhenWatchlistIsNotEnabled()
|
public async Task TerminatesWhenWatchlistIsNotEnabled()
|
||||||
{
|
{
|
||||||
|
|
||||||
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = false });
|
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = false });
|
||||||
await _subject.Execute(null);
|
await _subject.Execute(null);
|
||||||
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
|
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
|
||||||
|
@ -75,9 +79,74 @@ namespace Ombi.Schedule.Tests
|
||||||
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Never);
|
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Never);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task AuthenticationError()
|
||||||
|
{
|
||||||
|
|
||||||
|
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
||||||
|
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer { AuthError = true });
|
||||||
|
await _subject.Execute(_context.Object);
|
||||||
|
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
|
||||||
|
_mocker.Verify<IPlexApi>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
|
||||||
|
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Never);
|
||||||
|
_mocker.Verify<IRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Never);
|
||||||
|
_mocker.Verify<IRepository<PlexWatchlistUserError>>(x => x.Add(It.Is<PlexWatchlistUserError>(x => x.UserId == "abc")), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task FailedWatchListUser_NewToken_ShouldBeRemoved()
|
||||||
|
{
|
||||||
|
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll()).Returns(new List<PlexWatchlistUserError>
|
||||||
|
{
|
||||||
|
new PlexWatchlistUserError
|
||||||
|
{
|
||||||
|
UserId = "abc",
|
||||||
|
MediaServerToken = "dead"
|
||||||
|
}
|
||||||
|
}.AsQueryable().BuildMock().Object);
|
||||||
|
|
||||||
|
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
||||||
|
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer { AuthError = false });
|
||||||
|
await _subject.Execute(_context.Object);
|
||||||
|
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
|
||||||
|
_mocker.Verify<IPlexApi>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
|
||||||
|
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Never);
|
||||||
|
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Never);
|
||||||
|
_mocker.Verify<IRepository<PlexWatchlistUserError>>(x => x.Add(It.Is<PlexWatchlistUserError>(x => x.UserId == "abc")), Times.Never);
|
||||||
|
_mocker.Verify<IRepository<PlexWatchlistUserError>>(x => x.Delete(It.Is<PlexWatchlistUserError>(x => x.UserId == "abc")), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task FailedWatchListUser_OldToken_ShouldSkip()
|
||||||
|
{
|
||||||
|
_mocker.Setup<IRepository<PlexWatchlistUserError>, IQueryable<PlexWatchlistUserError>>(x => x.GetAll()).Returns(new List<PlexWatchlistUserError>
|
||||||
|
{
|
||||||
|
new PlexWatchlistUserError
|
||||||
|
{
|
||||||
|
UserId = "abc",
|
||||||
|
MediaServerToken = "token1"
|
||||||
|
}
|
||||||
|
}.AsQueryable().BuildMock().Object);
|
||||||
|
|
||||||
|
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
||||||
|
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer { AuthError = false });
|
||||||
|
await _subject.Execute(_context.Object);
|
||||||
|
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
|
||||||
|
_mocker.Verify<IPlexApi>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||||
|
_mocker.Verify<IMovieRequestEngine>(x => x.RequestMovie(It.IsAny<MovieRequestViewModel>()), Times.Never);
|
||||||
|
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.GetAll(), Times.Never);
|
||||||
|
_mocker.Verify<IExternalRepository<PlexWatchlistHistory>>(x => x.Add(It.IsAny<PlexWatchlistHistory>()), Times.Never);
|
||||||
|
_mocker.Verify<IRepository<PlexWatchlistUserError>>(x => x.Add(It.Is<PlexWatchlistUserError>(x => x.UserId == "abc")), Times.Never);
|
||||||
|
_mocker.Verify<IRepository<PlexWatchlistUserError>>(x => x.Delete(It.Is<PlexWatchlistUserError>(x => x.UserId == "abc")), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task NoPlexUsersWithToken()
|
public async Task NoPlexUsersWithToken()
|
||||||
{
|
{
|
||||||
|
|
||||||
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
||||||
var um = MockHelper.MockUserManager(new List<OmbiUser>
|
var um = MockHelper.MockUserManager(new List<OmbiUser>
|
||||||
{
|
{
|
||||||
|
@ -102,6 +171,7 @@ namespace Ombi.Schedule.Tests
|
||||||
[Test]
|
[Test]
|
||||||
public async Task MultipleUsers()
|
public async Task MultipleUsers()
|
||||||
{
|
{
|
||||||
|
|
||||||
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
||||||
var um = MockHelper.MockUserManager(new List<OmbiUser>
|
var um = MockHelper.MockUserManager(new List<OmbiUser>
|
||||||
{
|
{
|
||||||
|
@ -125,6 +195,7 @@ namespace Ombi.Schedule.Tests
|
||||||
[Test]
|
[Test]
|
||||||
public async Task MovieRequestFromWatchList_NoGuid()
|
public async Task MovieRequestFromWatchList_NoGuid()
|
||||||
{
|
{
|
||||||
|
|
||||||
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
||||||
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
|
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
|
||||||
{
|
{
|
||||||
|
@ -175,6 +246,7 @@ namespace Ombi.Schedule.Tests
|
||||||
[Test]
|
[Test]
|
||||||
public async Task TvRequestFromWatchList_NoGuid()
|
public async Task TvRequestFromWatchList_NoGuid()
|
||||||
{
|
{
|
||||||
|
|
||||||
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
||||||
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
|
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
|
||||||
{
|
{
|
||||||
|
@ -224,6 +296,7 @@ namespace Ombi.Schedule.Tests
|
||||||
[Test]
|
[Test]
|
||||||
public async Task MovieRequestFromWatchList_AlreadyRequested()
|
public async Task MovieRequestFromWatchList_AlreadyRequested()
|
||||||
{
|
{
|
||||||
|
|
||||||
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
||||||
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
|
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
|
||||||
{
|
{
|
||||||
|
@ -273,6 +346,7 @@ namespace Ombi.Schedule.Tests
|
||||||
[Test]
|
[Test]
|
||||||
public async Task TvRequestFromWatchList_AlreadyRequested()
|
public async Task TvRequestFromWatchList_AlreadyRequested()
|
||||||
{
|
{
|
||||||
|
|
||||||
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
||||||
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
|
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
|
||||||
{
|
{
|
||||||
|
@ -322,6 +396,7 @@ namespace Ombi.Schedule.Tests
|
||||||
[Test]
|
[Test]
|
||||||
public async Task MovieRequestFromWatchList_NoTmdbGuid()
|
public async Task MovieRequestFromWatchList_NoTmdbGuid()
|
||||||
{
|
{
|
||||||
|
|
||||||
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
||||||
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
|
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
|
||||||
{
|
{
|
||||||
|
@ -371,6 +446,7 @@ namespace Ombi.Schedule.Tests
|
||||||
[Test]
|
[Test]
|
||||||
public async Task TvRequestFromWatchList_NoTmdbGuid()
|
public async Task TvRequestFromWatchList_NoTmdbGuid()
|
||||||
{
|
{
|
||||||
|
|
||||||
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
||||||
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
|
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
|
||||||
{
|
{
|
||||||
|
@ -420,6 +496,7 @@ namespace Ombi.Schedule.Tests
|
||||||
[Test]
|
[Test]
|
||||||
public async Task MovieRequestFromWatchList_AlreadyImported()
|
public async Task MovieRequestFromWatchList_AlreadyImported()
|
||||||
{
|
{
|
||||||
|
|
||||||
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
_mocker.Setup<ISettingsService<PlexSettings>, Task<PlexSettings>>(x => x.GetSettingsAsync()).ReturnsAsync(new PlexSettings { Enable = true, EnableWatchlistImport = true });
|
||||||
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
|
_mocker.Setup<IPlexApi, Task<PlexWatchlistContainer>>(x => x.GetWatchlist(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(new PlexWatchlistContainer
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Ombi.Api.Plex;
|
using Ombi.Api.Plex;
|
||||||
using Ombi.Api.Plex.Models;
|
using Ombi.Api.Plex.Models;
|
||||||
|
@ -32,10 +33,11 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
private readonly IHubContext<NotificationHub> _hub;
|
private readonly IHubContext<NotificationHub> _hub;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IExternalRepository<PlexWatchlistHistory> _watchlistRepo;
|
private readonly IExternalRepository<PlexWatchlistHistory> _watchlistRepo;
|
||||||
|
private readonly IRepository<PlexWatchlistUserError> _userError;
|
||||||
|
|
||||||
public PlexWatchlistImport(IPlexApi plexApi, ISettingsService<PlexSettings> settings, OmbiUserManager ombiUserManager,
|
public PlexWatchlistImport(IPlexApi plexApi, ISettingsService<PlexSettings> settings, OmbiUserManager ombiUserManager,
|
||||||
IMovieRequestEngine movieRequestEngine, ITvRequestEngine tvRequestEngine, IHubContext<NotificationHub> hub,
|
IMovieRequestEngine movieRequestEngine, ITvRequestEngine tvRequestEngine, IHubContext<NotificationHub> hub,
|
||||||
ILogger<PlexWatchlistImport> logger, IExternalRepository<PlexWatchlistHistory> watchlistRepo)
|
ILogger<PlexWatchlistImport> logger, IExternalRepository<PlexWatchlistHistory> watchlistRepo, IRepository<PlexWatchlistUserError> userError)
|
||||||
{
|
{
|
||||||
_plexApi = plexApi;
|
_plexApi = plexApi;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
|
@ -45,6 +47,7 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
_hub = hub;
|
_hub = hub;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_watchlistRepo = watchlistRepo;
|
_watchlistRepo = watchlistRepo;
|
||||||
|
_userError = userError;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Execute(IJobExecutionContext context)
|
public async Task Execute(IJobExecutionContext context)
|
||||||
|
@ -64,9 +67,35 @@ namespace Ombi.Schedule.Jobs.Plex
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Check if the user has errors and the token is the same (not refreshed)
|
||||||
|
var failedUser = await _userError.GetAll().Where(x => x.UserId == user.Id).FirstOrDefaultAsync();
|
||||||
|
if (failedUser != null)
|
||||||
|
{
|
||||||
|
if (failedUser.MediaServerToken.Equals(user.MediaServerToken))
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"Skipping Plex Watchlist Import for user '{user.UserName}' as they failed previously and the token has not yet been refreshed");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// remove that guy
|
||||||
|
await _userError.Delete(failedUser);
|
||||||
|
failedUser = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogDebug($"Starting Watchlist Import for {user.UserName} with token {user.MediaServerToken}");
|
_logger.LogDebug($"Starting Watchlist Import for {user.UserName} with token {user.MediaServerToken}");
|
||||||
var watchlist = await _plexApi.GetWatchlist(user.MediaServerToken, context?.CancellationToken ?? CancellationToken.None);
|
var watchlist = await _plexApi.GetWatchlist(user.MediaServerToken, context?.CancellationToken ?? CancellationToken.None);
|
||||||
|
if (watchlist?.AuthError ?? false)
|
||||||
|
{
|
||||||
|
_logger.LogError($"Auth failed for user '{user.UserName}'. Need to re-authenticate with Ombi.");
|
||||||
|
await _userError.Add(new PlexWatchlistUserError
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
MediaServerToken = user.MediaServerToken,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (watchlist == null || !(watchlist.MediaContainer?.Metadata?.Any() ?? false))
|
if (watchlist == null || !(watchlist.MediaContainer?.Metadata?.Any() ?? false))
|
||||||
{
|
{
|
||||||
_logger.LogDebug($"No watchlist found for {user.UserName}");
|
_logger.LogDebug($"No watchlist found for {user.UserName}");
|
||||||
|
|
|
@ -45,6 +45,7 @@ namespace Ombi.Store.Context
|
||||||
public DbSet<RequestLog> RequestLogs { get; set; }
|
public DbSet<RequestLog> RequestLogs { get; set; }
|
||||||
public DbSet<RecentlyAddedLog> RecentlyAddedLogs { get; set; }
|
public DbSet<RecentlyAddedLog> RecentlyAddedLogs { get; set; }
|
||||||
public DbSet<Votes> Votes { get; set; }
|
public DbSet<Votes> Votes { get; set; }
|
||||||
|
public DbSet<PlexWatchlistUserError> PlexWatchListUserError { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public DbSet<Audit> Audit { get; set; }
|
public DbSet<Audit> Audit { get; set; }
|
||||||
|
|
11
src/Ombi.Store/Entities/PlexWatchlistUserError.cs
Normal file
11
src/Ombi.Store/Entities/PlexWatchlistUserError.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Ombi.Store.Entities
|
||||||
|
{
|
||||||
|
[Table(nameof(PlexWatchlistUserError))]
|
||||||
|
public class PlexWatchlistUserError : Entity
|
||||||
|
{
|
||||||
|
public string UserId { get; set; }
|
||||||
|
public string MediaServerToken { get; set; }
|
||||||
|
}
|
||||||
|
}
|
1302
src/Ombi.Store/Migrations/OmbiMySql/20220823190345_PlexWatchlistUserError.Designer.cs
generated
Normal file
1302
src/Ombi.Store/Migrations/OmbiMySql/20220823190345_PlexWatchlistUserError.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,36 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Ombi.Store.Migrations.OmbiMySql
|
||||||
|
{
|
||||||
|
public partial class PlexWatchlistUserError : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PlexWatchlistUserError",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
UserId = table.Column<string>(type: "longtext", nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
MediaServerToken = table.Column<string>(type: "longtext", nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PlexWatchlistUserError", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PlexWatchlistUserError");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -350,6 +350,23 @@ namespace Ombi.Store.Migrations.OmbiMySql
|
||||||
b.ToTable("AspNetUsers", (string)null);
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Ombi.Store.Entities.PlexWatchlistUserError", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("MediaServerToken")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("PlexWatchlistUserError");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b =>
|
modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
|
1300
src/Ombi.Store/Migrations/OmbiSqlite/20220823192128_PlexWatchlistUserError.Designer.cs
generated
Normal file
1300
src/Ombi.Store/Migrations/OmbiSqlite/20220823192128_PlexWatchlistUserError.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,32 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Ombi.Store.Migrations.OmbiSqlite
|
||||||
|
{
|
||||||
|
public partial class PlexWatchlistUserError : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PlexWatchlistUserError",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
UserId = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
MediaServerToken = table.Column<string>(type: "TEXT", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PlexWatchlistUserError", x => x.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PlexWatchlistUserError");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -348,6 +348,23 @@ namespace Ombi.Store.Migrations.OmbiSqlite
|
||||||
b.ToTable("AspNetUsers", (string)null);
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Ombi.Store.Entities.PlexWatchlistUserError", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("MediaServerToken")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("PlexWatchlistUserError");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b =>
|
modelBuilder.Entity("Ombi.Store.Entities.RecentlyAddedLog", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
|
|
@ -107,3 +107,16 @@ export interface IPlexServerResponse {
|
||||||
port: string;
|
port: string;
|
||||||
scheme: string;
|
scheme: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IPlexWatchlistUsers {
|
||||||
|
userId: string;
|
||||||
|
syncStatus: WatchlistSyncStatus;
|
||||||
|
userName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum WatchlistSyncStatus
|
||||||
|
{
|
||||||
|
Successful,
|
||||||
|
Failed,
|
||||||
|
NotEnabled
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { PlatformLocation, APP_BASE_HREF } from "@angular/common";
|
import { APP_BASE_HREF } from "@angular/common";
|
||||||
import { HttpClient } from "@angular/common/http";
|
import { HttpClient } from "@angular/common/http";
|
||||||
import { Injectable, Inject } from "@angular/core";
|
import { Injectable, Inject } from "@angular/core";
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { ServiceHelpers } from "../service.helpers";
|
import { ServiceHelpers } from "../service.helpers";
|
||||||
|
|
||||||
import { IPlexAuthentication, IPlexLibResponse, IPlexLibSimpleResponse, IPlexOAuthViewModel, IPlexServer, IPlexServerAddViewModel, IPlexServerViewModel, IPlexUserAddResponse, IPlexUserViewModel, IUsersModel } from "../../interfaces";
|
import { IPlexAuthentication, IPlexLibResponse, IPlexLibSimpleResponse, IPlexOAuthViewModel, IPlexServer, IPlexServerAddViewModel, IPlexServerViewModel, IPlexUserAddResponse, IPlexUserViewModel, IPlexWatchlistUsers, IUsersModel } from "../../interfaces";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PlexService extends ServiceHelpers {
|
export class PlexService extends ServiceHelpers {
|
||||||
|
@ -45,4 +45,8 @@ export class PlexService extends ServiceHelpers {
|
||||||
public oAuth(wizard: IPlexOAuthViewModel): Observable<any> {
|
public oAuth(wizard: IPlexOAuthViewModel): Observable<any> {
|
||||||
return this.http.post<any>(`${this.url}oauth`, JSON.stringify(wizard), {headers: this.headers});
|
return this.http.post<any>(`${this.url}oauth`, JSON.stringify(wizard), {headers: this.headers});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getWatchlistUsers(): Observable<IPlexWatchlistUsers[]> {
|
||||||
|
return this.http.get<IPlexWatchlistUsers[]>(`${this.url}WatchlistUsers`, {headers: this.headers});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
<div class="small-middle-container">
|
||||||
|
<fieldset style="fieldset">
|
||||||
|
<legend mat-dialog-title>Watchlist User Errors</legend>
|
||||||
|
<div mat-dialog-content>
|
||||||
|
<p>
|
||||||
|
If there is an authentication error, this is because of an authentication issue with Plex (Token has expired).
|
||||||
|
If this happens the user needs to re-login to Ombi.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<em class="fa-solid fa-check key"></em> Successfully syncing the watchlist
|
||||||
|
<br>
|
||||||
|
<em class="fa-solid fa-times key"></em> Authentication error syncing the watchlist
|
||||||
|
<br>
|
||||||
|
<em class="fas fa-user-alt-slash key"></em> Not enabled for user (They need to log into Ombi via Plex)
|
||||||
|
</p>
|
||||||
|
<table mat-table *ngIf="dataSource" [dataSource]="dataSource" matSort class="mat-elevation-z8">
|
||||||
|
<ng-container matColumnDef="userName">
|
||||||
|
<th mat-header-cell *matHeaderCellDef> Username </th>
|
||||||
|
<td mat-cell *matCellDef="let element"> {{element.userName}} </td>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="syncStatus">
|
||||||
|
<th mat-header-cell *matHeaderCellDef> Watchlist Sync Result </th>
|
||||||
|
<td mat-cell *matCellDef="let element">
|
||||||
|
<em *ngIf="element.syncStatus === WatchlistSyncStatus.Successful" class="fa-solid fa-check"></em>
|
||||||
|
<em *ngIf="element.syncStatus === WatchlistSyncStatus.Failed" class="fa-solid fa-times"></em>
|
||||||
|
<em *ngIf="element.syncStatus === WatchlistSyncStatus.NotEnabled" class="fas fa-user-alt-slash"></em>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<mat-dialog-actions align="end">
|
||||||
|
<button mat-button mat-dialog-close>Close</button>
|
||||||
|
</mat-dialog-actions>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
|
@ -0,0 +1,13 @@
|
||||||
|
@import "~styles/shared.scss";
|
||||||
|
.small-middle-container {
|
||||||
|
margin: auto;
|
||||||
|
width: 95%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.fieldset {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key {
|
||||||
|
width: 40px;
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
// also exported from '@storybook/angular' if you can deal with breaking changes in 6.1
|
||||||
|
import { APP_BASE_HREF, CommonModule } from '@angular/common';
|
||||||
|
import { Story, Meta, moduleMetadata } from '@storybook/angular';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { IPlexWatchlistUsers, WatchlistSyncStatus } from '../../../../interfaces';
|
||||||
|
import { PlexService } from '../../../../services';
|
||||||
|
import { SharedModule } from '../../../../shared/shared.module';
|
||||||
|
import { PlexWatchlistComponent } from './plex-watchlist.component';
|
||||||
|
|
||||||
|
|
||||||
|
const mockUsers: IPlexWatchlistUsers[] =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
userName: "Success User",
|
||||||
|
userId: "a",
|
||||||
|
syncStatus: WatchlistSyncStatus.Successful
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userName: "Failed User",
|
||||||
|
userId: "2",
|
||||||
|
syncStatus: WatchlistSyncStatus.Failed
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userName: "Not Enabled",
|
||||||
|
userId: "2",
|
||||||
|
syncStatus: WatchlistSyncStatus.NotEnabled
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function plexServiveMock(): Partial<PlexService> {
|
||||||
|
return {
|
||||||
|
getWatchlistUsers: () : Observable<IPlexWatchlistUsers[]> => of(mockUsers),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// More on default export: https://storybook.js.org/docs/angular/writing-stories/introduction#default-export
|
||||||
|
export default {
|
||||||
|
title: 'Plex Watchlist Component',
|
||||||
|
component: PlexWatchlistComponent,
|
||||||
|
decorators: [
|
||||||
|
moduleMetadata({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_BASE_HREF,
|
||||||
|
useValue: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PlexService,
|
||||||
|
useValue: plexServiveMock()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SharedModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
// More on component templates: https://storybook.js.org/docs/angular/writing-stories/introduction#using-args
|
||||||
|
const Template: Story<PlexWatchlistComponent> = (args: PlexWatchlistComponent) => ({
|
||||||
|
props: args,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Default = Template.bind({});
|
||||||
|
Default.args = {
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { MatTableDataSource } from "@angular/material/table";
|
||||||
|
import { take } from "rxjs";
|
||||||
|
import { IPlexWatchlistUsers, WatchlistSyncStatus } from "../../../../interfaces";
|
||||||
|
import { PlexService } from "../../../../services";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "./plex-watchlist.component.html",
|
||||||
|
styleUrls: ["./plex-watchlist.component.scss"]
|
||||||
|
})
|
||||||
|
export class PlexWatchlistComponent implements OnInit{
|
||||||
|
|
||||||
|
public dataSource: MatTableDataSource<IPlexWatchlistUsers> = new MatTableDataSource();
|
||||||
|
public displayedColumns: string[] = ['userName','syncStatus'];
|
||||||
|
|
||||||
|
public WatchlistSyncStatus = WatchlistSyncStatus;
|
||||||
|
|
||||||
|
constructor(private plexService: PlexService) { }
|
||||||
|
|
||||||
|
public ngOnInit() {
|
||||||
|
this.plexService.getWatchlistUsers().pipe(take(1)).subscribe((x: IPlexWatchlistUsers[]) => {
|
||||||
|
this.dataSource = new MatTableDataSource(x);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,8 +3,7 @@
|
||||||
<fieldset style="width:100%;">
|
<fieldset style="width:100%;">
|
||||||
<legend>Plex Configuration</legend>
|
<legend>Plex Configuration</legend>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 col-6 col-sm-6">
|
<div class="col-md-6 col-12">
|
||||||
|
|
||||||
<div class="md-form-field">
|
<div class="md-form-field">
|
||||||
<mat-slide-toggle [(ngModel)]="settings.enable" [checked]="settings.enable">Enable
|
<mat-slide-toggle [(ngModel)]="settings.enable" [checked]="settings.enable">Enable
|
||||||
</mat-slide-toggle>
|
</mat-slide-toggle>
|
||||||
|
@ -20,12 +19,18 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6 col-12">
|
||||||
|
<div class="md-form-field align-right">
|
||||||
|
<button (click)="openWatchlistUserLog()" type="button" class="mat-focus-indicator mat-flat-button mat-button-base mat-accent">Watchlist User Errors</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<mat-tab-group #tabGroup [selectedIndex]="selected.value" (selectedTabChange)="addTab($event)"
|
<mat-tab-group #tabGroup [selectedIndex]="selected.value" (selectedTabChange)="addTab($event)"
|
||||||
(selectedIndexChange)="selected.setValue($event)" animationDuration="0ms" style="width:100%;">
|
(selectedIndexChange)="selected.setValue($event)" animationDuration="0ms" style="width:100%;">
|
||||||
<mat-tab *ngFor="let server of settings.servers" [label]="server.name">
|
<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;">
|
<div class="col-md-6 col-6 col-sm-6 align-right">
|
||||||
<button type="button" (click)="removeServer(server)"
|
<button type="button" (click)="removeServer(server)"
|
||||||
class="mat-focus-indicator mat-flat-button mat-button-base mat-warn">Remove Server</button>
|
class="mat-focus-indicator mat-flat-button mat-button-base mat-warn">Remove Server</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,6 +12,9 @@
|
||||||
display: inline-table;
|
display: inline-table;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
|
||||||
display: block;
|
.align-right {
|
||||||
}
|
float: right;
|
||||||
|
width:100%;
|
||||||
|
text-align:right;
|
||||||
|
}
|
|
@ -6,6 +6,8 @@ import { IPlexLibrariesSettings, IPlexServer, IPlexServerResponse, IPlexServerVi
|
||||||
import { JobService, NotificationService, PlexService, SettingsService, TesterService } from "../../services";
|
import { JobService, NotificationService, PlexService, SettingsService, TesterService } from "../../services";
|
||||||
import { MatTabChangeEvent, MatTabGroup } from "@angular/material/tabs";
|
import { MatTabChangeEvent, MatTabGroup } from "@angular/material/tabs";
|
||||||
import {UntypedFormControl} from '@angular/forms';
|
import {UntypedFormControl} from '@angular/forms';
|
||||||
|
import { MatDialog } from "@angular/material/dialog";
|
||||||
|
import { PlexWatchlistComponent } from "./components/watchlist/plex-watchlist.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: "./plex.component.html",
|
templateUrl: "./plex.component.html",
|
||||||
|
@ -29,7 +31,8 @@ export class PlexComponent implements OnInit, OnDestroy {
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
private plexService: PlexService,
|
private plexService: PlexService,
|
||||||
private testerService: TesterService,
|
private testerService: TesterService,
|
||||||
private jobService: JobService) { }
|
private jobService: JobService,
|
||||||
|
private dialog: MatDialog) { }
|
||||||
|
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.settingsService.getPlex().subscribe(x => {
|
this.settingsService.getPlex().subscribe(x => {
|
||||||
|
@ -180,6 +183,10 @@ export class PlexComponent implements OnInit, OnDestroy {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public openWatchlistUserLog(): void {
|
||||||
|
this.dialog.open(PlexWatchlistComponent, { width: "700px", panelClass: 'modal-panel' });
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.subscriptions.next();
|
this.subscriptions.next();
|
||||||
this.subscriptions.complete();
|
this.subscriptions.complete();
|
||||||
|
|
|
@ -83,6 +83,7 @@ import { VoteComponent } from "./vote/vote.component";
|
||||||
import { WebhookComponent } from "./notifications/webhook.component";
|
import { WebhookComponent } from "./notifications/webhook.component";
|
||||||
import { WhatsAppComponent } from "./notifications/twilio/whatsapp.component";
|
import { WhatsAppComponent } from "./notifications/twilio/whatsapp.component";
|
||||||
import { WikiComponent } from "./wiki.component";
|
import { WikiComponent } from "./wiki.component";
|
||||||
|
import { PlexWatchlistComponent } from "./plex/components/watchlist/plex-watchlist.component";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: "Ombi", component: OmbiComponent, canActivate: [AuthGuard] },
|
{ path: "Ombi", component: OmbiComponent, canActivate: [AuthGuard] },
|
||||||
|
@ -189,6 +190,7 @@ const routes: Routes = [
|
||||||
FeaturesComponent,
|
FeaturesComponent,
|
||||||
CloudMobileComponent,
|
CloudMobileComponent,
|
||||||
UpdateDialogComponent,
|
UpdateDialogComponent,
|
||||||
|
PlexWatchlistComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
RouterModule,
|
RouterModule,
|
||||||
|
|
|
@ -9,6 +9,8 @@ using Ombi.Api.Plex;
|
||||||
using Ombi.Api.Plex.Models;
|
using Ombi.Api.Plex.Models;
|
||||||
using Ombi.Attributes;
|
using Ombi.Attributes;
|
||||||
using Ombi.Core.Authentication;
|
using Ombi.Core.Authentication;
|
||||||
|
using Ombi.Core.Models;
|
||||||
|
using Ombi.Core.Services;
|
||||||
using Ombi.Core.Settings;
|
using Ombi.Core.Settings;
|
||||||
using Ombi.Core.Settings.Models.External;
|
using Ombi.Core.Settings.Models.External;
|
||||||
using Ombi.Helpers;
|
using Ombi.Helpers;
|
||||||
|
@ -23,18 +25,20 @@ namespace Ombi.Controllers.V1.External
|
||||||
public class PlexController : Controller
|
public class PlexController : Controller
|
||||||
{
|
{
|
||||||
public PlexController(IPlexApi plexApi, ISettingsService<PlexSettings> plexSettings,
|
public PlexController(IPlexApi plexApi, ISettingsService<PlexSettings> plexSettings,
|
||||||
ILogger<PlexController> logger, IPlexOAuthManager manager)
|
ILogger<PlexController> logger, IPlexOAuthManager manager, IPlexService plexService)
|
||||||
{
|
{
|
||||||
PlexApi = plexApi;
|
PlexApi = plexApi;
|
||||||
PlexSettings = plexSettings;
|
PlexSettings = plexSettings;
|
||||||
_log = logger;
|
_log = logger;
|
||||||
_plexOAuthManager = manager;
|
_plexOAuthManager = manager;
|
||||||
|
_plexService = plexService;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IPlexApi PlexApi { get; }
|
private IPlexApi PlexApi { get; }
|
||||||
private ISettingsService<PlexSettings> PlexSettings { get; }
|
private ISettingsService<PlexSettings> PlexSettings { get; }
|
||||||
private readonly ILogger<PlexController> _log;
|
private readonly ILogger<PlexController> _log;
|
||||||
private readonly IPlexOAuthManager _plexOAuthManager;
|
private readonly IPlexOAuthManager _plexOAuthManager;
|
||||||
|
private readonly IPlexService _plexService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Signs into the Plex API.
|
/// Signs into the Plex API.
|
||||||
|
@ -300,5 +304,9 @@ namespace Ombi.Controllers.V1.External
|
||||||
|
|
||||||
return new JsonResult(new {url = url.ToString()});
|
return new JsonResult(new {url = url.ToString()});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Admin]
|
||||||
|
[HttpGet("WatchlistUsers")]
|
||||||
|
public async Task<List<PlexUserWatchlistModel>> GetPlexWatchlistUsers() => await _plexService.GetWatchlistUsers(HttpContext.RequestAborted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
14
src/Ombi/databasea.json
Normal file
14
src/Ombi/databasea.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"OmbiDatabase": {
|
||||||
|
"Type": "MySQL",
|
||||||
|
"ConnectionString": "Server=192.168.68.118;Port=3306;Database=Ombi;User=ombi;"
|
||||||
|
},
|
||||||
|
"SettingsDatabase": {
|
||||||
|
"Type": "MySQL",
|
||||||
|
"ConnectionString": "Server=192.168.68.118;Port=3306;Database=Ombi;User=ombi;"
|
||||||
|
},
|
||||||
|
"ExternalDatabase": {
|
||||||
|
"Type": "MySQL",
|
||||||
|
"ConnectionString": "Server=192.168.68.118;Port=3306;Database=Ombi;User=ombi;"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue