diff --git a/.gitignore b/.gitignore index 771a7bacd..587f09568 100644 --- a/.gitignore +++ b/.gitignore @@ -243,3 +243,6 @@ _Pvt_Extensions # CAKE - C# Make /Tools/* *.db-journal + +# Ignore local vscode config +*.vscode diff --git a/src/Ombi.Api.Lidarr/ILidarrApi.cs b/src/Ombi.Api.Lidarr/ILidarrApi.cs index 15d20fd28..4a23c6200 100644 --- a/src/Ombi.Api.Lidarr/ILidarrApi.cs +++ b/src/Ombi.Api.Lidarr/ILidarrApi.cs @@ -22,5 +22,6 @@ namespace Ombi.Api.Lidarr Task> GetMetadataProfile(string apiKey, string baseUrl); Task> GetLanguageProfile(string apiKey, string baseUrl); Task Status(string apiKey, string baseUrl); + Task AlbumSearch(int[] albumIds, string apiKey, string baseUrl); } } \ No newline at end of file diff --git a/src/Ombi.Api.Lidarr/LidarrApi.cs b/src/Ombi.Api.Lidarr/LidarrApi.cs index 2aded7e1a..8cda49cbf 100644 --- a/src/Ombi.Api.Lidarr/LidarrApi.cs +++ b/src/Ombi.Api.Lidarr/LidarrApi.cs @@ -154,6 +154,14 @@ namespace Ombi.Api.Lidarr return Api.Request(request); } + public Task AlbumSearch(int[] albumIds, string apiKey, string baseUrl) + { + var request = new Request($"{ApiVersion}/command/AlbumSearch", baseUrl, HttpMethod.Post); + request.AddJsonBody(albumIds); + AddHeaders(request, apiKey); + return Api.Request(request); + } + private void AddHeaders(Request request, string key) { request.AddHeader("X-Api-Key", key); diff --git a/src/Ombi.Api.Lidarr/Models/CommandResult.cs b/src/Ombi.Api.Lidarr/Models/CommandResult.cs new file mode 100644 index 000000000..7c6483a6a --- /dev/null +++ b/src/Ombi.Api.Lidarr/Models/CommandResult.cs @@ -0,0 +1,15 @@ +using System; + +namespace Ombi.Api.Lidarr.Models +{ + + public class CommandResult + { + public string name { get; set; } + public DateTime startedOn { get; set; } + public DateTime stateChangeTime { get; set; } + public bool sendUpdatesToClient { get; set; } + public string state { get; set; } + public int id { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Engine/Interfaces/IMovieRequestEngine.cs b/src/Ombi.Core/Engine/Interfaces/IMovieRequestEngine.cs index 91cbb9e72..152a1d923 100644 --- a/src/Ombi.Core/Engine/Interfaces/IMovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/Interfaces/IMovieRequestEngine.cs @@ -17,7 +17,5 @@ namespace Ombi.Core.Engine.Interfaces Task ApproveMovie(MovieRequests request); Task ApproveMovieById(int requestId); Task DenyMovieById(int modelId); - - } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/Interfaces/IRequestEngine.cs b/src/Ombi.Core/Engine/Interfaces/IRequestEngine.cs index 740428ec7..c8b7746f0 100644 --- a/src/Ombi.Core/Engine/Interfaces/IRequestEngine.cs +++ b/src/Ombi.Core/Engine/Interfaces/IRequestEngine.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Ombi.Core.Models; using Ombi.Core.Models.Requests; using Ombi.Core.Models.UI; using Ombi.Store.Entities; @@ -22,5 +23,6 @@ namespace Ombi.Core.Engine.Interfaces Task GetTotal(); Task UnSubscribeRequest(int requestId, RequestType type); Task SubscribeToRequest(int requestId, RequestType type); + Task GetRemainingRequests(OmbiUser user = null); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/MovieRequestEngine.cs b/src/Ombi.Core/Engine/MovieRequestEngine.cs index f0a4e1f29..9fd6033bf 100644 --- a/src/Ombi.Core/Engine/MovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/MovieRequestEngine.cs @@ -19,6 +19,7 @@ using Ombi.Core.Settings; using Ombi.Settings.Settings.Models; using Ombi.Store.Entities.Requests; using Ombi.Store.Repository; +using Ombi.Core.Models; namespace Ombi.Core.Engine { @@ -484,5 +485,49 @@ namespace Ombi.Core.Engine return new RequestEngineResult {Result = true, Message = $"{movieName} has been successfully added!"}; } + + public async Task GetRemainingRequests(OmbiUser user) + { + if (user == null) + { + user = await GetUser(); + + // If user is still null after attempting to get the logged in user, return null. + if (user == null) + { + return null; + } + } + + int limit = user.MovieRequestLimit ?? 0; + + if (limit <= 0) + { + return new RequestQuotaCountModel() + { + HasLimit = false, + Limit = 0, + Remaining = 0, + NextRequest = DateTime.Now, + }; + } + + IQueryable log = _requestLog.GetAll().Where(x => x.UserId == user.Id && x.RequestType == RequestType.Movie); + + int count = limit - await log.CountAsync(x => x.RequestDate >= DateTime.UtcNow.AddDays(-7)); + + DateTime oldestRequestedAt = await log.Where(x => x.RequestDate >= DateTime.UtcNow.AddDays(-7)) + .OrderBy(x => x.RequestDate) + .Select(x => x.RequestDate) + .FirstOrDefaultAsync(); + + return new RequestQuotaCountModel() + { + HasLimit = true, + Limit = limit, + Remaining = count, + NextRequest = DateTime.SpecifyKind(oldestRequestedAt.AddDays(7), DateTimeKind.Utc), + }; + } } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/TvRequestEngine.cs b/src/Ombi.Core/Engine/TvRequestEngine.cs index 90760f759..7994b23aa 100644 --- a/src/Ombi.Core/Engine/TvRequestEngine.cs +++ b/src/Ombi.Core/Engine/TvRequestEngine.cs @@ -23,6 +23,7 @@ using Ombi.Core.Settings; using Ombi.Settings.Settings.Models; using Ombi.Store.Entities.Requests; using Ombi.Store.Repository; +using Ombi.Core.Models; namespace Ombi.Core.Engine { @@ -587,6 +588,15 @@ namespace Ombi.Core.Engine NotificationHelper.NewRequest(model); } + await _requestLog.Add(new RequestLog + { + UserId = (await GetUser()).Id, + RequestDate = DateTime.UtcNow, + RequestId = model.Id, + RequestType = RequestType.TvShow, + EpisodeCount = model.SeasonRequests.Select(m => m.Episodes.Count).Sum(), + }); + if (model.Approved) { // Autosend @@ -602,15 +612,58 @@ namespace Ombi.Core.Engine }; } - await _requestLog.Add(new RequestLog + return new RequestEngineResult { Result = true }; + } + + public async Task GetRemainingRequests(OmbiUser user) + { + if (user == null) { - UserId = (await GetUser()).Id, - RequestDate = DateTime.UtcNow, - RequestId = model.Id, - RequestType = RequestType.TvShow, - }); + user = await GetUser(); - return new RequestEngineResult { Result = true }; + // If user is still null after attempting to get the logged in user, return null. + if (user == null) + { + return null; + } + } + + int limit = user.EpisodeRequestLimit ?? 0; + + if (limit <= 0) + { + return new RequestQuotaCountModel() + { + HasLimit = false, + Limit = 0, + Remaining = 0, + NextRequest = DateTime.Now, + }; + } + + IQueryable log = _requestLog.GetAll() + .Where(x => x.UserId == user.Id + && x.RequestType == RequestType.TvShow + && x.RequestDate >= DateTime.UtcNow.AddDays(-7)); + + // Needed, due to a bug which would cause all episode counts to be 0 + int zeroEpisodeCount = await log.Where(x => x.EpisodeCount == 0).Select(x => x.EpisodeCount).CountAsync(); + + int episodeCount = await log.Where(x => x.EpisodeCount != 0).Select(x => x.EpisodeCount).SumAsync(); + + int count = limit - (zeroEpisodeCount + episodeCount); + + DateTime oldestRequestedAt = await log.OrderBy(x => x.RequestDate) + .Select(x => x.RequestDate) + .FirstOrDefaultAsync(); + + return new RequestQuotaCountModel() + { + HasLimit = true, + Limit = limit, + Remaining = count, + NextRequest = DateTime.SpecifyKind(oldestRequestedAt.AddDays(7), DateTimeKind.Utc), + }; } } } \ No newline at end of file diff --git a/src/Ombi.Core/Models/RequestQuotaCountModel.cs b/src/Ombi.Core/Models/RequestQuotaCountModel.cs new file mode 100644 index 000000000..1af9ad819 --- /dev/null +++ b/src/Ombi.Core/Models/RequestQuotaCountModel.cs @@ -0,0 +1,15 @@ +using System; + +namespace Ombi.Core.Models +{ + public class RequestQuotaCountModel + { + public bool HasLimit { get; set; } + + public int Limit { get; set; } + + public int Remaining { get; set; } + + public DateTime NextRequest { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Core/Models/UI/UserViewModel.cs b/src/Ombi.Core/Models/UI/UserViewModel.cs index 62b296009..15045bc0a 100644 --- a/src/Ombi.Core/Models/UI/UserViewModel.cs +++ b/src/Ombi.Core/Models/UI/UserViewModel.cs @@ -16,6 +16,8 @@ namespace Ombi.Core.Models.UI public UserType UserType { get; set; } public int MovieRequestLimit { get; set; } public int EpisodeRequestLimit { get; set; } + public RequestQuotaCountModel EpisodeRequestQuota { get; set; } + public RequestQuotaCountModel MovieRequestQuota { get; set; } public int MusicRequestLimit { get; set; } } diff --git a/src/Ombi.Core/Ombi.Core.csproj b/src/Ombi.Core/Ombi.Core.csproj index c2aeb1fd0..10e07822a 100644 --- a/src/Ombi.Core/Ombi.Core.csproj +++ b/src/Ombi.Core/Ombi.Core.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Ombi.Core/Rule/Rules/Request/RequestLimitRule.cs b/src/Ombi.Core/Rule/Rules/Request/RequestLimitRule.cs index 53971bb3e..2afd0700b 100644 --- a/src/Ombi.Core/Rule/Rules/Request/RequestLimitRule.cs +++ b/src/Ombi.Core/Rule/Rules/Request/RequestLimitRule.cs @@ -82,15 +82,20 @@ namespace Ombi.Core.Rule.Rules.Request // Get the count of requests to be made foreach (var s in child.SeasonRequests) { - requestCount = s.Episodes.Count; + requestCount += s.Episodes.Count; } var tvLogs = requestLog.Where(x => x.RequestType == RequestType.TvShow); // Count how many requests in the past 7 days var tv = tvLogs.Where(x => x.RequestDate >= DateTime.UtcNow.AddDays(-7)); - var count = await tv.Select(x => x.EpisodeCount).CountAsync(); - count += requestCount; // Add the amount of requests in + + // Needed, due to a bug which would cause all episode counts to be 0 + var zeroEpisodeCount = await tv.Where(x => x.EpisodeCount == 0).Select(x => x.EpisodeCount).CountAsync(); + + var episodeCount = await tv.Where(x => x.EpisodeCount != 0).Select(x => x.EpisodeCount).SumAsync(); + + var count = requestCount + episodeCount + zeroEpisodeCount; // Add the amount of requests in if (count > episodeLimit) { return Fail("You have exceeded your Episode request quota!"); diff --git a/src/Ombi.Core/Senders/MovieSender.cs b/src/Ombi.Core/Senders/MovieSender.cs index e57a5bf2a..aa919e552 100644 --- a/src/Ombi.Core/Senders/MovieSender.cs +++ b/src/Ombi.Core/Senders/MovieSender.cs @@ -123,7 +123,10 @@ namespace Ombi.Core.Senders existingMovie.monitored = true; await RadarrApi.UpdateMovie(existingMovie, settings.ApiKey, settings.FullUri); // Search for it - await RadarrApi.MovieSearch(new[] { existingMovie.id }, settings.ApiKey, settings.FullUri); + if (!settings.AddOnly) + { + await RadarrApi.MovieSearch(new[] {existingMovie.id}, settings.ApiKey, settings.FullUri); + } return new SenderResult { Success = true, Sent = true }; } diff --git a/src/Ombi.Core/Senders/MusicSender.cs b/src/Ombi.Core/Senders/MusicSender.cs index 5e1e44126..937204be5 100644 --- a/src/Ombi.Core/Senders/MusicSender.cs +++ b/src/Ombi.Core/Senders/MusicSender.cs @@ -116,6 +116,10 @@ namespace Ombi.Core.Senders } var result = await _lidarrApi.MontiorAlbum(album.id, settings.ApiKey, settings.FullUri); + if (!settings.AddOnly) + { + await _lidarrApi.AlbumSearch(new[] {result.id}, settings.ApiKey, settings.FullUri); + } if (result.monitored) { return new SenderResult {Message = "Album has been requested!", Sent = true, Success = true}; diff --git a/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj b/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj index 6fe083fe3..028c37b43 100644 --- a/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj +++ b/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/src/Ombi.Helpers/Ombi.Helpers.csproj b/src/Ombi.Helpers/Ombi.Helpers.csproj index e94afc816..2aaaa076f 100644 --- a/src/Ombi.Helpers/Ombi.Helpers.csproj +++ b/src/Ombi.Helpers/Ombi.Helpers.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj b/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj index a124f01bd..ea1d17f8c 100644 --- a/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj +++ b/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/Ombi.Settings/Settings/Models/External/LidarrSettings.cs b/src/Ombi.Settings/Settings/Models/External/LidarrSettings.cs index e0bdbdc43..3a37b7d43 100644 --- a/src/Ombi.Settings/Settings/Models/External/LidarrSettings.cs +++ b/src/Ombi.Settings/Settings/Models/External/LidarrSettings.cs @@ -11,5 +11,6 @@ namespace Ombi.Settings.Settings.Models.External public bool AlbumFolder { get; set; } public int LanguageProfileId { get; set; } public int MetadataProfileId { get; set; } + public bool AddOnly { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Store/Ombi.Store.csproj b/src/Ombi.Store/Ombi.Store.csproj index 2ceb78424..cdbd3fe84 100644 --- a/src/Ombi.Store/Ombi.Store.csproj +++ b/src/Ombi.Store/Ombi.Store.csproj @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/src/Ombi/ClientApp/app/interfaces/IRemainingRequests.ts b/src/Ombi/ClientApp/app/interfaces/IRemainingRequests.ts new file mode 100644 index 000000000..a6ff1a66c --- /dev/null +++ b/src/Ombi/ClientApp/app/interfaces/IRemainingRequests.ts @@ -0,0 +1,6 @@ +export interface IRemainingRequests { + hasLimit: boolean; + limit: number; + remaining: number; + nextRequest: Date; +} diff --git a/src/Ombi/ClientApp/app/interfaces/ISettings.ts b/src/Ombi/ClientApp/app/interfaces/ISettings.ts index 2fb46a2b7..db4db935e 100644 --- a/src/Ombi/ClientApp/app/interfaces/ISettings.ts +++ b/src/Ombi/ClientApp/app/interfaces/ISettings.ts @@ -93,6 +93,7 @@ export interface ILidarrSettings extends IExternalSettings { metadataProfileId: number; languageProfileId: number; albumFolder: boolean; + addOnly: boolean; } export interface ILandingPageSettings extends ISettings { diff --git a/src/Ombi/ClientApp/app/interfaces/IUser.ts b/src/Ombi/ClientApp/app/interfaces/IUser.ts index 6cd1bc1d7..ff1e7f944 100644 --- a/src/Ombi/ClientApp/app/interfaces/IUser.ts +++ b/src/Ombi/ClientApp/app/interfaces/IUser.ts @@ -1,4 +1,5 @@ import { ICheckbox } from "."; +import { IRemainingRequests } from "./IRemainingRequests"; export interface IUser { id: string; @@ -14,7 +15,10 @@ export interface IUser { episodeRequestLimit: number; musicRequestLimit: number; userAccessToken: string; + // FOR UI + episodeRequestQuota: IRemainingRequests | null; + movieRequestQuota: IRemainingRequests | null; checked: boolean; } diff --git a/src/Ombi/ClientApp/app/login/login.component.ts b/src/Ombi/ClientApp/app/login/login.component.ts index abf387fa1..0c04edbad 100644 --- a/src/Ombi/ClientApp/app/login/login.component.ts +++ b/src/Ombi/ClientApp/app/login/login.component.ts @@ -127,18 +127,18 @@ export class LoginComponent implements OnDestroy, OnInit { } public oauth() { + const oAuthWindow = window.open(window.location.toString(), "_blank", `toolbar=0, + location=0, + status=0, + menubar=0, + scrollbars=1, + resizable=1, + width=500, + height=500`); this.plexTv.GetPin(this.clientId, this.appName).subscribe((pin: any) => { this.authService.login({ usePlexOAuth: true, password: "", rememberMe: true, username: "", plexTvPin: pin }).subscribe(x => { - - window.open(x.url, "_blank", `toolbar=0, - location=0, - status=0, - menubar=0, - scrollbars=1, - resizable=1, - width=500, - height=500`); + oAuthWindow!.location.replace(x.url); this.pinTimer = setInterval(() => { this.notify.info("Authenticating", "Loading... Please Wait"); diff --git a/src/Ombi/ClientApp/app/requests/movierequests.component.ts b/src/Ombi/ClientApp/app/requests/movierequests.component.ts index db7b400db..311adeb92 100644 --- a/src/Ombi/ClientApp/app/requests/movierequests.component.ts +++ b/src/Ombi/ClientApp/app/requests/movierequests.component.ts @@ -80,6 +80,7 @@ export class MovieRequestsComponent implements OnInit { }; this.loadInit(); this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); + } public paginate(event: IPagenator) { diff --git a/src/Ombi/ClientApp/app/requests/remainingrequests.component.html b/src/Ombi/ClientApp/app/requests/remainingrequests.component.html new file mode 100644 index 000000000..11cd28b34 --- /dev/null +++ b/src/Ombi/ClientApp/app/requests/remainingrequests.component.html @@ -0,0 +1,18 @@ +
+

+ {{'Requests.Remaining.Quota' | translate: {remaining: remaining.remaining, total: remaining.limit} }} +

+

+ {{'Requests.Remaining.NextDays' | translate: {time: daysUntil} }} +

+

+ {{'Requests.Remaining.NextHours' | translate: {time: hoursUntil} }} +

+

+ {{(minutesUntil == 1 ? 'Requests.Remaining.NextMinute' : 'Requests.Remaining.NextMinutes') | translate: {time: minutesUntil} }} +

+
+ +
+
+ diff --git a/src/Ombi/ClientApp/app/requests/remainingrequests.component.ts b/src/Ombi/ClientApp/app/requests/remainingrequests.component.ts new file mode 100644 index 000000000..4de649f6a --- /dev/null +++ b/src/Ombi/ClientApp/app/requests/remainingrequests.component.ts @@ -0,0 +1,67 @@ +import { IRemainingRequests } from "../interfaces/IRemainingRequests"; +import { RequestService } from "../services"; + +import { Component, Input, OnInit } from "@angular/core"; +import { Observable } from "rxjs"; + +@Component({ + selector: "remaining-requests", + templateUrl: "./remainingrequests.component.html", +}) + +export class RemainingRequestsComponent implements OnInit { + public remaining: IRemainingRequests; + @Input() public movie: boolean; + public daysUntil: number; + public hoursUntil: number; + public minutesUntil: number; + @Input() public quotaRefreshEvents: Observable; + + constructor(private requestService: RequestService) { + } + + public ngOnInit() { + const self = this; + + this.update(); + + this.quotaRefreshEvents.subscribe(() => { + this.update(); + }); + + setInterval(() => { + self.update(); + }, 60000); + } + + public update(): void { + const callback = (remaining => { + this.remaining = remaining; + this.calculateTime(); + }); + + if (this.movie) { + this.requestService.getRemainingMovieRequests().subscribe(callback); + } else { + this.requestService.getRemainingTvRequests().subscribe(callback); + } + } + + private calculateTime(): void { + this.daysUntil = Math.ceil(this.daysUntilNextRequest()); + this.hoursUntil = Math.ceil(this.hoursUntilNextRequest()); + this.minutesUntil = Math.ceil(this.minutesUntilNextRequest()); + } + + private daysUntilNextRequest(): number { + return (new Date(this.remaining.nextRequest).getTime() - new Date().getTime()) / 1000 / 60 / 60 / 24; + } + + private hoursUntilNextRequest(): number { + return (new Date(this.remaining.nextRequest).getTime() - new Date().getTime()) / 1000 / 60 / 60; + } + + private minutesUntilNextRequest(): number { + return (new Date(this.remaining.nextRequest).getTime() - new Date().getTime()) / 1000 / 60; + } +} diff --git a/src/Ombi/ClientApp/app/requests/remainingrequests.module.ts b/src/Ombi/ClientApp/app/requests/remainingrequests.module.ts new file mode 100644 index 000000000..411a94dfd --- /dev/null +++ b/src/Ombi/ClientApp/app/requests/remainingrequests.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { RouterModule } from "@angular/router"; + +import { NgbModule } from "@ng-bootstrap/ng-bootstrap"; + +import { SidebarModule, TooltipModule, TreeTableModule } from "primeng/primeng"; +import { RequestService } from "../services"; + +@NgModule({ + imports: [ + FormsModule, + NgbModule.forRoot(), + TreeTableModule, + SidebarModule, + TooltipModule, + ], + declarations: [ + ], + exports: [ + RouterModule, + ], + providers: [ + RequestService, + ], +}) +export class SearchModule { } diff --git a/src/Ombi/ClientApp/app/requests/request.component.html b/src/Ombi/ClientApp/app/requests/request.component.html index 45509fba3..d2df3c97a 100644 --- a/src/Ombi/ClientApp/app/requests/request.component.html +++ b/src/Ombi/ClientApp/app/requests/request.component.html @@ -3,14 +3,14 @@ diff --git a/src/Ombi/ClientApp/app/requests/tvrequest-children.component.html b/src/Ombi/ClientApp/app/requests/tvrequest-children.component.html index 12b093bca..f41b17f51 100644 --- a/src/Ombi/ClientApp/app/requests/tvrequest-children.component.html +++ b/src/Ombi/ClientApp/app/requests/tvrequest-children.component.html @@ -1,6 +1,6 @@ 

-
+
diff --git a/src/Ombi/ClientApp/app/search/moviesearch.component.html b/src/Ombi/ClientApp/app/search/moviesearch.component.html index 2cd7e2499..916e2e8de 100644 --- a/src/Ombi/ClientApp/app/search/moviesearch.component.html +++ b/src/Ombi/ClientApp/app/search/moviesearch.component.html @@ -1,5 +1,6 @@ 
+
@@ -18,8 +19,9 @@
-
-
+ + +
diff --git a/src/Ombi/ClientApp/app/search/moviesearch.component.ts b/src/Ombi/ClientApp/app/search/moviesearch.component.ts index 824308b21..59e9fc1b8 100644 --- a/src/Ombi/ClientApp/app/search/moviesearch.component.ts +++ b/src/Ombi/ClientApp/app/search/moviesearch.component.ts @@ -17,8 +17,10 @@ export class MovieSearchComponent implements OnInit { public searchText: string; public searchChanged: Subject = new Subject(); + public movieRequested: Subject = new Subject(); public movieResults: ISearchMovieResult[]; public result: IRequestEngineResult; + public searchApplied = false; @Input() public issueCategories: IIssueCategory[]; @@ -35,7 +37,6 @@ export class MovieSearchComponent implements OnInit { private notificationService: NotificationService, private authService: AuthService, private readonly translate: TranslateService, private sanitizer: DomSanitizer, private readonly platformLocation: PlatformLocation) { - this.searchChanged.pipe( debounceTime(600), // Wait Xms after the last event before emitting last event distinctUntilChanged(), // only emit if value is different from previous value @@ -69,6 +70,7 @@ export class MovieSearchComponent implements OnInit { result: false, errorMessage: "", }; + this.popularMovies(); } @@ -87,8 +89,8 @@ export class MovieSearchComponent implements OnInit { try { this.requestService.requestMovie({ theMovieDbId: searchResult.id }) .subscribe(x => { + this.movieRequested.next(); this.result = x; - if (this.result.result) { this.translate.get("Search.RequestAdded", { title: searchResult.title }).subscribe(x => { this.notificationService.success(x); @@ -150,7 +152,8 @@ export class MovieSearchComponent implements OnInit { public reportIssue(catId: IIssueCategory, req: ISearchMovieResult) { this.issueRequestId = req.id; - this.issueRequestTitle = req.title + `(${req.releaseDate.getFullYear})`; + const releaseDate = new Date(req.releaseDate); + this.issueRequestTitle = req.title + ` (${releaseDate.getFullYear()})`; this.issueCategorySelected = catId; this.issuesBarVisible = true; this.issueProviderId = req.id.toString(); diff --git a/src/Ombi/ClientApp/app/search/music/albumsearch.component.html b/src/Ombi/ClientApp/app/search/music/albumsearch.component.html index fd5f71075..d2430bd62 100644 --- a/src/Ombi/ClientApp/app/search/music/albumsearch.component.html +++ b/src/Ombi/ClientApp/app/search/music/albumsearch.component.html @@ -13,7 +13,7 @@

- {{result.title}} + {{result.title | truncate: 36}}

diff --git a/src/Ombi/ClientApp/app/search/search.component.ts b/src/Ombi/ClientApp/app/search/search.component.ts index 74221e71c..43d926970 100644 --- a/src/Ombi/ClientApp/app/search/search.component.ts +++ b/src/Ombi/ClientApp/app/search/search.component.ts @@ -20,7 +20,7 @@ export class SearchComponent implements OnInit { } public ngOnInit() { - this.settingsService.getLidarr().subscribe(x => this.musicEnabled = x.enabled); + this.settingsService.lidarrEnabled().subscribe(x => this.musicEnabled = x); this.showMovie = true; this.showTv = false; this.showMusic = false; diff --git a/src/Ombi/ClientApp/app/search/search.module.ts b/src/Ombi/ClientApp/app/search/search.module.ts index 7e72dba77..4e87940f5 100644 --- a/src/Ombi/ClientApp/app/search/search.module.ts +++ b/src/Ombi/ClientApp/app/search/search.module.ts @@ -21,6 +21,7 @@ import { SearchService } from "../services"; import { AuthGuard } from "../auth/auth.guard"; +import { RemainingRequestsComponent } from "../requests/remainingrequests.component"; import { SharedModule } from "../shared/shared.module"; const routes: Routes = [ @@ -44,6 +45,7 @@ const routes: Routes = [ TvSearchComponent, SeriesInformationComponent, MovieSearchGridComponent, + RemainingRequestsComponent, MusicSearchComponent, ArtistSearchComponent, AlbumSearchComponent, diff --git a/src/Ombi/ClientApp/app/search/seriesinformation.component.ts b/src/Ombi/ClientApp/app/search/seriesinformation.component.ts index cc8701a5b..6a918a69a 100644 --- a/src/Ombi/ClientApp/app/search/seriesinformation.component.ts +++ b/src/Ombi/ClientApp/app/search/seriesinformation.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit} from "@angular/core"; +import { Component, Input, OnInit } from "@angular/core"; import { NotificationService } from "../services"; import { RequestService } from "../services"; @@ -8,6 +8,8 @@ import { INewSeasonRequests, IRequestEngineResult, ISeasonsViewModel, ITvRequest import { IEpisodesRequests } from "../interfaces"; import { ISearchTvResult } from "../interfaces"; +import { Subject } from "rxjs"; + @Component({ selector: "seriesinformation", templateUrl: "./seriesinformation.component.html", @@ -18,7 +20,7 @@ export class SeriesInformationComponent implements OnInit { public result: IRequestEngineResult; public series: ISearchTvResult; public requestedEpisodes: IEpisodesRequests[] = []; - + @Input() public tvRequested: Subject; @Input() private seriesId: number; constructor(private searchService: SearchService, private requestService: RequestService, private notificationService: NotificationService) { } @@ -62,6 +64,7 @@ export class SeriesInformationComponent implements OnInit { this.requestService.requestTv(viewModel) .subscribe(x => { + this.tvRequested.next(); this.result = x as IRequestEngineResult; if (this.result.result) { this.notificationService.success( diff --git a/src/Ombi/ClientApp/app/search/tvsearch.component.html b/src/Ombi/ClientApp/app/search/tvsearch.component.html index 48e83578d..127077b3b 100644 --- a/src/Ombi/ClientApp/app/search/tvsearch.component.html +++ b/src/Ombi/ClientApp/app/search/tvsearch.component.html @@ -26,15 +26,13 @@
-
-
+ + +
- -
-
@@ -155,7 +153,7 @@
- +

diff --git a/src/Ombi/ClientApp/app/search/tvsearch.component.ts b/src/Ombi/ClientApp/app/search/tvsearch.component.ts index a41f34586..92d0d549a 100644 --- a/src/Ombi/ClientApp/app/search/tvsearch.component.ts +++ b/src/Ombi/ClientApp/app/search/tvsearch.component.ts @@ -18,6 +18,7 @@ export class TvSearchComponent implements OnInit { public searchText: string; public searchChanged = new Subject(); public tvResults: ISearchTvResult[]; + public tvRequested: Subject = new Subject(); public result: IRequestEngineResult; public searchApplied = false; public defaultPoster: string; @@ -161,6 +162,7 @@ export class TvSearchComponent implements OnInit { this.requestService.requestTv(viewModel) .subscribe(x => { + this.tvRequested.next(); this.result = x; if (this.result.result) { this.notificationService.success( @@ -195,7 +197,8 @@ export class TvSearchComponent implements OnInit { public reportIssue(catId: IIssueCategory, req: ISearchTvResult) { this.issueRequestId = req.id; - this.issueRequestTitle = req.title + `(${req.firstAired})`; + const firstAiredDate = new Date(req.firstAired); + this.issueRequestTitle = req.title + ` (${firstAiredDate.getFullYear()})`; this.issueCategorySelected = catId; this.issuesBarVisible = true; this.issueProviderId = req.id.toString(); diff --git a/src/Ombi/ClientApp/app/services/request.service.ts b/src/Ombi/ClientApp/app/services/request.service.ts index 039939a84..dd64a14b5 100644 --- a/src/Ombi/ClientApp/app/services/request.service.ts +++ b/src/Ombi/ClientApp/app/services/request.service.ts @@ -10,12 +10,22 @@ import { FilterType, IAlbumRequest, IAlbumRequestModel, IAlbumUpdateModel, IChil import { ITvRequestViewModel } from "../interfaces"; import { ServiceHelpers } from "./service.helpers"; +import { IRemainingRequests } from "../interfaces/IRemainingRequests"; + @Injectable() export class RequestService extends ServiceHelpers { constructor(http: HttpClient, public platformLocation: PlatformLocation) { super(http, "/api/v1/Request/", platformLocation); } + public getRemainingMovieRequests(): Observable { + return this.http.get(`${this.url}movie/remaining`, {headers: this.headers}); + } + + public getRemainingTvRequests(): Observable { + return this.http.get(`${this.url}tv/remaining`, {headers: this.headers}); + } + public requestMovie(movie: IMovieRequestModel): Observable { return this.http.post(`${this.url}Movie/`, JSON.stringify(movie), {headers: this.headers}); } diff --git a/src/Ombi/ClientApp/app/services/settings.service.ts b/src/Ombi/ClientApp/app/services/settings.service.ts index 2016d10b7..b4611bfa8 100644 --- a/src/Ombi/ClientApp/app/services/settings.service.ts +++ b/src/Ombi/ClientApp/app/services/settings.service.ts @@ -96,6 +96,10 @@ export class SettingsService extends ServiceHelpers { return this.http.get(`${this.url}/Lidarr`, {headers: this.headers}); } + public lidarrEnabled(): Observable { + return this.http.get(`${this.url}/lidarrenabled`, {headers: this.headers}); + } + public saveLidarr(settings: ILidarrSettings): Observable { return this.http.post(`${this.url}/Lidarr`, JSON.stringify(settings), {headers: this.headers}); } diff --git a/src/Ombi/ClientApp/app/settings/lidarr/lidarr.component.html b/src/Ombi/ClientApp/app/settings/lidarr/lidarr.component.html index 11d9baf2d..c834fbdbd 100644 --- a/src/Ombi/ClientApp/app/settings/lidarr/lidarr.component.html +++ b/src/Ombi/ClientApp/app/settings/lidarr/lidarr.component.html @@ -3,6 +3,10 @@
Lidarr Settings +
+ Advanced + +
@@ -110,6 +114,12 @@
+
+
+ + +
+
diff --git a/src/Ombi/ClientApp/app/settings/lidarr/lidarr.component.ts b/src/Ombi/ClientApp/app/settings/lidarr/lidarr.component.ts index 3860b5675..d1e28285f 100644 --- a/src/Ombi/ClientApp/app/settings/lidarr/lidarr.component.ts +++ b/src/Ombi/ClientApp/app/settings/lidarr/lidarr.component.ts @@ -7,7 +7,7 @@ import { NotificationService } from "../../services"; import { SettingsService } from "../../services"; @Component({ - templateUrl: "./Lidarr.component.html", + templateUrl: "./lidarr.component.html", }) export class LidarrComponent implements OnInit { @@ -45,6 +45,7 @@ export class LidarrComponent implements OnInit { albumFolder: [x.albumFolder], languageProfileId: [x.languageProfileId, [Validators.required]], metadataProfileId: [x.metadataProfileId, [Validators.required]], + addOnly: [x.addOnly], }); if (x.defaultQualityProfile) { diff --git a/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.html b/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.html index de78f64f8..856e8785c 100644 --- a/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.html +++ b/src/Ombi/ClientApp/app/usermanagement/usermanagement.component.html @@ -48,6 +48,12 @@ Roles + + Requests Remaining + + + Next Request Due + Last Logged In @@ -85,6 +91,22 @@
+ +
+ {{'UserManagment.MovieRemaining' | translate: {remaining: u.movieRequestQuota.remaining, total: u.movieRequestLimit} }} +
+
+ {{'UserManagment.TvRemaining' | translate: {remaining: u.episodeRequestQuota.remaining, total: u.episodeRequestLimit} }} +
+ + +
+ {{'UserManagment.MovieDue' | translate: {date: (u.movieRequestQuota.nextRequest | date: 'short')} }} +
+
+ {{'UserManagment.TvDue' | translate: {date: (u.episodeRequestQuota.nextRequest | date: 'short')} }} +
+ {{u.lastLoggedIn | date: 'short'}} diff --git a/src/Ombi/ClientApp/app/usermanagement/usermanagement.module.ts b/src/Ombi/ClientApp/app/usermanagement/usermanagement.module.ts index f12476677..91fa628e4 100644 --- a/src/Ombi/ClientApp/app/usermanagement/usermanagement.module.ts +++ b/src/Ombi/ClientApp/app/usermanagement/usermanagement.module.ts @@ -18,6 +18,8 @@ import { AuthGuard } from "../auth/auth.guard"; import { OrderModule } from "ngx-order-pipe"; import { AddPlexUserComponent } from "./addplexuser.component"; +import { SharedModule } from "../shared/shared.module"; + const routes: Routes = [ { path: "", component: UserManagementComponent, canActivate: [AuthGuard] }, { path: "user", component: UserManagementUserComponent, canActivate: [AuthGuard] }, @@ -38,6 +40,7 @@ const routes: Routes = [ TooltipModule, OrderModule, SidebarModule, + SharedModule, ], declarations: [ UserManagementComponent, diff --git a/src/Ombi/ClientApp/styles/_imports.scss b/src/Ombi/ClientApp/styles/_imports.scss index 09da286f8..5b22e1ac4 100644 --- a/src/Ombi/ClientApp/styles/_imports.scss +++ b/src/Ombi/ClientApp/styles/_imports.scss @@ -1,2 +1,2 @@ -@import './styles.scss'; -@import './scrollbar.scss'; \ No newline at end of file +@import './Styles.scss'; +@import './scrollbar.scss'; diff --git a/src/Ombi/Controllers/IdentityController.cs b/src/Ombi/Controllers/IdentityController.cs index 2acc1637f..e0b864185 100644 --- a/src/Ombi/Controllers/IdentityController.cs +++ b/src/Ombi/Controllers/IdentityController.cs @@ -16,6 +16,7 @@ using Ombi.Api.Plex; using Ombi.Attributes; using Ombi.Config; using Ombi.Core.Authentication; +using Ombi.Core.Engine.Interfaces; using Ombi.Core.Helpers; using Ombi.Core.Models.UI; using Ombi.Core.Settings; @@ -62,7 +63,9 @@ namespace Ombi.Controllers IRepository subscriptionRepository, ISettingsService umSettings, IRepository notificationPreferences, - IMusicRequestRepository musicRepo) + IMusicRequestRepository musicRepo), + IMovieRequestEngine movieRequestEngine, + ITvRequestEngine tvRequestEngine) { UserManager = user; Mapper = mapper; @@ -84,6 +87,8 @@ namespace Ombi.Controllers _requestSubscriptionRepository = subscriptionRepository; _notificationRepository = notificationRepository; _userManagementSettings = umSettings; + TvRequestEngine = tvRequestEngine; + MovieRequestEngine = movieRequestEngine; _userNotificationPreferences = notificationPreferences; } @@ -98,6 +103,8 @@ namespace Ombi.Controllers private IWelcomeEmail WelcomeEmail { get; } private IMovieRequestRepository MovieRepo { get; } private ITvRequestRepository TvRepo { get; } + private IMovieRequestEngine MovieRequestEngine { get; } + private ITvRequestEngine TvRequestEngine { get; } private IMusicRequestRepository MusicRepo { get; } private readonly ILogger _log; private readonly IPlexApi _plexApi; @@ -109,7 +116,6 @@ namespace Ombi.Controllers private readonly IRepository _requestSubscriptionRepository; private readonly IRepository _userNotificationPreferences; - /// /// This is what the Wizard will call when creating the user for the very first time. /// This should never be called after this. @@ -323,6 +329,16 @@ namespace Ombi.Controllers }); } + if (vm.EpisodeRequestLimit > 0) + { + vm.EpisodeRequestQuota = await TvRequestEngine.GetRemainingRequests(user); + } + + if (vm.MovieRequestLimit > 0) + { + vm.MovieRequestQuota = await MovieRequestEngine.GetRemainingRequests(user); + } + return vm; } diff --git a/src/Ombi/Controllers/RequestController.cs b/src/Ombi/Controllers/RequestController.cs index d794f6001..f2412727f 100644 --- a/src/Ombi/Controllers/RequestController.cs +++ b/src/Ombi/Controllers/RequestController.cs @@ -12,6 +12,7 @@ using Ombi.Attributes; using Ombi.Core.Models.UI; using Ombi.Models; using Ombi.Store.Entities; +using Ombi.Core.Models; namespace Ombi.Controllers { @@ -464,5 +465,23 @@ namespace Ombi.Controllers await TvRequestEngine.UnSubscribeRequest(requestId, RequestType.TvShow); return true; } + + /// + /// Gets model containing remaining number of requests. + /// + [HttpGet("movie/remaining")] + public async Task GetRemainingMovieRequests() + { + return await MovieRequestEngine.GetRemainingRequests(); + } + + /// + /// Gets model containing remaining number of requests. + /// + [HttpGet("tv/remaining")] + public async Task GetRemainingTvRequests() + { + return await TvRequestEngine.GetRemainingRequests(); + } } } \ No newline at end of file diff --git a/src/Ombi/Controllers/SearchController.cs b/src/Ombi/Controllers/SearchController.cs index 5d05ceae5..2a7327481 100644 --- a/src/Ombi/Controllers/SearchController.cs +++ b/src/Ombi/Controllers/SearchController.cs @@ -8,6 +8,7 @@ using Ombi.Api.Lidarr.Models; using Ombi.Core; using Ombi.Core.Engine; using Ombi.Core.Engine.Interfaces; +using Ombi.Core.Models; using Ombi.Core.Models.Search; using StackExchange.Profiling; diff --git a/src/Ombi/Controllers/SettingsController.cs b/src/Ombi/Controllers/SettingsController.cs index 84192c53c..474ae8823 100644 --- a/src/Ombi/Controllers/SettingsController.cs +++ b/src/Ombi/Controllers/SettingsController.cs @@ -328,6 +328,18 @@ namespace Ombi.Controllers return await Get(); } + /// + /// Gets the Lidarr Settings. + /// + /// + [HttpGet("lidarrenabled")] + [AllowAnonymous] + public async Task LidarrEnabled() + { + var settings = await Get(); + return settings.Enabled; + } + /// /// Save the Lidarr settings. /// diff --git a/src/Ombi/Ombi.csproj b/src/Ombi/Ombi.csproj index 1ca47d306..7489ad77f 100644 --- a/src/Ombi/Ombi.csproj +++ b/src/Ombi/Ombi.csproj @@ -71,7 +71,7 @@ - + diff --git a/src/Ombi/wwwroot/translations/en.json b/src/Ombi/wwwroot/translations/en.json index d5d8969df..c53229c3d 100644 --- a/src/Ombi/wwwroot/translations/en.json +++ b/src/Ombi/wwwroot/translations/en.json @@ -150,7 +150,14 @@ "SortRequestDateAsc": "Request Date ▲", "SortRequestDateDesc": "Request Date ▼", "SortStatusAsc":"Status ▲", - "SortStatusDesc":"Status ▼" + "SortStatusDesc":"Status ▼", + "Remaining": { + "Quota": "{{remaining}}/{{total}} requests remaining", + "NextDays": "Another request will be added in {{time}} days", + "NextHours": "Another request will be added in {{time}} hours", + "NextMinutes": "Another request will be added in {{time}} minutes", + "NextMinute": "Another request will be added in {{time}} minute" + } }, "Issues":{ "Title":"Issues", @@ -177,5 +184,11 @@ "FilterHeaderRequestStatus":"Status", "Approved":"Approved", "PendingApproval": "Pending Approval" + }, + "UserManagment": { + "TvRemaining": "TV: {{remaining}}/{{total}} remaining", + "MovieRemaining": "Movies: {{remaining}}/{{total}} remaining", + "TvDue": "TV: {{date}}", + "MovieDue": "Movie: {{date}}" } }