Merge branch 'feature/v4' of https://github.com/tidusjar/Ombi into feature/v4

This commit is contained in:
twanariens 2020-05-09 01:45:18 +02:00
commit abef3000ad
48 changed files with 3169 additions and 2974 deletions

1
.gitignore vendored
View file

@ -248,3 +248,4 @@ _Pvt_Extensions
*.vscode
/src/Ombi/database.json
/src/Ombi/healthchecksdb
/src/Ombi/ClientApp/package-lock.json

View file

@ -1,7 +0,0 @@
language: csharp
solution: src/Ombi.sln
install:
- mono Tools/nuget.exe restore Ombi.sln
- nuget install NUnit.Runners -OutputDirectory testrunner
script:
- xbuild /p:Configuration=Release Ombi.sln /p:TargetFrameworkVersion="v4.5"

View file

@ -2,16 +2,21 @@
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.5.0" />
<PackageReference Include="Moq" Version="4.10.0" />
<PackageReference Include="Nunit" Version="3.11.0" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
<PackageReference Include="AutoFixture" Version="4.11.0" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="Nunit" Version="3.12.0" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.11.1" />
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
<packagereference Include="Microsoft.NET.Test.Sdk" Version="16.0.1"></packagereference>
<packagereference Include="Microsoft.NET.Test.Sdk" Version="16.6.1"></packagereference>
</ItemGroup>
<ItemGroup>

View file

@ -70,7 +70,7 @@ namespace Ombi.Core.Tests.Rule.Search
var result = await Rule.Execute(search);
Assert.True(result.Success);
Assert.That(search.EmbyUrl, Is.EqualTo("http://test.com/#!/item/item.html?id=1"));
Assert.That(search.EmbyUrl, Is.EqualTo("http://test.com/#!/item.html?id=1"));
}
[Test]
@ -99,7 +99,7 @@ namespace Ombi.Core.Tests.Rule.Search
var result = await Rule.Execute(search);
Assert.True(result.Success);
Assert.That(search.EmbyUrl, Is.EqualTo("https://app.emby.media/#!/item/item.html?id=1"));
Assert.That(search.EmbyUrl, Is.EqualTo("https://app.emby.media/#!/item.html?id=1"));
}
[Test]

View file

@ -113,7 +113,7 @@ namespace Ombi.Core.Tests.Rule.Search
PercentOfTracks = 100
}
}.AsQueryable());
var request = new SearchAlbumViewModel { ForeignAlbumId = "ABC" };
var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" };
var result = await Rule.Execute(request);
Assert.True(result.Success);

View file

@ -63,7 +63,7 @@ namespace Ombi.Core.Tests.Rule.Search
ForeignArtistId = "abc",
}
}.AsQueryable());
var request = new SearchArtistViewModel { ForignArtistId = "ABC" };
var request = new SearchArtistViewModel { ForignArtistId = "abc" };
var result = await Rule.Execute(request);
Assert.True(result.Success);

View file

@ -267,10 +267,10 @@ namespace Ombi.Core.Engine
allRequests = allRequests.Where(x => x.Approved && !x.Available && (!x.Denied.HasValue || !x.Denied.Value));
break;
case RequestStatus.Available:
allRequests = allRequests.Where(x => x.Available && (!x.Denied.HasValue || !x.Denied.Value));
allRequests = allRequests.Where(x => x.Available);
break;
case RequestStatus.Denied:
allRequests = allRequests.Where(x => x.Denied.HasValue && x.Denied.Value);
allRequests = allRequests.Where(x => x.Denied.HasValue && x.Denied.Value && !x.Available);
break;
default:
break;
@ -332,12 +332,11 @@ namespace Ombi.Core.Engine
//var secondProp = TypeDescriptor.GetProperties(propType).Find(properties[1], true);
}
allRequests = sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase)
? allRequests.OrderBy(x => prop.GetValue(x))
: allRequests.OrderByDescending(x => prop.GetValue(x));
var total = await allRequests.CountAsync();
var requests = await allRequests.Skip(position).Take(count)
.ToListAsync();
var requests = (sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase)
? allRequests.ToList().OrderBy(x => prop.GetValue(x))
: allRequests.ToList().OrderByDescending(x => prop.GetValue(x))).ToList();
var total = requests.Count();
requests = requests.Skip(position).Take(count).ToList();
await CheckForSubscription(shouldHide, requests);
return new RequestsViewModel<MovieRequests>

View file

@ -93,7 +93,7 @@ namespace Ombi.Store.Context
}
needToSave = true;
NotificationTemplates notificationToAdd;
NotificationTemplates notificationToAdd = null;
switch (notificationType)
{
case NotificationType.NewRequest:
@ -159,6 +159,8 @@ namespace Ombi.Store.Context
};
break;
case NotificationType.WelcomeEmail:
if (agent == NotificationAgent.Email)
{
notificationToAdd = new NotificationTemplates
{
NotificationType = notificationType,
@ -167,6 +169,7 @@ namespace Ombi.Store.Context
Agent = agent,
Enabled = true,
};
}
break;
case NotificationType.IssueResolved:
notificationToAdd = new NotificationTemplates
@ -204,9 +207,12 @@ namespace Ombi.Store.Context
default:
throw new ArgumentOutOfRangeException();
}
if (notificationToAdd != null)
{
NotificationTemplates.Add(notificationToAdd);
}
}
}
if (needToSave)
{

View file

@ -1,6 +1,6 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using System;
namespace Ombi.Store.Migrations.OmbiMySql
{
@ -8,14 +8,34 @@ namespace Ombi.Store.Migrations.OmbiMySql
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"CREATE TABLE `MobileDevices` (
`Id` int NOT NULL AUTO_INCREMENT,
`Token` longtext CHARACTER SET utf8mb4 NULL,
`UserId` varchar(255) COLLATE utf8mb4_bin NOT NULL,
`AddedAt` datetime(6) NOT NULL,
CONSTRAINT `PK_MobileDevices` PRIMARY KEY (`Id`),
CONSTRAINT `FK_MobileDevices_AspNetUsers_UserId` FOREIGN KEY (`UserId`) REFERENCES `AspNetUsers` (`Id`) ON DELETE RESTRICT
);");
// migrationBuilder.Sql(@"CREATE TABLE `MobileDevices` (
// `Id` int NOT NULL AUTO_INCREMENT,
// `Token` longtext CHARACTER SET utf8mb4 NULL,
// `UserId` varchar(255) COLLATE utf8mb4_bin NOT NULL,
// `AddedAt` datetime(6) NOT NULL,
// CONSTRAINT `PK_MobileDevices` PRIMARY KEY (`Id`),
// CONSTRAINT `FK_MobileDevices_AspNetUsers_UserId` FOREIGN KEY (`UserId`) REFERENCES `AspNetUsers` (`Id`) ON DELETE RESTRICT
//);");
migrationBuilder.CreateTable(
name: "MobileDevices",
columns: table => new
{
Id = table.Column<int>(nullable: false).Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Token = table.Column<string>(maxLength: 256, nullable: true),
UserId = table.Column<string>(maxLength: 256, nullable: false),
AddedAt = table.Column<DateTime>(maxLength: 256, nullable: false),
},
constraints: table =>
{
table.PrimaryKey("PK_MobileDevices", x => x.Id);
table.ForeignKey(
name: "FK_MobileDevices_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(

View file

@ -18,7 +18,8 @@ import {
} from "primeng/primeng";
import {
MatButtonModule, MatNativeDateModule, MatIconModule, MatSidenavModule, MatListModule, MatToolbarModule, MatAutocompleteModule, MatCheckboxModule, MatSnackBarModule
MatButtonModule, MatNativeDateModule, MatIconModule, MatSidenavModule, MatListModule, MatToolbarModule, MatAutocompleteModule, MatCheckboxModule, MatSnackBarModule,
MatProgressSpinnerModule
} from '@angular/material';
import { MatCardModule, MatInputModule, MatTabsModule, MatSlideToggleModule } from "@angular/material";
@ -129,6 +130,7 @@ export function JwtTokenGetter() {
CardsFreeModule,
OverlayModule,
MatCheckboxModule,
MatProgressSpinnerModule,
MDBBootstrapModule.forRoot(),
JwtModule.forRoot({
config: {

View file

@ -196,9 +196,6 @@ export interface IAbout {
ombiDatabaseType: string;
externalDatabaseType: string;
settingsDatabaseType: string;
ombiConnectionString: string;
externalConnectionString: string;
settingsConnectionString: string;
storagePath: string;
notSupported: boolean;
}

View file

@ -16,7 +16,7 @@
<!--Next to poster-->
<div class="col-12 col-lg-3 col-xl-3 media-row">
<social-icons [homepage]="movie.homepage" [theMoviedbId]="movie.id" [hasTrailer]="movie.videos.results.length > 0" (openTrailer)="openDialog()" [imdbId]="movie.imdbId" [twitter]="movie.externalIds.twitterId" [facebook]="movie.externalIds.facebookId" [instagram]="movie.externalIds.instagramId"
<social-icons [homepage]="movie.homepage" [theMoviedbId]="movie.id" [hasTrailer]="movie.videos?.results?.length > 0" (openTrailer)="openDialog()" [imdbId]="movie.imdbId" [twitter]="movie.externalIds.twitterId" [facebook]="movie.externalIds.facebookId" [instagram]="movie.externalIds.instagramId"
[available]="movie.available" [plexUrl]="movie.plexUrl" [embyUrl]="movie.embyUrl"></social-icons>
</div>
@ -117,9 +117,9 @@
</mat-panel-title>
</mat-expansion-panel-header>
<div class="row card-spacer" *ngIf="movie.recommendations.results.length > 0">
<div class="row card-spacer" *ngIf="movie.recommendations?.results?.length > 0">
<div class="col-md-2" *ngFor="let r of movie.recommendations.results">
<div class="col-md-2" *ngFor="let r of movie.recommendations?.results">
<div class="sidebar affixable affix-top preview-poster">
<div class="poster">
<a [routerLink]="'/details/movie/'+r.id">
@ -138,7 +138,7 @@
</mat-panel-title>
</mat-expansion-panel-header>
<div class="row card-spacer" *ngIf="movie.similar.results.length > 0">
<div class="row card-spacer" *ngIf="movie.similar?.results?.length > 0">
<div class="col-md-2" *ngFor="let r of movie.similar.results">
<div class="sidebar affixable affix-top preview-poster">
@ -159,9 +159,9 @@
</mat-panel-title>
</mat-expansion-panel-header>
<div class="row card-spacer" *ngIf="movie.videos.results.length > 0">
<div class="row card-spacer" *ngIf="movie.videos?.results?.length > 0">
<div class="col-md-6" *ngFor="let video of movie.videos.results">
<div class="col-md-6" *ngFor="let video of movie.videos?.results">
<iframe width="100%" height="315px" [src]="'https://www.youtube.com/embed/' + video.key | safe" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
</div>

View file

@ -18,6 +18,10 @@
<div *ngIf="!movie.requested && !movie.available && !movie.approved">{{'Common.NotRequested' | translate}}
</div>
</div>
<div *ngIf="movie.quality">
<strong>Quality:</strong>
<div>{{movie.quality | quality}}</div>
</div>
<div *ngIf="advancedOptions">
<strong>Root Folder Override</strong>

View file

@ -1,7 +1,5 @@
<mat-sidenav-container *ngIf="showNav" class="sidenav-container">
<mat-sidenav #drawer class="sidenav" fixedInViewport="true"
[attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'" [mode]="(isHandset$ | async) ? 'over' : 'side'"
[opened]="!(isHandset$ | async)">
<mat-sidenav #drawer class="sidenav" fixedInViewport="true" [attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'" [mode]="(isHandset$ | async) ? 'over' : 'side'" [opened]="!(isHandset$ | async)">
<mat-toolbar>{{applicationName}}</mat-toolbar>
<mat-nav-list>
<span *ngFor="let nav of navItems">
@ -23,15 +21,16 @@
</mat-sidenav>
<mat-sidenav-content>
<mat-toolbar color="primary">
<button type="button" aria-label="Toggle sidenav" mat-icon-button (click)="drawer.toggle()"
*ngIf="isHandset$ | async">
<button type="button" aria-label="Toggle sidenav" mat-icon-button (click)="drawer.toggle()" *ngIf="isHandset$ | async">
<mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
</button>
<div class="col-md-10 offset-md-1 col-10">
<span class="middle justify-content-center align-items-center">
<!-- Search Bar -->
<div style="width: 50%;">
<app-nav-search></app-nav-search>
</div>
</span>

View file

@ -1,10 +1,21 @@
<input class="form-control quater-width search-bar" type="text" [(ngModel)]="selectedItem"
placeholder="{{'NavigationBar.Search' | translate}}" aria-label="Search" [ngbTypeahead]="searchModel"
[resultFormatter]="formatter" [inputFormatter]="formatter" [resultTemplate]="template" (selectItem)="selected($event)">
<!-- <input class="form-control quater-width search-bar" type="text" [(ngModel)]="selectedItem" placeholder="{{'NavigationBar.Search' | translate}}"
aria-label="Search" [ngbTypeahead]="searchModel" [resultFormatter]="formatter" [inputFormatter]="formatter" [resultTemplate]="template" (selectItem)="selected($event)">
<ng-template #template let-result="result">
</ng-template> -->
<form [formGroup]='searchForm'>
<mat-form-field floatLabel="never" style="width: 100%;">
<input matInput placeholder="{{'NavigationBar.Search' | translate}}" [matAutocomplete]="auto" formControlName='input'>
</mat-form-field>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" [displayWith]="displayFn">
<mat-option *ngIf="searching" color="accent">
<mat-spinner diameter="50"></mat-spinner>
</mat-option>
<ng-container *ngIf="!searching">
<mat-option *ngFor="let result of results" [value]="result">
<div *ngIf="result.mediaType === 'movie'">
<i class="fa fa-film"></i> &nbsp;
<span>{{result.title}}</span>
@ -25,6 +36,7 @@
<i class="fa fa-user"></i> &nbsp;
<span>{{result.title}}</span>
</div>
<!-- Collection -->
<!-- <i class="fa fa-file-video-o" aria-hidden="true"></i> -->
</ng-template>
</mat-option>
</ng-container>
</mat-autocomplete>
</form>

View file

@ -1,7 +1,6 @@
$ombi-primary:#3f3f3f;
$ombi-primary-darker:#2b2b2b;
$ombi-accent: #258a6d;
@media (max-width: 767px) {
.quater-width {
width: 15em !important;
@ -23,22 +22,6 @@ $ombi-accent: #258a6d;
padding: 0px 5px;
}
::ng-deep ngb-typeahead-window.dropdown-menu {
background-color: $ombi-primary;
overflow: auto;
height: 33em;
}
::ng-deep ngb-typeahead-window button.dropdown-item {
color: white;
}
::ng-deep ngb-typeahead-window .dropdown-item.active,
.dropdown-item:active {
text-decoration: none;
background-color: $ombi-accent;
}
.search-bar {
background-color: $ombi-primary-darker;
border: solid 1px $ombi-primary-darker;

View file

@ -1,56 +1,78 @@
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { Component, OnInit } from "@angular/core";
import { Observable } from "rxjs";
import {
debounceTime,
distinctUntilChanged,
switchMap,
tap,
finalize,
} from "rxjs/operators";
import { SearchV2Service } from '../services/searchV2.service';
import { IMultiSearchResult } from '../interfaces';
import { Router } from '@angular/router';
import { NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap';
import { empty, of } from "rxjs";
import { SearchV2Service } from "../services/searchV2.service";
import { IMultiSearchResult } from "../interfaces";
import { Router } from "@angular/router";
import { NgbTypeaheadSelectItemEvent } from "@ng-bootstrap/ng-bootstrap";
import { FormGroup, FormBuilder } from "@angular/forms";
import { MatAutocompleteSelectedEvent } from "@angular/material";
@Component({
selector: 'app-nav-search',
templateUrl: './nav-search.component.html',
styleUrls: ['./nav-search.component.scss']
selector: "app-nav-search",
templateUrl: "./nav-search.component.html",
styleUrls: ["./nav-search.component.scss"],
})
export class NavSearchComponent {
export class NavSearchComponent implements OnInit {
public selectedItem: string;
public results: IMultiSearchResult[];
public searching = false;
public searchFailed = false;
public formatter = (result: IMultiSearchResult) => {
return result.title;
}
public searchForm: FormGroup;
public searchModel = (text$: Observable<string>) =>
text$.pipe(
constructor(
private searchService: SearchV2Service,
private router: Router,
private fb: FormBuilder
) {}
public async ngOnInit() {
this.searchForm = this.fb.group({
input: null,
});
this.searchForm
.get("input")
.valueChanges.pipe(
debounceTime(600),
distinctUntilChanged(),
switchMap(term => term.length < 2 ? []
: this.searchService.multiSearch(term)
tap(() => (this.searching = true)),
switchMap((value: string) => {
if (value) {
return this.searchService
.multiSearch(value)
.pipe(finalize(() => (this.searching = false)));
}
return empty().pipe(finalize(() => (this.searching = false)));
})
)
)
constructor(private searchService: SearchV2Service, private router: Router) {
.subscribe((r) => (this.results = r));
}
public selected(event: NgbTypeaheadSelectItemEvent) {
if (event.item.mediaType == "movie") {
this.router.navigate([`details/movie/${event.item.id}`]);
public selected(event: MatAutocompleteSelectedEvent) {
const val = event.option.value as IMultiSearchResult;
if (val.mediaType == "movie") {
this.router.navigate([`details/movie/${val.id}`]);
return;
} else if (event.item.mediaType == "tv") {
this.router.navigate([`details/tv/${event.item.id}/true`]);
} else if (val.mediaType == "tv") {
this.router.navigate([`details/tv/${val.id}/true`]);
return;
} else if (event.item.mediaType == "person") {
this.router.navigate([`discover/actor/${event.item.id}`]);
} else if (val.mediaType == "person") {
this.router.navigate([`discover/actor/${val.id}`]);
return;
} else if (event.item.mediaType == "Artist") {
this.router.navigate([`details/artist/${event.item.id}`]);
} else if (val.mediaType == "Artist") {
this.router.navigate([`details/artist/${val.id}`]);
return;
}
}
displayFn(result: IMultiSearchResult) {
if (result) { return result.title; }
}
}

View file

@ -0,0 +1,11 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'quality' })
export class QualityPipe implements PipeTransform {
transform(value: string): string {
if (value.toUpperCase() === "4K" || value.toUpperCase() === "8K") {
return value;
}
return value + "p";
}
}

View file

@ -2,11 +2,12 @@
import { HumanizePipe } from "./HumanizePipe";
import { ThousandShortPipe } from "./ThousandShortPipe";
import { SafePipe } from "./SafePipe";
import { QualityPipe } from "./QualityPipe";
@NgModule({
imports: [],
declarations: [HumanizePipe, ThousandShortPipe, SafePipe],
exports: [HumanizePipe, ThousandShortPipe, SafePipe],
declarations: [HumanizePipe, ThousandShortPipe, SafePipe, QualityPipe],
exports: [HumanizePipe, ThousandShortPipe, SafePipe, QualityPipe],
})
export class PipeModule {

View file

@ -1,51 +1,68 @@
<div class="mat-elevation-z8">
<grid-spinner [loading]="isLoadingResults"></grid-spinner>
<!-- <div class="row"> -->
<div class="row justify-content-md-center top-spacing">
<div class="btn-group" role="group">
<button type="button" (click)="switchFilter(RequestFilter.All)" [attr.color]="currentFilter === RequestFilter.All ? 'accent' : 'primary'" [ngClass]="currentFilter === RequestFilter.All ? 'mat-accent' : 'mat-primary'" mat-raised-button class="btn grow">{{'Requests.AllRequests' | translate}}</button>
<button type="button" (click)="switchFilter(RequestFilter.Pending)" [attr.color]="currentFilter === RequestFilter.Pending ? 'accent' : 'primary'" [ngClass]="currentFilter === RequestFilter.Pending ? 'mat-accent' : 'mat-primary'" mat-raised-button class="btn grow">{{'Requests.PendingRequests' | translate}}</button>
<button type="button" (click)="switchFilter(RequestFilter.Processing)" [attr.color]="currentFilter === RequestFilter.Processing ? 'accent' : 'primary'" [ngClass]="currentFilter === RequestFilter.Processing ? 'mat-accent' : 'mat-primary'" mat-raised-button
class="btn grow">{{'Requests.ProcessingRequests' | translate}}</button>
<button type="button" (click)="switchFilter(RequestFilter.Available)" [attr.color]="currentFilter === RequestFilter.Available ? 'accent' : 'primary'" [ngClass]="currentFilter === RequestFilter.Available ? 'mat-accent' : 'mat-primary'" mat-raised-button
class="btn grow">{{'Requests.AvailableRequests' | translate}}</button>
<button type="button" (click)="switchFilter(RequestFilter.Denied)" [attr.color]="currentFilter === RequestFilter.Denied ? 'accent' : 'primary'" [ngClass]="currentFilter === RequestFilter.Denied ? 'mat-accent' : 'mat-primary'" mat-raised-button class="btn grow">{{'Requests.DeniedRequests' | translate}}</button>
</div>
</div>
<div class="row">
<div class="col-md-2 offset-md-10">
<mat-form-field>
<mat-select placeholder="Requests to Display" [(value)]="gridCount" (selectionChange)="ngAfterViewInit()">
<mat-select placeholder="{{'Requests.RequestsToDisplay' | translate}}" [(value)]="gridCount" (selectionChange)="ngAfterViewInit()">
<mat-option value="10">10</mat-option>
<mat-option value="15">15</mat-option>
<mat-option value="30">30</mat-option>
<mat-option value="100">100</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<!-- </div> -->
<table mat-table [dataSource]="dataSource" class="table" matSort [matSortActive]="defaultSort" matSortDisableClear [matSortDirection]="defaultOrder">
<table mat-table [dataSource]="dataSource" class="table" matSort [matSortActive]="defaultSort"
matSortDisableClear [matSortDirection]="defaultOrder">
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> {{ 'Requests.RequestsTitle' | translate}} </th>
<td mat-cell *matCellDef="let element"> {{element.title}} ({{element.releaseDate | amLocal | amDateFormat: 'YYYY'}}) </td>
</ng-container>
<ng-container matColumnDef="requestedUser.requestedBy">
<th mat-header-cell *matHeaderCellDef > Requested By </th>
<th mat-header-cell *matHeaderCellDef> {{'Requests.RequestedBy' | translate}} </th>
<td mat-cell *matCellDef="let element"> {{element.requestedUser?.userAlias}} </td>
</ng-container>
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> Title </th>
<td mat-cell *matCellDef="let element"> {{element.title}} ({{element.releaseDate | amLocal | amDateFormat:
'YYYY'}}) </td>
</ng-container>
<ng-container matColumnDef="requestedDate">
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> Request Date </th>
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> {{ 'Requests.RequestDate' | translate}} </th>
<td mat-cell *matCellDef="let element"> {{element.requestedDate | amLocal | amDateFormat: 'LL'}} </td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> Status </th>
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> {{ 'Requests.Status' | translate}} </th>
<td mat-cell *matCellDef="let element"> {{element.status}} </td>
</ng-container>
<ng-container matColumnDef="requestStatus">
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> Request Status </th>
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> {{ 'Requests.RequestStatus' | translate}} </th>
<td mat-cell *matCellDef="let element"> {{element.requestStatus | translate}} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> </th>
<td mat-cell *matCellDef="let element">
<button mat-raised-button color="accent" [routerLink]="'/details/movie/' + element.theMovieDbId">Details</button>
<button mat-raised-button color="warn" (click)="openOptions(element)" *ngIf="isAdmin">Options</button>
<button mat-raised-button color="accent" [routerLink]="'/details/movie/' + element.theMovieDbId">{{ 'Requests.Details' | translate}}</button>
<button mat-raised-button color="warn" (click)="openOptions(element)" *ngIf="isAdmin"> {{ 'Requests.Options' | translate}}</button>
</td>
</ng-container>

View file

@ -7,6 +7,7 @@ import { catchError, map, startWith, switchMap } from 'rxjs/operators';
import { RequestServiceV2 } from "../../../services/requestV2.service";
import { AuthService } from "../../../auth/auth.service";
import { StorageService } from "../../../shared/storage/storage-service";
import { RequestFilterType } from "../../models/RequestFilterType";
@Component({
templateUrl: "./movies-grid.component.html",
@ -19,13 +20,18 @@ export class MoviesGridComponent implements OnInit, AfterViewInit {
public isLoadingResults = true;
public displayedColumns: string[] = ['requestedUser.requestedBy', 'title', 'requestedDate', 'status', 'requestStatus', 'actions'];
public gridCount: string = "15";
public showUnavailableRequests: boolean;
public isAdmin: boolean;
public defaultSort: string = "requestedDate";
public defaultOrder: string = "desc";
public currentFilter: RequestFilterType = RequestFilterType.All;
public RequestFilter = RequestFilterType;
private storageKey = "Movie_DefaultRequestListSort";
private storageKeyOrder = "Movie_DefaultRequestListSortOrder";
private storageKeyGridCount = "Movie_DefaultGridCount";
private storageKeyCurrentFilter = "Movie_DefaultFilter";
@Output() public onOpenOptions = new EventEmitter<{ request: any, filter: any, onChange: any }>();
@ -38,28 +44,35 @@ export class MoviesGridComponent implements OnInit, AfterViewInit {
}
public ngOnInit() {
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
const defaultCount = this.storageService.get(this.storageKeyGridCount);
const defaultSort = this.storageService.get(this.storageKey);
const defaultOrder = this.storageService.get(this.storageKeyOrder);
const defaultFilter = +this.storageService.get(this.storageKeyCurrentFilter);
if (defaultSort) {
this.defaultSort = defaultSort;
}
if (defaultOrder) {
this.defaultOrder = defaultOrder;
}
if (defaultCount) {
this.gridCount = defaultCount;
}
if (defaultFilter) {
this.currentFilter = defaultFilter;
}
}
public async ngAfterViewInit() {
// const results = await this.requestService.getMovieRequests(this.gridCount, 0, OrderType.RequestedDateDesc,
// { availabilityFilter: FilterType.None, statusFilter: FilterType.None }).toPromise();
// this.dataSource = results.collection;
// this.resultsLength = results.total;
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
this.storageService.save(this.storageKeyGridCount, this.gridCount);
this.storageService.save(this.storageKeyCurrentFilter, (+this.currentFilter).toString());
// If the user changes the sort order, reset back to the first page.
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
merge(this.sort.sortChange, this.paginator.page)
merge(this.sort.sortChange, this.paginator.page, this.currentFilter)
.pipe(
startWith({}),
switchMap((value: any) => {
@ -85,11 +98,19 @@ export class MoviesGridComponent implements OnInit, AfterViewInit {
}
public loadData(): Observable<IRequestsViewModel<IMovieRequests>> {
if (this.showUnavailableRequests) {
return this.requestService.getMovieUnavailableRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction);
} else {
switch(RequestFilterType[RequestFilterType[this.currentFilter]]) {
case RequestFilterType.All:
return this.requestService.getMovieRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction);
case RequestFilterType.Pending:
return this.requestService.getMoviePendingRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction);
case RequestFilterType.Available:
return this.requestService.getMovieAvailableRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction);
case RequestFilterType.Processing:
return this.requestService.getMovieProcessingRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction);
case RequestFilterType.Denied:
return this.requestService.getMovieDeniedRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction);
}
}
public openOptions(request: IMovieRequests) {
@ -105,4 +126,9 @@ export class MoviesGridComponent implements OnInit, AfterViewInit {
this.onOpenOptions.emit({ request: request, filter: filter, onChange: onChange });
}
public switchFilter(type: RequestFilterType) {
this.currentFilter = type;
this.ngAfterViewInit();
}
}

View file

@ -2,47 +2,54 @@
<grid-spinner [loading]="isLoadingResults"></grid-spinner>
<div class="row justify-content-md-center top-spacing">
<div class="btn-group" role="group">
<button type="button" (click)="switchFilter(RequestFilter.All)" [attr.color]="currentFilter === RequestFilter.All ? 'accent' : 'primary'" [ngClass]="currentFilter === RequestFilter.All ? 'mat-accent' : 'mat-primary'" mat-raised-button class="btn grow">{{'Requests.AllRequests' | translate}}</button>
<button type="button" (click)="switchFilter(RequestFilter.Pending)" [attr.color]="currentFilter === RequestFilter.Pending ? 'accent' : 'primary'" [ngClass]="currentFilter === RequestFilter.Pending ? 'mat-accent' : 'mat-primary'" mat-raised-button class="btn grow">{{'Requests.PendingRequests' | translate}}</button>
<button type="button" (click)="switchFilter(RequestFilter.Processing)" [attr.color]="currentFilter === RequestFilter.Processing ? 'accent' : 'primary'" [ngClass]="currentFilter === RequestFilter.Processing ? 'mat-accent' : 'mat-primary'" mat-raised-button
class="btn grow">{{'Requests.ProcessingRequests' | translate}}</button>
<button type="button" (click)="switchFilter(RequestFilter.Available)" [attr.color]="currentFilter === RequestFilter.Available ? 'accent' : 'primary'" [ngClass]="currentFilter === RequestFilter.Available ? 'mat-accent' : 'mat-primary'" mat-raised-button
class="btn grow">{{'Requests.AvailableRequests' | translate}}</button>
<button type="button" (click)="switchFilter(RequestFilter.Denied)" [attr.color]="currentFilter === RequestFilter.Denied ? 'accent' : 'primary'" [ngClass]="currentFilter === RequestFilter.Denied ? 'mat-accent' : 'mat-primary'" mat-raised-button class="btn grow">{{'Requests.DeniedRequests' | translate}}</button>
</div>
</div>
<div class="row">
<div class="col-md-2 offset-md-10">
<mat-form-field>
<mat-select placeholder="Requests to Display" [(value)]="gridCount" (selectionChange)="ngAfterViewInit()">
<mat-select placeholder="{{'Requests.RequestsToDisplay' | translate}}" [(value)]="gridCount" (selectionChange)="ngAfterViewInit()">
<mat-option value="10">10</mat-option>
<mat-option value="15">15</mat-option>
<mat-option value="30">30</mat-option>
<mat-option value="100">100</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<table mat-table [dataSource]="dataSource" class="table" matSort [matSortActive]="defaultSort" matSortDisableClear
[matSortDirection]="defaultOrder">
<table mat-table [dataSource]="dataSource" class="table" matSort [matSortActive]="defaultSort" matSortDisableClear [matSortDirection]="defaultOrder">
<ng-container matColumnDef="series">
<th mat-header-cell *matHeaderCellDef> Series </th>
<th mat-header-cell *matHeaderCellDef> {{'Requests.RequestsTitle' | translate}} </th>
<td mat-cell *matCellDef="let element"> {{element.parentRequest.title}} </td>
</ng-container>
<ng-container matColumnDef="requestedBy">
<th mat-header-cell *matHeaderCellDef> Requested By </th>
<th mat-header-cell *matHeaderCellDef> {{'Requests.RequestedBy' | translate}} </th>
<td mat-cell *matCellDef="let element"> {{element.requestedUser.userAlias}} </td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef> Status </th>
<td mat-cell *matCellDef="let element">
{{element.parentRequest.status}}
</td>
</ng-container>
<ng-container matColumnDef="requestedDate">
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> Requested Date </th>
<th mat-header-cell *matHeaderCellDef mat-sort-header disableClear> {{'Requests.RequestDate' | translate}} </th>
<td mat-cell *matCellDef="let element">
{{element.requestedDate | amLocal | amDateFormat: 'LL'}}
</td>
</ng-container>
<ng-container matColumnDef="requestStatus">
<th mat-header-cell *matHeaderCellDef> Request Status </th>
<th mat-header-cell *matHeaderCellDef> {{'Requests.RequestStatus' | translate}} </th>
<td mat-cell *matCellDef="let element">
<div *ngIf="element.approved && !element.available">{{'Common.ProcessingRequest' | translate}}</div>
<div *ngIf="!element.approved && !element.available">{{'Common.PendingApproval' |translate}}</div>
@ -51,12 +58,18 @@
</td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef> {{'Requests.Status' | translate}} </th>
<td mat-cell *matCellDef="let element">
{{element.parentRequest.status}}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> </th>
<td mat-cell *matCellDef="let element">
<button mat-raised-button color="accent"
[routerLink]="'/details/tv/' + element.parentRequest.tvDbId">Details</button>
<button mat-raised-button color="warn" (click)="openOptions(element)" *ngIf="isAdmin">Options</button>
<button mat-raised-button color="accent" [routerLink]="'/details/tv/' + element.parentRequest.tvDbId">{{'Requests.Details' | translate}}</button>
<button mat-raised-button color="warn" (click)="openOptions(element)" *ngIf="isAdmin">{{'Requests.Options' | translate}}</button>
</td>
</ng-container>

View file

@ -7,6 +7,7 @@ import { catchError, map, startWith, switchMap } from 'rxjs/operators';
import { RequestServiceV2 } from "../../../services/requestV2.service";
import { AuthService } from "../../../auth/auth.service";
import { StorageService } from "../../../shared/storage/storage-service";
import { RequestFilterType } from "../../models/RequestFilterType";
@Component({
templateUrl: "./tv-grid.component.html",
@ -19,13 +20,17 @@ export class TvGridComponent implements OnInit, AfterViewInit {
public isLoadingResults = true;
public displayedColumns: string[] = ['series', 'requestedBy', 'status', 'requestStatus', 'requestedDate','actions'];
public gridCount: string = "15";
public showUnavailableRequests: boolean;
public isAdmin: boolean;
public defaultSort: string = "requestedDate";
public defaultOrder: string = "desc";
public currentFilter: RequestFilterType = RequestFilterType.All;
public RequestFilter = RequestFilterType;
private storageKey = "Tv_DefaultRequestListSort";
private storageKeyOrder = "Tv_DefaultRequestListSortOrder";
private storageKeyGridCount = "Tv_DefaultGridCount";
private storageKeyCurrentFilter = "Tv_DefaultFilter";
@Output() public onOpenOptions = new EventEmitter<{request: any, filter: any, onChange: any}>();
@ -38,19 +43,30 @@ export class TvGridComponent implements OnInit, AfterViewInit {
}
public ngOnInit() {
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
const defaultCount = this.storageService.get(this.storageKeyGridCount);
const defaultSort = this.storageService.get(this.storageKey);
const defaultOrder = this.storageService.get(this.storageKeyOrder);
const defaultFilter = +this.storageService.get(this.storageKeyCurrentFilter);
if (defaultSort) {
this.defaultSort = defaultSort;
}
if (defaultOrder) {
this.defaultOrder = defaultOrder;
}
if (defaultCount) {
this.gridCount = defaultCount;
}
if (defaultFilter) {
this.currentFilter = defaultFilter;
}
}
public async ngAfterViewInit() {
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
this.storageService.save(this.storageKeyGridCount, this.gridCount);
this.storageService.save(this.storageKeyCurrentFilter, (+this.currentFilter).toString());
// If the user changes the sort order, reset back to the first page.
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
@ -93,10 +109,22 @@ export class TvGridComponent implements OnInit, AfterViewInit {
}
private loadData(): Observable<IRequestsViewModel<IChildRequests>> {
if(this.showUnavailableRequests) {
return this.requestService.getTvUnavailableRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction);
} else {
switch(RequestFilterType[RequestFilterType[this.currentFilter]]) {
case RequestFilterType.All:
return this.requestService.getTvRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction);
case RequestFilterType.Pending:
return this.requestService.getPendingTvRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction);
case RequestFilterType.Available:
return this.requestService.getAvailableTvRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction);
case RequestFilterType.Processing:
return this.requestService.getProcessingTvRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction);
case RequestFilterType.Denied:
return this.requestService.getDeniedTvRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction);
}
}
public switchFilter(type: RequestFilterType) {
this.currentFilter = type;
this.ngAfterViewInit();
}
}

View file

@ -0,0 +1,7 @@
export enum RequestFilterType {
All,
Pending,
Processing,
Available,
Denied
}

View file

@ -17,10 +17,42 @@ export class RequestServiceV2 extends ServiceHelpers {
return this.http.get<IRequestsViewModel<IMovieRequests>>(`${this.url}movie/${count}/${position}/${sortProperty}/${order}`, {headers: this.headers});
}
public getMovieAvailableRequests(count: number, position: number, sortProperty: string , order: string): Observable<IRequestsViewModel<IMovieRequests>> {
return this.http.get<IRequestsViewModel<IMovieRequests>>(`${this.url}movie/available/${count}/${position}/${sortProperty}/${order}`, {headers: this.headers});
}
public getMovieProcessingRequests(count: number, position: number, sortProperty: string , order: string): Observable<IRequestsViewModel<IMovieRequests>> {
return this.http.get<IRequestsViewModel<IMovieRequests>>(`${this.url}movie/processing/${count}/${position}/${sortProperty}/${order}`, {headers: this.headers});
}
public getMoviePendingRequests(count: number, position: number, sortProperty: string , order: string): Observable<IRequestsViewModel<IMovieRequests>> {
return this.http.get<IRequestsViewModel<IMovieRequests>>(`${this.url}movie/pending/${count}/${position}/${sortProperty}/${order}`, {headers: this.headers});
}
public getMovieDeniedRequests(count: number, position: number, sortProperty: string , order: string): Observable<IRequestsViewModel<IMovieRequests>> {
return this.http.get<IRequestsViewModel<IMovieRequests>>(`${this.url}movie/denied/${count}/${position}/${sortProperty}/${order}`, {headers: this.headers});
}
public getTvRequests(count: number, position: number, sortProperty: string , order: string): Observable<IRequestsViewModel<IChildRequests>> {
return this.http.get<IRequestsViewModel<IChildRequests>>(`${this.url}tv/${count}/${position}/${sortProperty}/${order}`, {headers: this.headers});
}
public getPendingTvRequests(count: number, position: number, sortProperty: string , order: string): Observable<IRequestsViewModel<IChildRequests>> {
return this.http.get<IRequestsViewModel<IChildRequests>>(`${this.url}tv/pending/${count}/${position}/${sortProperty}/${order}`, {headers: this.headers});
}
public getProcessingTvRequests(count: number, position: number, sortProperty: string , order: string): Observable<IRequestsViewModel<IChildRequests>> {
return this.http.get<IRequestsViewModel<IChildRequests>>(`${this.url}tv/processing/${count}/${position}/${sortProperty}/${order}`, {headers: this.headers});
}
public getAvailableTvRequests(count: number, position: number, sortProperty: string , order: string): Observable<IRequestsViewModel<IChildRequests>> {
return this.http.get<IRequestsViewModel<IChildRequests>>(`${this.url}tv/available/${count}/${position}/${sortProperty}/${order}`, {headers: this.headers});
}
public getDeniedTvRequests(count: number, position: number, sortProperty: string , order: string): Observable<IRequestsViewModel<IChildRequests>> {
return this.http.get<IRequestsViewModel<IChildRequests>>(`${this.url}tv/denied/${count}/${position}/${sortProperty}/${order}`, {headers: this.headers});
}
public updateMovieAdvancedOptions(options: IMovieAdvancedOptions): Observable<IRequestEngineResult> {
return this.http.post<IRequestEngineResult>(`${this.url}movie/advancedoptions`, options, {headers: this.headers});
}

View file

@ -77,17 +77,17 @@
<div class="mat-row">
<div class="mat-cell">Ombi Database</div>
<div class="mat-cell">{{about.ombiDatabaseType}} - {{about.ombiConnectionString}}</div>
<div class="mat-cell">{{about.ombiDatabaseType}}</div>
</div>
<div class="mat-row">
<div class="mat-cell">External Database</div>
<div class="mat-cell">{{about.externalDatabaseType}} - {{about.externalConnectionString}}</div>
<div class="mat-cell">{{about.externalDatabaseType}}</div>
</div>
<div class="mat-row">
<div class="mat-cell">Settings Database</div>
<div class="mat-cell">{{about.settingsDatabaseType}} - {{about.settingsConnectionString}}</div>
<div class="mat-cell">{{about.settingsDatabaseType}}</div>
</div>

View file

@ -9,10 +9,11 @@
<div class="row">
<div class="form-group col-md-3">
<div class="checkbox">
<input type="checkbox" id="enable" [(ngModel)]="settings.enable" [checked]="settings.enable">
<label for="enable">Enable</label>
<div>
<mat-checkbox [(ngModel)]="settings.enable" [checked]="settings.enable">
Enable</mat-checkbox>
</div>
</div>
</div>
@ -24,59 +25,52 @@
<br />
<br />
<div class="col-md-6">
<div class="form-group">
<label for="name" class="control-label">Server name</label>
<div>
<input type="text" class="form-control form-control-custom " id="name" name="name" placeholder="Server" [(ngModel)]="server.name" value="{{server.name}}">
</div>
</div>
<div class="form-group">
<label for="Ip" class="control-label">Hostname or IP</label>
<div>
<input type="text" class="form-control form-control-custom " id="Ip" name="Ip" placeholder="localhost" [(ngModel)]="server.ip" value="{{server.ip}}">
</div>
</div>
<mat-form-field class="full">
<input matInput placeholder="Server Name" [(ngModel)]="server.name" value="{{server.name}}">
</mat-form-field>
<div class="form-group">
<label for="portNumber" class="control-label">Port</label>
<div>
<input type="text" [(ngModel)]="server.port" class="form-control form-control-custom " id="portNumber" name="Port" placeholder="Port Number" value="{{server.port}}">
</div>
</div>
<mat-form-field class="full">
<input matInput placeholder="Hostname or IP" [(ngModel)]="server.ip" value="{{server.ip}}">
</mat-form-field>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="ssl" [(ngModel)]="server.ssl" ng-checked="server.ssl">
<label for="ssl">SSL</label>
</div>
</div>
<div class="form-group">
<label for="authToken" class="control-label">Emby Api Key</label>
<div class="">
<input type="text" class="form-control-custom form-control" id="authToken" [(ngModel)]="server.apiKey" placeholder="Emby Api Key" value="{{server.apiKey}}">
</div>
</div>
<mat-form-field class="full">
<input matInput placeholder="Port" [(ngModel)]="server.port" value="{{server.port}}">
</mat-form-field>
<div class="form-group">
<label for="authToken" class="control-label">Externally Facing Hostname
<mat-checkbox [(ngModel)]="server.ssl" [checked]="server.ssl">
SSL</mat-checkbox>
<mat-form-field class="full">
<input matInput placeholder="Api Key" [(ngModel)]="server.apiKey" value="{{server.apiKey}}">
</mat-form-field>
<mat-form-field class="full">
<input matInput placeholder="Base Url" [(ngModel)]="server.subDir" value="{{server.subDir}}">
</mat-form-field>
<label> Externally Facing Hostname
<i class="fa fa-question-circle"
pTooltip="This will be the external address that users will navigate to when they press the 'View On Emby' button"></i>
matTooltip="This will be the external address that users will navigate to when they press the 'View On Emby' button"></i>
</label>
<mat-form-field class="full">
<input matInput placeholder="e.g. https://jellyfin.server.com/" [(ngModel)]="server.serverHostname" value="{{server.serverHostname}}">
</mat-form-field>
<small>
<span *ngIf="server.serverHostname">Current URL: "{{server.serverHostname}}/#!/{{settings.isJellyfin ? ("itemdetails"): ("item/item")}}.html?id=1"</span>
<span *ngIf="!server.serverHostname">Current URL: "https://app.emby.media/#!/{{settings.isJellyfin ? ("itemdetails"): ("item/item")}}.html?id=1</span>
</small>
<div class="form-group">
<div>
<input type="text" class="form-control-custom form-control" id="authToken" [(ngModel)]="server.serverHostname" placeholder="e.g. https://jellyfin.server.com/" value="{{server.serverHostname}}">
<small><span *ngIf="server.serverHostname">Current URL: "{{server.serverHostname}}/#!/{{settings.isJellyfin ? ("itemdetails"): ("item/item")}}.html?id=1"</span>
<span *ngIf="!server.serverHostname">Current URL: "https://app.emby.media/#!/{{settings.isJellyfin ? ("itemdetails"): ("item/item")}}.html?id=1</span></small>
<button mat-raised-button id="testEmby" type="button" (click)="test(server)" color="primary">Test Connectivity <div id="spinner"></div></button>
</div>
</div>
<div class="form-group">
<div>
<button id="testEmby" type="button" (click)="test(server)" class="btn btn-primary-outline">Test Connectivity <div id="spinner"></div></button>
</div>
</div>
<div class="form-group">
<div>
<button id="discover" type="button" (click)="discoverServerInfo(server)" class="btn btn-primary-outline">Discover Server Information <div id="spinner"></div></button>
<button mat-raised-button id="discover" type="button" (click)="discoverServerInfo(server)" color="accent">Discover Server Information <div id="spinner"></div></button>
</div>
</div>
</div>
@ -88,14 +82,14 @@
<div class="col-md-2">
<div class="form-group">
<div>
<button [disabled]="!hasDiscovered" (click)="save()" type="submit" id="save" class="btn btn-primary-outline">Submit</button>
<button mat-raised-button [disabled]="!hasDiscovered" (click)="save()" type="submit" id="save" color="accent">Submit</button>
</div>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<div>
<button (click)="runCacher()" type="button" id="save" class="btn btn-primary-outline">Manually Run Cacher</button>
<button mat-raised-button (click)="runCacher()" type="button" id="save" color="primary">Manually Run Cacher</button>
</div>
</div>
</div>

View file

@ -40,7 +40,7 @@
</div>
<div>
<mat-checkbox formControlName="ignoreCertificateErrors" matTooltip="Enable if you are having connectivity problems over SSL">
Ignore any certificate errors
Ignore any certificate errors (Please restart after changing)
</mat-checkbox>
</div>
<div>

View file

@ -3,7 +3,6 @@
<head>
<script type='text/javascript'>
function configExists(url) {
var req = new XMLHttpRequest();
req.open('GET', url, false);
@ -44,6 +43,13 @@ function configExists(url) {
<script src="styles/please-wait.js"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta property="og:image:height" content="375" />
<meta property="og:image:width" content="991" />
<meta property="og:image" content="~/images/logo.png" />
<meta property="og:site_name" content="Ombi" />
<meta property="og:title" content="Ombi" />
<meta property="og:type" content="website" />
<meta property="og:description" content="Ombi, media request tool">
<title>Ombi</title>

View file

@ -123,11 +123,8 @@ namespace Ombi.Controllers.V1
OsDescription = RuntimeInformation.OSDescription,
ProcessArchitecture = RuntimeInformation.ProcessArchitecture.ToString(),
ApplicationBasePath = Directory.GetCurrentDirectory(),
ExternalConnectionString = dbConfiguration.ExternalDatabase.ConnectionString,
ExternalDatabaseType = dbConfiguration.ExternalDatabase.Type,
OmbiConnectionString = dbConfiguration.OmbiDatabase.ConnectionString,
OmbiDatabaseType = dbConfiguration.OmbiDatabase.Type,
SettingsConnectionString = dbConfiguration.SettingsDatabase.ConnectionString,
SettingsDatabaseType = dbConfiguration.SettingsDatabase.Type,
StoragePath = storage.StoragePath.HasValue() ? storage.StoragePath : "None Specified",
NotSupported = Directory.GetCurrentDirectory().Contains("qpkg")

View file

@ -32,7 +32,7 @@ namespace Ombi.Controllers.V2
var model = new List<ConnectedUsersViewModel>();
foreach (var user in users)
{
var ombiUser = await allUsers.FirstOrDefaultAsync(x => x.Id.Equals(user.UserId, StringComparison.InvariantCultureIgnoreCase));
var ombiUser = await allUsers.FirstOrDefaultAsync(x => x.Id == user.UserId);
if (ombiUser == null)
{

View file

@ -39,6 +39,10 @@ namespace Ombi.Controllers.V2
var username = User.Identity.Name.ToUpper();
var user = await _userManager.Users.FirstOrDefaultAsync(x => x.NormalizedUserName == username);
if (user == null)
{
return Ok();
}
// Check if we already have this notification id
var alreadyExists = await _mobileDevices.GetAll().AnyAsync(x => x.Token == body.Token && x.UserId == user.Id);
@ -46,8 +50,14 @@ namespace Ombi.Controllers.V2
{
return Ok();
}
// Ensure we don't have too many already for this user
var tokens = await _mobileDevices.GetAll().Where(x => x.UserId == user.Id).OrderBy(x => x.AddedAt).ToListAsync();
if (tokens.Count() > 5)
{
var toDelete = tokens.Take(tokens.Count() - 5);
await _mobileDevices.DeleteRange(toDelete);
}
// let's add it
await _mobileDevices.Add(new MobileDevices
{
Token = body.Token,

View file

@ -37,6 +37,7 @@ namespace Ombi.Controllers.V2
}
[HttpGet("movie/availble/{count:int}/{position:int}/{sort}/{sortOrder}")]
[HttpGet("movie/available/{count:int}/{position:int}/{sort}/{sortOrder}")]
public async Task<RequestsViewModel<MovieRequests>> GetAvailableRequests(int count, int position, string sort, string sortOrder)
{
return await _movieRequestEngine.GetRequestsByStatus(count, position, sort, sortOrder, RequestStatus.Available);

View file

@ -106,15 +106,15 @@
"MoviesTab": "Movies",
"TvTab": "TV Shows",
"MusicTab": "Music",
"RequestedBy": "Requested By:",
"Status": "Status:",
"RequestStatus": "Request status:",
"RequestedBy": "Requested By",
"Status": "Status",
"RequestStatus": "Request status",
"Denied": " Denied:",
"TheatricalRelease": "Theatrical Release: {{date}}",
"ReleaseDate": "Released: {{date}}",
"TheatricalReleaseSort": "Theatrical Release",
"DigitalRelease": "Digital Release: {{date}}",
"RequestDate": "Request Date:",
"RequestDate": "Request Date",
"QualityOverride": "Quality Override:",
"RootFolderOverride": "Root Folder Override:",
"ChangeRootFolder": "Root Folder",

View file

@ -106,15 +106,15 @@
"MoviesTab": "Film",
"TvTab": "Tv-serier",
"MusicTab": "Musik",
"RequestedBy": "Anmodet af:",
"Status": "Status:",
"RequestStatus": "Status for anmodning:",
"RequestedBy": "Anmodet af",
"Status": "Status",
"RequestStatus": "Status for anmodning",
"Denied": " Afvist:",
"TheatricalRelease": "Biografudgivelse: {{date}}",
"ReleaseDate": "Udgivet: {{date}}",
"TheatricalReleaseSort": "Biografudgivelse",
"DigitalRelease": "Digital udgivelse: {{date}}",
"RequestDate": "Dato for anmodning:",
"RequestDate": "Dato for anmodning",
"QualityOverride": "Tilsidesæt kvalitet:",
"RootFolderOverride": "Tilsidesæt rodmappe:",
"ChangeRootFolder": "Skift rodmappe",

View file

@ -106,15 +106,15 @@
"MoviesTab": "Filme",
"TvTab": "Serien",
"MusicTab": "Musik",
"RequestedBy": "Angefordert von:",
"Status": "Status:",
"RequestStatus": "Anfrage Status:",
"RequestedBy": "Angefordert von",
"Status": "Status",
"RequestStatus": "Anfrage Status",
"Denied": " Abgelehnt:",
"TheatricalRelease": "Kinostart: {{date}}",
"ReleaseDate": "Veröffentlicht: {{date}}",
"TheatricalReleaseSort": "Kinostart",
"DigitalRelease": "Veröffentlichung der digitalen Version: {{date}}",
"RequestDate": "Datum der Anfrage:",
"RequestDate": "Datum der Anfrage",
"QualityOverride": "Qualitäts Überschreiben:",
"RootFolderOverride": "Stammverzeichnis Überschreiben:",
"ChangeRootFolder": "Stammordner ändern",

View file

@ -115,15 +115,15 @@
"MoviesTab": "Movies",
"TvTab": "TV Shows",
"MusicTab": "Music",
"RequestedBy": "Requested By:",
"Status": "Status:",
"RequestStatus": "Request status:",
"RequestedBy": "Requested By",
"Status": "Status",
"RequestStatus": "Request status",
"Denied": " Denied:",
"TheatricalRelease": "Theatrical Release: {{date}}",
"ReleaseDate": "Released: {{date}}",
"TheatricalReleaseSort": "Theatrical Release",
"DigitalRelease": "Digital Release: {{date}}",
"RequestDate": "Request Date:",
"RequestDate": "Request Date",
"QualityOverride": "Quality Override:",
"RootFolderOverride": "Root Folder Override:",
"ChangeRootFolder": "Root Folder",
@ -153,7 +153,16 @@
"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"
}
},
"AllRequests": "All Requests",
"PendingRequests": "Pending Requests",
"ProcessingRequests": "Processing Requests",
"AvailableRequests": "Available Requests",
"DeniedRequests": "Denied Requests",
"RequestsToDisplay": "Requests to display",
"RequestsTitle": "Title",
"Details": "Details",
"Options": "Options"
},
"Issues": {
"Title": "Issues",

View file

@ -106,15 +106,15 @@
"MoviesTab": "Películas",
"TvTab": "Series",
"MusicTab": "Música",
"RequestedBy": "Solicitado por:",
"Status": "Estado:",
"RequestStatus": "Estado de la solicitud:",
"RequestedBy": "Solicitado por",
"Status": "Estado",
"RequestStatus": "Estado de la solicitud",
"Denied": " Denegado:",
"TheatricalRelease": "En cines: {{date}}",
"ReleaseDate": "Publicado: {{date}}",
"TheatricalReleaseSort": "En cines",
"DigitalRelease": "Versión digital: {{date}}",
"RequestDate": "Fecha de solicitud:",
"RequestDate": "Fecha de solicitud",
"QualityOverride": "Sobreescribir calidad:",
"RootFolderOverride": "Sobreescribir carpeta raíz:",
"ChangeRootFolder": "Carpeta raíz",

View file

@ -106,15 +106,15 @@
"MoviesTab": "Films",
"TvTab": "Séries",
"MusicTab": "Musique",
"RequestedBy": "Demandé par :",
"Status": "Statut :",
"RequestStatus": "Statut de la demande :",
"RequestedBy": "Demandé par",
"Status": "Statut",
"RequestStatus": "Statut de la demande",
"Denied": " Refusé :",
"TheatricalRelease": "Sortie en salle: {{date}}",
"ReleaseDate": "Sortie : {{date}}",
"TheatricalReleaseSort": "Sortie en salle",
"DigitalRelease": "Sortie numérique: {{date}}",
"RequestDate": "Date de la demande :",
"RequestDate": "Date de la demande",
"QualityOverride": "Remplacement de la qualité :",
"RootFolderOverride": "Remplacement du répertoire racine :",
"ChangeRootFolder": "Modifier le répertoire racine",

View file

@ -106,15 +106,15 @@
"MoviesTab": "Filmek",
"TvTab": "Sorozatok",
"MusicTab": "Zene",
"RequestedBy": "Kérte:",
"Status": "Állapot:",
"RequestStatus": "Kérés állapota:",
"RequestedBy": "Kérte",
"Status": "Állapot",
"RequestStatus": "Kérés állapota",
"Denied": " Megtagadta:",
"TheatricalRelease": "Mozis kiadás: {{date}}",
"ReleaseDate": "Kiadva: {{date}}",
"TheatricalReleaseSort": "Mozis kiadás",
"DigitalRelease": "Digitális kiadás: {{date}}",
"RequestDate": "Kérés ideje:",
"RequestDate": "Kérés ideje",
"QualityOverride": "Minőség felülírása:",
"RootFolderOverride": "Gyökér mappa felülírása:",
"ChangeRootFolder": "Gyökér mappa",

View file

@ -106,15 +106,15 @@
"MoviesTab": "Film",
"TvTab": "Serie TV",
"MusicTab": "Music",
"RequestedBy": "Richiesta da:",
"Status": "Stato:",
"RequestStatus": "Stato della richiesta:",
"RequestedBy": "Richiesta da",
"Status": "Stato",
"RequestStatus": "Stato della richiesta",
"Denied": " Rifiutato:",
"TheatricalRelease": "Theatrical Release: {{date}}",
"ReleaseDate": "Released: {{date}}",
"TheatricalReleaseSort": "Theatrical Release",
"DigitalRelease": "Digital Release: {{date}}",
"RequestDate": "Data della richiesta:",
"RequestDate": "Data della richiesta",
"QualityOverride": "Sovrascrivi qualità:",
"RootFolderOverride": "Sovrascrivi cartella principale:",
"ChangeRootFolder": "Modifica cartella principale",

View file

@ -106,15 +106,15 @@
"MoviesTab": "Films",
"TvTab": "TV Series",
"MusicTab": "Muziek",
"RequestedBy": "Verzocht Door:",
"Status": "Status:",
"RequestStatus": "Aanvraagstatus:",
"RequestedBy": "Verzocht Door",
"Status": "Status",
"RequestStatus": "Aanvraagstatus",
"Denied": " Geweigerd:",
"TheatricalRelease": "Cinema Uitgave: {{date}}",
"ReleaseDate": "Uitgekomen: {{date}}",
"TheatricalReleaseSort": "Bioscoop Uitgave",
"DigitalRelease": "Digitale Uitgave: {{date}}",
"RequestDate": "Aanvraag Datum:",
"RequestDate": "Aanvraag Datum",
"QualityOverride": "Kwaliteit overschrijven:",
"RootFolderOverride": "Hoofdmap overschrijven:",
"ChangeRootFolder": "Hoofdmap wijzigen",

View file

@ -106,15 +106,15 @@
"MoviesTab": "Filmer",
"TvTab": "TV serier",
"MusicTab": "Musikk",
"RequestedBy": "Etterspurt av:",
"Status": "Status:",
"RequestStatus": "Status for forespørsel:",
"RequestedBy": "Etterspurt av",
"Status": "Status",
"RequestStatus": "Status for forespørsel",
"Denied": " Avslått:",
"TheatricalRelease": "Kinopremiere: {{date}}",
"ReleaseDate": "Utgitt: {{date}}",
"TheatricalReleaseSort": "Kinopremiere",
"DigitalRelease": "Digital utgivelse: {{date}}",
"RequestDate": "Dato for forespørsel:",
"RequestDate": "Dato for forespørsel",
"QualityOverride": "Overstyr kvalitet:",
"RootFolderOverride": "Overstyring av rotmappe:",
"ChangeRootFolder": "Endre rotmappe",

View file

@ -106,15 +106,15 @@
"MoviesTab": "Filmy",
"TvTab": "Seriale",
"MusicTab": "Muzyka",
"RequestedBy": "Zgłoszone przez:",
"Status": "Status:",
"RequestStatus": "Status zgłoszenia:",
"RequestedBy": "Zgłoszone przez",
"Status": "Status",
"RequestStatus": "Status zgłoszenia",
"Denied": "Odrzucono:",
"TheatricalRelease": "Premiera kinowa: {{date}}",
"ReleaseDate": "Wydany: {{date}}",
"TheatricalReleaseSort": "Premiera kinowa",
"DigitalRelease": "Wydanie cyfrowe: {{date}}",
"RequestDate": "Data zgłoszenia:",
"RequestDate": "Data zgłoszenia",
"QualityOverride": "Wymuszenie jakości:",
"RootFolderOverride": "Wymuszenie folderu głównego:",
"ChangeRootFolder": "Folder główny",

View file

@ -106,15 +106,15 @@
"MoviesTab": "Filmes",
"TvTab": "Séries",
"MusicTab": "Músicas",
"RequestedBy": "Solicitado por:",
"Status": "Status:",
"RequestStatus": "Status da solicitação:",
"RequestedBy": "Solicitado por",
"Status": "Status",
"RequestStatus": "Status da solicitação",
"Denied": " Negados:",
"TheatricalRelease": "Lançamento nos Cinemas: {{date}}",
"ReleaseDate": "Lançado: {{date}}",
"TheatricalReleaseSort": "Lançamento nos Cinemas",
"DigitalRelease": "Lançamento digital: {{date}}",
"RequestDate": "Data da Solicitação:",
"RequestDate": "Data da Solicitação",
"QualityOverride": "Substituição de qualidade:",
"RootFolderOverride": "Substituição da pasta raiz:",
"ChangeRootFolder": "Pasta raiz",

View file

@ -106,15 +106,15 @@
"MoviesTab": "Фильмы",
"TvTab": "Сериалы",
"MusicTab": "Музыка",
"RequestedBy": "Автор запроса:",
"Status": "Статус:",
"RequestStatus": "Статус запроса:",
"RequestedBy": "Автор запроса",
"Status": "Статус",
"RequestStatus": "Статус запроса",
"Denied": " Отказано:",
"TheatricalRelease": "Релиз в кинотеатрах: {{date}}",
"ReleaseDate": "Дата выхода: {{date}}",
"TheatricalReleaseSort": "Релиз в кинотеатрах",
"DigitalRelease": "Дигитальный релиз: {{date}}",
"RequestDate": "Дата запроса:",
"RequestDate": "Дата запроса",
"QualityOverride": "Переопределение качества:",
"RootFolderOverride": "Переопределение корневой папки:",
"ChangeRootFolder": "Корневая папка",

View file

@ -106,15 +106,15 @@
"MoviesTab": "Filmy",
"TvTab": "Seriály",
"MusicTab": "Hudba",
"RequestedBy": "Vyžiadané od:",
"Status": "Stav:",
"RequestStatus": "Stav požiadavky:",
"RequestedBy": "Vyžiadané od",
"Status": "Stav",
"RequestStatus": "Stav požiadavky",
"Denied": " Zamietnuté:",
"TheatricalRelease": "Kino vydanie: {{date}}",
"ReleaseDate": "Vydané: {{date}}",
"TheatricalReleaseSort": "Kino vydanie",
"DigitalRelease": "Online vydanie: {{date}}",
"RequestDate": "Dátum požiadavky:",
"RequestDate": "Dátum požiadavky",
"QualityOverride": "Prepísanie kvality:",
"RootFolderOverride": "Prepísanie Root priečinku:",
"ChangeRootFolder": "Koreňový priečinok",

View file

@ -106,15 +106,15 @@
"MoviesTab": "Filmer",
"TvTab": "TV-serier",
"MusicTab": "Musik",
"RequestedBy": "Efterfrågats av:",
"Status": "Status:",
"RequestStatus": "Status för begäran:",
"RequestedBy": "Efterfrågats av",
"Status": "Status",
"RequestStatus": "Status för begäran",
"Denied": " Nekad:",
"TheatricalRelease": "Biopremiär: {{date}}",
"ReleaseDate": "Releasedatum: {{date}}",
"TheatricalReleaseSort": "Biopremiär",
"DigitalRelease": "Digitalt Releasedatum: {{date}}",
"RequestDate": "Datum för begäran:",
"RequestDate": "Datum för begäran",
"QualityOverride": "Kvalitétsöverskridande:",
"RootFolderOverride": "Rotmappsöverskridande:",
"ChangeRootFolder": "Byt rotmapp",