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.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..53111fd95 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(); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/MovieRequestEngine.cs b/src/Ombi.Core/Engine/MovieRequestEngine.cs index f0a4e1f29..5a4bc51f5 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,39 @@ namespace Ombi.Core.Engine return new RequestEngineResult {Result = true, Message = $"{movieName} has been successfully added!"}; } + + public async Task GetRemainingRequests() + { + OmbiUser user = await GetUser(); + 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 = oldestRequestedAt.AddDays(7), + }; + } } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/TvRequestEngine.cs b/src/Ombi.Core/Engine/TvRequestEngine.cs index 90760f759..2d937a377 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 { @@ -608,9 +609,51 @@ namespace Ombi.Core.Engine RequestDate = DateTime.UtcNow, RequestId = model.Id, RequestType = RequestType.TvShow, + EpisodeCount = model.SeasonRequests.Select(m => m.Episodes.Count).Sum(), }); return new RequestEngineResult { Result = true }; } + + public async Task GetRemainingRequests() + { + OmbiUser user = await GetUser(); + 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 = oldestRequestedAt.AddDays(7), + }; + } } } \ 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/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/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/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/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..5fd3a37db 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); 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..632900091 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( 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/settings/lidarr/lidarr.component.ts b/src/Ombi/ClientApp/app/settings/lidarr/lidarr.component.ts index 3860b5675..5d85c3d73 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 { 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/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/wwwroot/translations/en.json b/src/Ombi/wwwroot/translations/en.json index d5d8969df..b38f5fb95 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",