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 *.vscode
/src/Ombi/database.json /src/Ombi/database.json
/src/Ombi/healthchecksdb /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> <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoFixture" Version="4.5.0" /> <PackageReference Include="AutoFixture" Version="4.11.0" />
<PackageReference Include="Moq" Version="4.10.0" /> <PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="Nunit" Version="3.11.0" /> <PackageReference Include="Nunit" Version="3.12.0" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" /> <PackageReference Include="NUnit.ConsoleRunner" Version="3.11.1" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" /> <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.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>
<ItemGroup> <ItemGroup>

View file

@ -70,7 +70,7 @@ namespace Ombi.Core.Tests.Rule.Search
var result = await Rule.Execute(search); var result = await Rule.Execute(search);
Assert.True(result.Success); 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] [Test]
@ -99,7 +99,7 @@ namespace Ombi.Core.Tests.Rule.Search
var result = await Rule.Execute(search); var result = await Rule.Execute(search);
Assert.True(result.Success); 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] [Test]

View file

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

View file

@ -63,7 +63,7 @@ namespace Ombi.Core.Tests.Rule.Search
ForeignArtistId = "abc", ForeignArtistId = "abc",
} }
}.AsQueryable()); }.AsQueryable());
var request = new SearchArtistViewModel { ForignArtistId = "ABC" }; var request = new SearchArtistViewModel { ForignArtistId = "abc" };
var result = await Rule.Execute(request); var result = await Rule.Execute(request);
Assert.True(result.Success); 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)); allRequests = allRequests.Where(x => x.Approved && !x.Available && (!x.Denied.HasValue || !x.Denied.Value));
break; break;
case RequestStatus.Available: case RequestStatus.Available:
allRequests = allRequests.Where(x => x.Available && (!x.Denied.HasValue || !x.Denied.Value)); allRequests = allRequests.Where(x => x.Available);
break; break;
case RequestStatus.Denied: 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; break;
default: default:
break; break;
@ -332,12 +332,11 @@ namespace Ombi.Core.Engine
//var secondProp = TypeDescriptor.GetProperties(propType).Find(properties[1], true); //var secondProp = TypeDescriptor.GetProperties(propType).Find(properties[1], true);
} }
allRequests = sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase) var requests = (sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase)
? allRequests.OrderBy(x => prop.GetValue(x)) ? allRequests.ToList().OrderBy(x => prop.GetValue(x))
: allRequests.OrderByDescending(x => prop.GetValue(x)); : allRequests.ToList().OrderByDescending(x => prop.GetValue(x))).ToList();
var total = await allRequests.CountAsync(); var total = requests.Count();
var requests = await allRequests.Skip(position).Take(count) requests = requests.Skip(position).Take(count).ToList();
.ToListAsync();
await CheckForSubscription(shouldHide, requests); await CheckForSubscription(shouldHide, requests);
return new RequestsViewModel<MovieRequests> return new RequestsViewModel<MovieRequests>

View file

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

View file

@ -1,6 +1,6 @@
using System; using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using System;
namespace Ombi.Store.Migrations.OmbiMySql namespace Ombi.Store.Migrations.OmbiMySql
{ {
@ -8,14 +8,34 @@ namespace Ombi.Store.Migrations.OmbiMySql
{ {
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.Sql(@"CREATE TABLE `MobileDevices` ( // migrationBuilder.Sql(@"CREATE TABLE `MobileDevices` (
`Id` int NOT NULL AUTO_INCREMENT, // `Id` int NOT NULL AUTO_INCREMENT,
`Token` longtext CHARACTER SET utf8mb4 NULL, // `Token` longtext CHARACTER SET utf8mb4 NULL,
`UserId` varchar(255) COLLATE utf8mb4_bin NOT NULL, // `UserId` varchar(255) COLLATE utf8mb4_bin NOT NULL,
`AddedAt` datetime(6) NOT NULL, // `AddedAt` datetime(6) NOT NULL,
CONSTRAINT `PK_MobileDevices` PRIMARY KEY (`Id`), // CONSTRAINT `PK_MobileDevices` PRIMARY KEY (`Id`),
CONSTRAINT `FK_MobileDevices_AspNetUsers_UserId` FOREIGN KEY (`UserId`) REFERENCES `AspNetUsers` (`Id`) ON DELETE RESTRICT // 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( migrationBuilder.CreateIndex(

View file

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

View file

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

View file

@ -16,7 +16,7 @@
<!--Next to poster--> <!--Next to poster-->
<div class="col-12 col-lg-3 col-xl-3 media-row"> <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> [available]="movie.available" [plexUrl]="movie.plexUrl" [embyUrl]="movie.embyUrl"></social-icons>
</div> </div>
@ -117,9 +117,9 @@
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </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="sidebar affixable affix-top preview-poster">
<div class="poster"> <div class="poster">
<a [routerLink]="'/details/movie/'+r.id"> <a [routerLink]="'/details/movie/'+r.id">
@ -138,7 +138,7 @@
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </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="col-md-2" *ngFor="let r of movie.similar.results">
<div class="sidebar affixable affix-top preview-poster"> <div class="sidebar affixable affix-top preview-poster">
@ -159,9 +159,9 @@
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </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> <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>
</div> </div>

View file

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

View file

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

View file

@ -1,13 +1,24 @@
<input class="form-control quater-width search-bar" type="text" [(ngModel)]="selectedItem" <!-- <input class="form-control quater-width search-bar" type="text" [(ngModel)]="selectedItem" placeholder="{{'NavigationBar.Search' | translate}}"
placeholder="{{'NavigationBar.Search' | translate}}" aria-label="Search" [ngbTypeahead]="searchModel" aria-label="Search" [ngbTypeahead]="searchModel" [resultFormatter]="formatter" [inputFormatter]="formatter" [resultTemplate]="template" (selectItem)="selected($event)">
[resultFormatter]="formatter" [inputFormatter]="formatter" [resultTemplate]="template" (selectItem)="selected($event)">
<ng-template #template let-result="result"> <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'"> <div *ngIf="result.mediaType === 'movie'">
<i class="fa fa-film"></i> &nbsp; <i class="fa fa-film"></i> &nbsp;
<span >{{result.title}}</span> <span>{{result.title}}</span>
</div> </div>
<div *ngIf="result.mediaType === 'tv'"> <div *ngIf="result.mediaType === 'tv'">
<i class="fa fa-tv"></i> &nbsp; <i class="fa fa-tv"></i> &nbsp;
@ -23,8 +34,9 @@
<div *ngIf="result.mediaType === 'person'"> <div *ngIf="result.mediaType === 'person'">
<i class="fa fa-user"></i> &nbsp; <i class="fa fa-user"></i> &nbsp;
<span >{{result.title}}</span> <span>{{result.title}}</span>
</div> </div>
<!-- Collection --> </mat-option>
<!-- <i class="fa fa-file-video-o" aria-hidden="true"></i> --> </ng-container>
</ng-template> </mat-autocomplete>
</form>

View file

@ -1,7 +1,6 @@
$ombi-primary:#3f3f3f; $ombi-primary:#3f3f3f;
$ombi-primary-darker:#2b2b2b; $ombi-primary-darker:#2b2b2b;
$ombi-accent: #258a6d; $ombi-accent: #258a6d;
@media (max-width: 767px) { @media (max-width: 767px) {
.quater-width { .quater-width {
width: 15em !important; width: 15em !important;
@ -23,22 +22,6 @@ $ombi-accent: #258a6d;
padding: 0px 5px; 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 { .search-bar {
background-color: $ombi-primary-darker; background-color: $ombi-primary-darker;
border: solid 1px $ombi-primary-darker; border: solid 1px $ombi-primary-darker;

View file

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

View file

@ -1,51 +1,68 @@
<div class="mat-elevation-z8"> <div class="mat-elevation-z8">
<grid-spinner [loading]="isLoadingResults"></grid-spinner> <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-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="10">10</mat-option>
<mat-option value="15">15</mat-option> <mat-option value="15">15</mat-option>
<mat-option value="30">30</mat-option> <mat-option value="30">30</mat-option>
<mat-option value="100">100</mat-option> <mat-option value="100">100</mat-option>
</mat-select> </mat-select>
</mat-form-field> </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"> <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> <td mat-cell *matCellDef="let element"> {{element.requestedUser?.userAlias}} </td>
</ng-container> </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"> <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> <td mat-cell *matCellDef="let element"> {{element.requestedDate | amLocal | amDateFormat: 'LL'}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="status"> <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> <td mat-cell *matCellDef="let element"> {{element.status}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="requestStatus"> <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> <td mat-cell *matCellDef="let element"> {{element.requestStatus | translate}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions" > <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> </th> <th mat-header-cell *matHeaderCellDef> </th>
<td mat-cell *matCellDef="let element" > <td mat-cell *matCellDef="let element">
<button mat-raised-button color="accent" [routerLink]="'/details/movie/' + element.theMovieDbId">Details</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">Options</button> <button mat-raised-button color="warn" (click)="openOptions(element)" *ngIf="isAdmin"> {{ 'Requests.Options' | translate}}</button>
</td> </td>
</ng-container> </ng-container>

View file

@ -7,6 +7,7 @@ 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";
@Component({ @Component({
templateUrl: "./movies-grid.component.html", templateUrl: "./movies-grid.component.html",
@ -19,13 +20,18 @@ export class MoviesGridComponent implements OnInit, AfterViewInit {
public isLoadingResults = true; public isLoadingResults = true;
public displayedColumns: string[] = ['requestedUser.requestedBy', 'title', 'requestedDate', 'status', 'requestStatus', 'actions']; public displayedColumns: string[] = ['requestedUser.requestedBy', 'title', 'requestedDate', 'status', 'requestStatus', 'actions'];
public gridCount: string = "15"; public gridCount: string = "15";
public showUnavailableRequests: boolean;
public isAdmin: boolean; public isAdmin: boolean;
public defaultSort: string = "requestedDate"; public defaultSort: string = "requestedDate";
public defaultOrder: string = "desc"; public defaultOrder: string = "desc";
public currentFilter: RequestFilterType = RequestFilterType.All;
public RequestFilter = RequestFilterType;
private storageKey = "Movie_DefaultRequestListSort"; private storageKey = "Movie_DefaultRequestListSort";
private storageKeyOrder = "Movie_DefaultRequestListSortOrder"; private storageKeyOrder = "Movie_DefaultRequestListSortOrder";
private storageKeyGridCount = "Movie_DefaultGridCount";
private storageKeyCurrentFilter = "Movie_DefaultFilter";
@Output() public onOpenOptions = new EventEmitter<{ request: any, filter: any, onChange: any }>(); @Output() public onOpenOptions = new EventEmitter<{ request: any, filter: any, onChange: any }>();
@ -38,28 +44,35 @@ export class MoviesGridComponent implements OnInit, AfterViewInit {
} }
public ngOnInit() { 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 defaultSort = this.storageService.get(this.storageKey);
const defaultOrder = this.storageService.get(this.storageKeyOrder); const defaultOrder = this.storageService.get(this.storageKeyOrder);
const defaultFilter = +this.storageService.get(this.storageKeyCurrentFilter);
if (defaultSort) { if (defaultSort) {
this.defaultSort = defaultSort; this.defaultSort = defaultSort;
} }
if (defaultOrder) { if (defaultOrder) {
this.defaultOrder = defaultOrder; this.defaultOrder = defaultOrder;
} }
if (defaultCount) {
this.gridCount = defaultCount;
}
if (defaultFilter) {
this.currentFilter = defaultFilter;
}
} }
public async ngAfterViewInit() { 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. // If the user changes the sort order, reset back to the first page.
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); 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( .pipe(
startWith({}), startWith({}),
switchMap((value: any) => { switchMap((value: any) => {
@ -85,11 +98,19 @@ export class MoviesGridComponent implements OnInit, AfterViewInit {
} }
public loadData(): Observable<IRequestsViewModel<IMovieRequests>> { public loadData(): Observable<IRequestsViewModel<IMovieRequests>> {
if (this.showUnavailableRequests) { switch(RequestFilterType[RequestFilterType[this.currentFilter]]) {
return this.requestService.getMovieUnavailableRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction); case RequestFilterType.All:
} else {
return this.requestService.getMovieRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction); 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) { public openOptions(request: IMovieRequests) {
@ -105,4 +126,9 @@ export class MoviesGridComponent implements OnInit, AfterViewInit {
this.onOpenOptions.emit({ request: request, filter: filter, onChange: onChange }); 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> <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-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="10">10</mat-option>
<mat-option value="15">15</mat-option> <mat-option value="15">15</mat-option>
<mat-option value="30">30</mat-option> <mat-option value="30">30</mat-option>
<mat-option value="100">100</mat-option> <mat-option value="100">100</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div>
</div>
<table mat-table [dataSource]="dataSource" class="table" matSort [matSortActive]="defaultSort" matSortDisableClear <table mat-table [dataSource]="dataSource" class="table" matSort [matSortActive]="defaultSort" matSortDisableClear [matSortDirection]="defaultOrder">
[matSortDirection]="defaultOrder">
<ng-container matColumnDef="series"> <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> <td mat-cell *matCellDef="let element"> {{element.parentRequest.title}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="requestedBy"> <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> <td mat-cell *matCellDef="let element"> {{element.requestedUser.userAlias}} </td>
</ng-container> </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"> <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"> <td mat-cell *matCellDef="let element">
{{element.requestedDate | amLocal | amDateFormat: 'LL'}} {{element.requestedDate | amLocal | amDateFormat: 'LL'}}
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="requestStatus"> <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"> <td mat-cell *matCellDef="let element">
<div *ngIf="element.approved && !element.available">{{'Common.ProcessingRequest' | translate}}</div> <div *ngIf="element.approved && !element.available">{{'Common.ProcessingRequest' | translate}}</div>
<div *ngIf="!element.approved && !element.available">{{'Common.PendingApproval' |translate}}</div> <div *ngIf="!element.approved && !element.available">{{'Common.PendingApproval' |translate}}</div>
@ -51,12 +58,18 @@
</td> </td>
</ng-container> </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"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> </th> <th mat-header-cell *matHeaderCellDef> </th>
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element">
<button mat-raised-button color="accent" <button mat-raised-button color="accent" [routerLink]="'/details/tv/' + element.parentRequest.tvDbId">{{'Requests.Details' | translate}}</button>
[routerLink]="'/details/tv/' + element.parentRequest.tvDbId">Details</button> <button mat-raised-button color="warn" (click)="openOptions(element)" *ngIf="isAdmin">{{'Requests.Options' | translate}}</button>
<button mat-raised-button color="warn" (click)="openOptions(element)" *ngIf="isAdmin">Options</button>
</td> </td>
</ng-container> </ng-container>

View file

@ -7,6 +7,7 @@ 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";
@Component({ @Component({
templateUrl: "./tv-grid.component.html", templateUrl: "./tv-grid.component.html",
@ -19,13 +20,17 @@ export class TvGridComponent implements OnInit, AfterViewInit {
public isLoadingResults = true; public isLoadingResults = true;
public displayedColumns: string[] = ['series', 'requestedBy', 'status', 'requestStatus', 'requestedDate','actions']; public displayedColumns: string[] = ['series', 'requestedBy', 'status', 'requestStatus', 'requestedDate','actions'];
public gridCount: string = "15"; public gridCount: string = "15";
public showUnavailableRequests: boolean;
public isAdmin: boolean; public isAdmin: boolean;
public defaultSort: string = "requestedDate"; public defaultSort: string = "requestedDate";
public defaultOrder: string = "desc"; public defaultOrder: string = "desc";
public currentFilter: RequestFilterType = RequestFilterType.All;
public RequestFilter = RequestFilterType;
private storageKey = "Tv_DefaultRequestListSort"; private storageKey = "Tv_DefaultRequestListSort";
private storageKeyOrder = "Tv_DefaultRequestListSortOrder"; private storageKeyOrder = "Tv_DefaultRequestListSortOrder";
private storageKeyGridCount = "Tv_DefaultGridCount";
private storageKeyCurrentFilter = "Tv_DefaultFilter";
@Output() public onOpenOptions = new EventEmitter<{request: any, filter: any, onChange: any}>(); @Output() public onOpenOptions = new EventEmitter<{request: any, filter: any, onChange: any}>();
@ -38,19 +43,30 @@ export class TvGridComponent implements OnInit, AfterViewInit {
} }
public ngOnInit() { 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 defaultSort = this.storageService.get(this.storageKey);
const defaultOrder = this.storageService.get(this.storageKeyOrder); const defaultOrder = this.storageService.get(this.storageKeyOrder);
const defaultFilter = +this.storageService.get(this.storageKeyCurrentFilter);
if (defaultSort) { if (defaultSort) {
this.defaultSort = defaultSort; this.defaultSort = defaultSort;
} }
if (defaultOrder) { if (defaultOrder) {
this.defaultOrder = defaultOrder; this.defaultOrder = defaultOrder;
} }
if (defaultCount) {
this.gridCount = defaultCount;
}
if (defaultFilter) {
this.currentFilter = defaultFilter;
}
} }
public async ngAfterViewInit() { 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. // If the user changes the sort order, reset back to the first page.
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
@ -93,10 +109,22 @@ export class TvGridComponent implements OnInit, AfterViewInit {
} }
private loadData(): Observable<IRequestsViewModel<IChildRequests>> { private loadData(): Observable<IRequestsViewModel<IChildRequests>> {
if(this.showUnavailableRequests) { switch(RequestFilterType[RequestFilterType[this.currentFilter]]) {
return this.requestService.getTvUnavailableRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction); case RequestFilterType.All:
} else {
return this.requestService.getTvRequests(+this.gridCount, this.paginator.pageIndex * +this.gridCount, this.sort.active, this.sort.direction); 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}); 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>> { 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}); 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> { public updateMovieAdvancedOptions(options: IMovieAdvancedOptions): Observable<IRequestEngineResult> {
return this.http.post<IRequestEngineResult>(`${this.url}movie/advancedoptions`, options, {headers: this.headers}); 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-row">
<div class="mat-cell">Ombi Database</div> <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>
<div class="mat-row"> <div class="mat-row">
<div class="mat-cell">External Database</div> <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>
<div class="mat-row"> <div class="mat-row">
<div class="mat-cell">Settings Database</div> <div class="mat-cell">Settings Database</div>
<div class="mat-cell">{{about.settingsDatabaseType}} - {{about.settingsConnectionString}}</div> <div class="mat-cell">{{about.settingsDatabaseType}}</div>
</div> </div>

View file

@ -9,10 +9,11 @@
<div class="row"> <div class="row">
<div class="form-group col-md-3"> <div class="form-group col-md-3">
<div class="checkbox"> <div>
<input type="checkbox" id="enable" [(ngModel)]="settings.enable" [checked]="settings.enable"> <mat-checkbox [(ngModel)]="settings.enable" [checked]="settings.enable">
<label for="enable">Enable</label> Enable</mat-checkbox>
</div> </div>
</div> </div>
</div> </div>
@ -24,59 +25,52 @@
<br /> <br />
<br /> <br />
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <mat-form-field class="full">
<label for="name" class="control-label">Server name</label> <input matInput placeholder="Server Name" [(ngModel)]="server.name" value="{{server.name}}">
<div> </mat-form-field>
<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>
<div class="form-group"> <mat-form-field class="full">
<label for="portNumber" class="control-label">Port</label> <input matInput placeholder="Hostname or IP" [(ngModel)]="server.ip" value="{{server.ip}}">
<div> </mat-form-field>
<input type="text" [(ngModel)]="server.port" class="form-control form-control-custom " id="portNumber" name="Port" placeholder="Port Number" value="{{server.port}}">
</div>
</div>
<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"> <mat-form-field class="full">
<label for="authToken" class="control-label">Emby Api Key</label> <input matInput placeholder="Port" [(ngModel)]="server.port" value="{{server.port}}">
<div class=""> </mat-form-field>
<input type="text" class="form-control-custom form-control" id="authToken" [(ngModel)]="server.apiKey" placeholder="Emby Api Key" value="{{server.apiKey}}">
</div>
</div>
<div class="form-group"> <mat-checkbox [(ngModel)]="server.ssl" [checked]="server.ssl">
<label for="authToken" class="control-label">Externally Facing Hostname 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" <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> </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> <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}}"> <button mat-raised-button id="testEmby" type="button" (click)="test(server)" color="primary">Test Connectivity <div id="spinner"></div></button>
<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> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div> <div>
<button id="testEmby" type="button" (click)="test(server)" class="btn btn-primary-outline">Test Connectivity <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 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>
</div> </div>
</div> </div>
</div> </div>
@ -88,14 +82,14 @@
<div class="col-md-2"> <div class="col-md-2">
<div class="form-group"> <div class="form-group">
<div> <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>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<div class="form-group"> <div class="form-group">
<div> <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> </div>
</div> </div>

View file

@ -40,7 +40,7 @@
</div> </div>
<div> <div>
<mat-checkbox formControlName="ignoreCertificateErrors" matTooltip="Enable if you are having connectivity problems over SSL"> <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> </mat-checkbox>
</div> </div>
<div> <div>

View file

@ -3,8 +3,7 @@
<head> <head>
<script type='text/javascript'> <script type='text/javascript'>
function configExists(url) {
function configExists(url) {
var req = new XMLHttpRequest(); var req = new XMLHttpRequest();
req.open('GET', url, false); req.open('GET', url, false);
req.send(); req.send();
@ -35,7 +34,7 @@ function configExists(url) {
document.write("<base href='" + (configFound ? basePath : '/') + "' />"); document.write("<base href='" + (configFound ? basePath : '/') + "' />");
console.log(window["baseHref"]); console.log(window["baseHref"]);
</script> </script>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
<link href="styles/please-wait.css" rel="stylesheet"> <link href="styles/please-wait.css" rel="stylesheet">
@ -44,6 +43,13 @@ function configExists(url) {
<script src="styles/please-wait.js"></script> <script src="styles/please-wait.js"></script>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <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> <title>Ombi</title>
@ -53,7 +59,7 @@ function configExists(url) {
<app-ombi> <app-ombi>
<script type="text/javascript"> <script type="text/javascript">
var colors = ["#f44336","#f44336","#9c27b0","#673ab7","#3f51b5","#2196f3","#03a9f4","#00bcd4","#009688","#4caf50","#cddc39","#ffeb3b","#ffc107","#ff9800","#ff5722","#9e9e9e","#607d8b"]; var colors = ["#f44336", "#f44336", "#9c27b0", "#673ab7", "#3f51b5", "#2196f3", "#03a9f4", "#00bcd4", "#009688", "#4caf50", "#cddc39", "#ffeb3b", "#ffc107", "#ff9800", "#ff5722", "#9e9e9e", "#607d8b"];
var bgColor = colors[Math.floor(Math.random() * colors.length)]; var bgColor = colors[Math.floor(Math.random() * colors.length)];
window.loading_screen = window.pleaseWait({ window.loading_screen = window.pleaseWait({
// logo: "assets/images/logo.png", // logo: "assets/images/logo.png",

View file

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

View file

@ -32,7 +32,7 @@ namespace Ombi.Controllers.V2
var model = new List<ConnectedUsersViewModel>(); var model = new List<ConnectedUsersViewModel>();
foreach (var user in users) 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) if (ombiUser == null)
{ {

View file

@ -39,6 +39,10 @@ namespace Ombi.Controllers.V2
var username = User.Identity.Name.ToUpper(); var username = User.Identity.Name.ToUpper();
var user = await _userManager.Users.FirstOrDefaultAsync(x => x.NormalizedUserName == username); var user = await _userManager.Users.FirstOrDefaultAsync(x => x.NormalizedUserName == username);
if (user == null)
{
return Ok();
}
// Check if we already have this notification id // Check if we already have this notification id
var alreadyExists = await _mobileDevices.GetAll().AnyAsync(x => x.Token == body.Token && x.UserId == user.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(); 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 await _mobileDevices.Add(new MobileDevices
{ {
Token = body.Token, Token = body.Token,

View file

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

View file

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

View file

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

View file

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

View file

@ -115,15 +115,15 @@
"MoviesTab": "Movies", "MoviesTab": "Movies",
"TvTab": "TV Shows", "TvTab": "TV Shows",
"MusicTab": "Music", "MusicTab": "Music",
"RequestedBy": "Requested By:", "RequestedBy": "Requested By",
"Status": "Status:", "Status": "Status",
"RequestStatus": "Request status:", "RequestStatus": "Request status",
"Denied": " Denied:", "Denied": " Denied:",
"TheatricalRelease": "Theatrical Release: {{date}}", "TheatricalRelease": "Theatrical Release: {{date}}",
"ReleaseDate": "Released: {{date}}", "ReleaseDate": "Released: {{date}}",
"TheatricalReleaseSort": "Theatrical Release", "TheatricalReleaseSort": "Theatrical Release",
"DigitalRelease": "Digital Release: {{date}}", "DigitalRelease": "Digital Release: {{date}}",
"RequestDate": "Request Date:", "RequestDate": "Request Date",
"QualityOverride": "Quality Override:", "QualityOverride": "Quality Override:",
"RootFolderOverride": "Root Folder Override:", "RootFolderOverride": "Root Folder Override:",
"ChangeRootFolder": "Root Folder", "ChangeRootFolder": "Root Folder",
@ -153,7 +153,16 @@
"NextHours": "Another request will be added in {{time}} hours", "NextHours": "Another request will be added in {{time}} hours",
"NextMinutes": "Another request will be added in {{time}} minutes", "NextMinutes": "Another request will be added in {{time}} minutes",
"NextMinute": "Another request will be added in {{time}} minute" "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": { "Issues": {
"Title": "Issues", "Title": "Issues",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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