feat(plex): Rework the Plex Settings page (#4805)

pull/4810/head
Jamie 2 years ago committed by GitHub
parent 46920032ba
commit 1b8c47f316
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,7 @@
import { IPlexServer } from "../../../../interfaces";
export interface PlexServerDialogData {
server: IPlexServer;
deleted?: boolean;
closed?: boolean;
}

@ -1,2 +1,3 @@
export * from './PlexSyncType';
export * from './PlexCreds';
export * from './PlexCreds';
export * from './PlexServerDialogData';

@ -0,0 +1,121 @@
<h1 mat-dialog-title>Server Configuration</h1>
<mat-dialog-content>
<h2>Connection</h2>
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Server Name</mat-label>
<input matInput placeholder="Server Name" name="name" [(ngModel)]="this.data.server.name" value="{{this.data.server.name}}">
</mat-form-field>
<div class="row">
<mat-form-field class="col-md-6 col-12" appearance="outline" floatLabel=auto>
<mat-label>Hostname / IP</mat-label>
<input matInput placeholder="Hostname or IP" name="ip" [(ngModel)]="this.data.server.ip" value="{{this.data.server.ip}}"
#serverHostnameIpControl="ngModel" required>
<mat-error *ngIf="serverHostnameIpControl.hasError('required')">Must be specified.</mat-error>
</mat-form-field>
<mat-form-field class="col-md-4 col-7" appearance="outline" floatLabel=auto>
<mat-label>Port</mat-label>
<input matInput placeholder="Port" name="port" [(ngModel)]="this.data.server.port" value="{{this.data.server.port}}"
#serverPortControl="ngModel" required pattern="^[0-9]*$">
<mat-error *ngIf="serverPortControl.hasError('required')">Must be specified.</mat-error>
<mat-error *ngIf="serverPortControl.hasError('pattern')">Must be a number.</mat-error>
</mat-form-field>
<mat-slide-toggle class="col-md-2 col-5 mt-3" id="ssl" name="ssl" [(ngModel)]="this.data.server.ssl" [checked]="this.data.server.ssl">
SSL
</mat-slide-toggle>
</div>
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Plex Authorization Token</mat-label>
<input matInput placeholder="Plex Authorization Token" name="authToken" [(ngModel)]="this.data.server.plexAuthToken" value="{{this.data.server.plexAuthToken}}"
#serverApiKeyControl="ngModel" required>
<mat-error *ngIf="serverApiKeyControl.hasError('required')">Must be specified.</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Machine Identifier</mat-label>
<input matInput placeholder="Machine Identifier" name="MachineIdentifier" [(ngModel)]="this.data.server.machineIdentifier" value="{{this.data.server.machineIdentifier}}"
#serverApiKeyControl="ngModel" required>
<mat-error *ngIf="serverApiKeyControl.hasError('required')">Must be specified.</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Externally Facing Hostname</mat-label>
<input matInput placeholder="e.g. https://emby.this.data.server.com/" name="serverHostname" name="hostname"
[(ngModel)]="this.data.server.serverHostname" value="{{this.data.server.serverHostname}}" >
<mat-hint>
This will be the external address that users will navigate to when they press the 'View On Plex' button
<br>
<span *ngIf="this.data.server.serverHostname">Current URL: "{{this.data.server.serverHostname}}/web/app#!/server/{{this.data.server.machineIdentifier}}/details?key=%2flibrary%2Fmetadata%2F53334"</span>
<span *ngIf="!this.data.server.serverHostname">Current URL: "https://app.plex.tv/web/app#!/server/{{this.data.server.machineIdentifier}}/details?key=%2flibrary%2Fmetadata%2F53334"</span>
</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Episode Batch Size</mat-label>
<input matInput placeholder="150" name="MachineIdentifier" [(ngModel)]="this.data.server.episodeBatchSize" value="{{this.data.server.episodeBatchSize}}">
<mat-hint>
150 by default, you shouldn't need to change this, this sets how many episodes we request from Plex at a single time.
</mat-hint>
</mat-form-field>
<h2>Libraries</h2>
<div>
<button mat-raised-button (click)="loadLibraries()"
class="mat-focus-indicator mat-stroked-button mat-button-base">Load Libraries
<i class="fas fa-film"></i>
</button>
</div>
<div *ngIf="this.data.server.plexSelectedLibraries && this.data.server.plexSelectedLibraries.length > 0">
<label>Please select the libraries for Ombi to monitor. If nothing is selected, Ombi will monitor all
libraries.</label>
<div *ngFor="let lib of this.data.server.plexSelectedLibraries">
<div class="md-form-field">
<div class="checkbox">
<mat-slide-toggle [(ngModel)]="lib.enabled" [checked]="lib.enabled"
for="{{lib.title}}">{{lib.title}}</mat-slide-toggle>
</div>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align=end>
<button style="margin: .5em 0 0 .5em;" align-middle mat-stroked-button color="accent"
(click)="testPlex()">
<span style="display: flex; align-items: baseline; white-space: pre-wrap;">
<i class="fas fa-vial"></i>
<span> Test</span>
</span>
</button>
<button style="margin: .5em 0 0 .5em;" align-middle mat-stroked-button color="warn"
(click)="delete()">
<span style="display: flex; align-items: baseline; white-space: pre-wrap;">
<i class="fas fa-trash"></i>
<span> Delete</span>
</span>
</button>
<button style="margin: .5em 0 0 0.5em;" mat-stroked-button color="basic" (click)="cancel()">
<span style="display: flex; align-items: baseline; white-space: pre-wrap;">
<i class="fas fa-times"></i>
<span> Cancel</span>
</span>
</button>
<button style="margin: .5em 0 0 .5em;" mat-stroked-button color="accent"
(click)="save()">
<span style="display: flex; align-items: baseline; white-space: pre-wrap;">
<i style="vertical-align: text-top;" class="fas fa-check"></i>
<span> Save</span>
</span>
</button>
</mat-dialog-actions>

@ -0,0 +1,27 @@
@media (max-width: 978px) {
::ng-deep .mat-dialog-container {
overflow: unset;
display: flex;
flex-direction: column;
.mat-dialog-content{
max-height: unset;
}
.mat-dialog-actions{
min-height: unset;
}
emby-server-dialog-component {
display: flex;
flex-direction: column;
min-height: 1px;
}
}
}
::ng-deep mat-form-field .mat-form-field {
&-subscript-wrapper {
position: static;
}
}

@ -0,0 +1,79 @@
import { Component, Inject } from "@angular/core";
import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
import {
PlexService,
NotificationService,
TesterService,
} from "../../../../services";
import { take } from "rxjs";
import { IPlexLibrariesSettings } from "../../../../interfaces";
import { PlexServerDialogData } from "../models";
@Component({
selector: "plex-server-dialog-component",
templateUrl: "plex-server-dialog.component.html",
styleUrls: ["plex-server-dialog.component.scss"],
})
export class PlexServerDialogComponent {
public password: string;
public username: string;
constructor(
private dialogRef: MatDialogRef<PlexServerDialogData>,
@Inject(MAT_DIALOG_DATA) public data: PlexServerDialogData,
private notificationService: NotificationService,
private testerService: TesterService,
private plexService: PlexService
) {
}
public cancel() {
this.dialogRef.close({closed: true});
}
public testPlex() {
this.testerService.plexTest(this.data.server).pipe(take(1))
.subscribe(x => {
if (x === true) {
this.notificationService.success(`Successfully connected to the Plex server ${this.data.server.name}!`);
} else {
this.notificationService.error(`We could not connect to the Plex server ${this.data.server.name}!`);
}
});
}
public delete() {
this.dialogRef.close({deleted: true});
}
public save() {
this.dialogRef.close({server: this.data.server});
}
public loadLibraries() {
if (this.data.server.ip == null) {
this.notificationService.error("Plex is not yet configured correctly");
return;
}
this.plexService.getLibraries(this.data.server).subscribe(x => {
this.data.server.plexSelectedLibraries = [];
if (x.successful) {
x.data.mediaContainer.directory.forEach((item) => {
const lib: IPlexLibrariesSettings = {
key: item.key,
title: item.title,
enabled: false,
};
this.data.server.plexSelectedLibraries.push(lib);
});
} else {
this.notificationService.error(x.message);
}
});
}
}

@ -2,54 +2,71 @@
<div class="small-middle-container" *ngIf="settings">
<fieldset style="width:100%;">
<legend>Plex Configuration</legend>
<div class="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>
<settings-plex-form-field [label]="'Enable'" [type]="'checkbox'" [id]="'enable'" [(value)]="settings.enable"></settings-plex-form-field>
<settings-plex-form-field [label]="'Enable User Watchlist Requests'" [type]="'checkbox'" [id]="'enableWatchlistImport'" [(value)]="settings.enableWatchlistImport">
<small bottom>When a Plex User adds something to their watchlist in Plex, it will turn up in Ombi as a Request if enabled. This <b>only</b> applies to users that are logging in with their Plex Account
<br>Request limits if set are all still applied
</small>
</settings-plex-form-field>
<settings-plex-form-field [label]="'Request All'" disabled [type]="'checkbox'" [id]="'monitorAll'" [(value)]="settings.monitorAll">
<small bottom>If true then watchlist requests for TV Shows, it will request the <strong><em>whole</em></strong> season. Otherwise it will only request the latest season.
</small>
</settings-plex-form-field>
</settings-plex-form-field>
<settings-plex-form-field [label]="'Advanced Options'" [type]="'checkbox'" [id]="'advanced'" [(value)]="advanced"></settings-plex-form-field>
<hr>
<div class="row">
<div class="col-md-7">
<h2 style="margin: 1em 0 0 0;">Servers</h2>
<mat-list style="display:flex; flex-flow: wrap;">
<mat-card class="server-card" *ngFor="let server of settings.servers">
<button mat-button (click)="edit(server)">
<h3>{{server.name}}</h3>
</button>
</mat-card>
<div class="row">
<mat-tab-group #tabGroup [selectedIndex]="selected.value" (selectedTabChange)="addTab($event)"
(selectedIndexChange)="selected.setValue($event)" animationDuration="0ms" style="width:100%;">
<mat-tab *ngFor="let server of settings.servers" [label]="server.name">
<mat-card class="server-card new-server-card">
<button mat-button (click)="newServer()">
<i class="fas fa-plus fa-xl"></i>
<h3>Manually Add Server</h3>
</button>
</mat-card>
</mat-list>
<div class="col-md-6 col-6 col-sm-6 align-right">
<button type="button" (click)="removeServer(server)"
class="mat-focus-indicator mat-flat-button mat-button-base mat-warn">Remove Server</button>
</div>
<br />
<br />
<settings-plex-form
[server]="server"
[advancedEnabled]="advanced"
[loadedServers]="loadedServers"
(loadLibraries)="loadLibraries(server)"
(loadServers)="requestServers($event)"
(test)="testPlex(server)"
(runSync)="runSync($event)"
(selectServer)="selectServer($event, server)"
>
</settings-plex-form>
<div class="row">
</mat-tab>
<mat-tab label="" disabled=true> </mat-tab>
<mat-tab label="Add Server" position=100> </mat-tab>
<br />
<div class="form-group col-2">
<button mat-raised-button (click)="runSync(PlexSyncType.Full)" type="button" id="fullSync"
class="mat-focus-indicator mat-stroked-button mat-button-base">Full
Sync</button><br />
</div>
<div class="form-group col-2">
<button mat-raised-button (click)="runSync(PlexSyncType.RecentlyAdded)" type="button" id="recentlyAddedSync"
class="mat-focus-indicator mat-stroked-button mat-button-base">Partial Sync</button>
</div>
<div class="form-group col-2">
<button mat-raised-button (click)="runSync(PlexSyncType.ClearAndReSync)" type="button" id="clearData"
class="mat-focus-indicator mat-stroked-button mat-button-base">
Clear Data And Resync
</button>
</div>
<div class="form-group col-12">
<button mat-raised-button (click)="runSync(PlexSyncType.WatchlistImport)" type="button" id="watchlistImport"
class="mat-focus-indicator mat-stroked-button mat-button-base">
Run Watchlist Import
</button>
</div>
</div>
<div class="row">
</mat-tab-group>
<div class="col-md-2">
<div class="form-group">
<div>
@ -59,6 +76,57 @@
</div>
</div>
</div>
</div>
<div class="col-md-5">
<div class="md-form-field">
<label for="username" class="control-label">
<h3>Plex Credentials</h3>
<small>These fields are optional to automatically fill in your Plex server settings. <br>
This will pass your username and password to the Plex.tv API to grab the servers associated with this user.
<br>
If you have 2FA enabled on your account, you need to append the 2FA code to the end of your password.</small>
</label>
</div>
<settings-plex-form-field [label]="'Username'" [id]="'username'" [(value)]="username"></settings-plex-form-field>
<settings-plex-form-field [label]="'Password'" [id]="'password'" [type]="'password'" [(value)]="password"></settings-plex-form-field>
<div class="md-form-field">
<div class="right">
<button mat-raised-button id="loadServers" (click)="requestServers()"
class="mat-stroked-button">Load Servers
<i class="fas fa-key"></i>
</button>
</div>
</div>
<div class="row">
<div class="col-2 align-self-center">
Please select the server:
</div>
<div class="md-form-field col-10">
<div *ngIf="!loadedServers">
<mat-form-field appearance="outline" floatLabel=auto>
<input disabled matInput placeholder="No Servers Loaded" id="selectServer-noservers">
</mat-form-field>
</div>
<div *ngIf="loadedServers">
<mat-form-field appearance="outline">
<mat-select placeholder="Servers Loaded! Please Select">
<mat-option (click)="selectServer(s)"
*ngFor="let s of loadedServers.servers.server" [value]="s.server">
{{s.name}}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
</div>
</div>
</fieldset>
</div>
<!--(){{settings|json}}-->

@ -21,4 +21,26 @@
::ng-deep div .mat-tab-body-content {
overflow: hidden;
}
}
.server-card {
margin: 0em 1em 1em 0;
width: 13em;
min-height: 8em;
button {
text-align: center;
align-content: center;
white-space: normal;
overflow-wrap: anywhere;
height: 100%;
width: 100%;
}
i {
margin-top: 0.25em;
}
h3 {
margin: 0;
}
}

@ -1,14 +1,14 @@
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { Subject } from "rxjs";
import { takeUntil } from "rxjs/operators";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { EMPTY, Subject } from "rxjs";
import { catchError, takeUntil } from "rxjs/operators";
import { IPlexLibrariesSettings, IPlexServer, IPlexServerResponse, IPlexServerViewModel, IPlexSettings } from "../../interfaces";
import { JobService, NotificationService, PlexService, SettingsService, TesterService } from "../../services";
import { MatTabChangeEvent, MatTabGroup } from "@angular/material/tabs";
import { IPlexServer, IPlexServerResponse, IPlexServerViewModel, IPlexSettings } from "../../interfaces";
import { JobService, NotificationService, PlexService, SettingsService } from "../../services";
import {UntypedFormControl} from '@angular/forms';
import { MatDialog } from "@angular/material/dialog";
import { PlexWatchlistComponent } from "./components/watchlist/plex-watchlist.component";
import { PlexCreds, PlexSyncType } from "./components/models";
import { PlexServerDialogComponent } from "./components/plex-server-dialog/plex-server-dialog.component";
import { PlexServerDialogData, PlexSyncType } from "./components/models";
@Component({
templateUrl: "./plex.component.html",
@ -19,17 +19,17 @@ export class PlexComponent implements OnInit, OnDestroy {
public loadedServers: IPlexServerViewModel; // This comes from the api call for the user to select a server
public serversButton = false;
selected = new UntypedFormControl(0);
@ViewChild("tabGroup", {static: false}) public tagGroup: MatTabGroup;
public advanced = false;
public username: string;
public password: string;
private subscriptions = new Subject<void>();
public PlexSyncType = PlexSyncType;
constructor(
private settingsService: SettingsService,
private notificationService: NotificationService,
private plexService: PlexService,
private testerService: TesterService,
private jobService: JobService,
private dialog: MatDialog) { }
@ -39,9 +39,13 @@ export class PlexComponent implements OnInit, OnDestroy {
});
}
public requestServers({ username, password }: PlexCreds) {
this.plexService.getServers(username, password).pipe(
public requestServers() {
this.plexService.getServers(this.username, this.password).pipe(
takeUntil(this.subscriptions),
catchError(() => {
this.notificationService.error("There was an issue. Please make sure your username and password are correct");
return EMPTY;
})
).subscribe(x => {
if (x.success) {
this.loadedServers = x;
@ -53,7 +57,9 @@ export class PlexComponent implements OnInit, OnDestroy {
});
}
public selectServer(selectedServer: IPlexServerResponse, server: IPlexServer) {
public selectServer(selectedServer: IPlexServerResponse) {
const server = <IPlexServer> { name: "New" + this.settings.servers.length + "*", id: Math.floor(Math.random() * (99999 - 0 + 1) + 1) };
var splitServers = selectedServer.localAddresses.split(",");
if (splitServers.length > 1) {
server.ip = splitServers[splitServers.length - 1];
@ -68,61 +74,7 @@ export class PlexComponent implements OnInit, OnDestroy {
server.serverHostname = "";
this.notificationService.success(`Selected ${server.name}!`);
}
public testPlex(server: IPlexServer) {
this.testerService.plexTest(server).subscribe(x => {
if (x === true) {
this.notificationService.success(`Successfully connected to the Plex server ${server.name}!`);
} else {
this.notificationService.error(`We could not connect to the Plex server ${server.name}!`);
}
});
}
public addTab(event: MatTabChangeEvent) {
const tabName = event.tab.textLabel;
if (tabName == "Add Server"){
if (this.settings.servers == null) {
this.settings.servers = [];
}
this.settings.servers.push(<IPlexServer> { name: "New" + this.settings.servers.length + "*", id: Math.floor(Math.random() * (99999 - 0 + 1) + 1) });
//this.tagGroup.selectedIndex = (0);
this.selected.setValue(this.settings.servers.length - 1);
}
}
public removeServer(server: IPlexServer) {
const index = this.settings.servers.indexOf(server, 0);
if (index > -1) {
this.settings.servers.splice(index, 1);
this.selected.setValue(this.settings.servers.length - 1);
}
}
public loadLibraries(server: IPlexServer) {
if (server.ip == null) {
this.notificationService.error("Plex is not yet configured correctly");
return;
}
this.plexService.getLibraries(server).subscribe(x => {
server.plexSelectedLibraries = [];
if (x.successful) {
x.data.mediaContainer.directory.forEach((item) => {
const lib: IPlexLibrariesSettings = {
key: item.key,
title: item.title,
enabled: false,
};
server.plexSelectedLibraries.push(lib);
});
} else {
this.notificationService.error(x.message);
}
},
err => { this.notificationService.error(err); });
this.newServer(server);
}
public save() {
@ -167,6 +119,55 @@ export class PlexComponent implements OnInit, OnDestroy {
}
}
public edit(server: IPlexServer) {
const data: PlexServerDialogData = {
server: server,
};
const dialog = this.dialog.open(PlexServerDialogComponent, {
width: "700px",
data: data,
panelClass: "modal-panel",
});
dialog.afterClosed().subscribe((x) => {
if (x.deleted) {
this.removeServer(server);
}
if (x.server) {
console.log(x.server);
var idx = this.settings.servers.findIndex(server => server.id === x.server.id);
if (idx >= 0) {
this.settings.servers[idx] = x.server;
} else {
this.settings.servers.push(x.server);
}
}
});
}
public newServer(server: IPlexServer) {
if(!server) {
server = <IPlexServer> { name: "New" + this.settings.servers.length + "*", id: Math.floor(Math.random() * (99999 - 0 + 1) + 1) };
}
const dialog = this.dialog.open(PlexServerDialogComponent, {
width: "700px",
data: {server: server},
panelClass: "modal-panel",
});
dialog.afterClosed().subscribe((x) => {
if (x.server) {
this.settings.servers.push(x.server);
}
});
}
private removeServer(server: IPlexServer) {
const index = this.settings.servers.indexOf(server, 0);
if (index > -1) {
this.settings.servers.splice(index, 1);
this.selected.setValue(this.settings.servers.length - 1);
}
}
private runCacher(): void {
this.jobService.runPlexCacher().subscribe(x => {
if (x) {

@ -50,7 +50,7 @@ import { LandingPageComponent } from "./landingpage/landingpage.component";
import { LidarrComponent } from "./lidarr/lidarr.component";
import { LogsComponent } from "./logs/logs.component";
import { MassEmailComponent } from "./massemail/massemail.component";
import { MatDialogModule } from "@angular/material/dialog";
import { MatDialogActions, MatDialogModule } from "@angular/material/dialog";
import { MatMenuModule } from "@angular/material/menu";
import { MattermostComponent } from "./notifications/mattermost.component";
import {MenuModule} from "primeng/menu";
@ -86,6 +86,7 @@ import { WikiComponent } from "./wiki.component";
import { PlexWatchlistComponent } from "./plex/components/watchlist/plex-watchlist.component";
import { PlexFormComponent } from "./plex/components/plex-form/plex-form.component";
import { PlexFormFieldComponent } from "./plex/components/form-field/plex-form-field.component";
import { PlexServerDialogComponent } from "./plex/components/plex-server-dialog/plex-server-dialog.component";
const routes: Routes = [
{ path: "Ombi", component: OmbiComponent, canActivate: [AuthGuard] },
@ -146,7 +147,7 @@ const routes: Routes = [
DialogModule,
SharedModule,
MatMenuModule,
MatDialogModule
MatDialogModule,
],
declarations: [
SettingsMenuComponent,
@ -195,6 +196,7 @@ const routes: Routes = [
PlexWatchlistComponent,
PlexFormComponent,
PlexFormFieldComponent,
PlexServerDialogComponent,
],
exports: [
RouterModule,

Loading…
Cancel
Save