Merge branch 'develop' of https://github.com/Ombi-app/Ombi into develop-kuraki

pull/4210/head
twanariens 3 years ago
commit 166e2dc34f

@ -40,8 +40,8 @@ Search the existing requests to see if your suggestion has already been submitte
___
<a href='https://play.google.com/store/apps/details?id=com.tidusjar.Ombi&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img width="150" alt='Get it on Google Play' src='https://play.google.com/intl/en_gb/badges/images/generic/en_badge_web_generic.png'/></a>
<br>
_**Note:** There is no longer an iOS app due to complications outside of our control._
<a href='https://apps.apple.com/us/app/ombi/id1335260043'><img width="130" alt='Get it on the App Store' src='https://developer.apple.com/app-store/marketing/guidelines/images/badge-example-preferred.png'/></a>
<br>
# Features
Here are some of the features Ombi has:

@ -48,6 +48,8 @@ namespace Ombi.Core.Engine
protected readonly ISettingsService<OmbiSettings> OmbiSettings;
protected readonly IRepository<RequestSubscription> _subscriptionRepository;
private bool _demo = DemoSingleton.Instance.Demo;
protected async Task<Dictionary<int, MovieRequests>> GetMovieRequests()
{
var now = DateTime.Now.Ticks;
@ -193,6 +195,23 @@ namespace Ombi.Core.Engine
return ombiSettings ?? (ombiSettings = await OmbiSettings.GetSettingsAsync());
}
protected bool DemoCheck(string title)
{
if (!title.HasValue())
{
return false;
}
if (_demo)
{
if (ExcludedDemo.ExcludedContent.Any(x => title.Contains(x, System.Globalization.CompareOptions.OrdinalIgnoreCase)))
{
return true;
}
return false;
}
return false;
}
public class HideResult
{
public bool Hide { get; set; }

@ -26,19 +26,21 @@ using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
using Ombi.Core.Models;
using System.Threading;
using Microsoft.Extensions.Logging;
namespace Ombi.Core.Engine
{
public class TvRequestEngine : BaseMediaEngine, ITvRequestEngine
{
public TvRequestEngine(ITvMazeApi tvApi, IMovieDbApi movApi, IRequestServiceMain requestService, IPrincipal user,
INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager,
INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager, ILogger<TvRequestEngine> logger,
ITvSender sender, IRepository<RequestLog> rl, ISettingsService<OmbiSettings> settings, ICacheService cache,
IRepository<RequestSubscription> sub) : base(user, requestService, rule, manager, cache, settings, sub)
{
TvApi = tvApi;
MovieDbApi = movApi;
NotificationHelper = helper;
_logger = logger;
TvSender = sender;
_requestLog = rl;
}
@ -47,6 +49,8 @@ namespace Ombi.Core.Engine
private ITvMazeApi TvApi { get; }
private IMovieDbApi MovieDbApi { get; }
private ITvSender TvSender { get; }
private readonly ILogger<TvRequestEngine> _logger;
private readonly IRepository<RequestLog> _requestLog;
public async Task<RequestEngineResult> RequestTvShow(TvRequestViewModel tv)
@ -69,7 +73,7 @@ namespace Ombi.Core.Engine
}
}
var tvBuilder = new TvShowRequestBuilder(TvApi, MovieDbApi);
var tvBuilder = new TvShowRequestBuilder(TvApi, MovieDbApi, _logger);
(await tvBuilder
.GetShowInfo(tv.TvDbId))
.CreateTvList(tv)

@ -315,6 +315,12 @@ namespace Ombi.Core.Engine.V2
foreach (var movie in movies)
{
var result = await ProcessSingleMovie(movie);
if (DemoCheck(result.Title))
{
continue;
}
if (settings.HideAvailableFromDiscover && result.Available)
{
continue;

@ -35,6 +35,8 @@ namespace Ombi.Core.Engine.V2
private readonly ISettingsService<LidarrSettings> _lidarrSettings;
private readonly IMusicBrainzApi _musicApi;
private bool _demo = DemoSingleton.Instance.Demo;
public async Task<List<MultiSearchResult>> MultiSearch(string searchTerm, MultiSearchFilter filter, CancellationToken cancellationToken)
{
@ -60,6 +62,12 @@ namespace Ombi.Core.Engine.V2
foreach (var multiSearch in movieDbData)
{
if (DemoCheck(multiSearch.title) || DemoCheck(multiSearch.name))
{
continue;
}
var result = new MultiSearchResult
{
MediaType = multiSearch.media_type,

@ -155,6 +155,10 @@ namespace Ombi.Core.Engine.V2
foreach (var tvMazeSearch in items)
{
if (DemoCheck(tvMazeSearch.Title))
{
continue;
}
if (settings.HideAvailableFromDiscover)
{
// To hide, we need to know if it's fully available, the only way to do this is to lookup it's episodes to check if we have every episode

@ -12,16 +12,19 @@ using Ombi.Helpers;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository.Requests;
using Microsoft.Extensions.Logging;
namespace Ombi.Core.Helpers
{
public class TvShowRequestBuilder
{
private readonly ILogger _logger;
public TvShowRequestBuilder(ITvMazeApi tvApi, IMovieDbApi movApi)
public TvShowRequestBuilder(ITvMazeApi tvApi, IMovieDbApi movApi, ILogger logger)
{
TvApi = tvApi;
MovieDbApi = movApi;
_logger = logger;
}
private ITvMazeApi TvApi { get; }
@ -45,6 +48,7 @@ namespace Ombi.Core.Helpers
{
if (result.Name.Equals(ShowInfo.name, StringComparison.InvariantCultureIgnoreCase))
{
_logger.LogInformation($"Found matching MovieDb entry for show name {ShowInfo.name}");
TheMovieDbRecord = result;
var showIds = await MovieDbApi.GetTvExternals(result.Id);
ShowInfo.externals.imdb = showIds.imdb_id;
@ -237,18 +241,19 @@ namespace Ombi.Core.Helpers
public TvShowRequestBuilder CreateNewRequest(TvRequestViewModel tv)
{
_logger.LogInformation($"Building Request for {ShowInfo.name} with Provider ID {TheMovieDbRecord?.Id ?? 0}");
NewRequest = new TvRequests
{
Overview = ShowInfo.summary.RemoveHtml(),
PosterPath = PosterPath,
Title = ShowInfo.name,
ReleaseDate = FirstAir,
ExternalProviderId = TheMovieDbRecord.Id,
ExternalProviderId = TheMovieDbRecord?.Id ?? 0,
Status = ShowInfo.status,
ImdbId = ShowInfo.externals?.imdb ?? string.Empty,
TvDbId = tv.TvDbId,
ChildRequests = new List<ChildRequests>(),
TotalSeasons = tv.Seasons.Count(),
TotalSeasons = tv.Seasons?.Count ?? 0,
Background = BackdropPath
};
NewRequest.ChildRequests.Add(ChildRequest);

@ -1,4 +1,6 @@
namespace Ombi.Helpers
using System.Collections.Generic;
namespace Ombi.Helpers
{
public class DemoSingleton
{
@ -10,4 +12,460 @@
public bool Demo { get; set; }
}
public static class ExcludedDemo
{
public static HashSet<string> ExcludedContent => new HashSet<string>
{
"101 Dalmatians",
"102 Dalmatians",
"20,000 Leagues Under the Sea",
"A Bug's Life",
"A Far Off Place",
"A Goofy Movie",
"A Kid in King Arthur's Court",
"A Tale of Two Critters",
"A Tiger Walks",
"A Wrinkle in Time",
"ABCD 2",
"African Cats",
"Air Bud",
"Air Bud: Golden Receiver",
"Aladdin",
"Aladdin",
"Alexander and the Terrible, Horrible, No Good, Very Bad Day",
"Alice Through the Looking Glass",
"Alice in Wonderland",
"Alice in Wonderland",
"Aliens of the Deep",
"Almost Angels",
"America's Heart and Soul",
"Amy",
"Anaganaga O Dheerudu",
"Angels in the Outfield",
"Arjun: The Warrior Prince",
"Around the World in 80 Days",
"Artemis Fowl",
"Atlantis: The Lost Empire",
"Babes in Toyland",
"Bambi",
"Bears",
"Beauty and the Beast",
"Beauty and the Beast",
"Bedknobs and Broomsticks",
"Bedtime Stories",
"Benji the Hunted",
"Beverly Hills Chihuahua",
"Big Hero 6",
"Big Red",
"Blackbeard's Ghost",
"Blank Check",
"Blue",
"Bolt",
"Bon Voyage!",
"Born in China",
"Brave",
"Bridge to Terabithia",
"Brother Bear",
"Candleshoe",
"Cars",
"Cars 2",
"Cars 3",
"Charley and the Angel",
"Charlie, the Lonesome Cougar",
"Cheetah",
"Chicken Little",
"Chimpanzee",
"Christopher Robin",
"Cinderella",
"Cinderella",
"Coco",
"College Road Trip",
"Condorman",
"Confessions of a Teenage Drama Queen",
"Cool Runnings",
"D2: The Mighty Ducks",
"D3: The Mighty Ducks",
"Dangal",
"Darby O'Gill and the Little People",
"Dasavathaaram",
"Davy Crockett and the River Pirates",
"Davy Crockett, King of the Wild Frontier",
"Dinosaur",
"Disney's A Christmas Carol",
"Disney's The Kid",
"Do Dooni Chaar",
"Dolphin Reef",
"Doug's 1st Movie",
"Dragonslayer",
"DuckTales the Movie: Treasure of the Lost Lamp",
"Dumbo",
"Dumbo",
"Earth",
"Eight Below",
"Emil and the Detectives",
"Enchanted",
"Endurance",
"Escape to Witch Mountain",
"Expedition China",
"Fantasia",
"Fantasia 2000",
"Finding Dory",
"Finding Nemo",
"First Kid",
"Flight of the Navigator",
"Flubber",
"Follow Me, Boys!",
"Frank and Ollie",
"Frankenweenie",
"Freaky Friday",
"Freaky Friday",
"Frozen",
"Frozen II",
"Onward",
"Star Wars",
"Raya",
"Mandalorian",
"Fun and Fancy Free",
"G-Force",
"George of the Jungle",
"Ghost in the Shell 2: Innocence GITS2",
"Ghost of the Mountains",
"Ghosts of the Abyss",
"Glory Road",
"Greyfriars Bobby",
"Growing Up Wild",
"Gus",
"Hannah Montana and Miley Cyrus: Best of Both Worlds Concert",
"Hannah Montana: The Movie",
"Heavyweights",
"Herbie Goes Bananas",
"Herbie Goes to Monte Carlo",
"Herbie Rides Again",
"Herbie: Fully Loaded",
"Hercules",
"High School Musical 3: Senior Year",
"Hocus Pocus",
"Holes",
"Home on the Range",
"Homeward Bound II: Lost in San Francisco",
"Homeward Bound: The Incredible Journey",
"Honey, I Blew Up the Kid",
"Honey, I Shrunk the Kids",
"Hot Lead and Cold Feet",
"I'll Be Home for Christmas",
"Ice Princess",
"In Search of the Castaways",
"Incredibles 2",
"Inside Out",
"Inspector Gadget",
"Into the Woods",
"Invincible",
"Iron Will",
"Jagga Jasoos",
"James and the Giant Peach",
"John Carter",
"Johnny Tremain",
"Jonas Brothers: The 3D Concert Experience",
"Jungle 2 Jungle",
"Jungle Cat",
"Khoobsurat",
"Kidnapped",
"King of the Grizzlies",
"L'Empereur - March of the Penguins 2: The Next Step[a]",
"Lady and the Tramp",
"Lady and the Tramp",
"Lilly the Witch: The Dragon and the Magic Book",
"Lilly the Witch: The Journey to Mandolan",
"Lilo & Stitch",
"Lt. Robin Crusoe, U.S.N.",
"Make Mine Music",
"Maleficent",
"Maleficent: Mistress of Evil",
"Man of the House",
"Mars Needs Moms",
"Mary Poppins",
"Mary Poppins Returns",
"Max Keeble's Big Move",
"McFarland, USA",
"Meet the Deedles",
"Meet the Robinsons",
"Melody Time",
"Midnight Madness",
"Mighty Joe Young",
"Million Dollar Arm",
"Miracle",
"Miracle of the White Stallions",
"Moana",
"Monkey Kingdom",
"Monkeys, Go Home!",
"Monsters University",
"Monsters, Inc.",
"Moon Pilot",
"Morning Light",
"Mr. Magoo",
"Mulan",
"Muppet Treasure Island",
"Muppets Most Wanted",
"My Favorite Martian",
"Napoleon and Samantha",
"National Treasure",
"National Treasure: Book of Secrets",
"Never Cry Wolf",
"Never a Dull Moment",
"Newsies",
"Night Crossing",
"Nikki, Wild Dog of the North",
"No Deposit, No Return",
"Now You See Him, Now You Don't",
"Oceans",
"Old Dogs",
"Old Yeller",
"Oliver & Company",
"One Hundred and One Dalmatians",
"One Little Indian",
"One Magic Christmas",
"One of Our Dinosaurs Is Missing",
"Operation Dumbo Drop",
"Oz the Great and Powerful",
"Penguins",
"Perri",
"Pete's Dragon",
"Pete's Dragon",
"Peter Pan",
"Piglet's Big Movie",
"Pinocchio",
"Pirates of the Caribbean: At World's End",
"Pirates of the Caribbean: Dead Man's Chest",
"Pirates of the Caribbean: Dead Men Tell No Tales",
"Pirates of the Caribbean: On Stranger Tides",
"Pirates of the Caribbean: The Curse of the Black Pearl",
"Planes",
"Planes: Fire & Rescue",
"Pocahontas",
"Pollyanna",
"Pooh's Heffalump Movie",
"Popeye",
"Prince of Persia: The Sands of Time",
"Prom",
"Queen of Katwe",
"Race to Witch Mountain",
"Ralph Breaks the Internet",
"Rascal",
"Ratatouille",
"Recess: School's Out",
"Remember the Titans",
"Return from Witch Mountain",
"Return to Never Land",
"Return to Oz",
"Return to Snowy River",
"Ride a Wild Pony",
"Roadside Romeo",
"Rob Roy, the Highland Rogue",
"Robin Hood",
"RocketMan",
"Roving Mars",
"Run, Cougar, Run",
"Sacred Planet",
"Saludos Amigos",
"Savage Sam",
"Saving Mr. Banks",
"Scandalous John",
"Secretariat",
"Secrets of Life",
"Shipwrecked",
"Sky High",
"Sleeping Beauty",
"Smith!",
"Snow Dogs",
"Snow White and the Seven Dwarfs",
"Snowball Express",
"So Dear to My Heart",
"Something Wicked This Way Comes",
"Son of Flubber",
"Song of the South",
"Squanto: A Warrior's Tale",
"Summer Magic",
"Superdad",
"Swiss Family Robinson",
"Tall Tale",
"Tangled",
"Tarzan",
"Teacher's Pet",
"Ten Who Dared",
"Tex",
"That Darn Cat",
"That Darn Cat!",
"The Absent-Minded Professor",
"The Adventures of Bullwhip Griffin",
"The Adventures of Huck Finn",
"The Adventures of Ichabod and Mr. Toad",
"The African Lion",
"The Apple Dumpling Gang",
"The Apple Dumpling Gang Rides Again",
"The Aristocats",
"The BFG",
"The Barefoot Executive",
"The Bears and I",
"The Best of Walt Disney's True-Life Adventures",
"The Big Green",
"The Biscuit Eater",
"The Black Cauldron",
"The Black Hole",
"The Boatniks",
"The Book of Masters",
"The Boys: The Sherman Brothers' Story",
"The Castaway Cowboy",
"The Cat from Outer Space",
"The Chronicles of Narnia: Prince Caspian",
"The Chronicles of Narnia: The Lion, the Witch and the Wardrobe",
"The Computer Wore Tennis Shoes",
"The Country Bears",
"The Crimson Wing: Mystery of the Flamingos",
"The Devil and Max Devlin",
"The Emperor's New Groove",
"The Fighting Prince of Donegal",
"The Finest Hours",
"The Fox and the Hound",
"The Game Plan",
"The Gnome-Mobile",
"The Good Dinosaur",
"The Great Locomotive Chase",
"The Great Mouse Detective",
"The Greatest Game Ever Played",
"The Happiest Millionaire",
"The Haunted Mansion",
"The Horse in the Gray Flannel Suit",
"The Hunchback of Notre Dame",
"The Incredible Journey",
"The Incredibles",
"The Island at the Top of the World",
"The Journey of Natty Gann",
"The Jungle Book",
"The Jungle Book",
"The Jungle Book",
"The Jungle Book 2",
"The Last Flight of Noah's Ark",
"The Legend of Lobo",
"The Light in the Forest",
"The Lion King",
"The Lion King",
"The Little Mermaid",
"The Littlest Horse Thieves",
"The Littlest Outlaw",
"The Living Desert",
"The Lizzie McGuire Movie",
"The London Connection",
"The Lone Ranger",
"The Love Bug",
"The Many Adventures of Winnie the Pooh",
"The Mighty Ducks",
"The Million Dollar Duck",
"The Misadventures of Merlin Jones",
"The Monkey's Uncle",
"The Moon-Spinners",
"The Muppet Christmas Carol",
"The Muppets",
"The Nightmare Before Christmas 3D TNBC",
"The North Avenue Irregulars",
"The Nutcracker and the Four Realms",
"The Odd Life of Timothy Green",
"The One and Only, Genuine, Original Family Band",
"The Pacifier",
"The Parent Trap",
"The Parent Trap",
"The Pixar Story",
"The Princess Diaries",
"The Princess Diaries 2: Royal Engagement",
"The Princess and the Frog",
"The Reluctant Dragon",
"The Rescuers",
"The Rescuers Down Under",
"The Rocketeer TR",
"The Rookie",
"The Santa Clause 2",
"The Santa Clause 3: The Escape Clause",
"The Santa Clause TSC",
"The Shaggy D.A.",
"The Shaggy Dog",
"The Shaggy Dog",
"The Sign of Zorro",
"The Sorcerer's Apprentice",
"The Story of Robin Hood and His Merrie Men",
"The Straight Story",
"The Strongest Man in the World",
"The Sword and the Rose",
"The Sword in the Stone",
"The Three Caballeros",
"The Three Lives of Thomasina",
"The Three Musketeers",
"The Tigger Movie",
"The Ugly Dachshund",
"The Vanishing Prairie",
"The Watcher in the Woods",
"The Wild",
"The Wild Country",
"The World's Greatest Athlete",
"The Young Black Stallion",
"Third Man on the Mountain",
"Those Calloways",
"Toby Tyler",
"Tom and Huck",
"Tomorrowland",
"Tonka",
"Toy Story",
"Toy Story 2",
"Toy Story 3",
"Toy Story 4",
"Trail of the Panda",
"Treasure Island",
"Treasure Planet",
"Treasure of Matecumbe",
"Trenchcoat",
"Tron",
"Tron: Legacy",
"Tuck Everlasting",
"Underdog",
"Unidentified Flying Oddball",
"Up",
"Valiant",
"Victory Through Air Power",
"WALL-E",
"Waking Sleeping Beauty",
"Walt & El Grupo",
"Westward Ho the Wagons!",
"White Fang",
"White Fang 2: Myth of the White Wolf",
"White Wilderness",
"Wild Hearts Can't Be Broken",
"Wings of Life",
"Winnie the Pooh",
"Wreck-It Ralph",
"Zokkomon",
"Zootopia",
"Zorro the Avenger",
"Iron Man",
"Hulk",
"Thor",
"Avengers",
"Guardians of the Galaxy",
"Ant-Man",
"Captain America",
"Doctor Strange",
"Guardians of the Galaxy",
"Spider-Man",
"Black Panther",
"Marvel",
"Spider Man",
"SpiderMan",
"Loki",
"Winter Soldier",
"Wanda",
"Small Fry",
"Rex",
"Lamp life",
"Toy",
"Hawaiian"
};
}
}

@ -7,5 +7,9 @@ namespace Ombi.Core.Settings.Models.External
public bool ShowAdultMovies { get; set; }
public List<int> ExcludedKeywordIds { get; set; }
public List<int> ExcludedMovieGenreIds { get; set; }
public List<int> ExcludedTvGenreIds { get; set; }
}
}

@ -4,6 +4,10 @@ using System.Threading.Tasks;
using Ombi.Api.TheMovieDb.Models;
using Ombi.TheMovieDbApi.Models;
// Due to conflicting Genre models in
// Ombi.TheMovieDbApi.Models and Ombi.Api.TheMovieDb.Models
using Genre = Ombi.TheMovieDbApi.Models.Genre;
namespace Ombi.Api.TheMovieDb
{
public interface IMovieDbApi
@ -34,5 +38,6 @@ namespace Ombi.Api.TheMovieDb
Task<Keyword> GetKeyword(int keywordId);
Task<WatchProviders> GetMovieWatchProviders(int theMoviedbId, CancellationToken token);
Task<WatchProviders> GetTvWatchProviders(int theMoviedbId, CancellationToken token);
Task<List<Genre>> GetGenres(string media);
}
}
}

@ -36,4 +36,9 @@ namespace Ombi.TheMovieDbApi.Models
public int total_results { get; set; }
public int total_pages { get; set; }
}
}
public class GenreContainer<T>
{
public List<T> genres { get; set; }
}
}

@ -13,6 +13,10 @@ using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.TheMovieDbApi.Models;
// Due to conflicting Genre models in
// Ombi.TheMovieDbApi.Models and Ombi.Api.TheMovieDb.Models
using Genre = Ombi.TheMovieDbApi.Models.Genre;
namespace Ombi.Api.TheMovieDb
{
public class TheMovieDbApi : IMovieDbApi
@ -198,6 +202,7 @@ namespace Ombi.Api.TheMovieDb
request.AddQueryString("page", page.ToString());
}
await AddDiscoverSettings(request);
await AddGenreFilter(request, type);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request, cancellationToken);
return Mapper.Map<List<MovieDbSearchResult>>(result.results);
@ -233,6 +238,7 @@ namespace Ombi.Api.TheMovieDb
request.AddQueryString("vote_count.gte", "250");
await AddDiscoverSettings(request);
await AddGenreFilter(request, type);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieDbSearchResult>>(result.results);
@ -269,6 +275,7 @@ namespace Ombi.Api.TheMovieDb
request.AddQueryString("page", page.ToString());
}
await AddDiscoverSettings(request);
await AddGenreFilter(request, type);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieDbSearchResult>>(result.results);
@ -297,6 +304,7 @@ namespace Ombi.Api.TheMovieDb
}
await AddDiscoverSettings(request);
await AddGenreFilter(request, "movie");
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieDbSearchResult>>(result.results);
@ -344,6 +352,16 @@ namespace Ombi.Api.TheMovieDb
return keyword == null || keyword.Id == 0 ? null : keyword;
}
public async Task<List<Genre>> GetGenres(string media)
{
var request = new Request($"genre/{media}/list", BaseUri, HttpMethod.Get);
request.AddQueryString("api_key", ApiToken);
AddRetry(request);
var result = await Api.Request<GenreContainer<Genre>>(request);
return result.genres ?? new List<Genre>();
}
public Task<TheMovieDbContainer<MultiSearch>> MultiSearch(string searchTerm, string languageCode, CancellationToken cancellationToken)
{
var request = new Request("search/multi", BaseUri, HttpMethod.Get);
@ -380,6 +398,28 @@ namespace Ombi.Api.TheMovieDb
}
}
private async Task AddGenreFilter(Request request, string media_type)
{
var settings = await Settings;
List<int> excludedGenres;
switch (media_type) {
case "tv":
excludedGenres = settings.ExcludedTvGenreIds;
break;
case "movie":
excludedGenres = settings.ExcludedMovieGenreIds;
break;
default:
return;
}
if (excludedGenres?.Any() == true)
{
request.AddQueryString("without_genres", string.Join(",", excludedGenres));
}
}
private static void AddRetry(Request request)
{
request.Retry = true;

@ -170,7 +170,17 @@
<div>
<app-my-nav id="main-container dark" [showNav]="showNav" [isAdmin]="isAdmin" [applicationName]="applicationName" [applicationLogo]="customizationSettings?.logo" [username]="username" [email]="user?.email " (logoutClick)="logOut();">
<app-my-nav id="main-container dark"
[showNav]="showNav"
[isAdmin]="isAdmin"
[applicationName]="applicationName"
[applicationLogo]="customizationSettings?.logo"
[username]="username"
[email]="user?.email"
[accessToken]="accessToken"
[applicationUrl]="customizationSettings?.applicationUrl"
(logoutClick)="logOut();"
>
</app-my-nav>

@ -33,6 +33,7 @@ export class AppComponent implements OnInit {
public applicationName: string = "Ombi"
public isAdmin: boolean;
public username: string;
public accessToken: string;
private hubConnected: boolean;
@ -55,6 +56,7 @@ export class AppComponent implements OnInit {
if (this.authService.loggedIn()) {
this.user = this.authService.claims();
this.username = this.user.name;
this.identity.getAccessToken().subscribe(x => this.accessToken = x);
if (!this.hubConnected) {
this.signalrNotification.initialize();
this.hubConnected = true;

@ -284,6 +284,8 @@ export interface IVoteSettings extends ISettings {
export interface ITheMovieDbSettings extends ISettings {
showAdultMovies: boolean;
excludedKeywordIds: number[];
excludedMovieGenreIds: number[];
excludedTvGenreIds: number[]
}
export interface IUpdateModel

@ -65,7 +65,7 @@
</button>
</ng-template>
<ng-template #notRequestedBtn>
<button id="requestBtn" mat-raised-button class="btn-spacing" color="primary" (click)="request()">
<button *ngIf="!movie.requested" id="requestBtn" mat-raised-button class="btn-spacing" color="primary" (click)="request()">
<i *ngIf="movie.requestProcessing" class="fas fa-circle-notch fa-spin fa-fw"></i>
<i *ngIf="!movie.requestProcessing && !movie.processed" class="fas fa-plus"></i>
<i *ngIf="movie.processed && !movie.requestProcessing" class="fas fa-check"></i>

@ -69,10 +69,11 @@
<button *ngIf="!request.available" mat-raised-button color="warn" (click)="changeAvailability(request, true);">{{ 'Requests.MarkAvailable' | translate }}</button>
<button *ngIf="request.available" mat-raised-button color="warn" (click)="changeAvailability(request, false);">{{ 'Requests.MarkUnavailable' | translate }}</button>
<button *ngIf="!request.denied" mat-raised-button color="danger" (click)="deny(request);">{{ 'Requests.Deny' | translate }}</button>
<button mat-raised-button color="danger" (click)="delete(request);">{{ 'Requests.RequestPanel.Delete' | translate }}</button>
<button mat-raised-button color="accent" (click)="reProcessRequest(request);">{{ 'MediaDetails.ReProcessRequest' | translate }}</button>
</div>
<div *ngIf="isAdmin || manageOwnRequests">
<button mat-raised-button color="danger" (click)="delete(request);">{{ 'Requests.RequestPanel.Delete' | translate }}</button>
</div>
</mat-expansion-panel>

@ -1,9 +1,10 @@
import { Component, Input } from "@angular/core";
import { IChildRequests, RequestType } from "../../../../../interfaces";
import { RequestService } from "../../../../../services/request.service";
import { MessageService } from "../../../../../services";
import { MatDialog } from "@angular/material/dialog";
import { DenyDialogComponent } from "../../../shared/deny-dialog/deny-dialog.component";
import { MatDialog } from "@angular/material/dialog";
import { MessageService } from "../../../../../services";
import { RequestService } from "../../../../../services/request.service";
import { RequestServiceV2 } from "../../../../../services/requestV2.service";
@Component({
@ -14,6 +15,7 @@ import { RequestServiceV2 } from "../../../../../services/requestV2.service";
export class TvRequestsPanelComponent {
@Input() public tvRequest: IChildRequests[];
@Input() public isAdmin: boolean;
@Input() public manageOwnRequests: boolean;
public displayedColumns: string[] = ['number', 'title', 'airDate', 'status'];

@ -126,7 +126,7 @@
{{'Requests.Title' | translate}}
</mat-panel-title>
</mat-expansion-panel-header>
<tv-requests-panel [tvRequest]="tvRequest" [isAdmin]="isAdmin"></tv-requests-panel>
<tv-requests-panel [tvRequest]="tvRequest" [isAdmin]="isAdmin" [manageOwnRequests]="manageOwnRequests"></tv-requests-panel>
</mat-expansion-panel>
</mat-accordion>

@ -27,6 +27,7 @@ export class TvDetailsComponent implements OnInit {
public showRequest: ITvRequests;
public fromSearch: boolean;
public isAdmin: boolean;
public manageOwnRequests: boolean;
public advancedOptions: IAdvancedData;
public showAdvanced: boolean; // Set on the UI
public requestType = RequestType.tvShow;
@ -53,6 +54,7 @@ export class TvDetailsComponent implements OnInit {
this.issuesEnabled = this.settingsState.getIssue();
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
this.manageOwnRequests = this.auth.hasRole('ManageOwnRequests');
if (this.isAdmin) {
this.showAdvanced = await this.sonarrService.isEnabled();

@ -28,6 +28,13 @@
</div>
</span>
<span mat-list-item >
<a mat-list-item [disableRipple]="true" id="nav-openMobile" [routerLinkActive]="'active-list-item'"
aria-label="Toggle sidenav" (click)="openMobileApp($event);">
<i class="fa-lg fas fa-mobile-alt icon-spacing"></i>
&nbsp;{{ 'NavigationBar.OpenMobileApp' | translate }}
</a></span>
<a mat-list-item [disableRipple]="true" class="menu-spacing" id="nav-logout" [routerLinkActive]="'active-list-item'"
aria-label="Toggle sidenav" (click)="logOut();">
<i class="fa-lg fas fa-sign-out-alt icon-spacing"></i>

@ -107,10 +107,21 @@
margin-right:5px;
}
#nav-openMobile {
display: none;
}
@media (max-width: 600px) {
.profile-username{
display:none;
}
}
@media (max-width: 950px) {
#nav-openMobile {
display: block;
}
}
.profile-img img {

@ -1,16 +1,17 @@
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { INavBar } from '../interfaces/ICommon';
import { StorageService } from '../shared/storage/storage-service';
import { SettingsService, SettingsStateService } from '../services';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { SearchFilter } from './SearchFilter';
import { Md5 } from 'ts-md5/dist/md5';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { IUser, RequestType, UserType } from '../interfaces';
import { SettingsService, SettingsStateService } from '../services';
import { FilterService } from '../discover/services/filter-service';
import { ILocalUser } from '../auth/IUserLogin';
import { INavBar } from '../interfaces/ICommon';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { Md5 } from 'ts-md5/dist/md5';
import { Observable } from 'rxjs';
import { SearchFilter } from './SearchFilter';
import { StorageService } from '../shared/storage/storage-service';
import { map } from 'rxjs/operators';
export enum SearchFilterType {
Movie = 1,
@ -34,6 +35,8 @@ export class MyNavComponent implements OnInit {
@Input() public showNav: boolean;
@Input() public applicationName: string;
@Input() public applicationLogo: string;
@Input() public applicationUrl: string;
@Input() public accessToken: string;
@Input() public username: string;
@Input() public isAdmin: string;
@Input() public email: string;
@ -122,4 +125,12 @@ export class MyNavComponent implements OnInit {
var fallback = this.applicationLogo ? this.applicationLogo : 'https://raw.githubusercontent.com/Ombi-app/Ombi/gh-pages/img/android-chrome-512x512.png';
return `https://www.gravatar.com/avatar/${this.emailHash}?d=${fallback}`;
}
public openMobileApp(event: any) {
event.preventDefault();
const url = `ombi://${this.applicationUrl}|${this.accessToken}`;
window.location.assign(url);
}
}

@ -76,7 +76,7 @@
<th mat-header-cell *matHeaderCellDef> </th>
<td mat-cell *matCellDef="let element">
<button id="detailsButton{{element.id}}" mat-raised-button color="accent" [routerLink]="'/details/movie/' + element.theMovieDbId">{{ 'Requests.Details' | translate}}</button>
<button id="optionsButton{{element.id}}" mat-raised-button color="warn" (click)="openOptions(element)" *ngIf="isAdmin"> {{ 'Requests.Options' | translate}}</button>
<button id="optionsButton{{element.id}}" mat-raised-button color="warn" (click)="openOptions(element)" *ngIf="isAdmin || manageOwnRequests"> {{ 'Requests.Options' | translate}}</button>
</td>
</ng-container>

@ -1,18 +1,18 @@
import { Component, AfterViewInit, ViewChild, EventEmitter, Output, ChangeDetectorRef, OnInit } from "@angular/core";
import { AfterViewInit, ChangeDetectorRef, Component, EventEmitter, OnInit, Output, ViewChild } from "@angular/core";
import { IMovieRequests, IRequestEngineResult, IRequestsViewModel } from "../../../interfaces";
import { MatPaginator } from "@angular/material/paginator";
import { MatSort } from "@angular/material/sort";
import { merge, Observable, of as observableOf, forkJoin } from 'rxjs';
import { NotificationService, RequestService } from "../../../services";
import { Observable, forkJoin, merge, of as observableOf } from 'rxjs';
import { catchError, map, startWith, switchMap } from 'rxjs/operators';
import { RequestServiceV2 } from "../../../services/requestV2.service";
import { AuthService } from "../../../auth/auth.service";
import { StorageService } from "../../../shared/storage/storage-service";
import { MatPaginator } from "@angular/material/paginator";
import { MatSort } from "@angular/material/sort";
import { MatTableDataSource } from "@angular/material/table";
import { RequestFilterType } from "../../models/RequestFilterType";
import { RequestServiceV2 } from "../../../services/requestV2.service";
import { SelectionModel } from "@angular/cdk/collections";
import { NotificationService, RequestService } from "../../../services";
import { StorageService } from "../../../shared/storage/storage-service";
import { TranslateService } from "@ngx-translate/core";
import { MatTableDataSource } from "@angular/material/table";
@Component({
templateUrl: "./movies-grid.component.html",
@ -26,6 +26,7 @@ export class MoviesGridComponent implements OnInit, AfterViewInit {
public displayedColumns: string[] = ['title', 'requestedUser.requestedBy', 'status', 'requestStatus','requestedDate', 'actions'];
public gridCount: string = "15";
public isAdmin: boolean;
public manageOwnRequests: boolean;
public defaultSort: string = "requestedDate";
public defaultOrder: string = "desc";
public currentFilter: RequestFilterType = RequestFilterType.All;
@ -39,7 +40,7 @@ export class MoviesGridComponent implements OnInit, AfterViewInit {
private storageKeyGridCount = "Movie_DefaultGridCount";
private storageKeyCurrentFilter = "Movie_DefaultFilter";
@Output() public onOpenOptions = new EventEmitter<{ request: any, filter: any, onChange: any }>();
@Output() public onOpenOptions = new EventEmitter<{ request: any, filter: any, onChange: any, manageOwnRequests: boolean, isAdmin: boolean }>();
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
@ -53,6 +54,7 @@ export class MoviesGridComponent implements OnInit, AfterViewInit {
public ngOnInit() {
this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser");
this.manageOwnRequests = this.auth.hasRole("ManageOwnRequests")
if (this.isAdmin) {
this.displayedColumns.unshift('select');
}
@ -135,7 +137,8 @@ export class MoviesGridComponent implements OnInit, AfterViewInit {
this.ref.detectChanges();
};
this.onOpenOptions.emit({ request: request, filter: filter, onChange: onChange });
const data = { request: request, filter: filter, onChange: onChange, manageOwnRequests: this.manageOwnRequests, isAdmin: this.isAdmin };
this.onOpenOptions.emit(data);
}
public switchFilter(type: RequestFilterType) {

@ -1,11 +1,11 @@
<mat-nav-list>
<a id="requestDelete" (click)="delete()" mat-list-item>
<a id="requestDelete" *ngIf="data.isAdmin || data.manageOwnRequests" (click)="delete()" mat-list-item>
<span mat-line>{{'Requests.RequestPanel.Delete' | translate}}</span>
</a>
<a id="requestApprove" *ngIf="data.canApprove" (click)="approve()" mat-list-item>
<a id="requestApprove" *ngIf="data.canApprove && data.isAdmin" (click)="approve()" mat-list-item>
<span mat-line>{{'Requests.RequestPanel.Approve' | translate}}</span>
</a>
<a id="requestChangeAvailability" *ngIf="data.type !== RequestType.tvShow" (click)="changeAvailability()" mat-list-item>
<a id="requestChangeAvailability" *ngIf="data.type !== RequestType.tvShow && data.isAdmin" (click)="changeAvailability()" mat-list-item>
<span mat-line>{{'Requests.RequestPanel.ChangeAvailability' | translate}}</span>
</a>
</mat-nav-list>

@ -1,8 +1,9 @@
import { Component, ViewChild } from "@angular/core";
import { MatBottomSheet } from "@angular/material/bottom-sheet";
import { MoviesGridComponent } from "./movies-grid/movies-grid.component";
import { RequestOptionsComponent } from "./options/request-options.component";
import { UpdateType } from "../models/UpdateType";
import { MoviesGridComponent } from "./movies-grid/movies-grid.component";
@Component({
templateUrl: "./requests-list.component.html",
@ -12,8 +13,8 @@ export class RequestsListComponent {
constructor(private bottomSheet: MatBottomSheet) { }
public onOpenOptions(event: { request: any, filter: any, onChange: any }) {
const ref = this.bottomSheet.open(RequestOptionsComponent, { data: { id: event.request.id, type: event.request.requestType, canApprove: event.request.canApprove } });
public onOpenOptions(event: { request: any, filter: any, onChange: any, manageOwnRequests: boolean, isAdmin: boolean }) {
const ref = this.bottomSheet.open(RequestOptionsComponent, { data: { id: event.request.id, type: event.request.requestType, canApprove: event.request.canApprove, manageOwnRequests: event.manageOwnRequests, isAdmin: event.isAdmin } });
ref.afterDismissed().subscribe((result) => {
if(!result) {

@ -22,4 +22,8 @@ export class TheMovieDbService extends ServiceHelpers {
return this.http.get<IMovieDbKeyword>(`${this.url}/Keywords/${keywordId}`, { headers: this.headers })
.pipe(catchError((error: HttpErrorResponse) => error.status === 404 ? empty() : throwError(error)));
}
public getGenres(media: string): Observable<IMovieDbKeyword[]> {
return this.http.get<IMovieDbKeyword[]>(`${this.url}/Genres/${media}`, { headers: this.headers })
}
}

@ -1,4 +1,4 @@
<settings-menu></settings-menu>
<settings-menu></settings-menu>
<div class="small-middle-container">
<wiki [path]="'/settings/customization/'"></wiki>
<fieldset *ngIf="settings">
@ -12,9 +12,8 @@
</mat-form-field>
</div>
<div class="md-form-field">
<mat-hint>The application url should be your Externally Accessible URL for example, your internal URL is http://192.168.1.50/ but your Externally
Accessible URL is 'https://mydomain.com/requests' Please ensure this field is correct as it drives a lot of functionality include the QR code for the
mobile app and it affects the way email notifications are sent.
<mat-hint>The application url should be your Externally Accessible URL (the address you use to reach Ombi from outside your system). For example, 'https://example.com/requests'. <br />Please ensure this field is correct as it drives a lot of functionality, including the QR code for the
mobile app, and the way email notifications are sent.
</mat-hint>
<mat-form-field appearance="outline">
<mat-label>Application URL</mat-label>
@ -81,4 +80,4 @@
</fieldset>
</div>
</div>

@ -15,7 +15,7 @@
<form [formGroup]='tagForm'>
<mat-form-field class="example-full-width">
<input type="text" placeholder="Excluded Keyword IDs for Movie Suggestions" matInput
<input type="text" placeholder="Excluded Keyword IDs for Movie & TV Suggestions" matInput
formControlName="input" [matAutocomplete]="auto"
matTooltip="Prevent movies with certain keywords from being suggested. May require a restart to take effect.">
<mat-autocomplete (optionSelected)="optionSelected($event.option.value)" autoActiveFirstOption
@ -28,7 +28,45 @@
<mat-chip-list #chipList>
<mat-chip *ngFor="let key of excludedKeywords" [selectable]="false" [removable]="true"
(removed)="remove(key)">
(removed)="remove(key, 'keyword')">
{{key.name}}
<i matChipRemove class="fas fa-times fa-lg"></i>
</mat-chip>
</mat-chip-list>
<div class="md-form-field" style="margin-top:1em;">
<mat-form-field appearance="outline" >
<mat-label>Movie Genres</mat-label>
<mat-select formControlName="excludedMovieGenres" multiple>
<mat-option *ngFor="let genre of filteredMovieGenres" [value]="genre.id">
{{genre.name}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<mat-chip-list #chipList>
<mat-chip *ngFor="let key of excludedMovieGenres" [selectable]="false" [removable]="true"
(removed)="remove(key, 'movieGenre')">
{{key.name}}
<i matChipRemove class="fas fa-times fa-lg"></i>
</mat-chip>
</mat-chip-list>
<div class="md-form-field" style="margin-top:1em;">
<mat-form-field appearance="outline" >
<mat-label>Tv Genres</mat-label>
<mat-select formControlName="excludedTvGenres" multiple>
<mat-option *ngFor="let genre of filteredTvGenres" [value]="genre.id">
{{genre.name}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<mat-chip-list #chipList>
<mat-chip *ngFor="let key of excludedTvGenres" [selectable]="false" [removable]="true"
(removed)="remove(key, 'tvGenre')">
{{key.name}}
<i matChipRemove class="fas fa-times fa-lg"></i>
</mat-chip>
@ -52,4 +90,4 @@
</div>
</div>
</fieldset>
</div>
</div>

@ -23,8 +23,13 @@ export class TheMovieDbComponent implements OnInit {
public settings: ITheMovieDbSettings;
public excludedKeywords: IKeywordTag[];
public excludedMovieGenres: IKeywordTag[];
public excludedTvGenres: IKeywordTag[];
public tagForm: FormGroup;
public filteredTags: IMovieDbKeyword[];
public filteredMovieGenres: IMovieDbKeyword[];
public filteredTvGenres: IMovieDbKeyword[];
@ViewChild('fruitInput') public fruitInput: ElementRef<HTMLInputElement>;
constructor(private settingsService: SettingsService,
@ -35,9 +40,13 @@ export class TheMovieDbComponent implements OnInit {
public ngOnInit() {
this.tagForm = this.fb.group({
input: null,
excludedMovieGenres: null,
excludedTvGenres: null,
});
this.settingsService.getTheMovieDbSettings().subscribe(settings => {
this.settings = settings;
// Map Keyword ids -> keyword name
this.excludedKeywords = settings.excludedKeywordIds
? settings.excludedKeywordIds.map(id => ({
id,
@ -45,13 +54,56 @@ export class TheMovieDbComponent implements OnInit {
initial: true,
}))
: [];
this.excludedKeywords.forEach(key => {
this.tmdbService.getKeyword(key.id).subscribe(keyResult => {
this.excludedKeywords.filter((val, idx) => {
val.name = keyResult.name;
})
this.excludedKeywords.forEach(key => {
this.tmdbService.getKeyword(key.id).subscribe(keyResult => {
this.excludedKeywords.filter((val, idx) => {
val.name = keyResult.name;
})
});
});
// Map Movie Genre ids -> genre name
this.excludedMovieGenres = settings.excludedMovieGenreIds
? settings.excludedMovieGenreIds.map(id => ({
id,
name: "",
initial: true,
}))
: [];
this.tmdbService.getGenres("movie").subscribe(results => {
this.filteredMovieGenres = results;
this.excludedMovieGenres.forEach(genre => {
results.forEach(result => {
if (genre.id == result.id) {
genre.name = result.name;
}
});
});
});
// Map Tv Genre ids -> genre name
this.excludedTvGenres = settings.excludedTvGenreIds
? settings.excludedTvGenreIds.map(id => ({
id,
name: "",
initial: true,
}))
: [];
this.tmdbService.getGenres("tv").subscribe(results => {
this.filteredTvGenres = results;
this.excludedTvGenres.forEach(genre => {
results.forEach(result => {
if (genre.id == result.id) {
genre.name = result.name;
}
});
});
});
});
this.tagForm
@ -65,19 +117,48 @@ export class TheMovieDbComponent implements OnInit {
})
)
.subscribe((r) => (this.filteredTags = r));
}
public remove(tag: IKeywordTag): void {
const index = this.excludedKeywords.indexOf(tag);
public remove(tag: IKeywordTag, tag_type: string): void {
var exclusion_list;
switch (tag_type) {
case "keyword":
exclusion_list = this.excludedKeywords;
break;
case "movieGenre":
exclusion_list = this.excludedMovieGenres;
break;
case "tvGenre":
exclusion_list = this.excludedTvGenres;
break;
default:
return;
}
const index = exclusion_list.indexOf(tag);
if (index >= 0) {
this.excludedKeywords.splice(index, 1);
exclusion_list.splice(index, 1);
}
}
public save() {
var selectedMovieGenres: number[] = this.tagForm.controls.excludedMovieGenres.value ?? [];
var selectedTvGenres: number[] = this.tagForm.controls.excludedTvGenres.value ?? [];
var movieIds: number[] = this.excludedMovieGenres.map(k => k.id);
var tvIds: number[] = this.excludedTvGenres.map(k => k.id)
// Concat and dedup already excluded genres + newly selected ones
selectedMovieGenres = movieIds.concat(selectedMovieGenres.filter(item => movieIds.indexOf(item) < 0));
selectedTvGenres = tvIds.concat(selectedTvGenres.filter(item => tvIds.indexOf(item) < 0));
this.settings.excludedKeywordIds = this.excludedKeywords.map(k => k.id);
this.settings.excludedMovieGenreIds = selectedMovieGenres;
this.settings.excludedTvGenreIds = selectedTvGenres;
this.settingsService.saveTheMovieDbSettings(this.settings).subscribe(x => {
if (x) {
this.notificationService.success("Successfully saved The Movie Database settings");

@ -56,17 +56,17 @@
<qrcode id="qrCode" *ngIf="qrCodeEnabled" [qrdata]="qrCode" [size]="256" [level]="'L'"></qrcode>
<div class="row">
<div class="col-12">
<a href='https://play.google.com/store/apps/details?id=com.tidusjar.Ombi&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'
target="_blank"><img width="200px" alt='Get it on Google Play'
src='https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png' /></a>
<a href="https://play.google.com/store/apps/details?id=com.tidusjar.Ombi&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1"
target="_blank"><img width="200" alt="Get it on Google Play"
src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" /></a>
</div>
<div class="col-12">
<a href='https://apps.apple.com/us/app/ombi/id1335260043' target="_blank"><img
style="margin-left:13px" width="170px" alt='Get it from the App Store'
src='{{baseUrl}}/images/appstore.svg' /></a>
<a href="https://apps.apple.com/us/app/ombi/id1335260043" target="_blank"><img
style="margin-left:13px" width="170" alt="Get it from the App Store"
src="{{baseUrl}}/images/appstore.svg" /></a>
</div>
<div class="col-12">
<button style="margin-left:13px; margin-top: 20px;" mat-raised-button color="accent" type="button" (click)="openMobileApp($event)">Open Mobile App</button>
<button style="margin-left:13px; margin-top: 20px;" mat-raised-button color="accent" type="button" (click)="openMobileApp($event)">Open Mobile App</button>
</div>
</div>
</div>

@ -145,13 +145,14 @@
<div class="row">
<div class="col-md-3 col-sm-12">
<div class="col-md-6 col-sm-12">
<button *ngIf="!edit" type="button" mat-raised-button color="accent" data-test="createuserbtn" (click)="create()">Create</button>
<div *ngIf="edit">
<button type="button" data-test="updatebtn" mat-raised-button color="accent" class="btn btn-primary-outline" (click)="update()">Update</button>
<button type="button" data-test="deletebtn" mat-raised-button color="warn" class="btn btn-danger-outline" (click)="delete()">Delete</button>
<button type="button" style="float:right;" mat-raised-button color="primary" class="btn btn-info-outline" (click)="resetPassword()" pTooltip="You need your SMTP settings setup">Send
<button type="button" style="float:right;" mat-raised-button color="primary" class="btn btn-info-outline" (click)="resetPassword()" matTooltip="You need your SMTP settings setup">Send
Reset Password Link</button>
<button *ngIf="customization.applicationUrl" type="button" mat-raised-button color="accent" class="btn btn-info-outline" (click)="appLink()" matTooltip="Send this link to the user and they can then open the app and directly login">Copy users App Link</button>
</div>

@ -1,9 +1,10 @@
import { Location } from "@angular/common";
import { AfterViewInit, Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { ActivatedRoute, Router } from "@angular/router";
import { Component, OnInit } from "@angular/core";
import { ICheckbox, ICustomizationSettings, INotificationAgent, INotificationPreferences, IRadarrProfile, IRadarrRootFolder, ISonarrProfile, ISonarrRootFolder, IUser, UserType } from "../interfaces";
import { IdentityService, MessageService, RadarrService, SettingsService, SonarrService } from "../services";
import { ICheckbox, INotificationAgent, INotificationPreferences, IRadarrProfile, IRadarrRootFolder, ISonarrProfile, ISonarrRootFolder, IUser, UserType } from "../interfaces";
import { IdentityService, RadarrService, SonarrService, MessageService } from "../services";
import { Clipboard } from '@angular/cdk/clipboard';
import { Location } from "@angular/common";
@Component({
templateUrl: "./usermanagement-user.component.html",
@ -27,12 +28,17 @@ export class UserManagementUserComponent implements OnInit {
public countries: string[];
private customization: ICustomizationSettings;
private accessToken: string;
constructor(private identityService: IdentityService,
private notificationService: MessageService,
private readonly settingsService: SettingsService,
private router: Router,
private route: ActivatedRoute,
private sonarrService: SonarrService,
private radarrService: RadarrService,
private clipboard: Clipboard,
private location: Location) {
this.route.params.subscribe((params: any) => {
@ -60,6 +66,9 @@ export class UserManagementUserComponent implements OnInit {
this.radarrService.getQualityProfilesFromSettings().subscribe(x => this.radarrQualities = x);
this.radarrService.getRootFoldersFromSettings().subscribe(x => this.radarrRootFolders = x);
this.settingsService.getCustomization().subscribe(x => this.customization = x);
this.identityService.getAccessToken().subscribe(x => this.accessToken = x);
if(!this.edit) {
this.user = {
alias: "",
@ -178,7 +187,12 @@ export class UserManagementUserComponent implements OnInit {
}
});
}
public async appLink() {
this.clipboard.copy(`ombi://${this.customization.applicationUrl}|${this.accessToken}`);
this.notificationService.send("Copied!");
}
public back() {
this.location.back();
}

@ -1,23 +1,19 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { RouterModule, Routes } from "@angular/router";
import { ConfirmDialogModule } from "primeng/confirmdialog";
import { MultiSelectModule } from "primeng/multiselect";
import { SidebarModule } from "primeng/sidebar";
import { TooltipModule } from "primeng/tooltip";
import { UserManagementUserComponent } from "./usermanagement-user.component";
import { UserManagementComponent } from "./usermanagement.component";
import { PipeModule } from "../pipes/pipe.module";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { IdentityService, PlexService, RadarrService, SonarrService } from "../services";
import { RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "../auth/auth.guard";
import { CommonModule } from "@angular/common";
import { ConfirmDialogModule } from "primeng/confirmdialog";
import { MultiSelectModule } from "primeng/multiselect";
import { NgModule } from "@angular/core";
import { OrderModule } from "ngx-order-pipe";
import { PipeModule } from "../pipes/pipe.module";
import { SharedModule } from "../shared/shared.module";
import { SidebarModule } from "primeng/sidebar";
import { TooltipModule } from "primeng/tooltip";
import { UserManagementComponent } from "./usermanagement.component";
import { UserManagementUserComponent } from "./usermanagement-user.component";
const routes: Routes = [
{ path: "", component: UserManagementComponent, canActivate: [AuthGuard] },

@ -5,6 +5,10 @@ using Ombi.Attributes;
using System.Collections.Generic;
using System.Threading.Tasks;
// Due to conflicting Genre models in
// Ombi.TheMovieDbApi.Models and Ombi.Api.TheMovieDb.Models
using Genre = Ombi.TheMovieDbApi.Models.Genre;
namespace Ombi.Controllers.External
{
[Admin]
@ -34,5 +38,13 @@ namespace Ombi.Controllers.External
var keyword = await TmdbApi.GetKeyword(keywordId);
return keyword == null ? NotFound() : (IActionResult)Ok(keyword);
}
/// <summary>
/// Gets the genres for either Tv or Movies depending on media type
/// </summary>
/// <param name="media">Either `tv` or `movie`.</param>
[HttpGet("Genres/{media}")]
public async Task<IEnumerable<Genre>> GetGenres(string media) =>
await TmdbApi.GetGenres(media);
}
}

@ -82,7 +82,17 @@ namespace Ombi.Controllers.V1
{
var settings = await Ombi.GetSettingsAsync();
return new { Result = settings?.Wizard ?? false};
return new { Result = settings?.Wizard ?? false };
}
[ApiExplorerSettings(IgnoreApi = true)]
[HttpGet("demo")]
public IActionResult Demo()
{
var instance = DemoSingleton.Instance;
instance.Demo = !instance.Demo;
return new OkResult();
}
}
}

@ -72,8 +72,6 @@ namespace Ombi
services.Configure<TokenAuthentication>(configuration.GetSection("TokenAuthentication"));
services.Configure<LandingPageBackground>(configuration.GetSection("LandingPageBackground"));
services.Configure<DemoLists>(configuration.GetSection("Demo"));
var enabledDemo = Convert.ToBoolean(configuration.GetSection("Demo:Enabled").Value);
DemoSingleton.Instance.Demo = enabledDemo;
}
public static void AddJwtAuthentication(this IServiceCollection services)

Loading…
Cancel
Save