From b7bb0869da1163af78f94891dc1a16c6c5b0b0bf Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 12 Feb 2021 21:39:57 +0000 Subject: [PATCH 01/63] Made a start on the issues rework --- src/Ombi.Core/Engine/V2/IssuesEngine.cs | 59 +++++++++++++++++++ src/Ombi.DependencyInjection/IocExtensions.cs | 1 + src/Ombi/ClientApp/angular.json | 5 +- .../ClientApp/src/app/interfaces/IIssues.ts | 6 ++ .../src/app/issues/components/index.ts | 5 ++ .../src/app/issues/issues.component.ts | 17 +++--- .../ClientApp/src/app/issues/issues.module.ts | 3 +- .../src/app/issues/issuestable.component.html | 25 ++------ .../src/app/issues/issuestable.component.ts | 10 +++- .../src/app/services/issuesv2.service.ts | 19 ++++++ src/Ombi/Controllers/V2/IssuesController.cs | 24 ++++++++ 11 files changed, 139 insertions(+), 35 deletions(-) create mode 100644 src/Ombi.Core/Engine/V2/IssuesEngine.cs create mode 100644 src/Ombi/ClientApp/src/app/services/issuesv2.service.ts create mode 100644 src/Ombi/Controllers/V2/IssuesController.cs diff --git a/src/Ombi.Core/Engine/V2/IssuesEngine.cs b/src/Ombi.Core/Engine/V2/IssuesEngine.cs new file mode 100644 index 000000000..a596e7ac9 --- /dev/null +++ b/src/Ombi.Core/Engine/V2/IssuesEngine.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore; +using Ombi.Store.Entities.Requests; +using Ombi.Store.Repository; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ombi.Core.Engine.V2 +{ + + public interface IIssuesEngine + { + Task> GetIssues(int position, int take, IssueStatus status); + } + + public class IssuesEngine : IIssuesEngine + { + private readonly IRepository _categories; + private readonly IRepository _issues; + private readonly IRepository _comments; + + public IssuesEngine(IRepository categories, + IRepository issues, + IRepository comments) + { + _categories = categories; + _issues = issues; + _comments = comments; + } + + public async Task> GetIssues(int position, int take, IssueStatus status) + { + var issues = await _issues.GetAll().Where(x => x.Status == status).Skip(position).Take(take).ToListAsync(); + var grouped = issues.GroupBy(x => x.Title, (key, g) => new { Title = key, Issues = g }); + + var model = new List(); + + foreach(var group in grouped) + { + model.Add(new IssuesSummaryModel + { + Count = group.Issues.Count(), + Title = group.Title, + Issues = group.Issues + }); + } + + return model; + } + + } + + public class IssuesSummaryModel + { + public string Title { get; set; } + public int Count { get; set; } + public IEnumerable Issues { get; set; } + } +} diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index d5e577d84..ef9c9230c 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -114,6 +114,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } public static void RegisterHttp(this IServiceCollection services) diff --git a/src/Ombi/ClientApp/angular.json b/src/Ombi/ClientApp/angular.json index 2e3ff44e5..41030a25a 100644 --- a/src/Ombi/ClientApp/angular.json +++ b/src/Ombi/ClientApp/angular.json @@ -124,5 +124,8 @@ } } }, - "defaultProject": "ombi" + "defaultProject": "ombi", + "cli": { + "analytics": false + } } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/interfaces/IIssues.ts b/src/Ombi/ClientApp/src/app/interfaces/IIssues.ts index dce9882ec..b01749057 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IIssues.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IIssues.ts @@ -62,3 +62,9 @@ export interface IUpdateStatus { issueId: number; status: IssueStatus; } + +export interface IIssuesSummary { + title: string; + count: number; + issues: IIssues[]; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/components/index.ts b/src/Ombi/ClientApp/src/app/issues/components/index.ts index 93bb4907d..932f1eeb4 100644 --- a/src/Ombi/ClientApp/src/app/issues/components/index.ts +++ b/src/Ombi/ClientApp/src/app/issues/components/index.ts @@ -1,6 +1,8 @@ import { AuthGuard } from "../../auth/auth.guard"; import { IssuesListComponent } from "./issues-list/issues-list.component"; import { Routes } from "@angular/router"; +import { IssuesV2Service } from "../../services/issuesv2.service"; +import { IdentityService, SearchService } from "../../services"; @@ -13,6 +15,9 @@ export const entryComponents: any[] = [ ]; export const providers: any[] = [ + IssuesV2Service, + IdentityService, + SearchService, ]; export const routes: Routes = [ diff --git a/src/Ombi/ClientApp/src/app/issues/issues.component.ts b/src/Ombi/ClientApp/src/app/issues/issues.component.ts index b8482f1ff..5dc02845e 100644 --- a/src/Ombi/ClientApp/src/app/issues/issues.component.ts +++ b/src/Ombi/ClientApp/src/app/issues/issues.component.ts @@ -2,9 +2,10 @@ import { Component, OnInit } from "@angular/core"; import { IssuesService } from "../services"; -import { IIssueCount, IIssues, IPagenator, IssueStatus } from "../interfaces"; +import { IIssueCount, IIssues, IIssuesSummary, IPagenator, IssueStatus } from "../interfaces"; import { PageEvent } from '@angular/material/paginator'; +import { IssuesV2Service } from "../services/issuesv2.service"; @Component({ templateUrl: "issues.component.html", @@ -12,9 +13,9 @@ import { PageEvent } from '@angular/material/paginator'; }) export class IssuesComponent implements OnInit { - public pendingIssues: IIssues[]; - public inProgressIssues: IIssues[]; - public resolvedIssues: IIssues[]; + public pendingIssues: IIssuesSummary[]; + public inProgressIssues: IIssuesSummary[]; + public resolvedIssues: IIssuesSummary[]; public count: IIssueCount; @@ -23,7 +24,7 @@ export class IssuesComponent implements OnInit { private inProgressSkip = 0; private resolvedSkip = 0; - constructor(private issueService: IssuesService) { } + constructor(private issuev2Service: IssuesV2Service, private issueService: IssuesService) { } public ngOnInit() { this.getPending(); @@ -48,19 +49,19 @@ export class IssuesComponent implements OnInit { } private getPending() { - this.issueService.getIssuesPage(this.takeAmount, this.pendingSkip, IssueStatus.Pending).subscribe(x => { + this.issuev2Service.getIssues(this.pendingSkip, this.takeAmount, IssueStatus.Pending).subscribe(x => { this.pendingIssues = x; }); } private getInProg() { - this.issueService.getIssuesPage(this.takeAmount, this.inProgressSkip, IssueStatus.InProgress).subscribe(x => { + this.issuev2Service.getIssues(this.inProgressSkip, this.takeAmount, IssueStatus.InProgress).subscribe(x => { this.inProgressIssues = x; }); } private getResolved() { - this.issueService.getIssuesPage(this.takeAmount, this.resolvedSkip, IssueStatus.Resolved).subscribe(x => { + this.issuev2Service.getIssues(this.resolvedSkip, this.takeAmount, IssueStatus.Resolved).subscribe(x => { this.resolvedIssues = x; }); } diff --git a/src/Ombi/ClientApp/src/app/issues/issues.module.ts b/src/Ombi/ClientApp/src/app/issues/issues.module.ts index 2607d336a..ab55f69cf 100644 --- a/src/Ombi/ClientApp/src/app/issues/issues.module.ts +++ b/src/Ombi/ClientApp/src/app/issues/issues.module.ts @@ -39,8 +39,7 @@ const routes: Routes = [ RouterModule, ], providers: [ - IdentityService, - SearchService, + ...fromComponents.providers ], }) diff --git a/src/Ombi/ClientApp/src/app/issues/issuestable.component.html b/src/Ombi/ClientApp/src/app/issues/issuestable.component.html index e266a718a..8244536ad 100644 --- a/src/Ombi/ClientApp/src/app/issues/issuestable.component.html +++ b/src/Ombi/ClientApp/src/app/issues/issuestable.component.html @@ -6,32 +6,15 @@ {{element.title}} - - {{ 'Issues.Category' | translate}} - {{element.issueCategory.value}} - - - - {{ 'Issues.Subject' | translate}} - {{element.subject}} - - - - {{ 'Issues.Status' | translate}} - {{IssueStatus[element.status] | humanize}} - - - - {{ 'Issues.ReportedBy' | translate}} - {{element.userReported.userAlias}} + + {{ 'Issues.Count' | translate}} + {{element.count}} - - - + diff --git a/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts b/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts index ff584a35e..1ad929741 100644 --- a/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts +++ b/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { IIssues, IPagenator, IssueStatus } from "../interfaces"; +import { IIssues, IIssuesSummary, IPagenator, IssueStatus } from "../interfaces"; @Component({ selector: "issues-table", @@ -8,12 +8,12 @@ import { IIssues, IPagenator, IssueStatus } from "../interfaces"; }) export class IssuesTableComponent { - @Input() public issues: IIssues[]; + @Input() public issues: IIssuesSummary[]; @Input() public totalRecords: number; @Output() public changePage = new EventEmitter(); - public displayedColumns = ["title", "category", "subject", "status", "reportedBy", "actions"] + public displayedColumns = ["title", "count", "actions"] public IssueStatus = IssueStatus; public resultsLength: number; public gridCount: string = "15"; @@ -49,4 +49,8 @@ export class IssuesTableComponent { this.changePage.emit(event); } + public openDetails(summary: IIssuesSummary) { + + } + } diff --git a/src/Ombi/ClientApp/src/app/services/issuesv2.service.ts b/src/Ombi/ClientApp/src/app/services/issuesv2.service.ts new file mode 100644 index 000000000..69b5c01c4 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/services/issuesv2.service.ts @@ -0,0 +1,19 @@ +import { PlatformLocation, APP_BASE_HREF } from "@angular/common"; +import { Injectable, Inject } from "@angular/core"; + +import { HttpClient } from "@angular/common/http"; +import { Observable } from "rxjs"; + +import { IIssueCategory, IIssueComments, IIssueCount, IIssues, IIssuesChat, IIssuesSummary, INewIssueComments, IssueStatus, IUpdateStatus } from "../interfaces"; +import { ServiceHelpers } from "./service.helpers"; + +@Injectable() +export class IssuesV2Service extends ServiceHelpers { + constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { + super(http, "/api/v2/Issues/", href); + } + + public getIssues(position: number, take: number, status: IssueStatus): Observable { + return this.http.get(`${this.url}${position}/${take}/${status}`, {headers: this.headers}); + } +} diff --git a/src/Ombi/Controllers/V2/IssuesController.cs b/src/Ombi/Controllers/V2/IssuesController.cs new file mode 100644 index 000000000..c2b65cc99 --- /dev/null +++ b/src/Ombi/Controllers/V2/IssuesController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc; +using Ombi.Core.Engine.V2; +using Ombi.Store.Entities.Requests; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Ombi.Controllers.V2 +{ + public class IssuesController : V2Controller + { + private readonly IIssuesEngine _engine; + + public IssuesController(IIssuesEngine engine) + { + _engine = engine; + } + + [HttpGet("{position}/{take}/{status}")] + public Task> GetIssuesSummary(int position, int take, IssueStatus status) + { + return _engine.GetIssues(position, take, status); + } + } +} From 1289b840a051141f43e7beb8e3e751325c5c311d Mon Sep 17 00:00:00 2001 From: tidusjar Date: Sat, 13 Feb 2021 22:35:07 +0000 Subject: [PATCH 02/63] more wip on the issues --- src/Ombi.Core/Engine/V2/IssuesEngine.cs | 29 +++++++++-- .../details-group.component.html | 27 +++++++++++ .../details-group.component.scss | 9 ++++ .../details-group/details-group.component.ts | 48 +++++++++++++++++++ .../src/app/issues/components/index.ts | 6 +-- .../app/issues/issueDetails.component.html | 2 +- .../src/app/issues/issues.component.html | 40 +++++++--------- .../ClientApp/src/app/issues/issues.module.ts | 4 +- .../src/app/issues/issuestable.component.ts | 9 +++- src/Ombi/Controllers/V1/IssuesController.cs | 2 +- src/Ombi/Controllers/V2/IssuesController.cs | 2 +- 11 files changed, 141 insertions(+), 37 deletions(-) create mode 100644 src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.html create mode 100644 src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.scss create mode 100644 src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.ts diff --git a/src/Ombi.Core/Engine/V2/IssuesEngine.cs b/src/Ombi.Core/Engine/V2/IssuesEngine.cs index a596e7ac9..f154d0eca 100644 --- a/src/Ombi.Core/Engine/V2/IssuesEngine.cs +++ b/src/Ombi.Core/Engine/V2/IssuesEngine.cs @@ -3,6 +3,7 @@ using Ombi.Store.Entities.Requests; using Ombi.Store.Repository; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Ombi.Core.Engine.V2 @@ -10,7 +11,8 @@ namespace Ombi.Core.Engine.V2 public interface IIssuesEngine { - Task> GetIssues(int position, int take, IssueStatus status); + Task> GetIssues(int position, int take, IssueStatus status, CancellationToken token); + Task> GetIssuesByTitle(string title, CancellationToken token); } public class IssuesEngine : IIssuesEngine @@ -28,9 +30,9 @@ namespace Ombi.Core.Engine.V2 _comments = comments; } - public async Task> GetIssues(int position, int take, IssueStatus status) + public async Task> GetIssues(int position, int take, IssueStatus status, CancellationToken token) { - var issues = await _issues.GetAll().Where(x => x.Status == status).Skip(position).Take(take).ToListAsync(); + var issues = await _issues.GetAll().Include(x => x.UserReported).Include(x => x.IssueCategory).Where(x => x.Status == status).Skip(position).Take(take).OrderBy(x => x.Title).ToListAsync(token); var grouped = issues.GroupBy(x => x.Title, (key, g) => new { Title = key, Issues = g }); var model = new List(); @@ -48,6 +50,27 @@ namespace Ombi.Core.Engine.V2 return model; } + public async Task> GetIssuesByTitle(string title, CancellationToken token) + { + var lowerTitle = title.ToLowerInvariant(); + var issues = await _issues.GetAll().Include(x => x.UserReported).Include(x => x.IssueCategory).Where(x => x.Title.ToLowerInvariant() == lowerTitle).ToListAsync(token); + var grouped = issues.GroupBy(x => x.Title, (key, g) => new { Title = key, Issues = g }); + + var model = new List(); + + foreach (var group in grouped) + { + model.Add(new IssuesSummaryModel + { + Count = group.Issues.Count(), + Title = group.Title, + Issues = group.Issues + }); + } + + return model; + } + } public class IssuesSummaryModel diff --git a/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.html b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.html new file mode 100644 index 000000000..7c3c04fdd --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.html @@ -0,0 +1,27 @@ +

Issues for {{data.title}}

+
+ +
+
+ + + {{issue.subject}} + {{issue.userReported?.userName}} on {{issue.createdDate | date:short}} + + +

+ {{issue.description}} +

+
+ + + + +
+
+
+
+
+ + +
diff --git a/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.scss b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.scss new file mode 100644 index 000000000..d6dcd67de --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.scss @@ -0,0 +1,9 @@ +@import "~styles/variables.scss"; + +::ng-deep .mat-card { + background: $ombi-background-primary-accent; +} + +.top-spacing { + margin-top:2%; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.ts b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.ts new file mode 100644 index 000000000..aea5eb123 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.ts @@ -0,0 +1,48 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { AuthService } from "../../../auth/auth.service"; +import { IIssues, IIssueSettings, IssueStatus } from "../../../interfaces"; +import { SettingsService } from "../../../services"; + + +export interface IssuesDetailsGroupData { + issues: IIssues[]; + title: string; + } + +@Component({ + selector: "issues-details-group", + templateUrl: "details-group.component.html", + styleUrls: ["details-group.component.scss"], +}) +export class DetailsGroupComponent implements OnInit { + + public isAdmin: boolean; + public IssueStatus = IssueStatus; + public settings: IIssueSettings; + public get hasRequest(): boolean { + return this.data.issues.some(x => x.requestId); + } + + constructor(public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: IssuesDetailsGroupData, + private authService: AuthService, private settingsService: SettingsService) { } + + public ngOnInit() { + this.isAdmin = this.authService.hasRole("Admin") || this.authService.hasRole("PowerUser"); + this.settingsService.getIssueSettings().subscribe(x => this.settings = x); + } + + public close() { + this.dialogRef.close(); + } + + public navToRequest() { + var issue = this.data.issues.filter(x => { + return x.requestId; + })[0]; + + // close dialog and tell calling component to navigate + } + +} diff --git a/src/Ombi/ClientApp/src/app/issues/components/index.ts b/src/Ombi/ClientApp/src/app/issues/components/index.ts index 932f1eeb4..7bf8e197a 100644 --- a/src/Ombi/ClientApp/src/app/issues/components/index.ts +++ b/src/Ombi/ClientApp/src/app/issues/components/index.ts @@ -3,15 +3,13 @@ import { IssuesListComponent } from "./issues-list/issues-list.component"; import { Routes } from "@angular/router"; import { IssuesV2Service } from "../../services/issuesv2.service"; import { IdentityService, SearchService } from "../../services"; +import { DetailsGroupComponent } from "./details-group/details-group.component"; export const components: any[] = [ IssuesListComponent, -]; - - -export const entryComponents: any[] = [ + DetailsGroupComponent, ]; export const providers: any[] = [ diff --git a/src/Ombi/ClientApp/src/app/issues/issueDetails.component.html b/src/Ombi/ClientApp/src/app/issues/issueDetails.component.html index c2de76c2b..b5fc59304 100644 --- a/src/Ombi/ClientApp/src/app/issues/issueDetails.component.html +++ b/src/Ombi/ClientApp/src/app/issues/issueDetails.component.html @@ -84,7 +84,7 @@
- +
diff --git a/src/Ombi/ClientApp/src/app/issues/issues.component.html b/src/Ombi/ClientApp/src/app/issues/issues.component.html index ff8440569..4d9daeffc 100644 --- a/src/Ombi/ClientApp/src/app/issues/issues.component.html +++ b/src/Ombi/ClientApp/src/app/issues/issues.component.html @@ -1,25 +1,19 @@
- - - -
- -
-
-
- - -
- -
-
-
- - -
- -
-
-
-
+
+ +
+

{{'Issues.PendingTitle' | translate}}

+ +
+ +
+

{{'Issues.InProgressTitle' | translate}}

+ +
+ +
+

{{'Issues.ResolvedTitle' | translate}}

+ +
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/issues.module.ts b/src/Ombi/ClientApp/src/app/issues/issues.module.ts index ab55f69cf..70b8a4bb1 100644 --- a/src/Ombi/ClientApp/src/app/issues/issues.module.ts +++ b/src/Ombi/ClientApp/src/app/issues/issues.module.ts @@ -3,8 +3,6 @@ import { RouterModule, Routes } from "@angular/router"; import { OrderModule } from "ngx-order-pipe"; -import { IdentityService, SearchService } from "../services"; - import { AuthGuard } from "../auth/auth.guard"; import { SharedModule as OmbiShared } from "../shared/shared.module"; @@ -27,7 +25,7 @@ const routes: Routes = [ RouterModule.forChild(routes), OrderModule, PipeModule, - OmbiShared, + OmbiShared ], declarations: [ IssuesComponent, diff --git a/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts b/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts index 1ad929741..7ed826fc5 100644 --- a/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts +++ b/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts @@ -1,6 +1,8 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; import { IIssues, IIssuesSummary, IPagenator, IssueStatus } from "../interfaces"; +import { DetailsGroupComponent, IssuesDetailsGroupData } from "./components/details-group/details-group.component"; @Component({ selector: "issues-table", @@ -8,6 +10,8 @@ import { IIssues, IIssuesSummary, IPagenator, IssueStatus } from "../interfaces" }) export class IssuesTableComponent { + constructor(public dialog: MatDialog) { } + @Input() public issues: IIssuesSummary[]; @Input() public totalRecords: number; @@ -50,7 +54,10 @@ export class IssuesTableComponent { } public openDetails(summary: IIssuesSummary) { - + const dialogRef = this.dialog.open(DetailsGroupComponent, { + width: "50vw", + data: { issues: summary.issues, title: summary.title }, + }); } } diff --git a/src/Ombi/Controllers/V1/IssuesController.cs b/src/Ombi/Controllers/V1/IssuesController.cs index 4dfd3781a..23b2320d1 100644 --- a/src/Ombi/Controllers/V1/IssuesController.cs +++ b/src/Ombi/Controllers/V1/IssuesController.cs @@ -142,7 +142,7 @@ namespace Ombi.Controllers.V1 var notificationModel = new NotificationOptions { RequestId = i.RequestId ?? 0, - DateTime = DateTime.Now, + DateTime = DateTime.UtcNow, NotificationType = NotificationType.Issue, RequestType = i.RequestType, Recipient = string.Empty, diff --git a/src/Ombi/Controllers/V2/IssuesController.cs b/src/Ombi/Controllers/V2/IssuesController.cs index c2b65cc99..214482b54 100644 --- a/src/Ombi/Controllers/V2/IssuesController.cs +++ b/src/Ombi/Controllers/V2/IssuesController.cs @@ -18,7 +18,7 @@ namespace Ombi.Controllers.V2 [HttpGet("{position}/{take}/{status}")] public Task> GetIssuesSummary(int position, int take, IssueStatus status) { - return _engine.GetIssues(position, take, status); + return _engine.GetIssues(position, take, status, HttpContext.RequestAborted); } } } From 6683f8070ca6f59e9c4e2c2b3b3c9d5b9c35d58f Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 15 Feb 2021 22:42:23 +0000 Subject: [PATCH 03/63] more wip on the issues --- src/Ombi.Core/Engine/V2/IssuesEngine.cs | 33 ++++--- src/Ombi/ClientApp/src/app/app.module.ts | 1 + .../ClientApp/src/app/interfaces/IIssues.ts | 1 + .../components/details/details.component.html | 38 ++++++++ .../components/details/details.component.scss | 9 ++ .../components/details/details.component.ts | 88 +++++++++++++++++++ .../src/app/issues/components/index.ts | 8 +- .../issues-list/issues-list.component.html | 21 ----- .../issues-list/issues-list.component.ts | 68 -------------- .../issues-list/issues-list.constants.ts | 3 - .../src/app/issues/issues.component.html | 39 ++++---- .../ClientApp/src/app/issues/issues.module.ts | 7 +- .../src/app/issues/issuestable.component.html | 2 +- .../src/app/issues/issuestable.component.ts | 11 +-- .../movie/movie-details.component.ts | 6 +- .../issues-panel/issues-panel.component.ts | 8 +- .../shared/new-issue/new-issue.component.ts | 6 +- .../src/app/services/issuesv2.service.ts | 4 + src/Ombi/Controllers/V2/IssuesController.cs | 7 ++ 19 files changed, 209 insertions(+), 151 deletions(-) create mode 100644 src/Ombi/ClientApp/src/app/issues/components/details/details.component.html create mode 100644 src/Ombi/ClientApp/src/app/issues/components/details/details.component.scss create mode 100644 src/Ombi/ClientApp/src/app/issues/components/details/details.component.ts delete mode 100644 src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.component.html delete mode 100644 src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.component.ts delete mode 100644 src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.constants.ts diff --git a/src/Ombi.Core/Engine/V2/IssuesEngine.cs b/src/Ombi.Core/Engine/V2/IssuesEngine.cs index f154d0eca..f938e8ec6 100644 --- a/src/Ombi.Core/Engine/V2/IssuesEngine.cs +++ b/src/Ombi.Core/Engine/V2/IssuesEngine.cs @@ -12,7 +12,7 @@ namespace Ombi.Core.Engine.V2 public interface IIssuesEngine { Task> GetIssues(int position, int take, IssueStatus status, CancellationToken token); - Task> GetIssuesByTitle(string title, CancellationToken token); + Task GetIssuesByProviderId(string providerId, CancellationToken token); } public class IssuesEngine : IIssuesEngine @@ -32,7 +32,7 @@ namespace Ombi.Core.Engine.V2 public async Task> GetIssues(int position, int take, IssueStatus status, CancellationToken token) { - var issues = await _issues.GetAll().Include(x => x.UserReported).Include(x => x.IssueCategory).Where(x => x.Status == status).Skip(position).Take(take).OrderBy(x => x.Title).ToListAsync(token); + var issues = await _issues.GetAll().Where(x => x.Status == status).Skip(position).Take(take).OrderBy(x => x.Title).ToListAsync(token); var grouped = issues.GroupBy(x => x.Title, (key, g) => new { Title = key, Issues = g }); var model = new List(); @@ -43,32 +43,30 @@ namespace Ombi.Core.Engine.V2 { Count = group.Issues.Count(), Title = group.Title, - Issues = group.Issues + ProviderId = group.Issues.FirstOrDefault()?.ProviderId }); } return model; } - public async Task> GetIssuesByTitle(string title, CancellationToken token) + public async Task GetIssuesByProviderId(string providerId, CancellationToken token) { - var lowerTitle = title.ToLowerInvariant(); - var issues = await _issues.GetAll().Include(x => x.UserReported).Include(x => x.IssueCategory).Where(x => x.Title.ToLowerInvariant() == lowerTitle).ToListAsync(token); - var grouped = issues.GroupBy(x => x.Title, (key, g) => new { Title = key, Issues = g }); - - var model = new List(); + var issues = await _issues.GetAll().Include(x => x.Comments).ThenInclude(x => x.User).Include(x => x.UserReported).Include(x => x.IssueCategory).Where(x => x.ProviderId == providerId).ToListAsync(token); + var grouped = issues.GroupBy(x => x.Title, (key, g) => new { Title = key, Issues = g }).FirstOrDefault(); - foreach (var group in grouped) + if (grouped == null) { - model.Add(new IssuesSummaryModel - { - Count = group.Issues.Count(), - Title = group.Title, - Issues = group.Issues - }); + return null; } - return model; + return new IssuesSummaryModel + { + Count = grouped.Issues.Count(), + Title = grouped.Title, + ProviderId = grouped.Issues.FirstOrDefault()?.ProviderId, + Issues = grouped.Issues + }; } } @@ -77,6 +75,7 @@ namespace Ombi.Core.Engine.V2 { public string Title { get; set; } public int Count { get; set; } + public string ProviderId { get; set; } public IEnumerable Issues { get; set; } } } diff --git a/src/Ombi/ClientApp/src/app/app.module.ts b/src/Ombi/ClientApp/src/app/app.module.ts index cfaf783cf..4de2c9f9f 100644 --- a/src/Ombi/ClientApp/src/app/app.module.ts +++ b/src/Ombi/ClientApp/src/app/app.module.ts @@ -145,6 +145,7 @@ export function JwtTokenGetter() { MatCheckboxModule, MatProgressSpinnerModule, MDBBootstrapModule.forRoot(), + // NbThemeModule.forRoot({ name: 'dark'}), JwtModule.forRoot({ config: { tokenGetter: JwtTokenGetter, diff --git a/src/Ombi/ClientApp/src/app/interfaces/IIssues.ts b/src/Ombi/ClientApp/src/app/interfaces/IIssues.ts index b01749057..10de2a596 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IIssues.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IIssues.ts @@ -66,5 +66,6 @@ export interface IUpdateStatus { export interface IIssuesSummary { title: string; count: number; + providerId: string; issues: IIssues[]; } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/components/details/details.component.html b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.html new file mode 100644 index 000000000..b8e8dc1f6 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.html @@ -0,0 +1,38 @@ +
+ +

Issues for {{details.title}}

+
+ Has Request {{hasRequest}} +
+
+
+
+
{{issue.subject}}
+ {{issue.userReported?.userName}} on {{issue.createdDate | date:short}} +
+
+

+ {{issue.description}} +

+
+
+ + + + + + +
+ +
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/components/details/details.component.scss b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.scss new file mode 100644 index 000000000..d6dcd67de --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.scss @@ -0,0 +1,9 @@ +@import "~styles/variables.scss"; + +::ng-deep .mat-card { + background: $ombi-background-primary-accent; +} + +.top-spacing { + margin-top:2%; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/components/details/details.component.ts b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.ts new file mode 100644 index 000000000..ed00bc041 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.ts @@ -0,0 +1,88 @@ +import { Component, Inject, OnInit, ViewEncapsulation } from "@angular/core"; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { ActivatedRoute, ActivatedRouteSnapshot, Router } from "@angular/router"; +import { TranslateService } from "@ngx-translate/core"; +import { AuthService } from "../../../auth/auth.service"; +import { IIssues, IIssueSettings, IIssuesSummary, IssueStatus, RequestType } from "../../../interfaces"; +import { IssuesService, NotificationService, SettingsService } from "../../../services"; +import { IssuesV2Service } from "../../../services/issuesv2.service"; + + +export interface IssuesDetailsGroupData { + issues: IIssues[]; + title: string; +} + +@Component({ + selector: "issues-details", + templateUrl: "details.component.html", + styleUrls: ["details.component.scss"], + encapsulation: ViewEncapsulation.None +}) +export class IssuesDetailsComponent implements OnInit { + + public details: IIssuesSummary; + public isAdmin: boolean; + public IssueStatus = IssueStatus; + public settings: IIssueSettings; + public get hasRequest(): boolean { + return this.details.issues.some(x => x.requestId); + } + + private providerId: string; + + constructor(private authService: AuthService, private settingsService: SettingsService, + private issueServiceV2: IssuesV2Service, private route: ActivatedRoute, private router: Router, + private issuesService: IssuesService, private translateService: TranslateService, private notificationService: NotificationService) { + this.route.params.subscribe(async (params: any) => { + if (typeof params.providerId === 'string' || params.providerId instanceof String) { + this.providerId = params.providerId; + } + }); + } + + public ngOnInit() { + this.isAdmin = this.authService.hasRole("Admin") || this.authService.hasRole("PowerUser"); + this.settingsService.getIssueSettings().subscribe(x => this.settings = x); + this.issueServiceV2.getIssuesByProviderId(this.providerId).subscribe(x => this.details = x); + } + + public resolve(issue: IIssues) { + this.issuesService.updateStatus({issueId: issue.id, status: IssueStatus.Resolved}).subscribe(x => { + this.notificationService.success(this.translateService.instant("Issues.MarkedAsResolved")); + issue.status = IssueStatus.Resolved; + }); + } + + public inProgress(issue: IIssues) { + this.issuesService.updateStatus({issueId: issue.id, status: IssueStatus.InProgress}).subscribe(x => { + this.notificationService.success(this.translateService.instant("Issues.MarkedAsInProgress")); + issue.status = IssueStatus.InProgress; + }); + } + + public async delete(issue: IIssues) { + await this.issuesService.deleteIssue(issue.id); + this.notificationService.success(this.translateService.instant("Issues.DeletedIssue")); + this.details.issues = this.details.issues.filter((el) => { return el.id !== issue.id; }); + } + + + public navToMedia() { + const firstIssue = this.details.issues[0]; + switch(firstIssue.requestType) { + case RequestType.movie: + this.router.navigate(['/details/movie/', firstIssue.providerId]); + return; + + case RequestType.album: + this.router.navigate(['/details/artist/', firstIssue.providerId]); + return; + + case RequestType.tvShow: + this.router.navigate(['/details/tv/', firstIssue.providerId]); + return; + } + } + +} diff --git a/src/Ombi/ClientApp/src/app/issues/components/index.ts b/src/Ombi/ClientApp/src/app/issues/components/index.ts index 7bf8e197a..86ceefe89 100644 --- a/src/Ombi/ClientApp/src/app/issues/components/index.ts +++ b/src/Ombi/ClientApp/src/app/issues/components/index.ts @@ -1,23 +1,19 @@ import { AuthGuard } from "../../auth/auth.guard"; -import { IssuesListComponent } from "./issues-list/issues-list.component"; import { Routes } from "@angular/router"; import { IssuesV2Service } from "../../services/issuesv2.service"; import { IdentityService, SearchService } from "../../services"; import { DetailsGroupComponent } from "./details-group/details-group.component"; +import { IssuesDetailsComponent } from "./details/details.component"; export const components: any[] = [ - IssuesListComponent, DetailsGroupComponent, + IssuesDetailsComponent, ]; export const providers: any[] = [ IssuesV2Service, IdentityService, SearchService, -]; - -export const routes: Routes = [ - { path: "", component: IssuesListComponent, canActivate: [AuthGuard] }, ]; \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.component.html b/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.component.html deleted file mode 100644 index 61f154193..000000000 --- a/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.component.html +++ /dev/null @@ -1,21 +0,0 @@ - \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.component.ts b/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.component.ts deleted file mode 100644 index 82883a4f2..000000000 --- a/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.component.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Component, OnInit } from "@angular/core"; - -import { IssuesService } from "../../../services"; - -import { IIssueCount, IIssues, IPagenator, IssueStatus } from "../../../interfaces"; -import { COLUMNS } from "./issues-list.constants"; - -@Component({ - selector: "issues-list", - templateUrl: "issues-list.component.html", -}) -export class IssuesListComponent implements OnInit { - - public columnsToDisplay = COLUMNS - - public pendingIssues: IIssues[]; - public inProgressIssues: IIssues[]; - public resolvedIssues: IIssues[]; - - public count: IIssueCount; - - private takeAmount = 10; - private pendingSkip = 0; - private inProgressSkip = 0; - private resolvedSkip = 0; - - constructor(private issueService: IssuesService) { } - - public ngOnInit() { - this.getPending(); - this.getInProg(); - this.getResolved(); - this.issueService.getIssuesCount().subscribe(x => this.count = x); - } - - public changePagePending(event: IPagenator) { - this.pendingSkip = event.first; - this.getPending(); - } - - public changePageInProg(event: IPagenator) { - this.inProgressSkip = event.first; - this.getInProg(); - } - - public changePageResolved(event: IPagenator) { - this.resolvedSkip = event.first; - this.getResolved(); - } - - private getPending() { - this.issueService.getIssuesPage(this.takeAmount, this.pendingSkip, IssueStatus.Pending).subscribe(x => { - this.pendingIssues = x; - }); - } - - private getInProg() { - this.issueService.getIssuesPage(this.takeAmount, this.inProgressSkip, IssueStatus.InProgress).subscribe(x => { - this.inProgressIssues = x; - }); - } - - private getResolved() { - this.issueService.getIssuesPage(this.takeAmount, this.resolvedSkip, IssueStatus.Resolved).subscribe(x => { - this.resolvedIssues = x; - }); - } -} diff --git a/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.constants.ts b/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.constants.ts deleted file mode 100644 index f4a00e24b..000000000 --- a/src/Ombi/ClientApp/src/app/issues/components/issues-list/issues-list.constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const COLUMNS = [ - "title" -] \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/issues.component.html b/src/Ombi/ClientApp/src/app/issues/issues.component.html index 4d9daeffc..72734ed85 100644 --- a/src/Ombi/ClientApp/src/app/issues/issues.component.html +++ b/src/Ombi/ClientApp/src/app/issues/issues.component.html @@ -1,19 +1,28 @@
-
+
-
-

{{'Issues.PendingTitle' | translate}}

- -
- -
-

{{'Issues.InProgressTitle' | translate}}

- -
- -
-

{{'Issues.ResolvedTitle' | translate}}

- -
+ + + +
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/issues.module.ts b/src/Ombi/ClientApp/src/app/issues/issues.module.ts index 70b8a4bb1..08896a086 100644 --- a/src/Ombi/ClientApp/src/app/issues/issues.module.ts +++ b/src/Ombi/ClientApp/src/app/issues/issues.module.ts @@ -1,5 +1,6 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +// import { NbChatModule, NbThemeModule } from '@nebular/theme'; import { OrderModule } from "ngx-order-pipe"; @@ -10,6 +11,7 @@ import { SharedModule as OmbiShared } from "../shared/shared.module"; import { IssueDetailsComponent } from "./issueDetails.component"; import { IssuesComponent } from "./issues.component"; import { IssuesTableComponent } from "./issuestable.component"; +import { IssuesDetailsComponent } from "./components/details/details.component"; import { PipeModule } from "../pipes/pipe.module"; @@ -17,7 +19,7 @@ import * as fromComponents from "./components"; const routes: Routes = [ { path: "", component: IssuesComponent, canActivate: [AuthGuard] }, - { path: ":id", component: IssueDetailsComponent, canActivate: [AuthGuard] }, + { path: ":providerId", component: IssuesDetailsComponent, canActivate: [AuthGuard] }, ]; @NgModule({ @@ -25,7 +27,8 @@ const routes: Routes = [ RouterModule.forChild(routes), OrderModule, PipeModule, - OmbiShared + OmbiShared, + // NbChatModule, ], declarations: [ IssuesComponent, diff --git a/src/Ombi/ClientApp/src/app/issues/issuestable.component.html b/src/Ombi/ClientApp/src/app/issues/issuestable.component.html index 8244536ad..8e22d3e51 100644 --- a/src/Ombi/ClientApp/src/app/issues/issuestable.component.html +++ b/src/Ombi/ClientApp/src/app/issues/issuestable.component.html @@ -14,7 +14,7 @@ - + diff --git a/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts b/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts index 7ed826fc5..0fd81a534 100644 --- a/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts +++ b/src/Ombi/ClientApp/src/app/issues/issuestable.component.ts @@ -1,8 +1,7 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; import { MatDialog } from "@angular/material/dialog"; -import { IIssues, IIssuesSummary, IPagenator, IssueStatus } from "../interfaces"; -import { DetailsGroupComponent, IssuesDetailsGroupData } from "./components/details-group/details-group.component"; +import { IIssuesSummary, IPagenator, IssueStatus } from "../interfaces"; @Component({ selector: "issues-table", @@ -52,12 +51,4 @@ export class IssuesTableComponent { public paginate(event: IPagenator) { this.changePage.emit(event); } - - public openDetails(summary: IIssuesSummary) { - const dialogRef = this.dialog.open(DetailsGroupComponent, { - width: "50vw", - data: { issues: summary.issues, title: summary.title }, - }); - } - } diff --git a/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.ts index 988859245..912f8a32c 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.ts +++ b/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.ts @@ -113,9 +113,13 @@ export class MovieDetailsComponent { } public async issue() { + let provider = this.movie.id.toString(); + if (this.movie.imdbId) { + provider = this.movie.imdbId; + } const dialogRef = this.dialog.open(NewIssueComponent, { width: '500px', - data: { requestId: this.movieRequest ? this.movieRequest.id : null, requestType: RequestType.movie, providerId: this.movie.imdbId ? this.movie.imdbId : this.movie.id, title: this.movie.title } + data: { requestId: this.movieRequest ? this.movieRequest.id : null, requestType: RequestType.movie, providerId: provider, title: this.movie.title } }); } diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/issues-panel/issues-panel.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/shared/issues-panel/issues-panel.component.ts index dda7bcb89..206be2141 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/shared/issues-panel/issues-panel.component.ts +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/issues-panel/issues-panel.component.ts @@ -6,10 +6,11 @@ import { TranslateService } from "@ngx-translate/core"; @Component({ selector: "issues-panel", templateUrl: "./issues-panel.component.html", - styleUrls: ["./issues-panel.component.scss"] + styleUrls: ["./issues-panel.component.scss"], + }) export class IssuesPanelComponent implements OnInit { - + @Input() public providerId: string; @Input() public isAdmin: boolean; @@ -22,7 +23,6 @@ export class IssuesPanelComponent implements OnInit { constructor(private issuesService: IssuesService, private notificationService: NotificationService, private translateService: TranslateService, private settingsService: SettingsService) { - } public async ngOnInit() { @@ -54,7 +54,7 @@ export class IssuesPanelComponent implements OnInit { this.issuesCount = this.issues.length; this.calculateOutstanding(); } - + private calculateOutstanding() { this.isOutstanding = this.issues.some((i) => { return i.status !== IssueStatus.Resolved; diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/new-issue/new-issue.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/shared/new-issue/new-issue.component.ts index c42c14fe2..94972c70d 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/shared/new-issue/new-issue.component.ts +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/new-issue/new-issue.component.ts @@ -10,7 +10,7 @@ import { TranslateService } from "@ngx-translate/core"; templateUrl: "./new-issue.component.html", }) export class NewIssueComponent implements OnInit { - + public issue: IIssues; public issueCategories: IIssueCategory[]; @@ -40,9 +40,9 @@ export class NewIssueComponent implements OnInit { public async ngOnInit(): Promise { this.issueCategories = await this.issueService.getCategories().toPromise(); - } + } - public async createIssue() { + public async createIssue() { const result = await this.issueService.createIssue(this.issue).toPromise(); if(result) { this.messageService.send(this.translate.instant("Issues.IssueDialog.IssueCreated")); diff --git a/src/Ombi/ClientApp/src/app/services/issuesv2.service.ts b/src/Ombi/ClientApp/src/app/services/issuesv2.service.ts index 69b5c01c4..d792702a0 100644 --- a/src/Ombi/ClientApp/src/app/services/issuesv2.service.ts +++ b/src/Ombi/ClientApp/src/app/services/issuesv2.service.ts @@ -16,4 +16,8 @@ export class IssuesV2Service extends ServiceHelpers { public getIssues(position: number, take: number, status: IssueStatus): Observable { return this.http.get(`${this.url}${position}/${take}/${status}`, {headers: this.headers}); } + + public getIssuesByProviderId(providerId: string): Observable { + return this.http.get(`${this.url}details/${providerId}`, {headers: this.headers}); + } } diff --git a/src/Ombi/Controllers/V2/IssuesController.cs b/src/Ombi/Controllers/V2/IssuesController.cs index 214482b54..84eafa3ad 100644 --- a/src/Ombi/Controllers/V2/IssuesController.cs +++ b/src/Ombi/Controllers/V2/IssuesController.cs @@ -20,5 +20,12 @@ namespace Ombi.Controllers.V2 { return _engine.GetIssues(position, take, status, HttpContext.RequestAborted); } + + + [HttpGet("details/{providerId}")] + public Task GetIssueDetails(string providerId) + { + return _engine.GetIssuesByProviderId(providerId, HttpContext.RequestAborted); + } } } From f777e5d171886a85f2968fa4d5a2c54a8a609df2 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 26 Feb 2021 23:10:52 +0000 Subject: [PATCH 04/63] Added the basic comment functionality --- .../ClientApp/src/app/auth/auth.service.ts | 4 + .../details-group.component.html | 1 - .../components/details/details.component.html | 5 +- .../components/details/details.component.ts | 9 +- .../src/app/issues/components/index.ts | 6 +- .../issue-chat/issue-chat.component.html | 6 + .../issue-chat/issue-chat.component.scss | 9 + .../issue-chat/issue-chat.component.ts | 87 +++++ .../ClientApp/src/app/issues/issues.module.ts | 4 +- .../shared/chat-box/chat-box.component.html | 23 ++ .../shared/chat-box/chat-box.component.scss | 329 ++++++++++++++++++ .../app/shared/chat-box/chat-box.component.ts | 46 +++ 12 files changed, 520 insertions(+), 9 deletions(-) create mode 100644 src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.html create mode 100644 src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.scss create mode 100644 src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.ts create mode 100644 src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.html create mode 100644 src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.scss create mode 100644 src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.ts diff --git a/src/Ombi/ClientApp/src/app/auth/auth.service.ts b/src/Ombi/ClientApp/src/app/auth/auth.service.ts index 3eace43a4..afc0a2491 100644 --- a/src/Ombi/ClientApp/src/app/auth/auth.service.ts +++ b/src/Ombi/ClientApp/src/app/auth/auth.service.ts @@ -74,6 +74,10 @@ export class AuthService extends ServiceHelpers { return false; } + public isAdmin() { + return this.hasRole("Admin") || this.hasRole("PowerUser"); + } + public logout() { this.store.remove("id_token"); } diff --git a/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.html b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.html index 7c3c04fdd..9738e24c7 100644 --- a/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.html +++ b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.html @@ -1,6 +1,5 @@

Issues for {{data.title}}

-
diff --git a/src/Ombi/ClientApp/src/app/issues/components/details/details.component.html b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.html index b8e8dc1f6..08cb2dc44 100644 --- a/src/Ombi/ClientApp/src/app/issues/components/details/details.component.html +++ b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.html @@ -3,6 +3,7 @@

Issues for {{details.title}}

Has Request {{hasRequest}} +
@@ -16,7 +17,7 @@

- +
-
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/components/details/details.component.ts b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.ts index ed00bc041..accc99e60 100644 --- a/src/Ombi/ClientApp/src/app/issues/components/details/details.component.ts +++ b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.ts @@ -1,11 +1,12 @@ import { Component, Inject, OnInit, ViewEncapsulation } from "@angular/core"; -import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { ActivatedRoute, ActivatedRouteSnapshot, Router } from "@angular/router"; import { TranslateService } from "@ngx-translate/core"; import { AuthService } from "../../../auth/auth.service"; import { IIssues, IIssueSettings, IIssuesSummary, IssueStatus, RequestType } from "../../../interfaces"; import { IssuesService, NotificationService, SettingsService } from "../../../services"; import { IssuesV2Service } from "../../../services/issuesv2.service"; +import { IssueChatComponent } from "../issue-chat/issue-chat.component"; export interface IssuesDetailsGroupData { @@ -33,7 +34,8 @@ export class IssuesDetailsComponent implements OnInit { constructor(private authService: AuthService, private settingsService: SettingsService, private issueServiceV2: IssuesV2Service, private route: ActivatedRoute, private router: Router, - private issuesService: IssuesService, private translateService: TranslateService, private notificationService: NotificationService) { + private issuesService: IssuesService, private translateService: TranslateService, private notificationService: NotificationService, + private dialog: MatDialog) { this.route.params.subscribe(async (params: any) => { if (typeof params.providerId === 'string' || params.providerId instanceof String) { this.providerId = params.providerId; @@ -67,6 +69,9 @@ export class IssuesDetailsComponent implements OnInit { this.details.issues = this.details.issues.filter((el) => { return el.id !== issue.id; }); } + public openChat(issue: IIssues) { + this.dialog.open(IssueChatComponent, { width: "80vh", data: { issueId: issue.id }, panelClass: 'modal-panel' }) + } public navToMedia() { const firstIssue = this.details.issues[0]; diff --git a/src/Ombi/ClientApp/src/app/issues/components/index.ts b/src/Ombi/ClientApp/src/app/issues/components/index.ts index 86ceefe89..99a07492b 100644 --- a/src/Ombi/ClientApp/src/app/issues/components/index.ts +++ b/src/Ombi/ClientApp/src/app/issues/components/index.ts @@ -1,15 +1,17 @@ -import { AuthGuard } from "../../auth/auth.guard"; -import { Routes } from "@angular/router"; import { IssuesV2Service } from "../../services/issuesv2.service"; import { IdentityService, SearchService } from "../../services"; import { DetailsGroupComponent } from "./details-group/details-group.component"; import { IssuesDetailsComponent } from "./details/details.component"; +import { IssueChatComponent } from "./issue-chat/issue-chat.component"; +import { ChatBoxComponent } from "../../shared/chat-box/chat-box.component"; export const components: any[] = [ DetailsGroupComponent, IssuesDetailsComponent, + IssueChatComponent, + ChatBoxComponent, ]; export const providers: any[] = [ diff --git a/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.html b/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.html new file mode 100644 index 000000000..fe6c13ce7 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.html @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.scss b/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.scss new file mode 100644 index 000000000..d6dcd67de --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.scss @@ -0,0 +1,9 @@ +@import "~styles/variables.scss"; + +::ng-deep .mat-card { + background: $ombi-background-primary-accent; +} + +.top-spacing { + margin-top:2%; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.ts b/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.ts new file mode 100644 index 000000000..a1ecf27f3 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.ts @@ -0,0 +1,87 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { AuthService } from "../../../auth/auth.service"; +import { ILocalUser } from "../../../auth/IUserLogin"; +import { IIssuesChat, IIssueSettings, IssueStatus } from "../../../interfaces"; +import { IssuesService, SettingsService } from "../../../services"; +import { ChatMessages, ChatType } from "../../../shared/chat-box/chat-box.component"; + + +export interface ChatData { + issueId: number; + title: string; + } + +@Component({ + selector: "issue-chat", + templateUrl: "issue-chat.component.html", + styleUrls: ["issue-chat.component.scss"], +}) +export class IssueChatComponent implements OnInit { + + public isAdmin: boolean; + public comments: IIssuesChat[]; + public IssueStatus = IssueStatus; + public settings: IIssueSettings; + public messages: ChatMessages[] = []; + + public loaded: boolean; + + private user: ILocalUser; + + + constructor(public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: ChatData, + private authService: AuthService, private settingsService: SettingsService, + private issueService: IssuesService) { } + + public ngOnInit() { + this.isAdmin = this.authService.isAdmin(); + this.user = this.authService.claims(); + this.settingsService.getIssueSettings().subscribe(x => this.settings = x); + this.issueService.getComments(this.data.issueId).subscribe(x => { + this.comments = x; + this.mapMessages(); + this.loaded = true; + }); + } + + + public deleteComment(commentId: number) { + + } + + public addComment(comment: string) { + this.issueService.addComment({ + comment: comment, + issueId: this.data.issueId + }).subscribe(comment => { + this.messages.push({ + chatType: ChatType.Sender, + date: comment.date, + id: -1, + message: comment.comment, + username: comment.user.userName + }); + }); + + } + + public close() { + this.dialogRef.close(); + } + + private mapMessages() { + this.comments.forEach((m: IIssuesChat) => { + this.messages.push({ + chatType: m.username === this.user.name ? ChatType.Sender : ChatType.Reciever, + date: m.date, + id: m.id, + message: m.comment, + username: m.username + }); + }); + } + + +} diff --git a/src/Ombi/ClientApp/src/app/issues/issues.module.ts b/src/Ombi/ClientApp/src/app/issues/issues.module.ts index 08896a086..4bf232f25 100644 --- a/src/Ombi/ClientApp/src/app/issues/issues.module.ts +++ b/src/Ombi/ClientApp/src/app/issues/issues.module.ts @@ -6,7 +6,7 @@ import { OrderModule } from "ngx-order-pipe"; import { AuthGuard } from "../auth/auth.guard"; -import { SharedModule as OmbiShared } from "../shared/shared.module"; +import { SharedModule } from "../shared/shared.module"; import { IssueDetailsComponent } from "./issueDetails.component"; import { IssuesComponent } from "./issues.component"; @@ -27,7 +27,7 @@ const routes: Routes = [ RouterModule.forChild(routes), OrderModule, PipeModule, - OmbiShared, + SharedModule, // NbChatModule, ], declarations: [ diff --git a/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.html b/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.html new file mode 100644 index 000000000..85463dea1 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.html @@ -0,0 +1,23 @@ +
+
+
+

Users

+
+

{{user}}

+
+
+
+
+
+

{{m.username}}

+
+

{{m.message}}

+

{{m.date | amLocal | amDateFormat: 'l LT'}}

+
+
+
+
+ + +
+
\ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.scss b/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.scss new file mode 100644 index 000000000..d64de471b --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.scss @@ -0,0 +1,329 @@ +// Variables +$primary: rgba(23, 190, 187, 1); +$secondary: rgba(240, 166, 202, 1); + +$active: rgba(23, 190, 187, 0.8); +$busy: rgba(252, 100, 113, 0.8); +$away: rgba(255, 253, 130, 0.8); + +// Triangle Mixin +@mixin triangle($color, $size, $direction) { + width: 0; + height: 0; + @if $direction == "up" { + border-right: ($size + px) solid transparent; + border-left: ($size + px) solid transparent; + border-bottom: ($size + px) solid $color; + } + @if $direction == "down" { + border-right: ($size + px) solid transparent; + border-left: ($size + px) solid transparent; + border-top: ($size + px) solid $color; + } + @if $direction == "right" { + border-top: ($size + px) solid transparent; + border-bottom: ($size + px) solid transparent; + border-left: ($size + px) solid $color; + } + @if $direction == "left" { + border-top: ($size + px) solid transparent; + border-bottom: ($size + px) solid transparent; + border-right: ($size + px) solid $color; + } +} + +* { + margin: 0; padding: 0; + box-sizing: border-box; + font-family: 'Nunito', sans-serif; +} + +html,body { + background: linear-gradient(120deg, $primary, $secondary); + overflow: hidden; +} + +.container { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + height: 70vh; + h1 { + margin: 0.5em auto; + color: #FFF; + text-align: center; + } +} + +.chatbox { + background: rgba(255, 255, 255, 0.05); + width: 600px; + height: 75%; + border-radius: 0.2em; + position: relative; + box-shadow: 1px 1px 12px rgba(0, 0, 0, 0.1); + + .sender { + float: right; + &:after { + content: ''; + position: absolute; + margin: -1.5em -18.98em; + @include triangle(rgba(255, 255, 255, 0.2), 10, left); + } + } + .reciever { + float: left; + &:after { + content: ''; + position: absolute; + margin: -1.5em 2.65em; + @include triangle(rgba(255, 255, 255, 0.2), 10, right); + } + } + &__messages__user-message { + width: 450px; + } + &__messages__user-message--ind-message { + background: rgba(255, 255, 255, 0.2); + padding: 1em 0; + height: auto; + width: 65%; + border-radius: 5px; + margin: 2em 1em; + overflow: auto; + & > p.name { + color: #FFF; + font-size: 1em; + } + & > p.message { + color: #FFF; + font-size: 0.7em; + margin: 0 2.8em; + }& > p.timestamp { + color: #FFF; + font-size: 0.7em; + margin: 0 2.8em; + } + } + &__user-list { + background: rgba(255, 255, 255, 0.1); + width: 25%; + height: 100%; + float: right; + border-top-right-radius: 0.2em; + border-bottom-right-radius: 0.2em; + h1 { + background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.9); + font-size: 0.9em; + padding: 1em; + margin: 0; + font-weight: 300; + text-align: center; + } + } + &__user { + width: 0.5em; + height: 0.5em; + border-radius: 100%; + margin: 1em 0.7em; + &--active { + @extend .chatbox__user; + background: $active; + } + &--busy { + @extend .chatbox__user; + background: $busy; + } + &--away { + @extend .chatbox__user; + background: $away; + } + } + p { + float: left; + text-align: left; + margin: -0.25em 2em; + font-size: 0.7em; + font-weight: 300; + color: #FFF; + width: 200px; + } + .form { + background: #222; + input { + background: rgba(255, 255, 255, 0.03); + position: absolute; + bottom: 0; + left: 0; + border: none; + width: 75%; + padding: 1.2em; + outline: none; + color: rgba(255, 255, 255, 0.9); + font-weight: 300; + } + .add-message { + background: rgba(255, 255, 255, 0.03); + position: absolute; + bottom: 1.5%; + right: 26%; + border: none; + outline: none; + color: rgba(255, 255, 255, 0.9); + font-weight: 300; + } + } +} + +// Placeholder Styling +::-webkit-input-placeholder { + color: rgba(255, 255, 255, 0.9); +} + +:-moz-placeholder { + color: rgba(255, 255, 255, 0.9); +} + +::-moz-placeholder { + color: rgba(255, 255, 255, 0.9); +} + +:-ms-input-placeholder { + color: rgba(255, 255, 255, 0.9); +} + +// ::-webkit-scrollbar { +// width: 4px; +// } +// ::-webkit-scrollbar-thumb { +// background-color: #4c4c6a; +// border-radius: 2px; +// } +// .chatbox { +// width: 300px; +// height: 400px; +// max-height: 400px; +// display: flex; +// flex-direction: column; +// overflow: hidden; +// box-shadow: 0 0 4px rgba(0,0,0,.14),0 4px 8px rgba(0,0,0,.28); +// } +// .chat-window { +// flex: auto; +// max-height: calc(100% - 60px); +// background: #2f323b; +// overflow: auto; +// } +// .chat-input { +// flex: 0 0 auto; +// height: 60px; +// background: #40434e; +// border-top: 1px solid #2671ff; +// box-shadow: 0 0 4px rgba(0,0,0,.14),0 4px 8px rgba(0,0,0,.28); +// } +// .chat-input input { +// height: 59px; +// line-height: 60px; +// outline: 0 none; +// border: none; +// width: calc(100% - 60px); +// color: white; +// text-indent: 10px; +// font-size: 12pt; +// padding: 0; +// background: #40434e; +// } +// .chat-input button { +// float: right; +// outline: 0 none; +// border: none; +// background: rgba(255,255,255,.25); +// height: 40px; +// width: 40px; +// border-radius: 50%; +// padding: 2px 0 0 0; +// margin: 10px; +// transition: all 0.15s ease-in-out; +// } +// .chat-input input[good] + button { +// box-shadow: 0 0 2px rgba(0,0,0,.12),0 2px 4px rgba(0,0,0,.24); +// background: #2671ff; +// } +// .chat-input input[good] + button:hover { +// box-shadow: 0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19); +// } +// .chat-input input[good] + button path { +// fill: white; +// } +// .msg-container { +// position: relative; +// display: inline-block; +// width: 100%; +// margin: 0 0 10px 0; +// padding: 0; +// } +// .msg-box { +// display: flex; +// background: #5b5e6c; +// padding: 10px 10px 0 10px; +// border-radius: 0 6px 6px 0; +// max-width: 80%; +// width: auto; +// float: left; +// box-shadow: 0 0 2px rgba(0,0,0,.12),0 2px 4px rgba(0,0,0,.24); +// } +// .user-img { +// display: inline-block; +// border-radius: 50%; +// height: 40px; +// width: 40px; +// background: #2671ff; +// margin: 0 10px 10px 0; +// } +// .flr { +// flex: 1 0 auto; +// display: flex; +// flex-direction: column; +// width: calc(100% - 50px); +// } +// .messages { +// flex: 1 0 auto; +// } +// .msg { +// display: inline-block; +// font-size: 11pt; +// line-height: 13pt; +// color: rgba(255,255,255,.7); +// margin: 0 0 4px 0; +// } +// .msg:first-of-type { +// margin-top: 8px; +// } +// .timestamp { +// color: rgba(0,0,0,.38); +// font-size: 8pt; +// margin-bottom: 10px; +// } +// .username { +// margin-right: 3px; +// } +// .posttime { +// margin-left: 3px; +// } +// .msg-self .msg-box { +// border-radius: 6px 0 0 6px; +// background: #2671ff; +// float: right; +// } +// .msg-self .user-img { +// margin: 0 0 10px 10px; +// } +// .msg-self .msg { +// text-align: right; +// } +// .msg-self .timestamp { +// text-align: right; +// } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.ts b/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.ts new file mode 100644 index 000000000..697977563 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.ts @@ -0,0 +1,46 @@ +import { AfterContentChecked, AfterViewInit, Component, EventEmitter, Inject, Input, OnInit, Output } from "@angular/core"; + + +export interface ChatMessages { + id: number; + message: string; + date: Date; + username: string; + chatType: ChatType; + } + + export enum ChatType { + Sender, + Reciever + } + +@Component({ + selector: "ombi-chat-box", + templateUrl: "chat-box.component.html", + styleUrls: ["chat-box.component.scss"], +}) +export class ChatBoxComponent implements OnInit { + @Input() messages: ChatMessages[]; + @Output() onAddMessage: EventEmitter = new EventEmitter(); + @Output() onDeleteMessage: EventEmitter = new EventEmitter(); + + public currentMessage: string; + public userList: string[]; + public ChatType = ChatType; + + public ngOnInit(): void { + const allUsernames = this.messages.map(x => x.username); + this.userList = allUsernames.filter((v, i, a) => a.indexOf(v) === i); + } + + public deleteMessage(id: number) { + this.onDeleteMessage.emit(id); + } + + public addMessage() { + if (this.currentMessage) { + this.onAddMessage.emit(this.currentMessage); + this.currentMessage = ''; + } + } +} From 67c7d73ca1cd839bc4c86d845d6cb1904e74ad3d Mon Sep 17 00:00:00 2001 From: tidusjar Date: Wed, 3 Mar 2021 21:28:37 +0000 Subject: [PATCH 05/63] think i've finished the new issues work --- src/Ombi.Core/Engine/V2/IssuesEngine.cs | 2 +- .../details-group.component.html | 53 ++++++++------- .../details-group.component.scss | 4 +- .../details-group/details-group.component.ts | 67 ++++++++++--------- .../components/details/details.component.html | 39 +++-------- .../components/details/details.component.ts | 2 +- .../src/app/issues/components/index.ts | 2 - .../issue-chat/issue-chat.component.ts | 2 +- .../src/app/media-details/components/index.ts | 1 + .../issues-panel/issues-panel.component.html | 7 +- .../shared/chat-box/chat-box.component.html | 2 +- .../shared/chat-box/chat-box.component.scss | 5 +- .../ClientApp/src/app/shared/shared.module.ts | 4 ++ src/Ombi/wwwroot/translations/en.json | 4 +- 14 files changed, 97 insertions(+), 97 deletions(-) diff --git a/src/Ombi.Core/Engine/V2/IssuesEngine.cs b/src/Ombi.Core/Engine/V2/IssuesEngine.cs index f938e8ec6..830dd4295 100644 --- a/src/Ombi.Core/Engine/V2/IssuesEngine.cs +++ b/src/Ombi.Core/Engine/V2/IssuesEngine.cs @@ -32,7 +32,7 @@ namespace Ombi.Core.Engine.V2 public async Task> GetIssues(int position, int take, IssueStatus status, CancellationToken token) { - var issues = await _issues.GetAll().Where(x => x.Status == status).Skip(position).Take(take).OrderBy(x => x.Title).ToListAsync(token); + var issues = await _issues.GetAll().Where(x => x.Status == status && x.ProviderId != null).Skip(position).Take(take).OrderBy(x => x.Title).ToListAsync(token); var grouped = issues.GroupBy(x => x.Title, (key, g) => new { Title = key, Issues = g }); var model = new List(); diff --git a/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.html b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.html index 9738e24c7..c4906baac 100644 --- a/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.html +++ b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.html @@ -1,26 +1,27 @@ -

Issues for {{data.title}}

-
-
-
- - - {{issue.subject}} - {{issue.userReported?.userName}} on {{issue.createdDate | date:short}} - - -

- {{issue.description}} -

-
- - - - -
-
-
-
-
- - -
+ + + {{issue.subject}} + {{issue.userReported?.userName}} on {{issue.createdDate | date:short}} + + +

+ {{issue.description}} +

+
+ + +
here is ignored
+ + + + + + + +
+
+ diff --git a/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.scss b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.scss index d6dcd67de..c244fdd09 100644 --- a/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.scss +++ b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.scss @@ -1,7 +1,7 @@ @import "~styles/variables.scss"; -::ng-deep .mat-card { - background: $ombi-background-primary-accent; +::ng-deep .issue-card { + border: 3px solid $ombi-background-primary-accent; } .top-spacing { diff --git a/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.ts b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.ts index aea5eb123..4f7ddc612 100644 --- a/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.ts +++ b/src/Ombi/ClientApp/src/app/issues/components/details-group/details-group.component.ts @@ -1,48 +1,55 @@ -import { Component, Inject, OnInit } from "@angular/core"; -import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { AuthService } from "../../../auth/auth.service"; +import { Component, Input } from "@angular/core"; +import { MatDialog } from '@angular/material/dialog'; +import { TranslateService } from "@ngx-translate/core"; import { IIssues, IIssueSettings, IssueStatus } from "../../../interfaces"; -import { SettingsService } from "../../../services"; - - -export interface IssuesDetailsGroupData { - issues: IIssues[]; - title: string; - } +import { IssuesService, NotificationService } from "../../../services"; +import { IssueChatComponent } from "../issue-chat/issue-chat.component"; @Component({ selector: "issues-details-group", templateUrl: "details-group.component.html", styleUrls: ["details-group.component.scss"], }) -export class DetailsGroupComponent implements OnInit { +export class DetailsGroupComponent { - public isAdmin: boolean; + @Input() public issue: IIssues; + @Input() public isAdmin: boolean; + @Input() public settings: IIssueSettings; + + public deleted: boolean; public IssueStatus = IssueStatus; - public settings: IIssueSettings; public get hasRequest(): boolean { - return this.data.issues.some(x => x.requestId); + if (this.issue.requestId) { + return true; + } + return false; } - constructor(public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: IssuesDetailsGroupData, - private authService: AuthService, private settingsService: SettingsService) { } + constructor( + private translateService: TranslateService, private issuesService: IssuesService, + private notificationService: NotificationService, private dialog: MatDialog) { } - public ngOnInit() { - this.isAdmin = this.authService.hasRole("Admin") || this.authService.hasRole("PowerUser"); - this.settingsService.getIssueSettings().subscribe(x => this.settings = x); + public async delete(issue: IIssues) { + await this.issuesService.deleteIssue(issue.id); + this.notificationService.success(this.translateService.instant("Issues.DeletedIssue")); + this.deleted = true; } - public close() { - this.dialogRef.close(); - } - - public navToRequest() { - var issue = this.data.issues.filter(x => { - return x.requestId; - })[0]; + public openChat(issue: IIssues) { + this.dialog.open(IssueChatComponent, { width: "100vh", data: { issueId: issue.id }, panelClass: 'modal-panel' }) + } - // close dialog and tell calling component to navigate - } + public resolve(issue: IIssues) { + this.issuesService.updateStatus({issueId: issue.id, status: IssueStatus.Resolved}).subscribe(() => { + this.notificationService.success(this.translateService.instant("Issues.MarkedAsResolved")); + issue.status = IssueStatus.Resolved; + }); + } + public inProgress(issue: IIssues) { + this.issuesService.updateStatus({issueId: issue.id, status: IssueStatus.InProgress}).subscribe(() => { + this.notificationService.success(this.translateService.instant("Issues.MarkedAsInProgress")); + issue.status = IssueStatus.InProgress; + }); + } } diff --git a/src/Ombi/ClientApp/src/app/issues/components/details/details.component.html b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.html index 08cb2dc44..bc0e8a794 100644 --- a/src/Ombi/ClientApp/src/app/issues/components/details/details.component.html +++ b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.html @@ -1,39 +1,20 @@ -
+

Issues for {{details.title}}

- Has Request {{hasRequest}} - + {{'Issues.Requested' | translate}} + + + +
+ +
-
-
-
{{issue.subject}}
- {{issue.userReported?.userName}} on {{issue.createdDate | date:short}} -
-
-

- {{issue.description}} -

-
-
- - - - - - -
- -
+
-
-
\ No newline at end of file +
diff --git a/src/Ombi/ClientApp/src/app/issues/components/details/details.component.ts b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.ts index accc99e60..4003acd40 100644 --- a/src/Ombi/ClientApp/src/app/issues/components/details/details.component.ts +++ b/src/Ombi/ClientApp/src/app/issues/components/details/details.component.ts @@ -70,7 +70,7 @@ export class IssuesDetailsComponent implements OnInit { } public openChat(issue: IIssues) { - this.dialog.open(IssueChatComponent, { width: "80vh", data: { issueId: issue.id }, panelClass: 'modal-panel' }) + this.dialog.open(IssueChatComponent, { width: "100vh", data: { issueId: issue.id }, panelClass: 'modal-panel' }) } public navToMedia() { diff --git a/src/Ombi/ClientApp/src/app/issues/components/index.ts b/src/Ombi/ClientApp/src/app/issues/components/index.ts index 99a07492b..52e4ed03c 100644 --- a/src/Ombi/ClientApp/src/app/issues/components/index.ts +++ b/src/Ombi/ClientApp/src/app/issues/components/index.ts @@ -1,6 +1,5 @@ import { IssuesV2Service } from "../../services/issuesv2.service"; import { IdentityService, SearchService } from "../../services"; -import { DetailsGroupComponent } from "./details-group/details-group.component"; import { IssuesDetailsComponent } from "./details/details.component"; import { IssueChatComponent } from "./issue-chat/issue-chat.component"; import { ChatBoxComponent } from "../../shared/chat-box/chat-box.component"; @@ -8,7 +7,6 @@ import { ChatBoxComponent } from "../../shared/chat-box/chat-box.component"; export const components: any[] = [ - DetailsGroupComponent, IssuesDetailsComponent, IssueChatComponent, ChatBoxComponent, diff --git a/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.ts b/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.ts index a1ecf27f3..023fdbf12 100644 --- a/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.ts +++ b/src/Ombi/ClientApp/src/app/issues/components/issue-chat/issue-chat.component.ts @@ -20,7 +20,7 @@ export interface ChatData { export class IssueChatComponent implements OnInit { public isAdmin: boolean; - public comments: IIssuesChat[]; + public comments: IIssuesChat[] = []; public IssueStatus = IssueStatus; public settings: IIssueSettings; public messages: ChatMessages[] = []; diff --git a/src/Ombi/ClientApp/src/app/media-details/components/index.ts b/src/Ombi/ClientApp/src/app/media-details/components/index.ts index 240fba28a..e8cb89d4a 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/index.ts +++ b/src/Ombi/ClientApp/src/app/media-details/components/index.ts @@ -20,6 +20,7 @@ import { IssuesPanelComponent } from "./shared/issues-panel/issues-panel.compone import { TvAdvancedOptionsComponent } from "./tv/panels/tv-advanced-options/tv-advanced-options.component"; import { RequestBehalfComponent } from "./shared/request-behalf/request-behalf.component"; import { TvRequestGridComponent } from "./tv/panels/tv-request-grid/tv-request-grid.component"; +import { DetailsGroupComponent } from "../../issues/components/details-group/details-group.component"; export const components: any[] = [ MovieDetailsComponent, diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/issues-panel/issues-panel.component.html b/src/Ombi/ClientApp/src/app/media-details/components/shared/issues-panel/issues-panel.component.html index c4c05dff9..d71ca8fcd 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/shared/issues-panel/issues-panel.component.html +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/issues-panel/issues-panel.component.html @@ -13,6 +13,11 @@ + +
+ +
+
diff --git a/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.html b/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.html index 85463dea1..aa19bf1db 100644 --- a/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.html +++ b/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.html @@ -1,4 +1,4 @@ -
+

Users

diff --git a/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.scss b/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.scss index d64de471b..70f83b85f 100644 --- a/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.scss +++ b/src/Ombi/ClientApp/src/app/shared/chat-box/chat-box.component.scss @@ -49,6 +49,7 @@ html,body { align-items: center; flex-direction: column; height: 70vh; + width: 100%; h1 { margin: 0.5em auto; color: #FFF; @@ -58,8 +59,8 @@ html,body { .chatbox { background: rgba(255, 255, 255, 0.05); - width: 600px; - height: 75%; + width: 85%; + height: 100%; border-radius: 0.2em; position: relative; box-shadow: 1px 1px 12px rgba(0, 0, 0, 0.1); diff --git a/src/Ombi/ClientApp/src/app/shared/shared.module.ts b/src/Ombi/ClientApp/src/app/shared/shared.module.ts index ed93fa375..afe27bb7e 100644 --- a/src/Ombi/ClientApp/src/app/shared/shared.module.ts +++ b/src/Ombi/ClientApp/src/app/shared/shared.module.ts @@ -36,11 +36,13 @@ import { MatProgressSpinnerModule } from "@angular/material/progress-spinner"; import { MatSlideToggleModule } from "@angular/material/slide-toggle"; import { MatTabsModule } from "@angular/material/tabs"; import { EpisodeRequestComponent } from "./episode-request/episode-request.component"; +import { DetailsGroupComponent } from "../issues/components/details-group/details-group.component"; @NgModule({ declarations: [ IssuesReportComponent, EpisodeRequestComponent, + DetailsGroupComponent, ], imports: [ SidebarModule, @@ -76,6 +78,7 @@ import { EpisodeRequestComponent } from "./episode-request/episode-request.compo ], entryComponents: [ EpisodeRequestComponent, + DetailsGroupComponent, ], exports: [ TranslateModule, @@ -86,6 +89,7 @@ import { EpisodeRequestComponent } from "./episode-request/episode-request.compo MatProgressSpinnerModule, IssuesReportComponent, EpisodeRequestComponent, + DetailsGroupComponent, TruncateModule, InputSwitchModule, MatTreeModule, diff --git a/src/Ombi/wwwroot/translations/en.json b/src/Ombi/wwwroot/translations/en.json index 0efb3fe88..4b82d8224 100644 --- a/src/Ombi/wwwroot/translations/en.json +++ b/src/Ombi/wwwroot/translations/en.json @@ -216,7 +216,9 @@ "MarkedAsResolved": "This issue has now been marked as resolved!", "MarkedAsInProgress": "This issue has now been marked as in progress!", "Delete": "Delete issue", - "DeletedIssue": "Issue has been deleted" + "DeletedIssue": "Issue has been deleted", + "Chat":"Chat", + "Requested":"Requested" }, "Filter": { "ClearFilter": "Clear Filter", From 9337311718c7a1930e022e44071852e3076fb785 Mon Sep 17 00:00:00 2001 From: Xirg Date: Sat, 6 Mar 2021 11:33:02 +0100 Subject: [PATCH 06/63] added icon for apple homescreen icon the icon is already here, just the tag missing --- src/Ombi/ClientApp/src/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ombi/ClientApp/src/index.html b/src/Ombi/ClientApp/src/index.html index 9b4005ca1..4d077697d 100644 --- a/src/Ombi/ClientApp/src/index.html +++ b/src/Ombi/ClientApp/src/index.html @@ -14,6 +14,7 @@ + From dd3a44b2845cc345d0aba5046049c01fbc07aebb Mon Sep 17 00:00:00 2001 From: tidusjar Date: Sat, 6 Mar 2021 23:51:53 +0000 Subject: [PATCH 07/63] Fixed #4092 --- src/Ombi/Controllers/V1/IdentityController.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Ombi/Controllers/V1/IdentityController.cs b/src/Ombi/Controllers/V1/IdentityController.cs index dfe13a029..b57a6e890 100644 --- a/src/Ombi/Controllers/V1/IdentityController.cs +++ b/src/Ombi/Controllers/V1/IdentityController.cs @@ -633,11 +633,16 @@ namespace Ombi.Controllers.V1 user.MovieRequestLimit = ui.MovieRequestLimit; user.EpisodeRequestLimit = ui.EpisodeRequestLimit; user.MusicRequestLimit = ui.MusicRequestLimit; + if (ui.Password.HasValue()) + { + user.PasswordHash = UserManager.PasswordHasher.HashPassword(user, ui.Password); + } if (ui.StreamingCountry.HasValue()) { user.StreamingCountry = ui.StreamingCountry; } var updateResult = await UserManager.UpdateAsync(user); + if (!updateResult.Succeeded) { return new OmbiIdentityResult From 6fda24c98a954a4a872147d7297f95a6aefe3905 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 7 Mar 2021 19:18:55 +0000 Subject: [PATCH 08/63] Delete nuget.config --- src/Ombi/nuget.config | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/Ombi/nuget.config diff --git a/src/Ombi/nuget.config b/src/Ombi/nuget.config deleted file mode 100644 index 34508488c..000000000 --- a/src/Ombi/nuget.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From b1fec7ad3f2e18796999bd7a94bc8ce082b01adf Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 8 Mar 2021 09:01:02 +0000 Subject: [PATCH 09/63] Fixed where you could not delete issues --- src/Ombi/ClientApp/src/index.html | 1 + src/Ombi/Controllers/V1/IssuesController.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Ombi/ClientApp/src/index.html b/src/Ombi/ClientApp/src/index.html index 4d077697d..289926e33 100644 --- a/src/Ombi/ClientApp/src/index.html +++ b/src/Ombi/ClientApp/src/index.html @@ -8,6 +8,7 @@ background-color: white !important; } + diff --git a/src/Ombi/Controllers/V1/IssuesController.cs b/src/Ombi/Controllers/V1/IssuesController.cs index d75389c3a..a7e4b42a7 100644 --- a/src/Ombi/Controllers/V1/IssuesController.cs +++ b/src/Ombi/Controllers/V1/IssuesController.cs @@ -278,7 +278,7 @@ namespace Ombi.Controllers.V1 [PowerUser] public async Task DeleteIssue(int id) { - var issue = await _issues.GetAll().FirstOrDefaultAsync(x => x.Id == id); + var issue = await _issues.GetAll().Include(x => x.Comments).FirstOrDefaultAsync(x => x.Id == id); await _issues.Delete(issue); return true; From 38fc9b23bb9c36430b00030d0a7286dd94ab169c Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 8 Mar 2021 09:12:40 +0000 Subject: [PATCH 10/63] Fixed the notification variables for the Alias for Issues --- .../NotificationMessageCurlys.cs | 18 +++++++++++++++--- src/Ombi/Controllers/V1/IssuesController.cs | 12 +++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/Ombi.Notifications/NotificationMessageCurlys.cs b/src/Ombi.Notifications/NotificationMessageCurlys.cs index 0aea728a4..dcf49138d 100644 --- a/src/Ombi.Notifications/NotificationMessageCurlys.cs +++ b/src/Ombi.Notifications/NotificationMessageCurlys.cs @@ -38,7 +38,12 @@ namespace Ombi.Notifications UserName = req?.RequestedUser?.UserName; } - Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName; + if (Alias.IsNullOrEmpty()) + { + // Can be set if it's an issue + Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName; + } + if (pref != null) { UserPreference = pref.Value.HasValue() ? pref.Value : Alias; @@ -95,7 +100,10 @@ namespace Ombi.Notifications AvailableDate = req?.MarkedAsAvailable?.ToString("D") ?? string.Empty; DenyReason = req?.DeniedReason; - Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName; + if (Alias.IsNullOrEmpty()) + { + Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName; + } if (pref != null) { UserPreference = pref.Value.HasValue() ? pref.Value : Alias; @@ -143,7 +151,10 @@ namespace Ombi.Notifications UserName = req?.RequestedUser?.UserName; } AvailableDate = req?.MarkedAsAvailable?.ToString("D") ?? string.Empty; - Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName; + if (Alias.IsNullOrEmpty()) + { + Alias = (req?.RequestedUser?.Alias.HasValue() ?? false) ? req?.RequestedUser?.Alias : req?.RequestedUser?.UserName; + } if (pref != null) { UserPreference = pref.Value.HasValue() ? pref.Value : Alias; @@ -223,6 +234,7 @@ namespace Ombi.Notifications IssueSubject = opts.Substitutes.TryGetValue("IssueSubject", out val) ? val : string.Empty; NewIssueComment = opts.Substitutes.TryGetValue("NewIssueComment", out val) ? val : string.Empty; UserName = opts.Substitutes.TryGetValue("IssueUser", out val) ? val : string.Empty; + Alias = opts.Substitutes.TryGetValue("IssueUserAlias", out val) ? val : string.Empty; Type = opts.Substitutes.TryGetValue("RequestType", out val) ? val.Humanize() : string.Empty; } diff --git a/src/Ombi/Controllers/V1/IssuesController.cs b/src/Ombi/Controllers/V1/IssuesController.cs index a7e4b42a7..5222cf4e7 100644 --- a/src/Ombi/Controllers/V1/IssuesController.cs +++ b/src/Ombi/Controllers/V1/IssuesController.cs @@ -132,7 +132,8 @@ namespace Ombi.Controllers.V1 i.IssueCategory = null; i.CreatedDate = DateTime.UtcNow; var username = User.Identity.Name.ToUpper(); - i.UserReportedId = (await _userManager.Users.FirstOrDefaultAsync(x => x.NormalizedUserName == username)).Id; + var reportedUser = await _userManager.Users.FirstOrDefaultAsync(x => x.NormalizedUserName == username); + i.UserReportedId = reportedUser.Id; await _issues.Add(i); var category = await _categories.GetAll().FirstOrDefaultAsync(x => i.IssueCategoryId == x.Id); if (category != null) @@ -151,7 +152,7 @@ namespace Ombi.Controllers.V1 }; - AddIssueNotificationSubstitutes(notificationModel, i, User.Identity.Name); + AddIssueNotificationSubstitutes(notificationModel, i, reportedUser.UserName, reportedUser.UserAlias); await _notification.Notify(notificationModel); @@ -242,7 +243,7 @@ namespace Ombi.Controllers.V1 }; var isAdmin = await _userManager.IsInRoleAsync(user, OmbiRoles.Admin) || user.IsSystemUser; - AddIssueNotificationSubstitutes(notificationModel, issue, issue.UserReported.UserAlias); + AddIssueNotificationSubstitutes(notificationModel, issue, user.UserName, user.UserAlias); notificationModel.Substitutes.Add("NewIssueComment", comment.Comment); notificationModel.Substitutes.Add("IssueId", comment.IssueId.ToString()); notificationModel.Substitutes.Add("AdminComment", isAdmin.ToString()); @@ -319,7 +320,7 @@ namespace Ombi.Controllers.V1 UserId = issue.UserReported.Id, // This is a resolved notification, so needs to go to the user who raised it }; - AddIssueNotificationSubstitutes(notificationModel, issue, issue.UserReported?.UserAlias ?? string.Empty); + AddIssueNotificationSubstitutes(notificationModel, issue, issue.UserReported?.UserName ?? string.Empty, issue.UserReported.UserAlias); await _notification.Notify(notificationModel); } @@ -328,7 +329,7 @@ namespace Ombi.Controllers.V1 return true; } - private static void AddIssueNotificationSubstitutes(NotificationOptions notificationModel, Issues issue, string issueReportedUsername) + private static void AddIssueNotificationSubstitutes(NotificationOptions notificationModel, Issues issue, string issueReportedUsername, string alias) { notificationModel.Substitutes.Add("Title", issue.Title); notificationModel.Substitutes.Add("IssueDescription", issue.Description); @@ -336,6 +337,7 @@ namespace Ombi.Controllers.V1 notificationModel.Substitutes.Add("IssueStatus", issue.Status.ToString()); notificationModel.Substitutes.Add("IssueSubject", issue.Subject); notificationModel.Substitutes.Add("IssueUser", issueReportedUsername); + notificationModel.Substitutes.Add("IssueUserAlias", alias); notificationModel.Substitutes.Add("RequestType", notificationModel.RequestType.ToString()); } } From a61594dcd6820237916f03b5601fc3b58d82e4b2 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 8 Mar 2021 10:13:24 +0000 Subject: [PATCH 11/63] Fixed the issue where the new mobile app was not recieving any notifications --- .../Agents/MobileNotification.cs | 43 +++++++++++-------- src/Ombi.Notifications/BaseNotification.cs | 2 +- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/Ombi.Notifications/Agents/MobileNotification.cs b/src/Ombi.Notifications/Agents/MobileNotification.cs index 61526802c..6080acdf0 100644 --- a/src/Ombi.Notifications/Agents/MobileNotification.cs +++ b/src/Ombi.Notifications/Agents/MobileNotification.cs @@ -131,7 +131,7 @@ namespace Ombi.Notifications.Agents }; // Send to user - var playerIds = GetUsers(model, NotificationType.IssueResolved); + var playerIds = await GetUsers(model, NotificationType.IssueResolved); await Send(playerIds, notification, settings, model); } @@ -172,7 +172,7 @@ namespace Ombi.Notifications.Agents }; // Send to user - var playerIds = GetUsers(model, NotificationType.RequestDeclined); + var playerIds = await GetUsers(model, NotificationType.RequestDeclined); await AddSubscribedUsers(playerIds); await Send(playerIds, notification, settings, model); } @@ -192,7 +192,7 @@ namespace Ombi.Notifications.Agents }; // Send to user - var playerIds = GetUsers(model, NotificationType.RequestApproved); + var playerIds = await GetUsers(model, NotificationType.RequestApproved); await AddSubscribedUsers(playerIds); await Send(playerIds, notification, settings, model); @@ -215,7 +215,7 @@ namespace Ombi.Notifications.Agents Data = data }; // Send to user - var playerIds = GetUsers(model, NotificationType.RequestAvailable); + var playerIds = await GetUsers(model, NotificationType.RequestAvailable); await AddSubscribedUsers(playerIds); await Send(playerIds, notification, settings, model); @@ -285,19 +285,23 @@ namespace Ombi.Notifications.Agents return playerIds; } - private List GetUsers(NotificationOptions model, NotificationType type) + private async Task> GetUsers(NotificationOptions model, NotificationType type) { - var notificationIds = new List(); + var notificationIds = new List(); if (MovieRequest != null || TvRequest != null) { - notificationIds = model.RequestType == RequestType.Movie - ? MovieRequest?.RequestedUser?.NotificationUserIds - : TvRequest?.RequestedUser?.NotificationUserIds; + var userId = model.RequestType == RequestType.Movie + ? MovieRequest?.RequestedUser?.Id + : TvRequest?.RequestedUser?.Id; + + var userNotificationIds = await _notifications.GetAll().Where(x => x.UserId == userId).ToListAsync(); + notificationIds.AddRange(userNotificationIds); } if (model.UserId.HasValue() && (!notificationIds?.Any() ?? true)) { - var user = _userManager.Users.Include(x => x.NotificationUserIds).FirstOrDefault(x => x.Id == model.UserId); - notificationIds = user.NotificationUserIds; + var user = _userManager.Users.FirstOrDefault(x => x.Id == model.UserId); + var userNotificationIds = await _notifications.GetAll().Where(x => x.UserId == model.UserId).ToListAsync(); + notificationIds.AddRange(userNotificationIds); } if (!notificationIds?.Any() ?? true) @@ -306,21 +310,21 @@ namespace Ombi.Notifications.Agents $"there are no users to send a notification for {type}, for agent {NotificationAgent.Mobile}"); return null; } - var playerIds = notificationIds.Select(x => x.PlayerId).ToList(); + var playerIds = notificationIds.Select(x => x.Token).ToList(); return playerIds; } private async Task> GetUsersForIssue(NotificationOptions model, int issueId, NotificationType type) { - var notificationIds = new List(); + var notificationIds = new List(); var issue = await _issueRepository.GetAll() .FirstOrDefaultAsync(x => x.Id == issueId); // Get the user that raised the issue to send the notification to - var userRaised = await _userManager.Users.Include(x => x.NotificationUserIds).FirstOrDefaultAsync(x => x.Id == issue.UserReportedId); + var userRaised = await _userManager.Users.FirstOrDefaultAsync(x => x.Id == issue.UserReportedId); - notificationIds = userRaised.NotificationUserIds; + notificationIds = await _notifications.GetAll().Where(x => x.UserId == userRaised.Id).ToListAsync(); if (!notificationIds?.Any() ?? true) { @@ -328,7 +332,7 @@ namespace Ombi.Notifications.Agents $"there are no users to send a notification for {type}, for agent {NotificationAgent.Mobile}"); return null; } - var playerIds = notificationIds.Select(x => x.PlayerId).ToList(); + var playerIds = notificationIds.Select(x => x.Token).ToList(); return playerIds; } @@ -338,10 +342,11 @@ namespace Ombi.Notifications.Agents { foreach (var user in SubsribedUsers) { - var notificationId = user.NotificationUserIds; - if (notificationId.Any()) + var notificationIds = await _notifications.GetAll().Where(x => x.UserId == user.Id).ToListAsync(); + + if (notificationIds.Any()) { - playerIds.AddRange(notificationId.Select(x => x.PlayerId)); + playerIds.AddRange(notificationIds.Select(x => x.Token)); } } } diff --git a/src/Ombi.Notifications/BaseNotification.cs b/src/Ombi.Notifications/BaseNotification.cs index 465ccd22c..c9404eb2c 100644 --- a/src/Ombi.Notifications/BaseNotification.cs +++ b/src/Ombi.Notifications/BaseNotification.cs @@ -191,7 +191,7 @@ namespace Ombi.Notifications protected IQueryable GetSubscriptions(int requestId, RequestType type) { - var subs = RequestSubscription.GetAll().Include(x => x.User).ThenInclude(x => x.NotificationUserIds).Where(x => x.RequestId == requestId && type == x.RequestType); + var subs = RequestSubscription.GetAll().Include(x => x.User).Where(x => x.RequestId == requestId && type == x.RequestType); return subs.Select(x => x.User); } From f6422cd556d6eda5ede5b06f8e1d9d73215aef3c Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 8 Mar 2021 21:34:04 +0000 Subject: [PATCH 12/63] fixed #4071 --- src/Ombi/ClientApp/src/app/login/login.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ombi/ClientApp/src/app/login/login.component.html b/src/Ombi/ClientApp/src/app/login/login.component.html index 0769a185f..8aa0faf94 100644 --- a/src/Ombi/ClientApp/src/app/login/login.component.html +++ b/src/Ombi/ClientApp/src/app/login/login.component.html @@ -38,8 +38,8 @@
- +
diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/tv-details.component.html b/src/Ombi/ClientApp/src/app/media-details/components/tv/tv-details.component.html index 7da2ee6ca..141c48b79 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/tv/tv-details.component.html +++ b/src/Ombi/ClientApp/src/app/media-details/components/tv/tv-details.component.html @@ -50,13 +50,19 @@ (click)="request()"> {{ 'Common.Request' | translate }} - - + + + + diff --git a/src/Ombi/Properties/launchSettings.json b/src/Ombi/Properties/launchSettings.json index 0116bc6e8..b3899f8c3 100644 --- a/src/Ombi/Properties/launchSettings.json +++ b/src/Ombi/Properties/launchSettings.json @@ -14,7 +14,7 @@ "profiles": { "IIS Express": { "commandName": "IISExpress", - "commandLineArgs": "--host http://*:3577" , + "commandLineArgs": "--host http://*:3577", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -22,7 +22,7 @@ }, "Ombi": { "commandName": "Project", - "commandLineArgs": "--host http://localhost:3577 --baseUrl /ombi/", + "commandLineArgs": "--host http://localhost:3577", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, From 0efdd3fa60d00e5eadc24b1e42935fde1d764480 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 8 Mar 2021 23:19:05 +0000 Subject: [PATCH 14/63] Small automation changes --- .../src/app/login/login.component.html | 2 +- .../src/app/login/login.component.ts | 6 ++--- .../tv-information-panel.component.html | 4 ++-- .../tv-request-grid.component.html | 2 +- .../components/tv/tv-details.component.html | 10 ++++----- .../usermanagement-user.component.html | 22 +++++++++---------- .../usermanagement.component.html | 12 +++++----- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Ombi/ClientApp/src/app/login/login.component.html b/src/Ombi/ClientApp/src/app/login/login.component.html index 8aa0faf94..0e1107008 100644 --- a/src/Ombi/ClientApp/src/app/login/login.component.html +++ b/src/Ombi/ClientApp/src/app/login/login.component.html @@ -27,7 +27,7 @@ {{'Login.RememberMe' | translate}} - + diff --git a/src/Ombi/ClientApp/src/app/login/login.component.ts b/src/Ombi/ClientApp/src/app/login/login.component.ts index 5a85783ae..5245b8239 100644 --- a/src/Ombi/ClientApp/src/app/login/login.component.ts +++ b/src/Ombi/ClientApp/src/app/login/login.component.ts @@ -73,7 +73,7 @@ export class LoginComponent implements OnDestroy, OnInit { }); this.form = this.fb.group({ - username: [""], + username: ["", Validators.required], password: [""], rememberMe: [false], }); @@ -112,7 +112,7 @@ export class LoginComponent implements OnDestroy, OnInit { public onSubmit(form: FormGroup) { if (form.invalid) { this.notify.open(this.errorValidation, "OK", { - duration: 3000 + duration: 300000 }); return; } @@ -139,7 +139,7 @@ export class LoginComponent implements OnDestroy, OnInit { }, err => { this.notify.open(this.errorBody, "OK", { - duration: 3000 + duration: 3000000 }) }); }); diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.html b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.html index 2997cda3c..3e0cd1952 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.html +++ b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.html @@ -21,8 +21,8 @@

- {{'MediaDetails.Status' | translate }}: - {{tv.status}} + {{'MediaDetails.Status' | translate }}: + {{tv.status}}
First Aired: {{tv.firstAired | date: 'mediumDate'}} diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-request-grid/tv-request-grid.component.html b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-request-grid/tv-request-grid.component.html index 91e4b2d8f..d6dbc9cab 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-request-grid/tv-request-grid.component.html +++ b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-request-grid/tv-request-grid.component.html @@ -67,7 +67,7 @@ --> - diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/tv-details.component.html b/src/Ombi/ClientApp/src/app/media-details/components/tv/tv-details.component.html index 141c48b79..66dc83f3d 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/tv/tv-details.component.html +++ b/src/Ombi/ClientApp/src/app/media-details/components/tv/tv-details.component.html @@ -46,24 +46,24 @@
- - - - - diff --git a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.html b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.html index 64f5d76d2..74770a3d8 100644 --- a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.html +++ b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement-user.component.html @@ -4,18 +4,18 @@
- +
-
- +
@@ -28,12 +28,12 @@
- +
- +
@@ -44,17 +44,17 @@
- +
- +
- +
@@ -117,13 +117,13 @@
- {{c.value | humanize}} + {{c.value | humanize}}
- {{c.value | humanize}} + {{c.value | humanize}}
@@ -134,7 +134,7 @@
- +
diff --git a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.html b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.html index b17b35dfd..1dcbf29c0 100644 --- a/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.html +++ b/src/Ombi/ClientApp/src/app/usermanagement/usermanagement.component.html @@ -73,10 +73,10 @@ Last Logged In - + {{u.lastLoggedIn | amLocal | amDateFormat: 'l LT'}} - + Not logged in yet! @@ -92,10 +92,10 @@ Jellyfin User - + Roles - +
{{claim.value}}
@@ -105,7 +105,7 @@ - + @@ -115,7 +115,7 @@
- + From 9670738a5299f0cccc3a31603db23b82b9e87b54 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Tue, 9 Mar 2021 08:43:32 +0000 Subject: [PATCH 15/63] Fixed an error with the retry queue when the request has been removed --- .../Jobs/Ombi/ResendFailedRequests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Ombi.Schedule/Jobs/Ombi/ResendFailedRequests.cs b/src/Ombi.Schedule/Jobs/Ombi/ResendFailedRequests.cs index 0072ec010..3a9a75835 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/ResendFailedRequests.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/ResendFailedRequests.cs @@ -43,6 +43,12 @@ namespace Ombi.Schedule.Jobs.Ombi if (request.Type == RequestType.Movie) { var movieRequest = await _movieRequestRepository.GetAll().FirstOrDefaultAsync(x => x.Id == request.RequestId); + if (movieRequest == null) + { + await _requestQueue.Delete(request); + await _requestQueue.SaveChangesAsync(); + continue; + } var result = await _movieSender.Send(movieRequest); if (result.Success) { @@ -53,6 +59,12 @@ namespace Ombi.Schedule.Jobs.Ombi if (request.Type == RequestType.TvShow) { var tvRequest = await _tvRequestRepository.GetChild().FirstOrDefaultAsync(x => x.Id == request.RequestId); + if (tvRequest == null) + { + await _requestQueue.Delete(request); + await _requestQueue.SaveChangesAsync(); + continue; + } var result = await _tvSender.Send(tvRequest); if (result.Success) { @@ -63,6 +75,12 @@ namespace Ombi.Schedule.Jobs.Ombi if (request.Type == RequestType.Album) { var musicRequest = await _musicRequestRepository.GetAll().FirstOrDefaultAsync(x => x.Id == request.RequestId); + if (musicRequest == null) + { + await _requestQueue.Delete(request); + await _requestQueue.SaveChangesAsync(); + continue; + } var result = await _musicSender.Send(musicRequest); if (result.Success) { From 57dfbd6748199032e97ece11586f9869e4317d04 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Tue, 9 Mar 2021 10:23:17 +0000 Subject: [PATCH 16/63] Few more tweaks for automation --- .../tv-information-panel.component.html | 6 +++--- .../tv-information-panel/tv-information-panel.component.ts | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.html b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.html index 3e0cd1952..85ebf2160 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.html +++ b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.html @@ -8,13 +8,13 @@ {{ratings.score}}%
-
+

{{'MediaDetails.StreamingOn' | translate }}:
- - + +
diff --git a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.ts index 350f76c26..f8b62368d 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.ts +++ b/src/Ombi/ClientApp/src/app/media-details/components/tv/panels/tv-information-panel/tv-information-panel.component.ts @@ -35,4 +35,8 @@ export class TvInformationPanelComponent implements OnInit { }); this.seasonCount = this.tv.seasonRequests.length; } + + public sortBy(prop: string) { + return this.streams.sort((a, b) => a[prop] > b[prop] ? 1 : a[prop] === b[prop] ? 0 : -1); + } } From d31da0a24992bcfee78f74660e7430e614c7dade Mon Sep 17 00:00:00 2001 From: tidusjar Date: Tue, 9 Mar 2021 15:01:43 +0000 Subject: [PATCH 17/63] Added automation --- .github/workflows/cypress.yml | 35 + .../app/wizard/welcome/welcome.component.html | 12 +- tests/.gitignore | 15 + tests/cypress.json | 19 + tests/cypress/config/demo.json | 20 + tests/cypress/config/regression.json | 25 + .../cypress/fixtures/details/tv/response.json | 2730 +++++++++++++++++ .../details/tv/streamingResponse.json | 12 + tests/cypress/fixtures/example.json | 5 + .../fixtures/login/authenticationSettngs.json | 10 + .../fixtures/login/landingPageSettings.json | 10 + .../integration/examples/actions.spec.js | 299 ++ .../integration/examples/aliasing.spec.js | 39 + .../integration/examples/assertions.spec.js | 177 ++ .../integration/examples/connectors.spec.js | 97 + .../integration/examples/cookies.spec.js | 77 + .../integration/examples/cypress_api.spec.js | 202 ++ .../integration/examples/files.spec.js | 89 + .../examples/local_storage.spec.js | 52 + .../integration/examples/location.spec.js | 32 + .../cypress/integration/examples/misc.spec.js | 104 + .../integration/examples/navigation.spec.js | 56 + .../examples/network_requests.spec.js | 163 + .../integration/examples/querying.spec.js | 114 + .../examples/spies_stubs_clocks.spec.js | 205 ++ .../integration/examples/traversal.spec.js | 121 + .../integration/examples/utilities.spec.js | 110 + .../integration/examples/viewport.spec.js | 59 + .../integration/examples/waiting.spec.js | 31 + .../integration/examples/window.spec.js | 22 + tests/cypress/interfaces/IClaims.ts | 4 + tests/cypress/plugins/index.js | 21 + tests/cypress/support/commands.ts | 91 + tests/cypress/support/index.ts | 21 + tests/cypress/support/request.commands.ts | 50 + .../details/tv/tvdetails-buttons.spec.ts | 83 + .../details/tv/tvdetails-info-panel.spec.ts | 35 + tests/cypress/tests/login/login.spec.ts | 83 + .../usermanagement/usermanagement.spec.ts | 163 + tests/cypress/tests/wizard/01-wizard.spec.ts | 49 + tests/global.d.ts | 20 + tests/package.json | 22 + tests/tsconfig.json | 15 + tests/yarn.lock | 1420 +++++++++ 44 files changed, 7013 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/cypress.yml create mode 100644 tests/.gitignore create mode 100644 tests/cypress.json create mode 100644 tests/cypress/config/demo.json create mode 100644 tests/cypress/config/regression.json create mode 100644 tests/cypress/fixtures/details/tv/response.json create mode 100644 tests/cypress/fixtures/details/tv/streamingResponse.json create mode 100644 tests/cypress/fixtures/example.json create mode 100644 tests/cypress/fixtures/login/authenticationSettngs.json create mode 100644 tests/cypress/fixtures/login/landingPageSettings.json create mode 100644 tests/cypress/integration/examples/actions.spec.js create mode 100644 tests/cypress/integration/examples/aliasing.spec.js create mode 100644 tests/cypress/integration/examples/assertions.spec.js create mode 100644 tests/cypress/integration/examples/connectors.spec.js create mode 100644 tests/cypress/integration/examples/cookies.spec.js create mode 100644 tests/cypress/integration/examples/cypress_api.spec.js create mode 100644 tests/cypress/integration/examples/files.spec.js create mode 100644 tests/cypress/integration/examples/local_storage.spec.js create mode 100644 tests/cypress/integration/examples/location.spec.js create mode 100644 tests/cypress/integration/examples/misc.spec.js create mode 100644 tests/cypress/integration/examples/navigation.spec.js create mode 100644 tests/cypress/integration/examples/network_requests.spec.js create mode 100644 tests/cypress/integration/examples/querying.spec.js create mode 100644 tests/cypress/integration/examples/spies_stubs_clocks.spec.js create mode 100644 tests/cypress/integration/examples/traversal.spec.js create mode 100644 tests/cypress/integration/examples/utilities.spec.js create mode 100644 tests/cypress/integration/examples/viewport.spec.js create mode 100644 tests/cypress/integration/examples/waiting.spec.js create mode 100644 tests/cypress/integration/examples/window.spec.js create mode 100644 tests/cypress/interfaces/IClaims.ts create mode 100644 tests/cypress/plugins/index.js create mode 100644 tests/cypress/support/commands.ts create mode 100644 tests/cypress/support/index.ts create mode 100644 tests/cypress/support/request.commands.ts create mode 100644 tests/cypress/tests/details/tv/tvdetails-buttons.spec.ts create mode 100644 tests/cypress/tests/details/tv/tvdetails-info-panel.spec.ts create mode 100644 tests/cypress/tests/login/login.spec.ts create mode 100644 tests/cypress/tests/usermanagement/usermanagement.spec.ts create mode 100644 tests/cypress/tests/wizard/01-wizard.spec.ts create mode 100644 tests/global.d.ts create mode 100644 tests/package.json create mode 100644 tests/tsconfig.json create mode 100644 tests/yarn.lock diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml new file mode 100644 index 000000000..1449e6cb4 --- /dev/null +++ b/.github/workflows/cypress.yml @@ -0,0 +1,35 @@ +name: .NET + +on: + push: + branches: [ feature/automation ] + pull_request: + branches: [ feature/automation ] + +jobs: + automation-tests: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.x + - name: Run backend + run: dotnet run -p ./src/Ombi -- --host http://*:3575 + - name: Run Frontend + uses: borales/actions-yarn@v2.0.0 + with: + cmd: start --cwd ./src/ombi/clientapp + + - name: Cypress Tests + uses: cypress-io/github-action@v2.8.2 + with: + browser: chrome + headless: true + project: ./tests + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + diff --git a/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.html b/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.html index c1189051c..7ca3de4ce 100644 --- a/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.html +++ b/src/Ombi/ClientApp/src/app/wizard/welcome/welcome.component.html @@ -25,7 +25,7 @@
- +
@@ -39,16 +39,16 @@ - + - +
Create a local admin
- +
@@ -58,7 +58,7 @@
- +
@@ -77,7 +77,7 @@
- +
diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 000000000..0361ba0b4 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,15 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# misc +.DS_Store +.idea + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +cypress/videos +cypress/screenshots diff --git a/tests/cypress.json b/tests/cypress.json new file mode 100644 index 000000000..d08128874 --- /dev/null +++ b/tests/cypress.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://on.cypress.io/cypress.schema.json", + "supportFile": "cypress/support/index.ts", + "baseUrl": "http://localhost:3577", + "integrationFolder": "cypress/tests", + "testFiles": "**/*.spec.ts*", + "watchForFileChanges": true, + "chromeWebSecurity": false, + "viewportWidth": 2560, + "viewportHeight": 1440, + "ignoreTestFiles": [ + "**/snapshots/*" + ], + "env": { + "username": "a", + "password": "a" + }, + "projectId": "o5451s" +} diff --git a/tests/cypress/config/demo.json b/tests/cypress/config/demo.json new file mode 100644 index 000000000..d9fafcab8 --- /dev/null +++ b/tests/cypress/config/demo.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://on.cypress.io/cypress.schema.json", + "supportFile": "cypress/support/index.ts", + "baseUrl": "https://app.ombi.io/", + "integrationFolder": "cypress/tests", + "testFiles": "**/*.spec.ts*", + "retries": { + "runMode": 2, + "openMode": 1 + }, + "watchForFileChanges": true, + "chromeWebSecurity": false, + "viewportWidth": 2880, + "viewportHeight": 2160, + "ignoreTestFiles": ["**/snapshots/*"], + "env": { + "username": "beta", + "password": "beta" + } +} diff --git a/tests/cypress/config/regression.json b/tests/cypress/config/regression.json new file mode 100644 index 000000000..213244b8c --- /dev/null +++ b/tests/cypress/config/regression.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://on.cypress.io/cypress.schema.json", + "supportFile": "cypress/support/index.ts", + "integrationFolder": "cypress/tests", + "testFiles": "**/*.spec.ts*", + "retries": { + "runMode": 2, + "openMode": 1 + }, + "watchForFileChanges": true, + "chromeWebSecurity": false, + "viewportWidth": 2880, + "viewportHeight": 2160, + "ignoreTestFiles": ["**/snapshots/*"], + "screenshotOnRunFailure": false, + "video": false, + "reporter": "junit", + "reporterOptions": { + "mochaFile": "results/junit/regression-[hash].xml" + }, + "env": { + "username": "beta", + "password": "beta" + } +} diff --git a/tests/cypress/fixtures/details/tv/response.json b/tests/cypress/fixtures/details/tv/response.json new file mode 100644 index 000000000..ff4f7981d --- /dev/null +++ b/tests/cypress/fixtures/details/tv/response.json @@ -0,0 +1,2730 @@ +{ + "title": "Game of Thrones", + "aliases": null, + "banner": "https://static.tvmaze.com/uploads/images/medium_portrait/190/476117.jpg", + "seriesId": 82, + "status": "Ended", + "firstAired": "2011-04-17", + "networkId": "8", + "runtime": "60", + "genre": null, + "overview": "Based on the bestselling book series A Song of Ice and Fire by George R.R. Martin, this sprawling new HBO drama is set in a world where summers span decades and winters can last a lifetime. From the scheming south and the savage eastern lands, to the frozen north and ancient Wall that protects the realm from the mysterious darkness beyond, the powerful families of the Seven Kingdoms are locked in a battle for the Iron Throne. This is a story of duplicity and treachery, nobility and honor, conquest and triumph. In the Game of Thrones, you either win or you die.", + "lastUpdated": 0, + "airsDayOfWeek": null, + "airsTime": null, + "rating": "9.1", + "siteRating": 0, + "network": { + "id": 8, + "name": "HBO", + "country": { + "name": "United States", + "code": "US", + "timezone": "America/New_York" + } + }, + "images": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/190/476117.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/190/476117.jpg" + }, + "cast": [ + { + "person": { + "id": 14072, + "url": "https://www.tvmaze.com/people/14072/peter-dinklage", + "name": "Peter Dinklage", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/74/186607.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/74/186607.jpg" + } + }, + "character": { + "id": 15604, + "url": "https://www.tvmaze.com/characters/15604/game-of-thrones-tyrion-lannister", + "name": "Tyrion Lannister", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/63/158804.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/63/158804.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14075, + "url": "https://www.tvmaze.com/people/14075/kit-harington", + "name": "Kit Harington", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3229.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3229.jpg" + } + }, + "character": { + "id": 15607, + "url": "https://www.tvmaze.com/characters/15607/game-of-thrones-jon-snow", + "name": "Jon Snow", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/63/158800.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/63/158800.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14076, + "url": "https://www.tvmaze.com/people/14076/lena-headey", + "name": "Lena Headey", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/97/244409.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/97/244409.jpg" + } + }, + "character": { + "id": 15608, + "url": "https://www.tvmaze.com/characters/15608/game-of-thrones-queen-cersei-lannister", + "name": "Queen Cersei Lannister", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/63/158806.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/63/158806.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14079, + "url": "https://www.tvmaze.com/people/14079/emilia-clarke", + "name": "Emilia Clarke", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/54/136753.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/54/136753.jpg" + } + }, + "character": { + "id": 15611, + "url": "https://www.tvmaze.com/characters/15611/game-of-thrones-daenerys-targaryen", + "name": "Daenerys Targaryen", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/63/158798.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/63/158798.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14080, + "url": "https://www.tvmaze.com/people/14080/sophie-turner", + "name": "Sophie Turner", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/82/205626.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/82/205626.jpg" + } + }, + "character": { + "id": 15612, + "url": "https://www.tvmaze.com/characters/15612/game-of-thrones-sansa-stark", + "name": "Sansa Stark", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/63/158812.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/63/158812.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14081, + "url": "https://www.tvmaze.com/people/14081/maisie-williams", + "name": "Maisie Williams", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/282/706106.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/282/706106.jpg" + } + }, + "character": { + "id": 15613, + "url": "https://www.tvmaze.com/characters/15613/game-of-thrones-arya-stark", + "name": "Arya Stark", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/64/162189.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/64/162189.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14070, + "url": "https://www.tvmaze.com/people/14070/nikolaj-coster-waldau", + "name": "Nikolaj Coster-Waldau", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/62/155678.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/62/155678.jpg" + } + }, + "character": { + "id": 15602, + "url": "https://www.tvmaze.com/characters/15602/game-of-thrones-ser-jaime-lannister", + "name": "Ser Jaime Lannister", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/64/162190.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/64/162190.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14074, + "url": "https://www.tvmaze.com/people/14074/iain-glen", + "name": "Iain Glen", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3233.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3233.jpg" + } + }, + "character": { + "id": 15606, + "url": "https://www.tvmaze.com/characters/15606/game-of-thrones-ser-jorah-mormont", + "name": "Ser Jorah Mormont", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/0/1457.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/0/1457.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14069, + "url": "https://www.tvmaze.com/people/14069/alfie-allen", + "name": "Alfie Allen", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3235.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3235.jpg" + } + }, + "character": { + "id": 15600, + "url": "https://www.tvmaze.com/characters/15600/game-of-thrones-theon-greyjoy", + "name": "Theon Greyjoy", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/0/599.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/0/599.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14090, + "url": "https://www.tvmaze.com/people/14090/john-bradley", + "name": "John Bradley", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3242.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3242.jpg" + } + }, + "character": { + "id": 15623, + "url": "https://www.tvmaze.com/characters/15623/game-of-thrones-samwell-tarly", + "name": "Samwell Tarly", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/0/591.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/0/591.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14085, + "url": "https://www.tvmaze.com/people/14085/conleth-hill", + "name": "Conleth Hill", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3247.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3247.jpg" + } + }, + "character": { + "id": 15617, + "url": "https://www.tvmaze.com/characters/15617/game-of-thrones-lord-varys", + "name": "Lord Varys", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/0/596.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/0/596.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14086, + "url": "https://www.tvmaze.com/people/14086/gwendoline-christie", + "name": "Gwendoline Christie", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/136/340397.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/136/340397.jpg" + } + }, + "character": { + "id": 15618, + "url": "https://www.tvmaze.com/characters/15618/game-of-thrones-brienne-of-tarth", + "name": "Brienne of Tarth", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/0/593.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/0/593.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14092, + "url": "https://www.tvmaze.com/people/14092/liam-cunningham", + "name": "Liam Cunningham", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3260.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3260.jpg" + } + }, + "character": { + "id": 15625, + "url": "https://www.tvmaze.com/characters/15625/game-of-thrones-davos-seaworth", + "name": "Davos Seaworth", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/0/594.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/0/594.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14083, + "url": "https://www.tvmaze.com/people/14083/aidan-gillen", + "name": "Aidan Gillen", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3240.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3240.jpg" + } + }, + "character": { + "id": 15615, + "url": "https://www.tvmaze.com/characters/15615/game-of-thrones-petyr-littlefinger-baelish", + "name": "Petyr \"Littlefinger\" Baelish", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/0/1463.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/0/1463.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14082, + "url": "https://www.tvmaze.com/people/14082/isaac-hempstead-wright", + "name": "Isaac Hempstead-Wright", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3238.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3238.jpg" + } + }, + "character": { + "id": 15614, + "url": "https://www.tvmaze.com/characters/15614/game-of-thrones-bran-stark", + "name": "Bran Stark", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/63/158816.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/63/158816.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14394, + "url": "https://www.tvmaze.com/people/14394/nathalie-emmanuel", + "name": "Nathalie Emmanuel", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/93/234475.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/93/234475.jpg" + } + }, + "character": { + "id": 15974, + "url": "https://www.tvmaze.com/characters/15974/game-of-thrones-missandei", + "name": "Missandei", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/0/611.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/0/611.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14071, + "url": "https://www.tvmaze.com/people/14071/rory-mccann", + "name": "Rory McCann", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3236.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3236.jpg" + } + }, + "character": { + "id": 15603, + "url": "https://www.tvmaze.com/characters/15603/game-of-thrones-sandor-clegane", + "name": "Sandor Clegane", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/63/158819.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/63/158819.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14088, + "url": "https://www.tvmaze.com/people/14088/jerome-flynn", + "name": "Jerome Flynn", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3259.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3259.jpg" + } + }, + "character": { + "id": 15621, + "url": "https://www.tvmaze.com/characters/15621/game-of-thrones-bronn", + "name": "Bronn", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/0/592.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/0/592.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14417, + "url": "https://www.tvmaze.com/people/14417/jacob-anderson", + "name": "Jacob Anderson", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/8/20737.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/8/20737.jpg" + } + }, + "character": { + "id": 15997, + "url": "https://www.tvmaze.com/characters/15997/game-of-thrones-grey-worm", + "name": "Grey Worm", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/172/431759.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/172/431759.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14091, + "url": "https://www.tvmaze.com/people/14091/kristofer-hivju", + "name": "Kristofer Hivju", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3330.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3330.jpg" + } + }, + "character": { + "id": 15624, + "url": "https://www.tvmaze.com/characters/15624/game-of-thrones-tormund-giantsbane", + "name": "Tormund Giantsbane", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3169.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3169.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14084, + "url": "https://www.tvmaze.com/people/14084/carice-van-houten", + "name": "Carice van Houten", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3261.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3261.jpg" + } + }, + "character": { + "id": 15616, + "url": "https://www.tvmaze.com/characters/15616/game-of-thrones-melisandre", + "name": "Melisandre", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/172/431775.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/172/431775.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14065, + "url": "https://www.tvmaze.com/people/14065/charles-dance", + "name": "Charles Dance", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/168/422293.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/168/422293.jpg" + } + }, + "character": { + "id": 15596, + "url": "https://www.tvmaze.com/characters/15596/game-of-thrones-lord-tywin-lannister", + "name": "Lord Tywin Lannister", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/0/602.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/0/602.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14066, + "url": "https://www.tvmaze.com/people/14066/natalie-dormer", + "name": "Natalie Dormer", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/243/608117.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/243/608117.jpg" + } + }, + "character": { + "id": 15597, + "url": "https://www.tvmaze.com/characters/15597/game-of-thrones-queen-margaery-tyrell", + "name": "Queen Margaery Tyrell", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/211/529552.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/211/529552.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14073, + "url": "https://www.tvmaze.com/people/14073/jack-gleeson", + "name": "Jack Gleeson", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3237.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3237.jpg" + } + }, + "character": { + "id": 15605, + "url": "https://www.tvmaze.com/characters/15605/game-of-thrones-prince-joffrey-baratheon", + "name": "Prince Joffrey Baratheon", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/63/158821.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/63/158821.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14199, + "url": "https://www.tvmaze.com/people/14199/hannah-murray", + "name": "Hannah Murray", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/7/18055.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/7/18055.jpg" + } + }, + "character": { + "id": 15720, + "url": "https://www.tvmaze.com/characters/15720/game-of-thrones-gilly", + "name": "Gilly", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/192/482446.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/192/482446.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14078, + "url": "https://www.tvmaze.com/people/14078/michelle-fairley", + "name": "Michelle Fairley", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/186/466394.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/186/466394.jpg" + } + }, + "character": { + "id": 15610, + "url": "https://www.tvmaze.com/characters/15610/game-of-thrones-lady-catelyn-stark", + "name": "Lady Catelyn Stark", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/63/158823.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/63/158823.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14404, + "url": "https://www.tvmaze.com/people/14404/dean-charles-chapman", + "name": "Dean Charles Chapman", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/2/5727.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/2/5727.jpg" + } + }, + "character": { + "id": 15632, + "url": "https://www.tvmaze.com/characters/15632/game-of-thrones-tommen-baratheon", + "name": "Tommen Baratheon", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/0/603.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/0/603.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14089, + "url": "https://www.tvmaze.com/people/14089/joe-dempsie", + "name": "Joe Dempsie", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/185/462746.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/185/462746.jpg" + } + }, + "character": { + "id": 15622, + "url": "https://www.tvmaze.com/characters/15622/game-of-thrones-gendry", + "name": "Gendry", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/0/589.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/0/589.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14096, + "url": "https://www.tvmaze.com/people/14096/stephen-dillane", + "name": "Stephen Dillane", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/68/171092.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/68/171092.jpg" + } + }, + "character": { + "id": 15629, + "url": "https://www.tvmaze.com/characters/15629/game-of-thrones-stannis-baratheon", + "name": "Stannis Baratheon", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3245.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3245.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14077, + "url": "https://www.tvmaze.com/people/14077/richard-madden", + "name": "Richard Madden", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/173/434079.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/173/434079.jpg" + } + }, + "character": { + "id": 15609, + "url": "https://www.tvmaze.com/characters/15609/game-of-thrones-robb-stark", + "name": "Robb Stark", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/63/158825.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/63/158825.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14218, + "url": "https://www.tvmaze.com/people/14218/michael-mcelhatton", + "name": "Michael McElhatton", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/3/8123.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/3/8123.jpg" + } + }, + "character": { + "id": 15739, + "url": "https://www.tvmaze.com/characters/15739/game-of-thrones-lord-roose-bolton", + "name": "Lord Roose Bolton", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/0/609.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/0/609.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 7267, + "url": "https://www.tvmaze.com/people/7267/michiel-huisman", + "name": "Michiel Huisman", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/8/20724.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/8/20724.jpg" + } + }, + "character": { + "id": 16007, + "url": "https://www.tvmaze.com/characters/16007/game-of-thrones-daario-naharis", + "name": "Daario Naharis", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/9/23151.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/9/23151.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14095, + "url": "https://www.tvmaze.com/people/14095/sibel-kekilli", + "name": "Sibel Kekilli", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3244.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3244.jpg" + } + }, + "character": { + "id": 15628, + "url": "https://www.tvmaze.com/characters/15628/game-of-thrones-shae", + "name": "Shae", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/211/529553.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/211/529553.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14087, + "url": "https://www.tvmaze.com/people/14087/iwan-rheon", + "name": "Iwan Rheon", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/158/396043.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/158/396043.jpg" + } + }, + "character": { + "id": 15619, + "url": "https://www.tvmaze.com/characters/15619/game-of-thrones-ramsay-snow", + "name": "Ramsay Snow", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3331.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3331.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14208, + "url": "https://www.tvmaze.com/people/14208/tom-wlaschiha", + "name": "Tom Wlaschiha", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/4/10120.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/4/10120.jpg" + } + }, + "character": { + "id": 15730, + "url": "https://www.tvmaze.com/characters/15730/game-of-thrones-jaqen-hghar", + "name": "Jaqen H'ghar", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/211/529595.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/211/529595.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14094, + "url": "https://www.tvmaze.com/people/14094/rose-leslie", + "name": "Rose Leslie", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/107/267661.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/107/267661.jpg" + } + }, + "character": { + "id": 15627, + "url": "https://www.tvmaze.com/characters/15627/game-of-thrones-ygritte", + "name": "Ygritte", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/211/529550.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/211/529550.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14440, + "url": "https://www.tvmaze.com/people/14440/indira-varma", + "name": "Indira Varma", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/3/8666.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/3/8666.jpg" + } + }, + "character": { + "id": 16022, + "url": "https://www.tvmaze.com/characters/16022/game-of-thrones-ellaria-sand", + "name": "Ellaria Sand", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/9/23152.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/9/23152.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 52759, + "url": "https://www.tvmaze.com/people/52759/jonathan-pryce", + "name": "Jonathan Pryce", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/92/231635.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/92/231635.jpg" + } + }, + "character": { + "id": 105050, + "url": "https://www.tvmaze.com/characters/105050/game-of-thrones-high-sparrow", + "name": "High Sparrow", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/11/28572.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/11/28572.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 7432, + "url": "https://www.tvmaze.com/people/7432/sean-bean", + "name": "Sean Bean", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3294.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3294.jpg" + } + }, + "character": { + "id": 15601, + "url": "https://www.tvmaze.com/characters/15601/game-of-thrones-lord-eddard-stark", + "name": "Lord Eddard Stark", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/211/529531.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/211/529531.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 11349, + "url": "https://www.tvmaze.com/people/11349/james-cosmo", + "name": "James Cosmo", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/181/453032.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/181/453032.jpg" + } + }, + "character": { + "id": 15620, + "url": "https://www.tvmaze.com/characters/15620/game-of-thrones-lord-commander-jeor-mormont", + "name": "Lord Commander Jeor Mormont", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/211/529534.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/211/529534.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14068, + "url": "https://www.tvmaze.com/people/14068/mark-addy", + "name": "Mark Addy", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3324.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3324.jpg" + } + }, + "character": { + "id": 15599, + "url": "https://www.tvmaze.com/characters/15599/game-of-thrones-king-robert-baratheon", + "name": "King Robert Baratheon", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/0/598.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/0/598.jpg" + } + }, + "self": false, + "voice": false + }, + { + "person": { + "id": 14067, + "url": "https://www.tvmaze.com/people/14067/harry-lloyd", + "name": "Harry Lloyd", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/1/3329.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/1/3329.jpg" + } + }, + "character": { + "id": 15598, + "url": "https://www.tvmaze.com/characters/15598/game-of-thrones-viserys-targaryen", + "name": "Viserys Targaryen", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/0/600.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/0/600.jpg" + } + }, + "self": false, + "voice": false + } + ], + "crew": [ + { + "type": "Executive Producer", + "person": { + "id": 282, + "url": "https://www.tvmaze.com/people/282/david-nutter", + "name": "David Nutter", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/182/455179.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/182/455179.jpg" + } + } + }, + { + "type": "Creator", + "person": { + "id": 14097, + "url": "https://www.tvmaze.com/people/14097/db-weiss", + "name": "D.B. Weiss", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/58/146139.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/58/146139.jpg" + } + } + }, + { + "type": "Executive Producer", + "person": { + "id": 14097, + "url": "https://www.tvmaze.com/people/14097/db-weiss", + "name": "D.B. Weiss", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/58/146139.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/58/146139.jpg" + } + } + }, + { + "type": "Creator", + "person": { + "id": 14098, + "url": "https://www.tvmaze.com/people/14098/david-benioff", + "name": "David Benioff", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/121/303084.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/121/303084.jpg" + } + } + }, + { + "type": "Executive Producer", + "person": { + "id": 14098, + "url": "https://www.tvmaze.com/people/14098/david-benioff", + "name": "David Benioff", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/121/303084.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/121/303084.jpg" + } + } + }, + { + "type": "Co-Executive Producer", + "person": { + "id": 14100, + "url": "https://www.tvmaze.com/people/14100/guymon-casady", + "name": "Guymon Casady", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/214/536938.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/214/536938.jpg" + } + } + }, + { + "type": "Co-Executive Producer", + "person": { + "id": 14101, + "url": "https://www.tvmaze.com/people/14101/vince-gerardis", + "name": "Vince Gerardis", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/33/84303.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/33/84303.jpg" + } + } + }, + { + "type": "Co-Executive Producer", + "person": { + "id": 14102, + "url": "https://www.tvmaze.com/people/14102/ralph-m-vicinanza", + "name": "Ralph M. Vicinanza", + "image": null + } + }, + { + "type": "Co-Executive Producer", + "person": { + "id": 14103, + "url": "https://www.tvmaze.com/people/14103/carolyn-strauss", + "name": "Carolyn Strauss", + "image": null + } + }, + { + "type": "Executive Producer", + "person": { + "id": 14103, + "url": "https://www.tvmaze.com/people/14103/carolyn-strauss", + "name": "Carolyn Strauss", + "image": null + } + }, + { + "type": "Co-Producer", + "person": { + "id": 14104, + "url": "https://www.tvmaze.com/people/14104/greg-spence", + "name": "Greg Spence", + "image": null + } + }, + { + "type": "Producer", + "person": { + "id": 14104, + "url": "https://www.tvmaze.com/people/14104/greg-spence", + "name": "Greg Spence", + "image": null + } + }, + { + "type": "Co-Producer", + "person": { + "id": 14105, + "url": "https://www.tvmaze.com/people/14105/christopher-newman", + "name": "Christopher Newman", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/199/498557.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/199/498557.jpg" + } + } + }, + { + "type": "Producer", + "person": { + "id": 14105, + "url": "https://www.tvmaze.com/people/14105/christopher-newman", + "name": "Christopher Newman", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/199/498557.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/199/498557.jpg" + } + } + }, + { + "type": "Consulting Producer", + "person": { + "id": 14106, + "url": "https://www.tvmaze.com/people/14106/thomas-mccarthy", + "name": "Thomas McCarthy", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/103/258930.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/103/258930.jpg" + } + } + }, + { + "type": "Associate Producer", + "person": { + "id": 14107, + "url": "https://www.tvmaze.com/people/14107/alan-freir", + "name": "Alan Freir", + "image": null + } + }, + { + "type": "Co-Producer", + "person": { + "id": 14107, + "url": "https://www.tvmaze.com/people/14107/alan-freir", + "name": "Alan Freir", + "image": null + } + }, + { + "type": "Associate Producer", + "person": { + "id": 14108, + "url": "https://www.tvmaze.com/people/14108/annick-wolkan", + "name": "Annick Wolkan", + "image": null + } + }, + { + "type": "Co-Producer", + "person": { + "id": 14108, + "url": "https://www.tvmaze.com/people/14108/annick-wolkan", + "name": "Annick Wolkan", + "image": null + } + }, + { + "type": "Associate Producer", + "person": { + "id": 14109, + "url": "https://www.tvmaze.com/people/14109/oliver-butler", + "name": "Oliver Butler", + "image": null + } + }, + { + "type": "Co-Producer", + "person": { + "id": 14109, + "url": "https://www.tvmaze.com/people/14109/oliver-butler", + "name": "Oliver Butler", + "image": null + } + }, + { + "type": "Associate Producer", + "person": { + "id": 14110, + "url": "https://www.tvmaze.com/people/14110/jonathan-brytus", + "name": "Jonathan Brytus", + "image": null + } + }, + { + "type": "Casting", + "person": { + "id": 14142, + "url": "https://www.tvmaze.com/people/14142/robert-sterne", + "name": "Robert Sterne", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/8/20509.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/8/20509.jpg" + } + } + }, + { + "type": "Co-Executive Producer", + "person": { + "id": 14146, + "url": "https://www.tvmaze.com/people/14146/bryan-cogman", + "name": "Bryan Cogman", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/101/254778.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/101/254778.jpg" + } + } + }, + { + "type": "Supervising Producer", + "person": { + "id": 14146, + "url": "https://www.tvmaze.com/people/14146/bryan-cogman", + "name": "Bryan Cogman", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/101/254778.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/101/254778.jpg" + } + } + }, + { + "type": "Executive Producer", + "person": { + "id": 14940, + "url": "https://www.tvmaze.com/people/14940/miguel-sapochnik", + "name": "Miguel Sapochnik", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/104/260096.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/104/260096.jpg" + } + } + }, + { + "type": "Executive Producer", + "person": { + "id": 49831, + "url": "https://www.tvmaze.com/people/49831/frank-doelger", + "name": "Frank Doelger", + "image": null + } + }, + { + "type": "Executive Producer", + "person": { + "id": 50704, + "url": "https://www.tvmaze.com/people/50704/bernadette-caulfield", + "name": "Bernadette Caulfield", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/199/498528.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/199/498528.jpg" + } + } + }, + { + "type": "Re-Recording Mixer", + "person": { + "id": 59666, + "url": "https://www.tvmaze.com/people/59666/mathew-waters", + "name": "Mathew Waters", + "image": null + } + }, + { + "type": "Co-Executive Producer", + "person": { + "id": 95525, + "url": "https://www.tvmaze.com/people/95525/george-r-r-martin", + "name": "George R. R. Martin", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/77/194417.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/77/194417.jpg" + } + } + }, + { + "type": "Based on the Novel Of", + "person": { + "id": 95525, + "url": "https://www.tvmaze.com/people/95525/george-r-r-martin", + "name": "George R. R. Martin", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/77/194417.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/77/194417.jpg" + } + } + }, + { + "type": "Supervising Sound Editor", + "person": { + "id": 97917, + "url": "https://www.tvmaze.com/people/97917/tim-kimmel", + "name": "Tim Kimmel", + "image": null + } + }, + { + "type": "Music Editor", + "person": { + "id": 98358, + "url": "https://www.tvmaze.com/people/98358/david-klotz", + "name": "David Klotz", + "image": null + } + }, + { + "type": "Music", + "person": { + "id": 98955, + "url": "https://www.tvmaze.com/people/98955/ramin-djawadi", + "name": "Ramin Djawadi", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/82/207123.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/82/207123.jpg" + } + } + }, + { + "type": "Editor", + "person": { + "id": 107088, + "url": "https://www.tvmaze.com/people/107088/yan-miles", + "name": "Yan Miles", + "image": null + } + }, + { + "type": "Casting", + "person": { + "id": 128925, + "url": "https://www.tvmaze.com/people/128925/nina-gold", + "name": "Nina Gold", + "image": null + } + }, + { + "type": "Visual Effects Supervisor", + "person": { + "id": 128926, + "url": "https://www.tvmaze.com/people/128926/joe-bauer", + "name": "Joe Bauer", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/286/715326.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/286/715326.jpg" + } + } + }, + { + "type": "Visual Effects Producer", + "person": { + "id": 128927, + "url": "https://www.tvmaze.com/people/128927/steve-kullback", + "name": "Steve Kullback", + "image": null + } + }, + { + "type": "Costume Designer", + "person": { + "id": 128928, + "url": "https://www.tvmaze.com/people/128928/april-ferry", + "name": "April Ferry", + "image": null + } + }, + { + "type": "Editor", + "person": { + "id": 128929, + "url": "https://www.tvmaze.com/people/128929/crispin-green", + "name": "Crispin Green", + "image": null + } + }, + { + "type": "Production Designer", + "person": { + "id": 128930, + "url": "https://www.tvmaze.com/people/128930/deborah-riley", + "name": "Deborah Riley", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/199/498555.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/199/498555.jpg" + } + } + }, + { + "type": "Director Of Photography", + "person": { + "id": 128931, + "url": "https://www.tvmaze.com/people/128931/gregory-middleton", + "name": "Gregory Middleton", + "image": null + } + }, + { + "type": "Producer", + "person": { + "id": 128932, + "url": "https://www.tvmaze.com/people/128932/lisa-mcatackney", + "name": "Lisa McAtackney", + "image": null + } + }, + { + "type": "Editor", + "person": { + "id": 131299, + "url": "https://www.tvmaze.com/people/131299/katie-weiland", + "name": "Katie Weiland", + "image": null + } + }, + { + "type": "Director Of Photography", + "person": { + "id": 131300, + "url": "https://www.tvmaze.com/people/131300/anette-haellmigk", + "name": "Anette Haellmigk", + "image": null + } + }, + { + "type": "Editor", + "person": { + "id": 134987, + "url": "https://www.tvmaze.com/people/134987/jesse-parker", + "name": "Jesse Parker", + "image": null + } + }, + { + "type": "Director Of Photography", + "person": { + "id": 134988, + "url": "https://www.tvmaze.com/people/134988/pj-dillon", + "name": "P.J. Dillon", + "image": null + } + }, + { + "type": "Costume Designer", + "person": { + "id": 136603, + "url": "https://www.tvmaze.com/people/136603/michele-clapton", + "name": "Michele Clapton", + "image": null + } + }, + { + "type": "Editor", + "person": { + "id": 136604, + "url": "https://www.tvmaze.com/people/136604/tim-porter", + "name": "Tim Porter", + "image": null + } + }, + { + "type": "Director Of Photography", + "person": { + "id": 136605, + "url": "https://www.tvmaze.com/people/136605/fabian-wagner", + "name": "Fabian Wagner", + "image": null + } + }, + { + "type": "Director Of Photography", + "person": { + "id": 137070, + "url": "https://www.tvmaze.com/people/137070/robert-mclachlan", + "name": "Robert McLachlan", + "image": null + } + }, + { + "type": "Music Supervisor", + "person": { + "id": 148425, + "url": "https://www.tvmaze.com/people/148425/evyen-klean", + "name": "Evyen Klean", + "image": null + } + }, + { + "type": "Casting", + "person": { + "id": 157724, + "url": "https://www.tvmaze.com/people/157724/carla-stronge", + "name": "Carla Stronge", + "image": null + } + }, + { + "type": "Re-Recording Mixer", + "person": { + "id": 161594, + "url": "https://www.tvmaze.com/people/161594/onnalee-blank", + "name": "Onnalee Blank", + "image": null + } + }, + { + "type": "Associate Producer", + "person": { + "id": 175859, + "url": "https://www.tvmaze.com/people/175859/allison-hooper", + "name": "Allison Hooper", + "image": null + } + }, + { + "type": "Co-Producer", + "person": { + "id": 176152, + "url": "https://www.tvmaze.com/people/176152/dave-hill", + "name": "Dave Hill", + "image": null + } + }, + { + "type": "Executive Story Editor", + "person": { + "id": 176152, + "url": "https://www.tvmaze.com/people/176152/dave-hill", + "name": "Dave Hill", + "image": null + } + }, + { + "type": "Unit Production Manager", + "person": { + "id": 183392, + "url": "https://www.tvmaze.com/people/183392/lisa-byrne", + "name": "Lisa Byrne", + "image": null + } + }, + { + "type": "Line Producer", + "person": { + "id": 183393, + "url": "https://www.tvmaze.com/people/183393/duncan-muggoch", + "name": "Duncan Muggoch", + "image": null + } + }, + { + "type": "Producer", + "person": { + "id": 183393, + "url": "https://www.tvmaze.com/people/183393/duncan-muggoch", + "name": "Duncan Muggoch", + "image": null + } + }, + { + "type": "Associate Producer", + "person": { + "id": 183394, + "url": "https://www.tvmaze.com/people/183394/alanna-riddell-bond", + "name": "Alanna Riddell Bond", + "image": null + } + }, + { + "type": "Re-Recording Mixer", + "person": { + "id": 183395, + "url": "https://www.tvmaze.com/people/183395/mark-paterson", + "name": "Mark Paterson", + "image": null + } + }, + { + "type": "Visual Effects Supervisor", + "person": { + "id": 232975, + "url": "https://www.tvmaze.com/people/232975/stefen-fangmeier", + "name": "Stefen Fangmeier", + "image": null + } + }, + { + "type": "Director Of Photography", + "person": { + "id": 247133, + "url": "https://www.tvmaze.com/people/247133/jonathan-freeman", + "name": "Jonathan Freeman", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/213/534277.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/213/534277.jpg" + } + } + }, + { + "type": "Director Of Photography", + "person": { + "id": 275491, + "url": "https://www.tvmaze.com/people/275491/david-franco", + "name": "David Franco", + "image": { + "medium": "https://static.tvmaze.com/uploads/images/medium_portrait/262/655879.jpg", + "original": "https://static.tvmaze.com/uploads/images/original_untouched/262/655879.jpg" + } + } + } + ], + "certification": "TV-MA", + "trailer": "https://youtube.com/watch?v=bjqEWgDVPe0", + "homepage": "https://www.hbo.com/game-of-thrones", + "seasonRequests": [ + { + "seasonNumber": 1, + "episodes": [ + { + "episodeNumber": 1, + "title": "Winter is Coming", + "airDate": "2011-04-18T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4952/game-of-thrones-1x01-winter-is-coming", + "available": true, + "approved": true, + "requested": false, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/18/2011 02:00:00", + "requestStatus": "Common.Available", + "id": 0 + }, + { + "episodeNumber": 2, + "title": "The Kingsroad", + "airDate": "2011-04-25T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4953/game-of-thrones-1x02-the-kingsroad", + "available": true, + "approved": true, + "requested": false, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/25/2011 02:00:00", + "requestStatus": "Common.Available", + "id": 0 + }, + { + "episodeNumber": 3, + "title": "Lord Snow", + "airDate": "2011-05-02T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4954/game-of-thrones-1x03-lord-snow", + "available": true, + "approved": true, + "requested": false, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/02/2011 02:00:00", + "requestStatus": "Common.Available", + "id": 0 + }, + { + "episodeNumber": 4, + "title": "Cripples, Bastards, and Broken Things", + "airDate": "2011-05-09T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4955/game-of-thrones-1x04-cripples-bastards-and-broken-things", + "available": true, + "approved": true, + "requested": false, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/09/2011 02:00:00", + "requestStatus": "Common.Available", + "id": 0 + }, + { + "episodeNumber": 5, + "title": "The Wolf and the Lion", + "airDate": "2011-05-16T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4956/game-of-thrones-1x05-the-wolf-and-the-lion", + "available": true, + "approved": true, + "requested": false, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/16/2011 02:00:00", + "requestStatus": "Common.Available", + "id": 0 + }, + { + "episodeNumber": 6, + "title": "A Golden Crown", + "airDate": "2011-05-23T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4957/game-of-thrones-1x06-a-golden-crown", + "available": true, + "approved": true, + "requested": false, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/23/2011 02:00:00", + "requestStatus": "Common.Available", + "id": 0 + }, + { + "episodeNumber": 7, + "title": "You Win or You Die", + "airDate": "2011-05-30T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4958/game-of-thrones-1x07-you-win-or-you-die", + "available": true, + "approved": true, + "requested": false, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/30/2011 02:00:00", + "requestStatus": "Common.Available", + "id": 0 + }, + { + "episodeNumber": 8, + "title": "The Pointy End", + "airDate": "2011-06-06T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4959/game-of-thrones-1x08-the-pointy-end", + "available": true, + "approved": true, + "requested": false, + "seasonId": 0, + "season": null, + "airDateDisplay": "06/06/2011 02:00:00", + "requestStatus": "Common.Available", + "id": 0 + }, + { + "episodeNumber": 9, + "title": "Baelor", + "airDate": "2011-06-13T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4960/game-of-thrones-1x09-baelor", + "available": true, + "approved": true, + "requested": false, + "seasonId": 0, + "season": null, + "airDateDisplay": "06/13/2011 02:00:00", + "requestStatus": "Common.Available", + "id": 0 + }, + { + "episodeNumber": 10, + "title": "Fire and Blood", + "airDate": "2011-06-20T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4961/game-of-thrones-1x10-fire-and-blood", + "available": true, + "approved": true, + "requested": false, + "seasonId": 0, + "season": null, + "airDateDisplay": "06/20/2011 02:00:00", + "requestStatus": "Common.Available", + "id": 0 + } + ], + "childRequestId": 0, + "childRequest": null, + "seasonAvailable": true, + "id": 0 + }, + { + "seasonNumber": 2, + "episodes": [ + { + "episodeNumber": 1, + "title": "The North Remembers", + "airDate": "2012-04-02T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4962/game-of-thrones-2x01-the-north-remembers", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/02/2012 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 2, + "title": "The Night Lands", + "airDate": "2012-04-09T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4963/game-of-thrones-2x02-the-night-lands", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/09/2012 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 3, + "title": "What is Dead May Never Die", + "airDate": "2012-04-16T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4964/game-of-thrones-2x03-what-is-dead-may-never-die", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/16/2012 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 4, + "title": "Garden of Bones", + "airDate": "2012-04-23T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4965/game-of-thrones-2x04-garden-of-bones", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/23/2012 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 5, + "title": "The Ghost of Harrenhal", + "airDate": "2012-04-30T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4966/game-of-thrones-2x05-the-ghost-of-harrenhal", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/30/2012 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 6, + "title": "The Old Gods and the New", + "airDate": "2012-05-07T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4967/game-of-thrones-2x06-the-old-gods-and-the-new", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/07/2012 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 7, + "title": "A Man Without Honor", + "airDate": "2012-05-14T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4968/game-of-thrones-2x07-a-man-without-honor", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/14/2012 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 8, + "title": "The Prince of Winterfell", + "airDate": "2012-05-21T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4969/game-of-thrones-2x08-the-prince-of-winterfell", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/21/2012 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 9, + "title": "Blackwater", + "airDate": "2012-05-28T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4970/game-of-thrones-2x09-blackwater", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/28/2012 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 10, + "title": "Valar Morghulis", + "airDate": "2012-06-04T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4971/game-of-thrones-2x10-valar-morghulis", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "06/04/2012 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + } + ], + "childRequestId": 0, + "childRequest": null, + "seasonAvailable": false, + "id": 0 + }, + { + "seasonNumber": 3, + "episodes": [ + { + "episodeNumber": 1, + "title": "Valar Dohaeris", + "airDate": "2013-04-01T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4972/game-of-thrones-3x01-valar-dohaeris", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/01/2013 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 2, + "title": "Dark Wings, Dark Words", + "airDate": "2013-04-08T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4973/game-of-thrones-3x02-dark-wings-dark-words", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/08/2013 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 3, + "title": "Walk of Punishment", + "airDate": "2013-04-15T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4974/game-of-thrones-3x03-walk-of-punishment", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/15/2013 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 4, + "title": "And Now His Watch is Ended", + "airDate": "2013-04-22T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4975/game-of-thrones-3x04-and-now-his-watch-is-ended", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/22/2013 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 5, + "title": "Kissed by Fire", + "airDate": "2013-04-29T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4976/game-of-thrones-3x05-kissed-by-fire", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/29/2013 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 6, + "title": "The Climb", + "airDate": "2013-05-06T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4977/game-of-thrones-3x06-the-climb", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/06/2013 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 7, + "title": "The Bear and the Maiden Fair", + "airDate": "2013-05-13T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4978/game-of-thrones-3x07-the-bear-and-the-maiden-fair", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/13/2013 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 8, + "title": "Second Sons", + "airDate": "2013-05-20T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4979/game-of-thrones-3x08-second-sons", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/20/2013 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 9, + "title": "The Rains of Castamere", + "airDate": "2013-06-03T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4980/game-of-thrones-3x09-the-rains-of-castamere", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "06/03/2013 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 10, + "title": "Mhysa", + "airDate": "2013-06-10T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4981/game-of-thrones-3x10-mhysa", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "06/10/2013 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + } + ], + "childRequestId": 0, + "childRequest": null, + "seasonAvailable": false, + "id": 0 + }, + { + "seasonNumber": 4, + "episodes": [ + { + "episodeNumber": 1, + "title": "Two Swords", + "airDate": "2014-04-07T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4982/game-of-thrones-4x01-two-swords", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/07/2014 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 2, + "title": "The Lion and the Rose", + "airDate": "2014-04-14T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4983/game-of-thrones-4x02-the-lion-and-the-rose", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/14/2014 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 3, + "title": "Breaker of Chains", + "airDate": "2014-04-21T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4984/game-of-thrones-4x03-breaker-of-chains", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/21/2014 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 4, + "title": "Oathkeeper", + "airDate": "2014-04-28T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4985/game-of-thrones-4x04-oathkeeper", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/28/2014 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 5, + "title": "First of His Name", + "airDate": "2014-05-05T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4986/game-of-thrones-4x05-first-of-his-name", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/05/2014 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 6, + "title": "The Laws of Gods and Men", + "airDate": "2014-05-12T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4987/game-of-thrones-4x06-the-laws-of-gods-and-men", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/12/2014 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 7, + "title": "Mockingbird", + "airDate": "2014-05-19T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4988/game-of-thrones-4x07-mockingbird", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/19/2014 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 8, + "title": "The Mountain and the Viper", + "airDate": "2014-06-02T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4989/game-of-thrones-4x08-the-mountain-and-the-viper", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "06/02/2014 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 9, + "title": "The Watchers on the Wall", + "airDate": "2014-06-09T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4990/game-of-thrones-4x09-the-watchers-on-the-wall", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "06/09/2014 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 10, + "title": "The Children", + "airDate": "2014-06-16T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/4991/game-of-thrones-4x10-the-children", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "06/16/2014 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + } + ], + "childRequestId": 0, + "childRequest": null, + "seasonAvailable": false, + "id": 0 + }, + { + "seasonNumber": 5, + "episodes": [ + { + "episodeNumber": 1, + "title": "The Wars to Come", + "airDate": "2015-04-13T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/116522/game-of-thrones-5x01-the-wars-to-come", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/13/2015 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 2, + "title": "The House of Black and White", + "airDate": "2015-04-20T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/144328/game-of-thrones-5x02-the-house-of-black-and-white", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/20/2015 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 3, + "title": "High Sparrow", + "airDate": "2015-04-27T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/144329/game-of-thrones-5x03-high-sparrow", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/27/2015 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 4, + "title": "Sons of the Harpy", + "airDate": "2015-05-04T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/144330/game-of-thrones-5x04-sons-of-the-harpy", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/04/2015 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 5, + "title": "Kill the Boy", + "airDate": "2015-05-11T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/151777/game-of-thrones-5x05-kill-the-boy", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/11/2015 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 6, + "title": "Unbowed, Unbent, Unbroken", + "airDate": "2015-05-18T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/152766/game-of-thrones-5x06-unbowed-unbent-unbroken", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/18/2015 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 7, + "title": "The Gift", + "airDate": "2015-05-25T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/153327/game-of-thrones-5x07-the-gift", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/25/2015 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 8, + "title": "Hardhome", + "airDate": "2015-06-01T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/155299/game-of-thrones-5x08-hardhome", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "06/01/2015 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 9, + "title": "The Dance of Dragons", + "airDate": "2015-06-08T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/160040/game-of-thrones-5x09-the-dance-of-dragons", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "06/08/2015 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 10, + "title": "Mother's Mercy", + "airDate": "2015-06-15T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/162186/game-of-thrones-5x10-mothers-mercy", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "06/15/2015 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + } + ], + "childRequestId": 0, + "childRequest": null, + "seasonAvailable": false, + "id": 0 + }, + { + "seasonNumber": 6, + "episodes": [ + { + "episodeNumber": 1, + "title": "The Red Woman", + "airDate": "2016-04-25T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/560813/game-of-thrones-6x01-the-red-woman", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/25/2016 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 2, + "title": "Home", + "airDate": "2016-05-02T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/664672/game-of-thrones-6x02-home", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/02/2016 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 3, + "title": "Oathbreaker", + "airDate": "2016-05-09T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/664673/game-of-thrones-6x03-oathbreaker", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/09/2016 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 4, + "title": "Book of the Stranger", + "airDate": "2016-05-16T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/664674/game-of-thrones-6x04-book-of-the-stranger", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/16/2016 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 5, + "title": "The Door", + "airDate": "2016-05-23T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/664675/game-of-thrones-6x05-the-door", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/23/2016 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 6, + "title": "Blood of My Blood", + "airDate": "2016-05-30T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/664676/game-of-thrones-6x06-blood-of-my-blood", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/30/2016 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 7, + "title": "The Broken Man", + "airDate": "2016-06-06T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/717449/game-of-thrones-6x07-the-broken-man", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "06/06/2016 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 8, + "title": "No One", + "airDate": "2016-06-13T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/729573/game-of-thrones-6x08-no-one", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "06/13/2016 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 9, + "title": "Battle of the Bastards", + "airDate": "2016-06-20T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/729574/game-of-thrones-6x09-battle-of-the-bastards", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "06/20/2016 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 10, + "title": "The Winds of Winter", + "airDate": "2016-06-27T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/729575/game-of-thrones-6x10-the-winds-of-winter", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "06/27/2016 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + } + ], + "childRequestId": 0, + "childRequest": null, + "seasonAvailable": false, + "id": 0 + }, + { + "seasonNumber": 7, + "episodes": [ + { + "episodeNumber": 1, + "title": "Dragonstone", + "airDate": "2017-07-17T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/937256/game-of-thrones-7x01-dragonstone", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "07/17/2017 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 2, + "title": "Stormborn", + "airDate": "2017-07-24T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/1221410/game-of-thrones-7x02-stormborn", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "07/24/2017 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 3, + "title": "The Queen's Justice", + "airDate": "2017-07-31T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/1221411/game-of-thrones-7x03-the-queens-justice", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "07/31/2017 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 4, + "title": "The Spoils of War", + "airDate": "2017-08-07T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/1221412/game-of-thrones-7x04-the-spoils-of-war", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "08/07/2017 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 5, + "title": "Eastwatch", + "airDate": "2017-08-14T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/1221413/game-of-thrones-7x05-eastwatch", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "08/14/2017 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 6, + "title": "Beyond the Wall", + "airDate": "2017-08-21T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/1221414/game-of-thrones-7x06-beyond-the-wall", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "08/21/2017 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + }, + { + "episodeNumber": 7, + "title": "The Dragon and the Wolf", + "airDate": "2017-08-28T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/1221415/game-of-thrones-7x07-the-dragon-and-the-wolf", + "available": false, + "approved": false, + "requested": true, + "seasonId": 0, + "season": null, + "airDateDisplay": "08/28/2017 02:00:00", + "requestStatus": "Common.PendingApproval", + "id": 0 + } + ], + "childRequestId": 0, + "childRequest": null, + "seasonAvailable": false, + "id": 0 + }, + { + "seasonNumber": 8, + "episodes": [ + { + "episodeNumber": 1, + "title": "Winterfell", + "airDate": "2019-04-15T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/1590943/game-of-thrones-8x01-winterfell", + "available": true, + "approved": true, + "requested": false, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/15/2019 02:00:00", + "requestStatus": "Common.Available", + "id": 0 + }, + { + "episodeNumber": 2, + "title": "A Knight of the Seven Kingdoms", + "airDate": "2019-04-22T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/1623964/game-of-thrones-8x02-a-knight-of-the-seven-kingdoms", + "available": true, + "approved": true, + "requested": false, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/22/2019 02:00:00", + "requestStatus": "Common.Available", + "id": 0 + }, + { + "episodeNumber": 3, + "title": "The Long Night", + "airDate": "2019-04-29T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/1623965/game-of-thrones-8x03-the-long-night", + "available": true, + "approved": true, + "requested": false, + "seasonId": 0, + "season": null, + "airDateDisplay": "04/29/2019 02:00:00", + "requestStatus": "Common.Available", + "id": 0 + }, + { + "episodeNumber": 4, + "title": "The Last of the Starks", + "airDate": "2019-05-06T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/1623966/game-of-thrones-8x04-the-last-of-the-starks", + "available": true, + "approved": true, + "requested": false, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/06/2019 02:00:00", + "requestStatus": "Common.Available", + "id": 0 + }, + { + "episodeNumber": 5, + "title": "The Bells", + "airDate": "2019-05-13T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/1623967/game-of-thrones-8x05-the-bells", + "available": true, + "approved": true, + "requested": false, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/13/2019 02:00:00", + "requestStatus": "Common.Available", + "id": 0 + }, + { + "episodeNumber": 6, + "title": "The Iron Throne", + "airDate": "2019-05-20T02:00:00+01:00", + "url": "https://www.tvmaze.com/episodes/1623968/game-of-thrones-8x06-the-iron-throne", + "available": true, + "approved": true, + "requested": false, + "seasonId": 0, + "season": null, + "airDateDisplay": "05/20/2019 02:00:00", + "requestStatus": "Common.Available", + "id": 0 + } + ], + "childRequestId": 0, + "childRequest": null, + "seasonAvailable": true, + "id": 0 + } + ], + "requestAll": false, + "firstSeason": false, + "latestSeason": false, + "fullyAvailable": false, + "partlyAvailable": false, + "type": 0, + "id": 121361, + "approved": true, + "denied": null, + "deniedReason": null, + "requested": true, + "requestId": 5, + "available": false, + "plexUrl": null, + "embyUrl": null, + "jellyfinUrl": null, + "quality": null, + "imdbId": "tt0944947", + "theTvDbId": "121361", + "theMovieDbId": null, + "subscribed": false, + "showSubscribe": false +} diff --git a/tests/cypress/fixtures/details/tv/streamingResponse.json b/tests/cypress/fixtures/details/tv/streamingResponse.json new file mode 100644 index 000000000..325ac6ecf --- /dev/null +++ b/tests/cypress/fixtures/details/tv/streamingResponse.json @@ -0,0 +1,12 @@ +[ + { + "order": 6, + "streamingProvider": "JamiesNetwork", + "logo": "/hYrcCS72d2alfXdGS1QXNEvwYDY.jpg" + }, + { + "order": 3, + "streamingProvider": "Super1", + "logo": "/zLX0ExkHc8xJ9W4u9JgnldDQLKv.jpg" + } +] diff --git a/tests/cypress/fixtures/example.json b/tests/cypress/fixtures/example.json new file mode 100644 index 000000000..da18d9352 --- /dev/null +++ b/tests/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/tests/cypress/fixtures/login/authenticationSettngs.json b/tests/cypress/fixtures/login/authenticationSettngs.json new file mode 100644 index 000000000..a3bfe250d --- /dev/null +++ b/tests/cypress/fixtures/login/authenticationSettngs.json @@ -0,0 +1,10 @@ +{ + "allowNoPassword": false, + "requireDigit": false, + "requiredLength": 0, + "requireLowercase": false, + "requireNonAlphanumeric": false, + "requireUppercase": false, + "enableOAuth": true, + "id": 14 +} diff --git a/tests/cypress/fixtures/login/landingPageSettings.json b/tests/cypress/fixtures/login/landingPageSettings.json new file mode 100644 index 000000000..e6261df29 --- /dev/null +++ b/tests/cypress/fixtures/login/landingPageSettings.json @@ -0,0 +1,10 @@ +{ + "enabled": false, + "noticeEnabled": false, + "noticeText": "Hey what's up!\n
\n
\nThe username and password is beta\n
\n
\nEnjoy!", + "timeLimit": false, + "startDateTime": "0001-01-01T00:00:00", + "endDateTime": "0001-01-01T00:00:00", + "expired": false, + "id": 0 +} diff --git a/tests/cypress/integration/examples/actions.spec.js b/tests/cypress/integration/examples/actions.spec.js new file mode 100644 index 000000000..092637998 --- /dev/null +++ b/tests/cypress/integration/examples/actions.spec.js @@ -0,0 +1,299 @@ +/// + +context('Actions', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/actions') + }) + + // https://on.cypress.io/interacting-with-elements + + it('.type() - type into a DOM element', () => { + // https://on.cypress.io/type + cy.get('.action-email') + .type('fake@email.com').should('have.value', 'fake@email.com') + + // .type() with special character sequences + .type('{leftarrow}{rightarrow}{uparrow}{downarrow}') + .type('{del}{selectall}{backspace}') + + // .type() with key modifiers + .type('{alt}{option}') //these are equivalent + .type('{ctrl}{control}') //these are equivalent + .type('{meta}{command}{cmd}') //these are equivalent + .type('{shift}') + + // Delay each keypress by 0.1 sec + .type('slow.typing@email.com', { delay: 100 }) + .should('have.value', 'slow.typing@email.com') + + cy.get('.action-disabled') + // Ignore error checking prior to type + // like whether the input is visible or disabled + .type('disabled error checking', { force: true }) + .should('have.value', 'disabled error checking') + }) + + it('.focus() - focus on a DOM element', () => { + // https://on.cypress.io/focus + cy.get('.action-focus').focus() + .should('have.class', 'focus') + .prev().should('have.attr', 'style', 'color: orange;') + }) + + it('.blur() - blur off a DOM element', () => { + // https://on.cypress.io/blur + cy.get('.action-blur').type('About to blur').blur() + .should('have.class', 'error') + .prev().should('have.attr', 'style', 'color: red;') + }) + + it('.clear() - clears an input or textarea element', () => { + // https://on.cypress.io/clear + cy.get('.action-clear').type('Clear this text') + .should('have.value', 'Clear this text') + .clear() + .should('have.value', '') + }) + + it('.submit() - submit a form', () => { + // https://on.cypress.io/submit + cy.get('.action-form') + .find('[type="text"]').type('HALFOFF') + + cy.get('.action-form').submit() + .next().should('contain', 'Your form has been submitted!') + }) + + it('.click() - click on a DOM element', () => { + // https://on.cypress.io/click + cy.get('.action-btn').click() + + // You can click on 9 specific positions of an element: + // ----------------------------------- + // | topLeft top topRight | + // | | + // | | + // | | + // | left center right | + // | | + // | | + // | | + // | bottomLeft bottom bottomRight | + // ----------------------------------- + + // clicking in the center of the element is the default + cy.get('#action-canvas').click() + + cy.get('#action-canvas').click('topLeft') + cy.get('#action-canvas').click('top') + cy.get('#action-canvas').click('topRight') + cy.get('#action-canvas').click('left') + cy.get('#action-canvas').click('right') + cy.get('#action-canvas').click('bottomLeft') + cy.get('#action-canvas').click('bottom') + cy.get('#action-canvas').click('bottomRight') + + // .click() accepts an x and y coordinate + // that controls where the click occurs :) + + cy.get('#action-canvas') + .click(80, 75) // click 80px on x coord and 75px on y coord + .click(170, 75) + .click(80, 165) + .click(100, 185) + .click(125, 190) + .click(150, 185) + .click(170, 165) + + // click multiple elements by passing multiple: true + cy.get('.action-labels>.label').click({ multiple: true }) + + // Ignore error checking prior to clicking + cy.get('.action-opacity>.btn').click({ force: true }) + }) + + it('.dblclick() - double click on a DOM element', () => { + // https://on.cypress.io/dblclick + + // Our app has a listener on 'dblclick' event in our 'scripts.js' + // that hides the div and shows an input on double click + cy.get('.action-div').dblclick().should('not.be.visible') + cy.get('.action-input-hidden').should('be.visible') + }) + + it('.rightclick() - right click on a DOM element', () => { + // https://on.cypress.io/rightclick + + // Our app has a listener on 'contextmenu' event in our 'scripts.js' + // that hides the div and shows an input on right click + cy.get('.rightclick-action-div').rightclick().should('not.be.visible') + cy.get('.rightclick-action-input-hidden').should('be.visible') + }) + + it('.check() - check a checkbox or radio element', () => { + // https://on.cypress.io/check + + // By default, .check() will check all + // matching checkbox or radio elements in succession, one after another + cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]') + .check().should('be.checked') + + cy.get('.action-radios [type="radio"]').not('[disabled]') + .check().should('be.checked') + + // .check() accepts a value argument + cy.get('.action-radios [type="radio"]') + .check('radio1').should('be.checked') + + // .check() accepts an array of values + cy.get('.action-multiple-checkboxes [type="checkbox"]') + .check(['checkbox1', 'checkbox2']).should('be.checked') + + // Ignore error checking prior to checking + cy.get('.action-checkboxes [disabled]') + .check({ force: true }).should('be.checked') + + cy.get('.action-radios [type="radio"]') + .check('radio3', { force: true }).should('be.checked') + }) + + it('.uncheck() - uncheck a checkbox element', () => { + // https://on.cypress.io/uncheck + + // By default, .uncheck() will uncheck all matching + // checkbox elements in succession, one after another + cy.get('.action-check [type="checkbox"]') + .not('[disabled]') + .uncheck().should('not.be.checked') + + // .uncheck() accepts a value argument + cy.get('.action-check [type="checkbox"]') + .check('checkbox1') + .uncheck('checkbox1').should('not.be.checked') + + // .uncheck() accepts an array of values + cy.get('.action-check [type="checkbox"]') + .check(['checkbox1', 'checkbox3']) + .uncheck(['checkbox1', 'checkbox3']).should('not.be.checked') + + // Ignore error checking prior to unchecking + cy.get('.action-check [disabled]') + .uncheck({ force: true }).should('not.be.checked') + }) + + it('.select() - select an option in a