Merge pull request #4029 from Ombi-app/develop-test2

More fixes as features
pull/4041/head v4.0.1078
Jamie 3 years ago committed by GitHub
commit d8bf196087
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Core.Settings; using Ombi.Core.Settings;
@ -106,6 +107,9 @@ namespace Ombi.Schedule.Jobs.Ombi
await _plexRepo.ExecuteSql(episodeSQL); await _plexRepo.ExecuteSql(episodeSQL);
await _plexRepo.ExecuteSql(seasonsSql); await _plexRepo.ExecuteSql(seasonsSql);
await _plexRepo.ExecuteSql(mainSql); await _plexRepo.ExecuteSql(mainSql);
await OmbiQuartz.Scheduler.TriggerJob(new JobKey(nameof(IPlexContentSync), "Plex"), new JobDataMap(new Dictionary<string, string> { { "recentlyAddedSearch", "false" } }));
} }
catch (Exception e) catch (Exception e)
{ {

@ -12,7 +12,7 @@
<img [routerLink]="generateDetailsLink()" id="cardImage" src="{{result.posterPath}}" class="image" <img [routerLink]="generateDetailsLink()" id="cardImage" src="{{result.posterPath}}" class="image"
alt="{{result.title}}"> alt="{{result.title}}">
<div class="middle"> <div class="middle">
<a class="poster-overlay"> <a class="poster-overlay" [routerLink]="generateDetailsLink()">
<div class="summary"> <div class="summary">
<div class="title">{{result.title}}</div> <div class="title">{{result.title}}</div>
<div class="small-text ellipsis">{{result.overview}}</div> <div class="small-text ellipsis">{{result.overview}}</div>

@ -236,4 +236,8 @@ a.poster-overlay:hover{
.top-right span.indicator{ .top-right span.indicator{
padding-right:1em; padding-right:1em;
} }
}
.ombi-card #cardImage:hover{
cursor: pointer;
} }

@ -206,4 +206,4 @@
<div class="bottom-page-gap"> <div class="bottom-page-gap">
</div> </div>
</section> </section>
</div> </div>

@ -1,4 +1,4 @@
import { Component, ViewEncapsulation } from "@angular/core"; import { AfterViewInit, Component, ViewChild, ViewEncapsulation } from "@angular/core";
import { ImageService, SearchV2Service, RequestService, MessageService, RadarrService } from "../../../services"; import { ImageService, SearchV2Service, RequestService, MessageService, RadarrService } from "../../../services";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { DomSanitizer } from "@angular/platform-browser"; import { DomSanitizer } from "@angular/platform-browser";
@ -12,6 +12,7 @@ import { NewIssueComponent } from "../shared/new-issue/new-issue.component";
import { MovieAdvancedOptionsComponent } from "./panels/movie-advanced-options/movie-advanced-options.component"; import { MovieAdvancedOptionsComponent } from "./panels/movie-advanced-options/movie-advanced-options.component";
import { RequestServiceV2 } from "../../../services/requestV2.service"; import { RequestServiceV2 } from "../../../services/requestV2.service";
import { RequestBehalfComponent } from "../shared/request-behalf/request-behalf.component"; import { RequestBehalfComponent } from "../shared/request-behalf/request-behalf.component";
import { forkJoin } from "rxjs";
@Component({ @Component({
templateUrl: "./movie-details.component.html", templateUrl: "./movie-details.component.html",
@ -70,6 +71,7 @@ export class MovieDetailsComponent {
// Load up this request // Load up this request
this.hasRequest = true; this.hasRequest = true;
this.movieRequest = await this.requestService.getMovieRequest(this.movie.requestId); this.movieRequest = await this.requestService.getMovieRequest(this.movie.requestId);
this.loadAdvancedInfo();
} }
this.loadBanner(); this.loadBanner();
}); });
@ -138,10 +140,10 @@ export class MovieDetailsComponent {
public setAdvancedOptions(data: IAdvancedData) { public setAdvancedOptions(data: IAdvancedData) {
this.advancedOptions = data; this.advancedOptions = data;
if (data.rootFolderId) { if (data.rootFolderId) {
this.movieRequest.qualityOverrideTitle = data.rootFolders.filter(x => x.id == data.rootFolderId)[0].path; this.movieRequest.qualityOverrideTitle = data.profiles.filter(x => x.id == data.profileId)[0].name;
} }
if (data.profileId) { if (data.profileId) {
this.movieRequest.rootPathOverrideTitle = data.profiles.filter(x => x.id == data.profileId)[0].name; this.movieRequest.rootPathOverrideTitle = data.rootFolders.filter(x => x.id == data.rootFolderId)[0].path;
} }
} }
@ -177,4 +179,30 @@ export class MovieDetailsComponent {
} }
}); });
} }
private loadAdvancedInfo() {
const profile = this.radarrService.getQualityProfilesFromSettings();
const folders = this.radarrService.getRootFoldersFromSettings();
forkJoin([profile, folders]).subscribe(x => {
debugger;
const radarrProfiles = x[0];
const radarrRootFolders = x[1];
const profile = radarrProfiles.filter((p) => {
return p.id === this.movieRequest.qualityOverride;
});
if (profile.length > 0) {
this.movieRequest.qualityOverrideTitle = profile[0].name;
}
const path = radarrRootFolders.filter((folder) => {
return folder.id === this.movieRequest.rootPathOverride;
});
if (path.length > 0) {
this.movieRequest.rootPathOverrideTitle = path[0].path;
}
});
}
} }

@ -9,7 +9,7 @@
<div class="profile-img-container"> <div class="profile-img-container">
<div class="profile-img"> <div class="profile-img">
<img <img
src="https://www.gravatar.com/avatar/{{emailHash}}?d={{applicationLogo ? applicationLogo : 'https://raw.githubusercontent.com/tidusjar/Ombi/gh-pages/img/android-chrome-512x512.png'}}" /> src="https://www.gravatar.com/avatar/{{emailHash}}?d={{applicationLogo ? applicationLogo : 'https://raw.githubusercontent.com/Ombi-app/Ombi/gh-pages/img/android-chrome-512x512.png'}}" />
</div> </div>
<div class="profile-info"> <div class="profile-info">
<h3>{{username}}</h3> <h3>{{username}}</h3>

@ -30,6 +30,20 @@
<!-- </div> --> <!-- </div> -->
<table mat-table [dataSource]="dataSource" class="requests table" matSort [matSortActive]="defaultSort" matSortDisableClear [matSortDirection]="defaultOrder"> <table mat-table [dataSource]="dataSource" class="requests table" matSort [matSortActive]="defaultSort" matSortDisableClear [matSortDirection]="defaultOrder">
<ng-container matColumnDef="select" *ngIf="isAdmin">
<th mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let row">
<mat-checkbox (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="title"> <ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> {{ 'Requests.RequestsTitle' | translate}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> {{ 'Requests.RequestsTitle' | translate}} </th>
@ -71,4 +85,11 @@
</table> </table>
<mat-paginator [length]="resultsLength" [pageSize]="gridCount"></mat-paginator> <mat-paginator [length]="resultsLength" [pageSize]="gridCount"></mat-paginator>
</div> </div>
<button *ngIf="selection.hasValue() && isAdmin" mat-fab color="accent" class="floating-fab" [matMenuTriggerFor]="aboveMenu">
<mat-icon>add</mat-icon></button>
<mat-menu #aboveMenu="matMenu" yPosition="above">
<button mat-menu-item (click)="bulkDelete()">{{'Requests.RequestPanel.Delete' | translate}}</button>
<button mat-menu-item (click)="bulkApprove()">{{'Requests.RequestPanel.Approve' | translate}}</button>
</mat-menu>

@ -1,14 +1,18 @@
import { Component, AfterViewInit, ViewChild, EventEmitter, Output, ChangeDetectorRef, OnInit } from "@angular/core"; import { Component, AfterViewInit, ViewChild, EventEmitter, Output, ChangeDetectorRef, OnInit } from "@angular/core";
import { IMovieRequests, IRequestsViewModel } from "../../../interfaces"; import { IMovieRequests, IRequestEngineResult, IRequestsViewModel } from "../../../interfaces";
import { MatPaginator } from "@angular/material/paginator"; import { MatPaginator } from "@angular/material/paginator";
import { MatSort } from "@angular/material/sort"; import { MatSort } from "@angular/material/sort";
import { merge, Observable, of as observableOf } from 'rxjs'; import { merge, Observable, of as observableOf, forkJoin } from 'rxjs';
import { catchError, map, startWith, switchMap } from 'rxjs/operators'; import { catchError, map, startWith, switchMap } from 'rxjs/operators';
import { RequestServiceV2 } from "../../../services/requestV2.service"; import { RequestServiceV2 } from "../../../services/requestV2.service";
import { AuthService } from "../../../auth/auth.service"; import { AuthService } from "../../../auth/auth.service";
import { StorageService } from "../../../shared/storage/storage-service"; import { StorageService } from "../../../shared/storage/storage-service";
import { RequestFilterType } from "../../models/RequestFilterType"; import { RequestFilterType } from "../../models/RequestFilterType";
import { SelectionModel } from "@angular/cdk/collections";
import { NotificationService, RequestService } from "../../../services";
import { TranslateService } from "@ngx-translate/core";
import { MatTableDataSource } from "@angular/material/table";
@Component({ @Component({
templateUrl: "./movies-grid.component.html", templateUrl: "./movies-grid.component.html",
@ -16,7 +20,7 @@ import { RequestFilterType } from "../../models/RequestFilterType";
styleUrls: ["./movies-grid.component.scss"] styleUrls: ["./movies-grid.component.scss"]
}) })
export class MoviesGridComponent implements OnInit, AfterViewInit { export class MoviesGridComponent implements OnInit, AfterViewInit {
public dataSource: IMovieRequests[] = []; public dataSource: MatTableDataSource<IMovieRequests>;
public resultsLength: number; public resultsLength: number;
public isLoadingResults = true; public isLoadingResults = true;
public displayedColumns: string[] = ['title', 'requestedUser.requestedBy', 'status', 'requestStatus','requestedDate', 'actions']; public displayedColumns: string[] = ['title', 'requestedUser.requestedBy', 'status', 'requestStatus','requestedDate', 'actions'];
@ -25,6 +29,7 @@ export class MoviesGridComponent implements OnInit, AfterViewInit {
public defaultSort: string = "requestedDate"; public defaultSort: string = "requestedDate";
public defaultOrder: string = "desc"; public defaultOrder: string = "desc";
public currentFilter: RequestFilterType = RequestFilterType.All; public currentFilter: RequestFilterType = RequestFilterType.All;
public selection = new SelectionModel<IMovieRequests>(true, []);
public RequestFilter = RequestFilterType; public RequestFilter = RequestFilterType;
@ -40,13 +45,17 @@ export class MoviesGridComponent implements OnInit, AfterViewInit {
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
constructor(private requestService: RequestServiceV2, private ref: ChangeDetectorRef, constructor(private requestService: RequestServiceV2, private ref: ChangeDetectorRef,
private auth: AuthService, private storageService: StorageService) { private auth: AuthService, private storageService: StorageService,
private requestServiceV1: RequestService, private notification: NotificationService,
private translateService: TranslateService) {
} }
public ngOnInit() {
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
public ngOnInit() {
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
if (this.isAdmin) {
this.displayedColumns.unshift('select');
}
const defaultCount = this.storageService.get(this.storageKeyGridCount); const defaultCount = this.storageService.get(this.storageKeyGridCount);
const defaultSort = this.storageService.get(this.storageKey); const defaultSort = this.storageService.get(this.storageKey);
const defaultOrder = this.storageService.get(this.storageKeyOrder); const defaultOrder = this.storageService.get(this.storageKeyOrder);
@ -96,7 +105,7 @@ export class MoviesGridComponent implements OnInit, AfterViewInit {
this.isLoadingResults = false; this.isLoadingResults = false;
return observableOf([]); return observableOf([]);
}) })
).subscribe(data => this.dataSource = data); ).subscribe(data => this.dataSource = new MatTableDataSource(data));
} }
public loadData(): Observable<IRequestsViewModel<IMovieRequests>> { public loadData(): Observable<IRequestsViewModel<IMovieRequests>> {
@ -117,9 +126,9 @@ export class MoviesGridComponent implements OnInit, AfterViewInit {
public openOptions(request: IMovieRequests) { public openOptions(request: IMovieRequests) {
const filter = () => { const filter = () => {
this.dataSource = this.dataSource.filter((req) => { this.dataSource.data = this.dataSource.data.filter((req) => {
return req.id !== request.id; return req.id !== request.id;
}) });
}; };
const onChange = () => { const onChange = () => {
@ -133,4 +142,56 @@ export class MoviesGridComponent implements OnInit, AfterViewInit {
this.currentFilter = type; this.currentFilter = type;
this.ngAfterViewInit(); this.ngAfterViewInit();
} }
}
public isAllSelected() {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.data.length;
return numSelected === numRows;
}
public masterToggle() {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.data.forEach(row => this.selection.select(row));
}
public async bulkDelete() {
if (this.selection.isEmpty()) {
return;
}
let tasks = new Array();
this.selection.selected.forEach((selected) => {
tasks.push(this.requestServiceV1.removeMovieRequestAsync(selected.id));
});
await Promise.all(tasks);
this.notification.success(this.translateService.instant('Requests.RequestPanel.Deleted'))
this.selection.clear();
this.ngAfterViewInit();
}
public bulkApprove() {
if (this.selection.isEmpty()) {
return;
}
let tasks = new Array<Observable<IRequestEngineResult>>();
this.selection.selected.forEach((selected) => {
tasks.push(this.requestServiceV1.approveMovie({ id: selected.id }));
});
this.isLoadingResults = true;
forkJoin(tasks).subscribe((result: IRequestEngineResult[]) => {
this.isLoadingResults = false;
const failed = result.filter(x => !x.result);
if(failed.length > 0) {
this.notification.error("Some requests failed to approve: " + failed[0].errorMessage);
this.selection.clear();
return;
}
this.notification.success(this.translateService.instant('Requests.RequestPanel.Approved'));
this.selection.clear();
this.ngAfterViewInit();
})
}
}

@ -62,4 +62,11 @@
::ng-deep table.requests button{ ::ng-deep table.requests button{
margin:5px; margin:5px;
} }
::ng-deep .floating-fab {
position: fixed;
right: 3%;
bottom: 6%;
z-index: 10;
}

@ -74,7 +74,7 @@
<mat-label for="episodeBatchSize" class="control-label">Episode Batch Size</mat-label> <mat-label for="episodeBatchSize" class="control-label">Episode Batch Size</mat-label>
<input matInput type="number" id="episodeBatchSize" name="episodeBatchSize" <input matInput type="number" id="episodeBatchSize" name="episodeBatchSize"
[(ngModel)]="server.episodeBatchSize" value="{{server.episodeBatchSize}}" [(ngModel)]="server.episodeBatchSize" value="{{server.episodeBatchSize}}"
matTooltip="This is used when we cache the episodes, we cache in batches of 50 by default, you can configure how many we do at a time here" matTooltip="This is used when we cache the episodes, we cache in batches of 150 by default, you can configure how many we do at a time here"
matTooltipPosition="right"> matTooltipPosition="right">
</mat-form-field> </mat-form-field>
</div> </div>
@ -185,4 +185,4 @@
</div> </div>
</fieldset> </fieldset>
</div> </div>
<!--(){{settings|json}}--> <!--(){{settings|json}}-->

@ -1,6 +1,5 @@
<div class="wizard-background"> <div class="wizard-background">
<div class="container wizard-inner"> <div class="container wizard-inner">
<mat-horizontal-stepper linear #stepper> <mat-horizontal-stepper linear #stepper>
<mat-step > <mat-step >
<form > <form >

@ -137,4 +137,4 @@ table {
::ng-deep .mat-form-field.mat-focused .mat-form-field-label { ::ng-deep .mat-form-field.mat-focused .mat-form-field-label {
color: $accent; color: $accent;
} }

@ -25,7 +25,7 @@ namespace Ombi.Controllers.V2
[HttpGet("news")] [HttpGet("news")]
public async Task<IActionResult> GetNews() public async Task<IActionResult> GetNews()
{ {
var result = await _client.GetAsync("https://raw.githubusercontent.com/tidusjar/Ombi.News/main/README.md"); var result = await _client.GetAsync("https://raw.githubusercontent.com/Ombi-app/Ombi.News/main/README.md");
var content = await result.Content.ReadAsStringAsync(); var content = await result.Content.ReadAsStringAsync();
var md = Markdown.ToHtml(content); var md = Markdown.ToHtml(content);
return Ok(md); return Ok(md);

@ -180,7 +180,9 @@
"RequestPanel": { "RequestPanel": {
"Delete":"Delete Request", "Delete":"Delete Request",
"Approve":"Approve Request", "Approve":"Approve Request",
"ChangeAvailability":"Mark Available" "ChangeAvailability":"Mark Available",
"Deleted": "Successfully deleted selected items",
"Approved": "Successfully approved selected items"
} }
}, },
"Issues": { "Issues": {

Loading…
Cancel
Save