!wip Made a start on adding CouchPotato, most of the UI is there but nothing is done with it yet #865

Also improved the error messages on the settings
pull/1526/head^2
Jamie.Rees 7 years ago
parent 532530a954
commit e735df5d0a

@ -0,0 +1,84 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using Ombi.Api.CouchPotato.Models;
using Ombi.Helpers;
namespace Ombi.Api.CouchPotato
{
public class CouchPotatoApi : ICouchPotatoApi
{
public CouchPotatoApi(IApi api, ILogger<CouchPotatoApi> log)
{
_api = api;
_log = log;
}
private readonly IApi _api;
private readonly ILogger<CouchPotatoApi> _log;
public async Task<bool> AddMovie(string imdbid, string apiKey, string title, string baseUrl, string profileId = default(string))
{
var request = new Request($"/api/{apiKey}/movie.add", baseUrl, HttpMethod.Get);
request.AddQueryString("title", title);
request.AddQueryString("identifier", imdbid);
if (!string.IsNullOrEmpty(profileId))
{
request.AddQueryString("profile_id", profileId);
}
var obj = await _api.Request<JObject>(request);
if (obj.Count > 0)
{
try
{
var result = (bool)obj["success"];
return result;
}
catch (Exception e)
{
_log.LogError(LoggingEvents.CouchPotatoApi, e, "Error calling AddMovie");
return false;
}
}
return false;
}
public async Task<CouchPotatoStatus> Status(string url, string apiKey)
{
var request = new Request($"api/{apiKey}/app.available/", url, HttpMethod.Get);
return await _api.Request<CouchPotatoStatus>(request);
}
public async Task<CouchPotatoProfiles> GetProfiles(string url, string apiKey)
{
var request = new Request($"api/{apiKey}/profile.list/", url, HttpMethod.Get);
return await _api.Request<CouchPotatoProfiles>(request);
}
public async Task<CouchPotatoMovies> GetMovies(string baseUrl, string apiKey, string[] status)
{
var request = new Request($"/api/{apiKey}/movie.list", baseUrl, HttpMethod.Get);
request.AddQueryString("status",string.Join(",", status));
request.OnBeforeDeserialization = json =>
{
json.Replace("[]", "{}");
};
return await _api.Request<CouchPotatoMovies>(request);
}
public async Task<CouchPotatoApiKey> GetApiKey(string baseUrl, string username, string password)
{
var request = new Request("getkey/",baseUrl, HttpMethod.Get);
request.AddQueryString("u",username.CalcuateMd5Hash());
request.AddQueryString("p",password.CalcuateMd5Hash());
return await _api.Request<CouchPotatoApiKey>(request);
}
}
}

@ -0,0 +1,14 @@
using System.Threading.Tasks;
using Ombi.Api.CouchPotato.Models;
namespace Ombi.Api.CouchPotato
{
public interface ICouchPotatoApi
{
Task<bool> AddMovie(string imdbid, string apiKey, string title, string baseUrl, string profileId = null);
Task<CouchPotatoApiKey> GetApiKey(string baseUrl, string username, string password);
Task<CouchPotatoMovies> GetMovies(string baseUrl, string apiKey, string[] status);
Task<CouchPotatoProfiles> GetProfiles(string url, string apiKey);
Task<CouchPotatoStatus> Status(string url, string apiKey);
}
}

@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace Ombi.Api.CouchPotato.Models
{
public class CouchPotatoApiKey
{
[JsonProperty("success")]
public bool success { get; set; }
[JsonProperty("api_key")]
public string ApiKey { get; set; }
}
}

@ -0,0 +1,99 @@
using System.Collections.Generic;
namespace Ombi.Api.CouchPotato.Models
{
public class CouchPotatoMovies
{
public List<Movie> movies { get; set; }
public int total { get; set; }
public bool success { get; set; }
public bool empty { get; set; }
}
public class Movie
{
public string _id { get; set; }
public string _rev { get; set; }
public string _t { get; set; }
public object category_id { get; set; }
public Files files { get; set; }
public Identifiers identifiers { get; set; }
public Info info { get; set; }
public int last_edit { get; set; }
public string profile_id { get; set; }
public List<object> releases { get; set; }
public string status { get; set; }
public List<object> tags { get; set; }
public string title { get; set; }
public string type { get; set; }
}
public class CouchPotatoAdd
{
public Movie movie { get; set; }
public bool success { get; set; }
}
public class Rating
{
public List<string> imdb { get; set; }
}
public class Images
{
public List<string> actors { get; set; }
public List<string> backdrop { get; set; }
public List<string> backdrop_original { get; set; }
public List<object> banner { get; set; }
public List<object> clear_art { get; set; }
public List<object> disc_art { get; set; }
public List<object> extra_fanart { get; set; }
public List<object> extra_thumbs { get; set; }
public List<object> landscape { get; set; }
public List<object> logo { get; set; }
public List<string> poster { get; set; }
public List<string> poster_original { get; set; }
}
public class Info
{
public List<string> actor_roles { get; set; }
public List<string> actors { get; set; }
public List<string> directors { get; set; }
public List<string> genres { get; set; }
public Images images { get; set; }
public string imdb { get; set; }
public string mpaa { get; set; }
public string original_title { get; set; }
public string plot { get; set; }
public Rating rating { get; set; }
public Release_Date release_date { get; set; }
public string released { get; set; }
public int runtime { get; set; }
public string tagline { get; set; }
public List<string> titles { get; set; }
public int tmdb_id { get; set; }
public string type { get; set; }
public bool via_imdb { get; set; }
public bool via_tmdb { get; set; }
public List<string> writers { get; set; }
public int year { get; set; }
}
public class Release_Date
{
public bool bluray { get; set; }
public int dvd { get; set; }
public int expires { get; set; }
public int theater { get; set; }
}
public class Files
{
public List<string> image_poster { get; set; }
}
public class Identifiers
{
public string imdb { get; set; }
}
}

@ -0,0 +1,29 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Ombi.Api.CouchPotato.Models
{
public class ProfileList
{
public bool core { get; set; }
public bool hide { get; set; }
public string _rev { get; set; }
public List<bool> finish { get; set; }
public List<string> qualities { get; set; }
public string _id { get; set; }
public string _t { get; set; }
public string label { get; set; }
public int minimum_score { get; set; }
public List<int> stop_after { get; set; }
public List<object> wait_for { get; set; }
public int order { get; set; }
[JsonProperty(PropertyName = "3d")]
public List<object> threeD { get; set; }
}
public class CouchPotatoProfiles
{
public List<ProfileList> list { get; set; }
public bool success { get; set; }
}
}

@ -0,0 +1,7 @@
namespace Ombi.Api.CouchPotato.Models
{
public class CouchPotatoStatus
{
public bool success { get; set; }
}
}

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ombi.Api\Ombi.Api.csproj" />
</ItemGroup>
</Project>

@ -52,6 +52,7 @@ namespace Ombi.Api
var receivedString = await data.ReadAsStringAsync();
if (request.ContentType == ContentType.Json)
{
request.OnBeforeDeserialization?.Invoke(receivedString);
return JsonConvert.DeserializeObject<T>(receivedString, Settings);
}
else

@ -25,6 +25,8 @@ namespace Ombi.Api
public string BaseUrl { get; }
public HttpMethod HttpMethod { get; }
public Action<string> OnBeforeDeserialization { get; set; }
private string FullUrl
{
get

@ -8,6 +8,7 @@ using Ombi.Api.Sonarr.Models;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities.Requests;
namespace Ombi.Core.Senders

@ -31,6 +31,7 @@ using Ombi.Store.Repository;
using Ombi.Notifications.Agents;
using Ombi.Schedule.Jobs.Radarr;
using Ombi.Api;
using Ombi.Api.CouchPotato;
using Ombi.Api.FanartTv;
using Ombi.Api.Mattermost;
using Ombi.Api.Pushbullet;
@ -95,6 +96,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IFanartTvApi, FanartTvApi>();
services.AddTransient<IPushoverApi, PushoverApi>();
services.AddTransient<IMattermostApi, MattermostApi>();
services.AddTransient<ICouchPotatoApi, CouchPotatoApi>();
}
public static void RegisterStore(this IServiceCollection services)

@ -15,6 +15,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ombi.Api.CouchPotato\Ombi.Api.CouchPotato.csproj" />
<ProjectReference Include="..\Ombi.Api.Discord\Ombi.Api.Discord.csproj" />
<ProjectReference Include="..\Ombi.Api.Emby\Ombi.Api.Emby.csproj" />
<ProjectReference Include="..\Ombi.Api.FanartTv\Ombi.Api.FanartTv.csproj" />

@ -8,6 +8,7 @@ namespace Ombi.Helpers
public static EventId Api => new EventId(1000);
public static EventId RadarrApi => new EventId(1001);
public static EventId CouchPotatoApi => new EventId(1002);
public static EventId Cacher => new EventId(2000);
public static EventId RadarrCacher => new EventId(2001);

@ -8,6 +8,7 @@ using Ombi.Api.Sonarr;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Context;
using Ombi.Store.Entities;

@ -0,0 +1,13 @@
using Ombi.Core.Settings.Models.External;
namespace Ombi.Settings.Settings.Models.External
{
public class CouchPotatoSettings : ExternalSettings
{
public bool Enabled { get; set; }
public string ApiKey { get; set; }
public string DefaultProfileId { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
}

@ -1,4 +1,5 @@
using System.Collections.Generic;
using Ombi.Settings.Settings.Models.External;
namespace Ombi.Core.Settings.Models.External
{

@ -1,10 +1,9 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json;
using Ombi.Helpers;
namespace Ombi.Core.Settings.Models.External
namespace Ombi.Settings.Settings.Models.External
{
public abstract class ExternalSettings : Ombi.Settings.Settings.Models.Settings
public abstract class ExternalSettings : Models.Settings
{
public bool Ssl { get; set; }
public string SubDir { get; set; }

@ -1,4 +1,5 @@
using System.Collections.Generic;
using Ombi.Settings.Settings.Models.External;
namespace Ombi.Core.Settings.Models.External
{

@ -1,4 +1,4 @@
namespace Ombi.Core.Settings.Models.External
namespace Ombi.Settings.Settings.Models.External
{
public class SonarrSettings : ExternalSettings
{

@ -80,6 +80,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Pushover", "Ombi.A
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Schedule.Tests", "Ombi.Schedule.Tests\Ombi.Schedule.Tests.csproj", "{BDD8B924-016E-4CDA-9FFA-50B0A34BCD3C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Api.CouchPotato", "Ombi.Api.CouchPotato\Ombi.Api.CouchPotato.csproj", "{87D7897D-7C73-4856-A0AA-FF5948F4EA86}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -202,6 +204,10 @@ Global
{BDD8B924-016E-4CDA-9FFA-50B0A34BCD3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BDD8B924-016E-4CDA-9FFA-50B0A34BCD3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BDD8B924-016E-4CDA-9FFA-50B0A34BCD3C}.Release|Any CPU.Build.0 = Release|Any CPU
{87D7897D-7C73-4856-A0AA-FF5948F4EA86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{87D7897D-7C73-4856-A0AA-FF5948F4EA86}.Debug|Any CPU.Build.0 = Debug|Any CPU
{87D7897D-7C73-4856-A0AA-FF5948F4EA86}.Release|Any CPU.ActiveCfg = Release|Any CPU
{87D7897D-7C73-4856-A0AA-FF5948F4EA86}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -230,6 +236,7 @@ Global
{737B2620-FE5A-4135-A017-79C269A7D36C} = {9293CA11-360A-4C20-A674-B9E794431BF5}
{CA55DD4F-4EFF-4906-A848-35FCC7BD5654} = {9293CA11-360A-4C20-A674-B9E794431BF5}
{BDD8B924-016E-4CDA-9FFA-50B0A34BCD3C} = {6F42AB98-9196-44C4-B888-D5E409F415A1}
{87D7897D-7C73-4856-A0AA-FF5948F4EA86} = {9293CA11-360A-4C20-A674-B9E794431BF5}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {192E9BF8-00B4-45E4-BCCC-4C215725C869}

@ -0,0 +1,25 @@
export interface ICouchPotatoProfiles {
success: boolean;
list: IProfileList[];
}
export interface IProfileList {
core: boolean;
hide: boolean;
_rev: string;
finish: boolean[];
qualities: string[];
_id: string;
_t: string;
label: string;
minimum_score: number;
stop_after: number[];
wait_for: object[];
order: number;
threeD: object[];
}
export interface ICouchPotatoApiKey {
success: boolean;
apiKey: string;
}

@ -119,3 +119,11 @@ export interface IAbout {
processArchitecture: string;
applicationBasePath: string;
}
export interface ICouchPotatoSettings extends IExternalSettings {
enabled: boolean;
apiKey: string;
defaultProfileId: string;
username: string;
password: string;
}

@ -1,4 +1,5 @@
export * from "./ICommon";
export * from "./ICouchPotato";
export * from "./IImages";
export * from "./IMediaServerStatus";
export * from "./INotificationSettings";

@ -0,0 +1,23 @@
import { PlatformLocation } from "@angular/common";
import { Injectable } from "@angular/core";
import { AuthHttp } from "angular2-jwt";
import { Observable } from "rxjs/Rx";
import { ServiceAuthHelpers } from "../service.helpers";
import { ICouchPotatoApiKey, ICouchPotatoProfiles, ICouchPotatoSettings } from "../../interfaces";
@Injectable()
export class CouchPotatoService extends ServiceAuthHelpers {
constructor(http: AuthHttp, public platformLocation: PlatformLocation) {
super(http, "/api/v1/CouchPotato/", platformLocation);
}
public getProfiles(settings: ICouchPotatoSettings): Observable<ICouchPotatoProfiles> {
return this.http.post(`${this.url}profile`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData);
}
public getApiKey(settings: ICouchPotatoSettings): Observable<ICouchPotatoApiKey> {
return this.http.post(`${this.url}apikey`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData);
}
}

@ -1,4 +1,5 @@
export * from "./emby.service";
export * from "./couchpotato.service";
export * from "./emby.service";
export * from "./plex.service";
export * from "./radarr.service";
export * from "./sonarr.service";

@ -7,6 +7,7 @@ import { Observable } from "rxjs/Rx";
import {
IAbout,
IAuthenticationSettings,
ICouchPotatoSettings,
ICustomizationSettings,
IDiscordNotifcationSettings,
IEmailNotificationSettings,
@ -195,4 +196,14 @@ export class SettingsService extends ServiceAuthHelpers {
.post(`${this.url}/UserManagement`, JSON.stringify(settings), { headers: this.headers })
.map(this.extractData).catch(this.handleError);
}
public getCouchPotatoSettings(): Observable<ICouchPotatoSettings> {
return this.httpAuth.get(`${this.url}/UserManagement`).map(this.extractData).catch(this.handleError);
}
public saveCouchPotatoSettings(settings: ICouchPotatoSettings): Observable<boolean> {
return this.httpAuth
.post(`${this.url}/UserManagement`, JSON.stringify(settings), { headers: this.headers })
.map(this.extractData).catch(this.handleError);
}
}

@ -0,0 +1,100 @@
<settings-menu>
</settings-menu>
<div *ngIf="form">
<fieldset>
<legend>CouchPotato Settings</legend>
<form novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)" style="padding-top:5%;">
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="enable" formControlName="enabled" ng-checked="form.enabled">
<label for="enable">Enable</label>
</div>
</div>
<div class="form-group">
<label for="Ip" class="control-label">Hostname or IP</label>
<div class="">
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('ip').hasError('required')}"
id="Ip" name="Ip" placeholder="localhost" formControlName="ip">
<small *ngIf="form.get('ip').hasError('required')" class="error-text">The IP/Hostname is required</small>
</div>
</div>
<div class="form-group">
<label for="portNumber" class="control-label">Port</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('port').hasError('required')}" formControlName="port" id="portNumber" name="Port" placeholder="Port Number">
<small *ngIf="form.get('port').hasError('required')" class="error-text">The Port is required</small>
</div>
<div class="form-group">
<label for="ApiKey" class="control-label">API Key</label>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('apiKey').hasError('required')}" id="ApiKey" name="ApiKey" formControlName="apiKey">
<small *ngIf="form.get('apiKey').hasError('required')" class="error-text">The API Key is required</small>
</div>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" id="Ssl" name="Ssl" formControlName="ssl"><label for="Ssl">SSL</label>
</div>
</div>
<div class="form-group">
<label for="SubDir" class="control-label">Base Url</label>
<div>
<input type="text" class="form-control form-control-custom" formControlName="subDir" id="SubDir" name="SubDir">
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="username" class="control-label">Username</label>
<input type="text" class="form-control form-control-custom " formControlName="username" name="username">
</div>
<div class="form-group">
<label for="password" class="control-label">Password</label>
<input type="text" class="form-control form-control-custom " formControlName="password" name="password">
</div>
<div class="form-group">
<button class="btn btn-primary-outline" (click)="requestToken(form)">Request Api Key <i class="fa fa-key"></i></button>
</div>
<div class="form-group">
<div>
<button type="submit" (click)="getProfiles(form)" class="btn btn-primary-outline">Get Quality Profiles <span *ngIf="profilesRunning" class="fa fa-spinner fa-spin"> </span></button>
</div>
</div>
<div class="form-group">
<label for="select" class="control-label">Quality Profiles</label>
<div id="profiles">
<select formControlName="defaultProfileId" class="form-control form-control-custom" id="select">
<option *ngFor="let profile of profiles?.list" value="{{profile._id}}">{{profile.label}}</option>
</select>
</div>
</div>
<div class="form-group">
<div>
<button [disabled]="form.invalid" (click)="test(form)" class="btn btn-primary-outline">Test Connectivity <span id="spinner"></span></button>
</div>
</div>
<div class="form-group">
<div>
<button type="submit" [disabled]="form.invalid" class="btn btn-primary-outline ">Submit</button>
</div>
</div>
</div>
</form>
</fieldset>
</div>

@ -0,0 +1,90 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms";
import { CouchPotatoService, NotificationService, SettingsService, TesterService } from "../../services";
import { ICouchPotatoProfiles } from "../../interfaces";
@Component({
templateUrl: "./couchpotato.component.html",
})
export class CouchPotatoComponent implements OnInit {
public form: FormGroup;
public profiles: ICouchPotatoProfiles;
public profilesRunning: boolean;
constructor(private readonly settingsService: SettingsService,
private readonly fb: FormBuilder,
private readonly notificationService: NotificationService,
private readonly couchPotatoService: CouchPotatoService,
private readonly testerService: TesterService) { }
public ngOnInit() {
this.settingsService.getCouchPotatoSettings().subscribe(x => {
this.form = this.fb.group({
enabled: [x.enabled],
username: [x.username],
password: [x.password],
apiKey: [x.apiKey, Validators.required],
ip: [x.ip, Validators.required],
port: [x.port, Validators.required],
ssl: [x.ssl],
subDir: [x.subDir],
defaultProfileId: [x.defaultProfileId],
});
});
}
public getProfiles(form: FormGroup) {
this.profilesRunning = true;
this.couchPotatoService.getProfiles(form.value).subscribe(x => {
this.profiles = x;
this.profilesRunning = false;
});
}
public onSubmit(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Validation", "Please check your entered values");
return;
}
const settings = form.value;
this.settingsService.saveCouchPotatoSettings(settings).subscribe(x => {
if (x) {
this.notificationService.success("Settings Saved", "Successfully saved the CouchPotato settings");
} else {
this.notificationService.success("Settings Saved", "There was an error when saving the CouchPotato settings");
}
});
}
public test(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Validation", "Please check your entered values");
return;
}
const settings = form.value;
this.testerService.radarrTest(settings).subscribe(x => {
if (x === true) {
this.notificationService.success("Connected", "Successfully connected to Radarr!");
} else {
this.notificationService.error("Connected", "We could not connect to Radarr!");
}
});
}
public requestToken(form: FormGroup) {
this.couchPotatoService.getApiKey(form.value).subscribe(x => {
if (x.success === true) {
(<FormControl>this.form.controls.apiKey).setValue(x.apiKey);
this.notificationService.success("Api Key", "Successfully got the Api Key");
} else {
this.notificationService.error("Api Key", "Could not get the Api Key");
}
});
}
}

@ -13,15 +13,11 @@
</div>
</div>
<div *ngIf="form.invalid && form.dirty" class="alert alert-danger">
<div *ngIf="form.get('webhookUrl').hasError('required')">The Webhook Url is required</div>
</div>
<div class="form-group">
<label for="webhookUrl" class="control-label">Webhook Url</label>
<div>
<input type="text" class="form-control form-control-custom " id="webhookUrl" name="webhookUrl" formControlName="webhookUrl">
</div>
<input type="text" class="form-control form-control-custom " id="webhookUrl" name="webhookUrl" formControlName="webhookUrl" [ngClass]="{'form-error': form.get('webhookUrl').hasError('required')}">
<small *ngIf="form.get('webhookUrl').hasError('required')" class="error-text">The Webhook Url is required</small>
</div>
<div class="form-group">

@ -18,35 +18,27 @@
<input type="checkbox" id="Authentication" formControlName="authentication"><label for="Authentication">Enable SMTP Authentication</label>
</div>
</div>
<div *ngIf="emailForm.invalid && emailForm.dirty" class="alert alert-danger">
<div *ngIf="emailForm.get('host').hasError('required')">Host is required</div>
<div *ngIf="emailForm.get('port').hasError('required')">The Port is required</div>
<div *ngIf="emailForm.get('senderAddress').hasError('required')">The Email Sender Address is required</div>
<div *ngIf="emailForm.get('senderAddress').hasError('incorrectMailFormat')">The Email Sender Address needs to be a valid email address</div>
<div *ngIf="emailForm.get('adminEmail').hasError('required')">The Email Sender is required</div>
<div *ngIf="emailForm.get('adminEmail').hasError('email')">The Admin Email needs to be a valid email address</div>
<div *ngIf="emailForm.get('username').hasError('required')">The Username is required</div>
<div *ngIf="emailForm.get('password').hasError('required')">The Password is required</div>
</div>
<div class="form-group">
<label for="host" class="control-label">SMTP Host</label>
<div>
<input type="text" class="form-control form-control-custom " id="host" name="host" placeholder="localhost" formControlName="host">
</div>
<input type="text" class="form-control form-control-custom " id="host" name="host" placeholder="localhost" formControlName="host" [ngClass]="{'form-error': emailForm.get('host').hasError('required')}">
<small *ngIf="emailForm.get('host').hasError('required')" class="error-text">The Host is required</small>
</div>
<div class="form-group">
<label for="portNumber" class="control-label">SMTP Port</label>
<div>
<input type="text" class="form-control form-control-custom " id="portNumber" name="Port" placeholder="Port Number" formControlName="port">
<input type="text" class="form-control form-control-custom " [ngClass]="{'form-error': emailForm.get('port').hasError('required')}" id="portNumber" name="Port" placeholder="Port Number" formControlName="port">
<small *ngIf="emailForm.get('port').hasError('required')" class="error-text">The Port is required</small>
</div>
</div>
<div class="form-group">
<label style="padding-left: 0" for="senderAddress" class="control-label col-md-12">Email Sender</label>
<div style="padding-left: 0" class="col-md-6">
<input type="text" class="form-control form-control-custom " id="senderAddress" name="senderAddress" formControlName="senderAddress" tooltipPosition="top" placeholder="Sender Address" pTooltip="The email address that the emails will be sent from">
<input type="text" class="form-control form-control-custom " id="senderAddress" [ngClass]="{'form-error': emailForm.get('senderAddress').hasError('required'), 'form-error': emailForm.get('senderAddress').hasError('incorrectMailFormat')}" name="senderAddress" formControlName="senderAddress" tooltipPosition="top" placeholder="Sender Address" pTooltip="The email address that the emails will be sent from">
<small *ngIf="emailForm.get('senderAddress').hasError('required')" class="error-text">The Email Sender Address is required</small>
<small *ngIf="emailForm.get('senderAddress').hasError('email') && !emailForm.get('senderAddress').hasError('required')" class="error-text">The Email Sender Address needs to be a valid email address</small>
</div>
<div style="padding-left: 0" class="col-md-6">
<input type="text" class="form-control form-control-custom " id="senderName" name="senderName" formControlName="senderName" tooltipPosition="top" placeholder="Sender Name" pTooltip="The 'Friendly' name that will appear in the 'FROM:' part of the email">
@ -58,24 +50,26 @@
<div class="form-group">
<label for="adminEmail" class="control-label">Admin Email</label>
<div>
<input type="text" class="form-control form-control-custom " id="adminEmail" name="adminEmail" formControlName="adminEmail" tooltipPosition="top" pTooltip="The administrator email will be used to send emails for admin only notifications (e.g. New Requests that require approvals)">
</div>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': emailForm.get('adminEmail').hasError('required'), 'form-error': emailForm.get('adminEmail').hasError('email')}" id="adminEmail" name="adminEmail" formControlName="adminEmail" tooltipPosition="top" pTooltip="The administrator email will be used to send emails for admin only notifications (e.g. New Requests that require approvals)">
<small *ngIf="emailForm.get('adminEmail').hasError('required')" class="error-text">The Admin Email Address is required</small>
<small *ngIf="emailForm.get('adminEmail').hasError('email') && !emailForm.get('adminEmail').hasError('required')" class="error-text">The Admin Email needs to be a valid email address</small>
</div>
<div class="form-group" *ngIf="emailForm.controls['username'].validator">
<label for="username" class="control-label">Username</label>
<div>
<input type="text" class="form-control form-control-custom " id="username" name="username" formControlName="username" pTooltip="The username if authentication is enabled" tooltipPosition="top">
</div>
<input type="text" class="form-control form-control-custom" [ngClass]="{'form-error': emailForm.get('username').hasError('required')}" id="username" name="username" formControlName="username" pTooltip="The username if authentication is enabled" tooltipPosition="top">
<small *ngIf="emailForm.get('username').hasError('required')" class="error-text">The Username is required</small>
</div>
<div class="form-group" *ngIf="emailForm.get('password').validator">
<label for="password" class="control-label">Password</label>
<div>
<input type="password" class="form-control form-control-custom " id="password" name="password" formControlName="password" pTooltip="The password if authentication is enabled" tooltipPosition="top">
</div>
<input type="password" class="form-control form-control-custom" [ngClass]="{'form-error': emailForm.get('password').hasError('required')}" id="password" name="password" formControlName="password" pTooltip="The password if authentication is enabled" tooltipPosition="top">
<small *ngIf="emailForm.get('password').hasError('required')" class="error-text">The Password is required</small>
</div>
<div class="form-group">

@ -13,16 +13,12 @@
</div>
</div>
<div *ngIf="form.invalid && form.dirty" class="alert alert-danger">
<div *ngIf="form.get('webhookUrl').hasError('required')">The Incoming Webhook Url is required</div>
</div>
<div class="form-group">
<small class="control-label"> Mattermost > Integrations > Incoming Webhook > Add Incoming Webhook. You will then have a Webhook</small>
<label for="webhookUrl" class="control-label">Incoming Webhook Url</label>
<div>
<input type="text" class="form-control form-control-custom " id="webhookUrl" name="webhookUrl" formControlName="webhookUrl">
</div>
<input type="text" class="form-control form-control-custom " id="webhookUrl" name="webhookUrl" formControlName="webhookUrl" [ngClass]="{'form-error': form.get('webhookUrl').hasError('required')}">
<small *ngIf="form.get('webhookUrl').hasError('required')" class="error-text">The Webhook Url is required</small>
</div>

@ -13,15 +13,12 @@
</div>
</div>
<div *ngIf="form.invalid && form.dirty" class="alert alert-danger">
<div *ngIf="form.get('accessToken').hasError('required')">The Access Token is required</div>
</div>
<small>You can find this here: <a href="https://www.pushbullet.com/#settings/account">https://www.pushbullet.com/#settings/account </a></small>
<div class="form-group">
<label for="accessToken" class="control-label">Access Token</label>
<div>
<input type="text" class="form-control form-control-custom " id="accessToken" name="accessToken" formControlName="accessToken">
</div>
<input type="text" class="form-control form-control-custom " id="accessToken" name="accessToken" formControlName="accessToken" [ngClass]="{'form-error': form.get('accessToken').hasError('required')}">
<small *ngIf="form.get('accessToken').hasError('required')" class="error-text">The Access Token is required</small>
</div>
<div class="form-group">

@ -13,14 +13,12 @@
</div>
</div>
<div *ngIf="form.invalid && form.dirty" class="alert alert-danger">
<div *ngIf="form.get('accessToken').hasError('required')">The Access Token is required</div>
</div>
<div class="form-group">
<label for="accessToken" class="control-label">Access Token</label>
<div>
<input type="text" class="form-control form-control-custom " id="accessToken" name="accessToken" formControlName="accessToken" pTooltip="Enter your API Key from Pushover.">
</div>
<input type="text" class="form-control form-control-custom " id="accessToken" name="accessToken" [ngClass]="{'form-error': form.get('accessToken').hasError('required')}" formControlName="accessToken" pTooltip="Enter your API Key from Pushover.">
<small *ngIf="form.get('accessToken').hasError('required')" class="error-text">The Access Token is required</small>
</div>
<div class="form-group">

@ -14,17 +14,15 @@
</div>
</div>
<div *ngIf="form.invalid && form.dirty" class="alert alert-danger">
<div *ngIf="form.get('webhookUrl').hasError('required')">The Webhook Url is required</div>
</div>
<div class="form-group">
<label for="webhookUrl" class="control-label">Webhook Url</label>
<div>
<small class="control-label"> Click <a target="_blank" href="https://my.slack.com/services/new/incoming-webhook/">Here</a> and follow the guide. You will then have a Webhook Url</small>
<input type="text" class="form-control form-control-custom " id="webhookUrl" name="webhookUrl" formControlName="webhookUrl">
<input type="text" class="form-control form-control-custom " id="webhookUrl" name="webhookUrl" formControlName="webhookUrl" [ngClass]="{'form-error': form.get('webhookUrl').hasError('required')}">
<small *ngIf="form.get('webhookUrl').hasError('required')" class="error-text">The Webhook Url is required</small>
</div>
</div>

@ -8,19 +8,6 @@
<p-inputSwitch id="customInputSwitch" [(ngModel)]="advanced"></p-inputSwitch>
</div>
<form novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)" style="padding-top:5%;">
<div *ngIf="form.invalid" class="alert alert-danger">
<div *ngIf="form.dirty">
<div *ngIf="form.get('ip').hasError('required')">The IP/Hostname is required</div>
<div *ngIf="form.get('port').hasError('required')">The Port is required</div>
<div *ngIf="form.get('apiKey').hasError('required')">The Api Key is required</div>
</div>
<div>
<div *ngIf="form.get('defaultQualityProfile').hasError('required')">A Default Quality Profile is required</div>
<div *ngIf="form.get('defaultRootPath').hasError('required')">A Default Root Path is required</div>
<div *ngIf="form.get('minimumAvailability').hasError('required')">A Default Minimum Availability is required</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
@ -33,25 +20,24 @@
<div class="form-group">
<label for="Ip" class="control-label">Hostname or IP</label>
<div class="">
<input type="text" class="form-control form-control-custom " id="Ip" name="Ip" placeholder="localhost" formControlName="ip">
</div>
<input type="text" class="form-control form-control-custom " id="Ip" name="Ip" placeholder="localhost" formControlName="ip" [ngClass]="{'form-error': form.get('ip').hasError('required')}">
<small *ngIf="form.get('ip').hasError('required')" class="error-text">The IP/Hostname is required</small>
</div>
<div class="form-group">
<label for="portNumber" class="control-label">Port</label>
<div class="">
<input type="text" class="form-control form-control-custom " formControlName="port" id="portNumber" name="Port" placeholder="Port Number">
</div>
<input type="text" class="form-control form-control-custom " formControlName="port" id="portNumber" name="Port" placeholder="Port Number" [ngClass]="{'form-error': form.get('port').hasError('required')}">
<small *ngIf="form.get('port').hasError('required')" class="error-text">The Port is required</small>
</div>
<div class="form-group">
<label for="ApiKey" class="control-label">API Key</label>
<div>
<input type="text" class="form-control form-control-custom " id="ApiKey" name="ApiKey" formControlName="apiKey">
</div>
<input type="text" class="form-control form-control-custom " [ngClass]="{'form-error': form.get('apiKey').hasError('required')}" id="ApiKey" name="ApiKey" formControlName="apiKey">
<small *ngIf="form.get('apiKey').hasError('required')" class="error-text">The API Key is required</small>
</div>
<div class="form-group">
<div class="checkbox">
@ -76,10 +62,11 @@
<div class="form-group">
<label for="select" class="control-label">Quality Profiles</label>
<div id="profiles">
<select formControlName="defaultQualityProfile" class="form-control form-control-custom" id="select">
<select formControlName="defaultQualityProfile" class="form-control form-control-custom" id="select" [ngClass]="{'form-error': form.get('defaultQualityProfile').hasError('required')}">
<option *ngFor="let quality of qualities" value="{{quality.id}}">{{quality.name}}</option>
</select>
</div>
<small *ngIf="form.get('defaultQualityProfile').hasError('required')" class="error-text">A Default Quality Profile is required</small>
</div>
<div class="form-group">
@ -92,19 +79,22 @@
<div class="form-group">
<label for="rootFolders" class="control-label">Default Root Folders</label>
<div id="rootFolders">
<select formControlName="defaultRootPath" class="form-control form-control-custom">
<select formControlName="defaultRootPath" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('defaultRootPath').hasError('required')}">
<option *ngFor="let folder of rootFolders" value="{{folder.path}}">{{folder.path}}</option>
</select>
</div>
</div>
<small *ngIf="form.get('defaultRootPath').hasError('required')" class="error-text">A Default Root Path is required</small>
</div>
<div class="form-group">
<label for="rootFolders" class="control-label">Default Minimum Availability</label>
<div id="rootFolders">
<select formControlName="minimumAvailability" class="form-control form-control-custom">
<select formControlName="minimumAvailability" class="form-control form-control-custom" [ngClass]="{'form-error': form.get('minimumAvailability').hasError('required')}">
<option *ngFor="let min of minimumAvailabilityOptions" value="{{min.value}}">{{min.name}}</option>
</select>
</div>
<small *ngIf="form.get('minimumAvailability').hasError('required')" class="error-text">A Default Minimum Availability is required</small>
</div>
<div class="form-group" *ngIf="advanced" style="color:#ff761b">

@ -8,10 +8,11 @@ import { ClipboardModule } from "ngx-clipboard/dist";
import { AuthGuard } from "../auth/auth.guard";
import { AuthModule } from "../auth/auth.module";
import { AuthService } from "../auth/auth.service";
import { JobService, RadarrService, SonarrService, TesterService, ValidationService } from "../services";
import { CouchPotatoService, JobService, RadarrService, SonarrService, TesterService, ValidationService } from "../services";
import { PipeModule } from "../pipes/pipe.module";
import { AboutComponent } from "./about/about.component";
import { CouchPotatoComponent } from "./couchpotato/couchpotato.component";
import { CustomizationComponent } from "./customization/customization.component";
import { EmbyComponent } from "./emby/emby.component";
import { LandingPageComponent } from "./landingpage/landingpage.component";
@ -51,6 +52,7 @@ const routes: Routes = [
{ path: "Settings/Mattermost", component: MattermostComponent, canActivate: [AuthGuard] },
{ path: "Settings/UserManagement", component: UserManagementComponent, canActivate: [AuthGuard] },
{ path: "Settings/Update", component: UpdateComponent, canActivate: [AuthGuard] },
{ path: "Settings/CouchPotato", component: CouchPotatoComponent, canActivate: [AuthGuard] },
];
@NgModule({
@ -91,6 +93,7 @@ const routes: Routes = [
UpdateComponent,
AboutComponent,
WikiComponent,
CouchPotatoComponent,
],
exports: [
RouterModule,
@ -103,6 +106,7 @@ const routes: Routes = [
ValidationService,
TesterService,
JobService,
CouchPotatoService,
],
})

@ -39,7 +39,7 @@
<i class="fa fa-film" aria-hidden="true"></i> Movies <span class="caret"></span>
</a>
<ul class="dropdown-menu">
<!--<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/CouchPotato']">CouchPotato</a></li>-->
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/CouchPotato']">CouchPotato (NOT YET READY)</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Radarr']">Radarr</a></li>
<!--<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Watcher']">Watcher</a></li>-->
<li [routerLinkActive]="['active']"><a>More Coming Soon...</a></li>

@ -8,17 +8,7 @@
<p-inputSwitch id="customInputSwitch" [(ngModel)]="advanced"></p-inputSwitch>
</div>
<form novalidate [formGroup]="form" (ngSubmit)="onSubmit(form)" style="padding-top:5%;">
<div *ngIf="form.invalid" class="alert alert-danger">
<div *ngIf="form.dirty">
<div *ngIf="form.get('ip').hasError('required')">The IP/Hostname is required</div>
<div *ngIf="form.get('port').hasError('required')">The Port is required</div>
<div *ngIf="form.get('apiKey').hasError('required')">The Api Key is required</div>
</div>
<div>
<div *ngIf="form.get('qualityProfile').hasError('required')">A Default Quality Profile is required</div>
<div *ngIf="form.get('rootPath').hasError('required')">A Default Root Path is required</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
@ -29,25 +19,24 @@
<div class="form-group">
<label for="Ip" class="control-label">Sonarr Hostname or IP</label>
<div class="">
<input type="text" class="form-control form-control-custom " formControlName="ip" id="Ip" name="Ip" placeholder="localhost">
</div>
<input type="text" class="form-control form-control-custom " formControlName="ip" id="Ip" name="Ip" placeholder="localhost" [ngClass]="{'form-error': form.get('ip').hasError('required')}">
<small *ngIf="form.get('ip').hasError('required')" class="error-text">The IP/Hostname is required</small>
</div>
<div class="form-group">
<label for="portNumber" class="control-label">Port</label>
<div class="">
<input type="text" class="form-control form-control-custom " formControlName="port" id="portNumber" name="Port" placeholder="Port Number">
</div>
<input type="text" class="form-control form-control-custom " [ngClass]="{'form-error': form.get('port').hasError('required')}" formControlName="port" id="portNumber" name="Port" placeholder="Port Number">
<small *ngIf="form.get('port').hasError('required')" class="error-text">The Port is required</small>
</div>
<div class="form-group">
<label for="ApiKey" class="control-label">Sonarr API Key</label>
<div>
<input type="text" class="form-control form-control-custom " formControlName="apiKey" id="ApiKey" name="ApiKey">
</div>
<input type="text" class="form-control form-control-custom " [ngClass]="{'form-error': form.get('apiKey').hasError('required')}" formControlName="apiKey" id="ApiKey" name="ApiKey">
<small *ngIf="form.get('apiKey').hasError('required')" class="error-text">The API Key is required</small>
</div>
<div class="form-group">
<div class="checkbox">
@ -72,10 +61,12 @@
<div class="form-group">
<label for="select" class="control-label">Quality Profiles</label>
<div id="profiles">
<select class="form-control form-control-custom" id="select" formControlName="qualityProfile">
<select class="form-control form-control-custom" [ngClass]="{'form-error': form.get('qualityProfile').hasError('required')}" id="select" formControlName="qualityProfile">
<option *ngFor="let quality of qualities" value="{{quality.id}}">{{quality.name}}</option>
</select>
</div>
<small *ngIf="form.get('qualityProfile').hasError('required')" class="error-text">A Default Quality Profile is required</small>
</div>
<div class="form-group">
@ -88,10 +79,12 @@
<div class="form-group">
<label for="rootFolders" class="control-label">Default Root Folders</label>
<div id="rootFolders">
<select class="form-control form-control-custom" formControlName="rootPath">
<select class="form-control form-control-custom" formControlName="rootPath" [ngClass]="{'form-error': form.get('rootPath').hasError('required')}">
<option *ngFor="let folder of rootFolders" value="{{folder.id}}">{{folder.path}}</option>
</select>
</div>
<small *ngIf="form.get('rootPath').hasError('required')" class="error-text">A Default Root Path is required</small>
</div>

@ -816,3 +816,11 @@ a > h4 {
a > h4:hover {
text-decoration: underline;
}
.form-error {
border: 1px solid #d9534f;
}
.error-text {
color: #d9534f;
}

@ -0,0 +1,37 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Ombi.Api.CouchPotato;
using Ombi.Api.CouchPotato.Models;
using Ombi.Attributes;
using Ombi.Settings.Settings.Models.External;
namespace Ombi.Controllers.External
{
[Admin]
[ApiV1]
[Produces("application/json")]
public class CouchPotatoController
{
public CouchPotatoController(ICouchPotatoApi api)
{
_api = api;
}
private readonly ICouchPotatoApi _api;
[HttpPost("profile")]
public async Task<CouchPotatoProfiles> GetQualityProfiles([FromBody] CouchPotatoSettings settings)
{
var profiles = await _api.GetProfiles(settings.FullUri, settings.ApiKey);
return profiles;
}
[HttpPost("apikey")]
public async Task<CouchPotatoApiKey> GetApiKey([FromBody] CouchPotatoSettings settings)
{
var apiKey = await _api.GetApiKey(settings.FullUri, settings.Username, settings.Password);
return apiKey;
}
}
}

@ -13,9 +13,6 @@ using Ombi.Models.External;
namespace Ombi.Controllers.External
{
/// <summary>
///
/// </summary>
[Admin]
[ApiV1]
[Produces("application/json")]

@ -7,6 +7,7 @@ using Ombi.Api.Sonarr.Models;
using Ombi.Attributes;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Settings.Settings.Models.External;
namespace Ombi.Controllers.External
{

@ -343,6 +343,26 @@ namespace Ombi.Controllers
return await Get<UpdateSettings>();
}
/// <summary>
/// Gets the CouchPotatoSettings Settings.
/// </summary>
/// <returns></returns>
[HttpGet("CouchPotato")]
public async Task<CouchPotatoSettings> CouchPotatoSettings()
{
return await Get<CouchPotatoSettings>();
}
/// <summary>
/// Save the CouchPotatoSettings settings.
/// </summary>
/// <param name="settings">The settings.</param>
/// <returns></returns>
[HttpPost("CouchPotato")]
public async Task<bool> CouchPotatoSettings([FromBody]CouchPotatoSettings settings)
{
return await Save(settings);
}
/// <summary>
/// Saves the email notification settings.

Loading…
Cancel
Save