We now show streaming information on the details page

pull/3970/head
tidusjar 3 years ago
parent 88453f0a99
commit ea7307ac07

@ -0,0 +1,94 @@
using NUnit.Framework;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Core.Helpers;
using Ombi.Store.Entities;
using System.Collections.Generic;
namespace Ombi.Core.Tests
{
[TestFixture]
public class WatchProviderParserTests
{
[TestCase("GB", TestName = "UpperCase")]
[TestCase("gb", TestName = "LowerCase")]
[TestCase("gB", TestName = "MixedCase")]
public void GetValidStreamData(string streamingCountry)
{
var result = WatchProviderParser.GetUserWatchProviders(new WatchProviders
{
Results = new Results
{
GB = new WatchProviderData()
{
StreamInformation = new List<StreamData>
{
new StreamData
{
provider_name = "Netflix",
display_priority = 0,
logo_path = "logo",
provider_id = 8
}
}
}
}
}, new OmbiUser { StreamingCountry = streamingCountry });
Assert.That(result[0].provider_name, Is.EqualTo("Netflix"));
}
[TestCase("GB", TestName = "Missing_UpperCase")]
[TestCase("gb", TestName = "Missing_LowerCase")]
[TestCase("gB", TestName = "Missing_MixedCase")]
public void GetMissingStreamData(string streamingCountry)
{
var result = WatchProviderParser.GetUserWatchProviders(new WatchProviders
{
Results = new Results
{
AR = new WatchProviderData()
{
StreamInformation = new List<StreamData>
{
new StreamData
{
provider_name = "Netflix",
display_priority = 0,
logo_path = "logo",
provider_id = 8
}
}
}
}
}, new OmbiUser { StreamingCountry = streamingCountry });
Assert.That(result, Is.Empty);
}
[Test]
public void GetInvalidStreamData()
{
var result = WatchProviderParser.GetUserWatchProviders(new WatchProviders
{
Results = new Results
{
AR = new WatchProviderData()
{
StreamInformation = new List<StreamData>
{
new StreamData
{
provider_name = "Netflix",
display_priority = 0,
logo_path = "logo",
provider_id = 8
}
}
}
}
}, new OmbiUser { StreamingCountry = "BLAH" });
Assert.That(result, Is.Empty);
}
}
}

@ -15,6 +15,8 @@ using Ombi.Core.Settings;
using Ombi.Settings.Settings.Models; using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities; using Ombi.Store.Entities;
using Ombi.Store.Repository; using Ombi.Store.Repository;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Core.Helpers;
namespace Ombi.Core.Engine namespace Ombi.Core.Engine
{ {
@ -179,6 +181,12 @@ namespace Ombi.Core.Engine
return user.Language; return user.Language;
} }
protected async Task<List<StreamData>> GetUserWatchProvider(WatchProviders providers)
{
var user = await GetUser();
return WatchProviderParser.GetUserWatchProviders(providers, user);
}
private OmbiSettings ombiSettings; private OmbiSettings ombiSettings;
protected async Task<OmbiSettings> GetOmbiSettings() protected async Task<OmbiSettings> GetOmbiSettings()
{ {

@ -26,5 +26,6 @@ namespace Ombi.Core.Engine.Interfaces
int ResultLimit { get; set; } int ResultLimit { get; set; }
Task<MovieFullInfoViewModel> GetMovieInfoByImdbId(string imdbId, CancellationToken requestAborted); Task<MovieFullInfoViewModel> GetMovieInfoByImdbId(string imdbId, CancellationToken requestAborted);
Task<IEnumerable<StreamingData>> GetStreamInformation(int movieDbId, CancellationToken cancellationToken);
} }
} }

@ -1,4 +1,6 @@
using System.Threading.Tasks; using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Ombi.Core.Models.Search.V2; using Ombi.Core.Models.Search.V2;
namespace Ombi.Core namespace Ombi.Core
@ -7,5 +9,6 @@ namespace Ombi.Core
{ {
Task<SearchFullInfoTvShowViewModel> GetShowInformation(int tvdbid); Task<SearchFullInfoTvShowViewModel> GetShowInformation(int tvdbid);
Task<SearchFullInfoTvShowViewModel> GetShowByRequest(int requestId); Task<SearchFullInfoTvShowViewModel> GetShowByRequest(int requestId);
Task<IEnumerable<StreamingData>> GetStreamInformation(int tvDbId, int tvMazeId, CancellationToken cancellationToken);
} }
} }

@ -249,6 +249,26 @@ namespace Ombi.Core.Engine.V2
return result; return result;
} }
public async Task<IEnumerable<StreamingData>> GetStreamInformation(int movieDbId, CancellationToken cancellationToken)
{
var providers = await MovieApi.GetMovieWatchProviders(movieDbId, cancellationToken);
var results = await GetUserWatchProvider(providers);
var data = new List<StreamingData>();
foreach (var result in results)
{
data.Add(new StreamingData
{
Logo = result.logo_path,
Order = result.display_priority,
StreamingProvider = result.provider_name
});
}
return data;
}
protected async Task<List<SearchMovieViewModel>> TransformMovieResultsToResponse( protected async Task<List<SearchMovieViewModel>> TransformMovieResultsToResponse(
IEnumerable<MovieSearchResult> movies) IEnumerable<MovieSearchResult> movies)
{ {

@ -19,6 +19,8 @@ using Ombi.Core.Settings;
using Ombi.Store.Repository; using Ombi.Store.Repository;
using TraktSharp.Entities; using TraktSharp.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Threading;
using Ombi.Api.TheMovieDb;
namespace Ombi.Core.Engine.V2 namespace Ombi.Core.Engine.V2
{ {
@ -27,15 +29,17 @@ namespace Ombi.Core.Engine.V2
private readonly ITvMazeApi _tvMaze; private readonly ITvMazeApi _tvMaze;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly ITraktApi _traktApi; private readonly ITraktApi _traktApi;
private readonly IMovieDbApi _movieApi;
public TvSearchEngineV2(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper, public TvSearchEngineV2(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper,
ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um, ICacheService memCache, ISettingsService<OmbiSettings> s, ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um, ICacheService memCache, ISettingsService<OmbiSettings> s,
IRepository<RequestSubscription> sub) IRepository<RequestSubscription> sub, IMovieDbApi movieApi)
: base(identity, service, r, um, memCache, s, sub) : base(identity, service, r, um, memCache, s, sub)
{ {
_tvMaze = tvMaze; _tvMaze = tvMaze;
_mapper = mapper; _mapper = mapper;
_traktApi = trakt; _traktApi = trakt;
_movieApi = movieApi;
} }
@ -106,6 +110,39 @@ namespace Ombi.Core.Engine.V2
return await ProcessResult(mapped, traktInfoTask); return await ProcessResult(mapped, traktInfoTask);
} }
public async Task<IEnumerable<StreamingData>> GetStreamInformation(int tvDbId, int tvMazeId, CancellationToken cancellationToken)
{
var tvdbshow = await Cache.GetOrAdd(nameof(GetShowInformation) + tvMazeId,
async () => await _tvMaze.ShowLookupByTheTvDbId(tvMazeId), DateTime.Now.AddHours(12));
if (tvdbshow == null)
{
return null;
}
/// this is a best effort guess since TV maze do not provide the TheMovieDbId
var movieDbResults = await _movieApi.SearchTv(tvdbshow.name, tvdbshow.premiered.Substring(0, 4));
var potential = movieDbResults.FirstOrDefault();
tvDbId = potential.Id;
// end guess
var providers = await _movieApi.GetTvWatchProviders(tvDbId, cancellationToken);
var results = await GetUserWatchProvider(providers);
var data = new List<StreamingData>();
foreach (var result in results)
{
data.Add(new StreamingData
{
Logo = result.logo_path,
Order = result.display_priority,
StreamingProvider = result.provider_name
});
}
return data;
}
private IEnumerable<SearchTvShowViewModel> ProcessResults<T>(IEnumerable<T> items) private IEnumerable<SearchTvShowViewModel> ProcessResults<T>(IEnumerable<T> items)
{ {
var retVal = new List<SearchTvShowViewModel>(); var retVal = new List<SearchTvShowViewModel>();
@ -141,7 +178,7 @@ namespace Ombi.Core.Engine.V2
{ {
item.Images.Medium = item.Images.Medium.ToHttpsUrl(); item.Images.Medium = item.Images.Medium.ToHttpsUrl();
} }
if (item.Cast?.Any() ?? false) if (item.Cast?.Any() ?? false)
{ {
foreach (var cast in item.Cast) foreach (var cast in item.Cast)

@ -0,0 +1,35 @@
using Ombi.Api.TheMovieDb.Models;
using Ombi.Store.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ombi.Core.Helpers
{
public static class WatchProviderParser
{
public static List<StreamData> GetUserWatchProviders(WatchProviders providers, OmbiUser user)
{
var data = new List<StreamData>();
if (providers?.Results == null)
{
return data;
}
var resultsProp = providers.Results.GetType().GetProperties();
var matchingStreamingCountry = resultsProp.FirstOrDefault(x => x.Name.Equals(user.StreamingCountry, StringComparison.InvariantCultureIgnoreCase));
if (matchingStreamingCountry == null)
{
return data;
}
var result = (WatchProviderData)matchingStreamingCountry.GetValue(providers.Results);
if (result == null || result.StreamInformation == null)
{
return data;
}
return result.StreamInformation;
}
}
}

@ -0,0 +1,9 @@
namespace Ombi.Core.Models.Search.V2
{
public class StreamingData
{
public int Order { get; set; }
public string StreamingProvider { get; set; }
public string Logo { get; set; }
}
}

@ -18,6 +18,7 @@ namespace Ombi.Core.Models.UI
public UserType UserType { get; set; } public UserType UserType { get; set; }
public int MovieRequestLimit { get; set; } public int MovieRequestLimit { get; set; }
public int EpisodeRequestLimit { get; set; } public int EpisodeRequestLimit { get; set; }
public string StreamingCountry { get; set; }
public RequestQuotaCountModel EpisodeRequestQuota { get; set; } public RequestQuotaCountModel EpisodeRequestQuota { get; set; }
public RequestQuotaCountModel MovieRequestQuota { get; set; } public RequestQuotaCountModel MovieRequestQuota { get; set; }
public RequestQuotaCountModel MusicRequestQuota { get; set; } public RequestQuotaCountModel MusicRequestQuota { get; set; }

@ -1,9 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Newtonsoft.Json; using Newtonsoft.Json;
using Ombi.Helpers;
namespace Ombi.Store.Entities namespace Ombi.Store.Entities
{ {
@ -21,6 +21,12 @@ namespace Ombi.Store.Entities
public string Language { get; set; } public string Language { get; set; }
/// <summary>
/// Used to get the Streaming information for media
/// </summary>
[Required]
public string StreamingCountry { get; set; }
public int? MovieRequestLimit { get; set; } public int? MovieRequestLimit { get; set; }
public int? EpisodeRequestLimit { get; set; } public int? EpisodeRequestLimit { get; set; }
public int? MusicRequestLimit { get; set; } public int? MusicRequestLimit { get; set; }
@ -40,14 +46,14 @@ namespace Ombi.Store.Entities
public bool EmailLogin { get; set; } public bool EmailLogin { get; set; }
[NotMapped] public bool IsSystemUser => UserType == UserType.SystemUser; [NotMapped] public bool IsSystemUser => UserType == UserType.SystemUser;
[JsonIgnore] [JsonIgnore]
public override string PasswordHash public override string PasswordHash
{ {
get => base.PasswordHash; get => base.PasswordHash;
set => base.PasswordHash = value; set => base.PasswordHash = value;
} }
[JsonIgnore] [JsonIgnore]
public override string SecurityStamp public override string SecurityStamp
{ {

@ -14,29 +14,6 @@ If running migrations for any db provider other than Sqlite, then ensure the dat
export PATH="$HOME/.dotnet/tools:$PATH" export PATH="$HOME/.dotnet/tools:$PATH"
``` ```
1. In `src/Ombi`, install the `Microsoft.EntityFrameworkCore.Design` package:
```
cd src/Ombi
dotnet add package Microsoft.EntityFrameworkCore.Design
```
1. For some reason, the `StartupSingleton.Instance.SecurityKey` in `src/Ombi/Extensions/StartupExtensions.cs` is invalid when running `dotnet ef migrations add` so we must fix it; apply this patch which seems to do the job:
```
@@ -79,7 +79,7 @@ namespace Ombi
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
- IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(StartupSingleton.Instance.SecurityKey)),
+ IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(StartupSingleton.Instance.SecurityKey + "s")),
RequireExpirationTime = true,
ValidateLifetime = true,
ValidAudience = "Ombi",
```
*WARNING*: Don't forget to undo this before building Ombi, or things will be broken!
1. List the available `dbcontext`s, and select the one that matches the database your fields will go in: 1. List the available `dbcontext`s, and select the one that matches the database your fields will go in:
``` ```

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Ombi.Store.Migrations.OmbiMySql
{
public partial class UserStreamingCountry : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "StreamingCountry",
table: "AspNetUsers",
type: "longtext",
nullable: false,
defaultValue: "US");
migrationBuilder.Sql("UPDATE AspNetUsers SET StreamingCountry = 'US'");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "StreamingCountry",
table: "AspNetUsers");
}
}
}

@ -14,8 +14,8 @@ namespace Ombi.Store.Migrations.OmbiMySql
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "3.1.1") .HasAnnotation("Relational:MaxIdentifierLength", 64)
.HasAnnotation("Relational:MaxIdentifierLength", 64); .HasAnnotation("ProductVersion", "5.0.1");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{ {
@ -27,18 +27,18 @@ namespace Ombi.Store.Migrations.OmbiMySql
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("varchar(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("varchar(256)");
b.Property<string>("NormalizedName") b.Property<string>("NormalizedName")
.HasColumnType("varchar(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("varchar(256)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("NormalizedName") b.HasIndex("NormalizedName")
.IsUnique() .IsUnique()
.HasName("RoleNameIndex"); .HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles"); b.ToTable("AspNetRoles");
}); });
@ -257,8 +257,8 @@ namespace Ombi.Store.Migrations.OmbiMySql
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<string>("Email") b.Property<string>("Email")
.HasColumnType("varchar(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("varchar(256)");
b.Property<bool>("EmailConfirmed") b.Property<bool>("EmailConfirmed")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
@ -285,12 +285,12 @@ namespace Ombi.Store.Migrations.OmbiMySql
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("NormalizedEmail") b.Property<string>("NormalizedEmail")
.HasColumnType("varchar(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("varchar(256)");
b.Property<string>("NormalizedUserName") b.Property<string>("NormalizedUserName")
.HasColumnType("varchar(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("varchar(256)");
b.Property<string>("PasswordHash") b.Property<string>("PasswordHash")
.HasColumnType("longtext"); .HasColumnType("longtext");
@ -307,6 +307,9 @@ namespace Ombi.Store.Migrations.OmbiMySql
b.Property<string>("SecurityStamp") b.Property<string>("SecurityStamp")
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<string>("StreamingCountry")
.HasColumnType("longtext");
b.Property<bool>("TwoFactorEnabled") b.Property<bool>("TwoFactorEnabled")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
@ -314,8 +317,8 @@ namespace Ombi.Store.Migrations.OmbiMySql
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<string>("UserName") b.Property<string>("UserName")
.HasColumnType("varchar(256)") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("varchar(256)");
b.Property<int>("UserType") b.Property<int>("UserType")
.HasColumnType("int"); .HasColumnType("int");
@ -323,11 +326,11 @@ namespace Ombi.Store.Migrations.OmbiMySql
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("NormalizedEmail") b.HasIndex("NormalizedEmail")
.HasName("EmailIndex"); .HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName") b.HasIndex("NormalizedUserName")
.IsUnique() .IsUnique()
.HasName("UserNameIndex"); .HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers"); b.ToTable("AspNetUsers");
}); });
@ -1017,6 +1020,8 @@ namespace Ombi.Store.Migrations.OmbiMySql
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany() .WithMany()
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b => modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b =>
@ -1024,6 +1029,8 @@ namespace Ombi.Store.Migrations.OmbiMySql
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany("NotificationUserIds") .WithMany("NotificationUserIds")
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b => modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b =>
@ -1031,6 +1038,8 @@ namespace Ombi.Store.Migrations.OmbiMySql
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany() .WithMany()
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b => modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b =>
@ -1038,6 +1047,8 @@ namespace Ombi.Store.Migrations.OmbiMySql
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany() .WithMany()
.HasForeignKey("RequestedUserId"); .HasForeignKey("RequestedUserId");
b.Navigation("RequestedUser");
}); });
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
@ -1051,6 +1062,10 @@ namespace Ombi.Store.Migrations.OmbiMySql
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany() .WithMany()
.HasForeignKey("RequestedUserId"); .HasForeignKey("RequestedUserId");
b.Navigation("ParentRequest");
b.Navigation("RequestedUser");
}); });
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b => modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b =>
@ -1062,6 +1077,10 @@ namespace Ombi.Store.Migrations.OmbiMySql
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany() .WithMany()
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("Issues");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b =>
@ -1083,6 +1102,10 @@ namespace Ombi.Store.Migrations.OmbiMySql
b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported") b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported")
.WithMany() .WithMany()
.HasForeignKey("UserReportedId"); .HasForeignKey("UserReportedId");
b.Navigation("IssueCategory");
b.Navigation("UserReported");
}); });
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
@ -1090,6 +1113,8 @@ namespace Ombi.Store.Migrations.OmbiMySql
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany() .WithMany()
.HasForeignKey("RequestedUserId"); .HasForeignKey("RequestedUserId");
b.Navigation("RequestedUser");
}); });
modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b => modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b =>
@ -1097,6 +1122,8 @@ namespace Ombi.Store.Migrations.OmbiMySql
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany() .WithMany()
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => modelBuilder.Entity("Ombi.Store.Entities.Tokens", b =>
@ -1104,6 +1131,8 @@ namespace Ombi.Store.Migrations.OmbiMySql
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany() .WithMany()
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Entities.UserNotificationPreferences", b => modelBuilder.Entity("Ombi.Store.Entities.UserNotificationPreferences", b =>
@ -1111,6 +1140,8 @@ namespace Ombi.Store.Migrations.OmbiMySql
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany("UserNotificationPreferences") .WithMany("UserNotificationPreferences")
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Entities.UserQualityProfiles", b => modelBuilder.Entity("Ombi.Store.Entities.UserQualityProfiles", b =>
@ -1118,6 +1149,8 @@ namespace Ombi.Store.Migrations.OmbiMySql
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany() .WithMany()
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Entities.Votes", b => modelBuilder.Entity("Ombi.Store.Entities.Votes", b =>
@ -1125,6 +1158,8 @@ namespace Ombi.Store.Migrations.OmbiMySql
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany() .WithMany()
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
@ -1134,6 +1169,8 @@ namespace Ombi.Store.Migrations.OmbiMySql
.HasForeignKey("SeasonId") .HasForeignKey("SeasonId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Season");
}); });
modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b =>
@ -1143,6 +1180,42 @@ namespace Ombi.Store.Migrations.OmbiMySql
.HasForeignKey("ChildRequestId") .HasForeignKey("ChildRequestId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("ChildRequest");
});
modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b =>
{
b.Navigation("NotificationUserIds");
b.Navigation("UserNotificationPreferences");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.Navigation("Issues");
b.Navigation("SeasonRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b =>
{
b.Navigation("Comments");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
{
b.Navigation("Issues");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b =>
{
b.Navigation("ChildRequests");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b =>
{
b.Navigation("Episodes");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Ombi.Store.Migrations.OmbiSqlite
{
public partial class UserStreamingCountry : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "StreamingCountry",
table: "AspNetUsers",
type: "TEXT",
nullable: false,
defaultValue: "US");
migrationBuilder.Sql("UPDATE AspNetUsers SET StreamingCountry = 'US'");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "StreamingCountry",
table: "AspNetUsers");
}
}
}

@ -14,7 +14,7 @@ namespace Ombi.Store.Migrations.OmbiSqlite
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "3.1.1"); .HasAnnotation("ProductVersion", "5.0.1");
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{ {
@ -26,18 +26,18 @@ namespace Ombi.Store.Migrations.OmbiSqlite
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("TEXT") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("TEXT");
b.Property<string>("NormalizedName") b.Property<string>("NormalizedName")
.HasColumnType("TEXT") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("TEXT");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("NormalizedName") b.HasIndex("NormalizedName")
.IsUnique() .IsUnique()
.HasName("RoleNameIndex"); .HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles"); b.ToTable("AspNetRoles");
}); });
@ -256,8 +256,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("Email") b.Property<string>("Email")
.HasColumnType("TEXT") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed") b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -284,12 +284,12 @@ namespace Ombi.Store.Migrations.OmbiSqlite
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("NormalizedEmail") b.Property<string>("NormalizedEmail")
.HasColumnType("TEXT") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("TEXT");
b.Property<string>("NormalizedUserName") b.Property<string>("NormalizedUserName")
.HasColumnType("TEXT") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("TEXT");
b.Property<string>("PasswordHash") b.Property<string>("PasswordHash")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -306,6 +306,10 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.Property<string>("SecurityStamp") b.Property<string>("SecurityStamp")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("StreamingCountry")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled") b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -313,8 +317,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("UserName") b.Property<string>("UserName")
.HasColumnType("TEXT") .HasMaxLength(256)
.HasMaxLength(256); .HasColumnType("TEXT");
b.Property<int>("UserType") b.Property<int>("UserType")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -322,11 +326,11 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("NormalizedEmail") b.HasIndex("NormalizedEmail")
.HasName("EmailIndex"); .HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName") b.HasIndex("NormalizedUserName")
.IsUnique() .IsUnique()
.HasName("UserNameIndex"); .HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers"); b.ToTable("AspNetUsers");
}); });
@ -1016,6 +1020,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany() .WithMany()
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b => modelBuilder.Entity("Ombi.Store.Entities.NotificationUserId", b =>
@ -1023,6 +1029,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany("NotificationUserIds") .WithMany("NotificationUserIds")
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b => modelBuilder.Entity("Ombi.Store.Entities.RequestSubscription", b =>
@ -1030,6 +1038,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany() .WithMany()
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b => modelBuilder.Entity("Ombi.Store.Entities.Requests.AlbumRequest", b =>
@ -1037,6 +1047,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany() .WithMany()
.HasForeignKey("RequestedUserId"); .HasForeignKey("RequestedUserId");
b.Navigation("RequestedUser");
}); });
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b => modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
@ -1050,6 +1062,10 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany() .WithMany()
.HasForeignKey("RequestedUserId"); .HasForeignKey("RequestedUserId");
b.Navigation("ParentRequest");
b.Navigation("RequestedUser");
}); });
modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b => modelBuilder.Entity("Ombi.Store.Entities.Requests.IssueComments", b =>
@ -1061,6 +1077,10 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany() .WithMany()
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("Issues");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b => modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b =>
@ -1082,6 +1102,10 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported") b.HasOne("Ombi.Store.Entities.OmbiUser", "UserReported")
.WithMany() .WithMany()
.HasForeignKey("UserReportedId"); .HasForeignKey("UserReportedId");
b.Navigation("IssueCategory");
b.Navigation("UserReported");
}); });
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b => modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
@ -1089,6 +1113,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser") b.HasOne("Ombi.Store.Entities.OmbiUser", "RequestedUser")
.WithMany() .WithMany()
.HasForeignKey("RequestedUserId"); .HasForeignKey("RequestedUserId");
b.Navigation("RequestedUser");
}); });
modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b => modelBuilder.Entity("Ombi.Store.Entities.Requests.RequestLog", b =>
@ -1096,6 +1122,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany() .WithMany()
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Entities.Tokens", b => modelBuilder.Entity("Ombi.Store.Entities.Tokens", b =>
@ -1103,6 +1131,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany() .WithMany()
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Entities.UserNotificationPreferences", b => modelBuilder.Entity("Ombi.Store.Entities.UserNotificationPreferences", b =>
@ -1110,6 +1140,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany("UserNotificationPreferences") .WithMany("UserNotificationPreferences")
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Entities.UserQualityProfiles", b => modelBuilder.Entity("Ombi.Store.Entities.UserQualityProfiles", b =>
@ -1117,6 +1149,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany() .WithMany()
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Entities.Votes", b => modelBuilder.Entity("Ombi.Store.Entities.Votes", b =>
@ -1124,6 +1158,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite
b.HasOne("Ombi.Store.Entities.OmbiUser", "User") b.HasOne("Ombi.Store.Entities.OmbiUser", "User")
.WithMany() .WithMany()
.HasForeignKey("UserId"); .HasForeignKey("UserId");
b.Navigation("User");
}); });
modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b => modelBuilder.Entity("Ombi.Store.Repository.Requests.EpisodeRequests", b =>
@ -1133,6 +1169,8 @@ namespace Ombi.Store.Migrations.OmbiSqlite
.HasForeignKey("SeasonId") .HasForeignKey("SeasonId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Season");
}); });
modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b => modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b =>
@ -1142,6 +1180,42 @@ namespace Ombi.Store.Migrations.OmbiSqlite
.HasForeignKey("ChildRequestId") .HasForeignKey("ChildRequestId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("ChildRequest");
});
modelBuilder.Entity("Ombi.Store.Entities.OmbiUser", b =>
{
b.Navigation("NotificationUserIds");
b.Navigation("UserNotificationPreferences");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.ChildRequests", b =>
{
b.Navigation("Issues");
b.Navigation("SeasonRequests");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.Issues", b =>
{
b.Navigation("Comments");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.MovieRequests", b =>
{
b.Navigation("Issues");
});
modelBuilder.Entity("Ombi.Store.Entities.Requests.TvRequests", b =>
{
b.Navigation("ChildRequests");
});
modelBuilder.Entity("Ombi.Store.Repository.Requests.SeasonRequests", b =>
{
b.Navigation("Episodes");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }

@ -13,7 +13,7 @@ namespace Ombi.Api.TheMovieDb
Task<List<MovieSearchResult>> NowPlaying(string languageCode, int? page = null); Task<List<MovieSearchResult>> NowPlaying(string languageCode, int? page = null);
Task<List<MovieSearchResult>> PopularMovies(string languageCode, int? page = null, CancellationToken cancellationToken = default(CancellationToken)); Task<List<MovieSearchResult>> PopularMovies(string languageCode, int? page = null, CancellationToken cancellationToken = default(CancellationToken));
Task<List<MovieSearchResult>> SearchMovie(string searchTerm, int? year, string languageCode); Task<List<MovieSearchResult>> SearchMovie(string searchTerm, int? year, string languageCode);
Task<List<TvSearchResult>> SearchTv(string searchTerm); Task<List<TvSearchResult>> SearchTv(string searchTerm, string year = default);
Task<List<MovieSearchResult>> TopRated(string languageCode, int? page = null); Task<List<MovieSearchResult>> TopRated(string languageCode, int? page = null);
Task<List<MovieSearchResult>> Upcoming(string languageCode, int? page = null); Task<List<MovieSearchResult>> Upcoming(string languageCode, int? page = null);
Task<List<MovieSearchResult>> SimilarMovies(int movieId, string langCode); Task<List<MovieSearchResult>> SimilarMovies(int movieId, string langCode);
@ -28,5 +28,7 @@ namespace Ombi.Api.TheMovieDb
Task<Collections> GetCollection(string langCode, int collectionId, CancellationToken cancellationToken); Task<Collections> GetCollection(string langCode, int collectionId, CancellationToken cancellationToken);
Task<List<Keyword>> SearchKeyword(string searchTerm); Task<List<Keyword>> SearchKeyword(string searchTerm);
Task<Keyword> GetKeyword(int keywordId); Task<Keyword> GetKeyword(int keywordId);
Task<WatchProviders> GetMovieWatchProviders(int theMoviedbId, CancellationToken token);
Task<WatchProviders> GetTvWatchProviders(int theMoviedbId, CancellationToken token);
} }
} }

@ -0,0 +1,77 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace Ombi.Api.TheMovieDb.Models
{
public class WatchProviders
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("results")]
public Results Results { get; set; }
}
public class Results
{
public WatchProviderData AR { get; set; }
public WatchProviderData AT { get; set; }
public WatchProviderData AU { get; set; }
public WatchProviderData BE { get; set; }
public WatchProviderData BR { get; set; }
public WatchProviderData CA { get; set; }
public WatchProviderData CH { get; set; }
public WatchProviderData CL { get; set; }
public WatchProviderData CO { get; set; }
public WatchProviderData CZ { get; set; }
public WatchProviderData DE { get; set; }
public WatchProviderData DK { get; set; }
public WatchProviderData EC { get; set; }
public WatchProviderData EE { get; set; }
public WatchProviderData ES { get; set; }
public WatchProviderData FI { get; set; }
public WatchProviderData FR { get; set; }
public WatchProviderData GB { get; set; }
public WatchProviderData GR { get; set; }
public WatchProviderData HU { get; set; }
public WatchProviderData ID { get; set; }
public WatchProviderData IE { get; set; }
public WatchProviderData IN { get; set; }
public WatchProviderData IT { get; set; }
public WatchProviderData JP { get; set; }
public WatchProviderData KR { get; set; }
public WatchProviderData LT { get; set; }
public WatchProviderData LV { get; set; }
public WatchProviderData MX { get; set; }
public WatchProviderData MY { get; set; }
public WatchProviderData NL { get; set; }
public WatchProviderData NO { get; set; }
public WatchProviderData NZ { get; set; }
public WatchProviderData PE { get; set; }
public WatchProviderData PH { get; set; }
public WatchProviderData PL { get; set; }
public WatchProviderData PT { get; set; }
public WatchProviderData RU { get; set; }
public WatchProviderData SE { get; set; }
public WatchProviderData SG { get; set; }
public WatchProviderData TH { get; set; }
public WatchProviderData TR { get; set; }
public WatchProviderData US { get; set; }
public WatchProviderData VE { get; set; }
public WatchProviderData ZA { get; set; }
}
public class WatchProviderData
{
public string link { get; set; }
[JsonProperty("flatrate")]
public List<StreamData> StreamInformation { get; set; }
}
public class StreamData
{
public int display_priority { get; set; }
public string logo_path { get; set; }
public int provider_id { get; set; }
public string provider_name { get; set; }
}
}

@ -10,6 +10,7 @@ using Nito.AsyncEx;
using Ombi.Api.TheMovieDb.Models; using Ombi.Api.TheMovieDb.Models;
using Ombi.Core.Settings; using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External; using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.TheMovieDbApi.Models; using Ombi.TheMovieDbApi.Models;
namespace Ombi.Api.TheMovieDb namespace Ombi.Api.TheMovieDb
@ -24,7 +25,7 @@ namespace Ombi.Api.TheMovieDb
} }
private const string ApiToken = "b8eabaf5608b88d0298aa189dd90bf00"; private const string ApiToken = "b8eabaf5608b88d0298aa189dd90bf00";
private const string BaseUri ="http://api.themoviedb.org/3/"; private const string BaseUri = "http://api.themoviedb.org/3/";
private IMapper Mapper { get; } private IMapper Mapper { get; }
private IApi Api { get; } private IApi Api { get; }
private AsyncLazy<TheMovieDbSettings> Settings { get; } private AsyncLazy<TheMovieDbSettings> Settings { get; }
@ -107,11 +108,15 @@ namespace Ombi.Api.TheMovieDb
return result; return result;
} }
public async Task<List<TvSearchResult>> SearchTv(string searchTerm) public async Task<List<TvSearchResult>> SearchTv(string searchTerm, string year = default)
{ {
var request = new Request($"search/tv", BaseUri, HttpMethod.Get); var request = new Request($"search/tv", BaseUri, HttpMethod.Get);
request.AddQueryString("api_key", ApiToken); request.AddQueryString("api_key", ApiToken);
request.AddQueryString("query", searchTerm); request.AddQueryString("query", searchTerm);
if (year.HasValue())
{
request.AddQueryString("first_air_date_year", year);
}
AddRetry(request); AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request); var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
@ -126,7 +131,7 @@ namespace Ombi.Api.TheMovieDb
return await Api.Request<TvExternals>(request); return await Api.Request<TvExternals>(request);
} }
public async Task<List<MovieSearchResult>> SimilarMovies(int movieId, string langCode) public async Task<List<MovieSearchResult>> SimilarMovies(int movieId, string langCode)
{ {
var request = new Request($"movie/{movieId}/similar", BaseUri, HttpMethod.Get); var request = new Request($"movie/{movieId}/similar", BaseUri, HttpMethod.Get);
@ -165,7 +170,7 @@ namespace Ombi.Api.TheMovieDb
AddRetry(request); AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request); var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
return Mapper.Map<List<MovieSearchResult>>(result.results); return Mapper.Map<List<MovieSearchResult>>(result.results);
} }
@ -299,6 +304,32 @@ namespace Ombi.Api.TheMovieDb
return keyword == null || keyword.Id == 0 ? null : keyword; return keyword == null || keyword.Id == 0 ? null : keyword;
} }
public Task<TheMovieDbContainer<MultiSearch>> MultiSearch(string searchTerm, string languageCode, CancellationToken cancellationToken)
{
var request = new Request("search/multi", BaseUri, HttpMethod.Get);
request.AddQueryString("api_key", ApiToken);
request.AddQueryString("language", languageCode);
request.AddQueryString("query", searchTerm);
var result = Api.Request<TheMovieDbContainer<MultiSearch>>(request, cancellationToken);
return result;
}
public Task<WatchProviders> GetMovieWatchProviders(int theMoviedbId, CancellationToken token)
{
var request = new Request($"movie/{theMoviedbId}/watch/providers", BaseUri, HttpMethod.Get);
request.AddQueryString("api_key", ApiToken);
return Api.Request<WatchProviders>(request, token);
}
public Task<WatchProviders> GetTvWatchProviders(int theMoviedbId, CancellationToken token)
{
var request = new Request($"tv/{theMoviedbId}/watch/providers", BaseUri, HttpMethod.Get);
request.AddQueryString("api_key", ApiToken);
return Api.Request<WatchProviders>(request, token);
}
private async Task AddDiscoverMovieSettings(Request request) private async Task AddDiscoverMovieSettings(Request request)
{ {
var settings = await Settings; var settings = await Settings;
@ -309,17 +340,6 @@ namespace Ombi.Api.TheMovieDb
} }
} }
public async Task<TheMovieDbContainer<MultiSearch>> MultiSearch(string searchTerm, string languageCode, CancellationToken cancellationToken)
{
var request = new Request("search/multi", BaseUri, HttpMethod.Get);
request.AddQueryString("api_key", ApiToken);
request.AddQueryString("language", languageCode);
request.AddQueryString("query", searchTerm);
var result = await Api.Request<TheMovieDbContainer<MultiSearch>>(request, cancellationToken);
return result;
}
private static void AddRetry(Request request) private static void AddRetry(Request request)
{ {
request.Retry = true; request.Retry = true;

@ -0,0 +1,5 @@
export interface IStreamingData {
order: number;
streamingProvider: string;
logo: string;
}

@ -17,6 +17,7 @@ export interface IUser {
userAccessToken: string; userAccessToken: string;
language: string; language: string;
userQualityProfiles: IUserQualityProfiles; userQualityProfiles: IUserQualityProfiles;
streamingCountry: string;
// FOR UI // FOR UI
episodeRequestQuota: IRemainingRequests | null; episodeRequestQuota: IRemainingRequests | null;
@ -35,7 +36,7 @@ export interface IUserQualityProfiles {
sonarrRootPath: number; sonarrRootPath: number;
sonarrQualityProfile: number; sonarrQualityProfile: number;
radarrRootPath: number; radarrRootPath: number;
radarrQualityProfile: number; radarrQualityProfile: number;
} }
export interface ICreateWizardUser { export interface ICreateWizardUser {
@ -49,6 +50,10 @@ export interface IWizardUserResult {
errors: string[]; errors: string[];
} }
export interface IStreamingCountries {
code: string;
}
export enum UserType { export enum UserType {
LocalUser = 1, LocalUser = 1,
PlexUser = 2, PlexUser = 2,

@ -1,13 +1,27 @@
<div *ngIf="movie"> <div *ngIf="movie">
<span *ngIf="movie.voteAverage" matTooltip="{{'MediaDetails.Votes' | translate }} {{movie.voteCount | thousandShort: 1}}"> <span *ngIf="movie.voteAverage"
matTooltip="{{'MediaDetails.Votes' | translate }} {{movie.voteCount | thousandShort: 1}}">
<img class="rating-small" src="{{baseUrl}}/images/tmdb-logo.svg"> {{movie.voteAverage | number:'1.0-1'}}/10 <img class="rating-small" src="{{baseUrl}}/images/tmdb-logo.svg"> {{movie.voteAverage | number:'1.0-1'}}/10
</span> </span>
<span *ngIf="ratings?.critics_rating && ratings?.critics_score"> <span *ngIf="ratings?.critics_rating && ratings?.critics_score">
<img class="rating-small" src="{{baseUrl}}/images/{{ratings.critics_rating === 'Rotten' ? 'rotten-rotten.svg' : 'rotten-fresh.svg'}}"> {{ratings.critics_score}}% <img class="rating-small"
src="{{baseUrl}}/images/{{ratings.critics_rating === 'Rotten' ? 'rotten-rotten.svg' : 'rotten-fresh.svg'}}">
{{ratings.critics_score}}%
</span> </span>
<span *ngIf="ratings?.audience_rating && ratings?.audience_score"> <span *ngIf="ratings?.audience_rating && ratings?.audience_score">
<img class="rating-small" src="{{baseUrl}}/images/{{ratings.audience_rating === 'Upright' ? 'rotten-audience-fresh.svg' : 'rotten-audience-rotten.svg'}}"> {{ratings.audience_score}}% <img class="rating-small"
src="{{baseUrl}}/images/{{ratings.audience_rating === 'Upright' ? 'rotten-audience-fresh.svg' : 'rotten-audience-rotten.svg'}}">
{{ratings.audience_score}}%
</span> </span>
<div *ngIf="streams?.length > 0">
<hr>
<strong>{{'MediaDetails.StreamingOn' | translate }}:</strong>
<div>
<span *ngFor="let stream of streams">
<img class="stream-small" [matTooltip]="stream.streamingProvider" src="https://image.tmdb.org/t/p/original{{stream.logo}}">
</span>
</div>
</div>
<hr> <hr>
<div> <div>
<strong>{{'MediaDetails.Status' | translate }}:</strong> <strong>{{'MediaDetails.Status' | translate }}:</strong>
@ -59,48 +73,48 @@
<hr> <hr>
<strong>{{'MediaDetails.TheatricalRelease' | translate }}:</strong> <strong>{{'MediaDetails.TheatricalRelease' | translate }}:</strong>
{{movie.releaseDate | date: 'mediumDate'}} {{movie.releaseDate | date: 'mediumDate'}}
<div *ngIf="movie.digitalReleaseDate"> <div *ngIf="movie.digitalReleaseDate">
<strong>{{'MediaDetails.DigitalRelease' | translate }}:</strong> <strong>{{'MediaDetails.DigitalRelease' | translate }}:</strong>
{{movie.digitalReleaseDate | date: 'mediumDate'}} {{movie.digitalReleaseDate | date: 'mediumDate'}}
</div> </div>
<div *ngIf="movie.voteCount">
<strong>{{'MediaDetails.Votes' | translate }}:</strong>
{{movie.voteCount | thousandShort: 1}}
</div>
<div>
<strong>{{'MediaDetails.Runtime' | translate }}:</strong>
{{'MediaDetails.Minutes' | translate:{runtime: movie.runtime} }}
</div>
<div *ngIf="movie.revenue">
<strong>{{'MediaDetails.Revenue' | translate }}:</strong>
{{movie.revenue | currency: 'USD'}}
</div>
<div *ngIf="movie.budget">
<strong>{{'MediaDetails.Budget' | translate }}:</strong>
{{movie.budget | currency: 'USD'}}
</div>
<hr /> <div *ngIf="movie.voteCount">
<div *ngIf="movie.genres"> <strong>{{'MediaDetails.Votes' | translate }}:</strong>
<strong>{{'MediaDetails.Genres' | translate }}:</strong> {{movie.voteCount | thousandShort: 1}}
<div> </div>
<mat-chip-list> <div>
<mat-chip color="accent" selected *ngFor="let genre of movie.genres"> <strong>{{'MediaDetails.Runtime' | translate }}:</strong>
{{genre.name}} {{'MediaDetails.Minutes' | translate:{runtime: movie.runtime} }}
</mat-chip> </div>
</mat-chip-list> <div *ngIf="movie.revenue">
</div> <strong>{{'MediaDetails.Revenue' | translate }}:</strong>
</div> {{movie.revenue | currency: 'USD'}}
</div>
<div *ngIf="movie.budget">
<strong>{{'MediaDetails.Budget' | translate }}:</strong>
{{movie.budget | currency: 'USD'}}
</div>
<hr /> <hr />
<div *ngIf="movie?.keywords?.keywordsValue?.length > 0"> <div *ngIf="movie.genres">
<strong>{{'MediaDetails.Keywords' | translate }}:</strong> <strong>{{'MediaDetails.Genres' | translate }}:</strong>
<div>
<mat-chip-list> <mat-chip-list>
<mat-chip color="accent" selected *ngFor="let keyword of movie.keywords.keywordsValue"> <mat-chip color="accent" selected *ngFor="let genre of movie.genres">
{{keyword.name}} {{genre.name}}
</mat-chip> </mat-chip>
</mat-chip-list> </mat-chip-list>
</div> </div>
</div>
<hr />
<div *ngIf="movie?.keywords?.keywordsValue?.length > 0">
<strong>{{'MediaDetails.Keywords' | translate }}:</strong>
<mat-chip-list>
<mat-chip color="accent" selected *ngFor="let keyword of movie.keywords.keywordsValue">
{{keyword.name}}
</mat-chip>
</mat-chip-list>
</div>

@ -4,6 +4,7 @@ import { IMovieRequests } from "../../../../interfaces";
import { SearchV2Service } from "../../../../services/searchV2.service"; import { SearchV2Service } from "../../../../services/searchV2.service";
import { IMovieRatings } from "../../../../interfaces/IRatings"; import { IMovieRatings } from "../../../../interfaces/IRatings";
import { APP_BASE_HREF } from "@angular/common"; import { APP_BASE_HREF } from "@angular/common";
import { IStreamingData } from "../../../../interfaces/IStreams";
@Component({ @Component({
templateUrl: "./movie-information-panel.component.html", templateUrl: "./movie-information-panel.component.html",
styleUrls: ["../../../media-details.component.scss"], styleUrls: ["../../../media-details.component.scss"],
@ -19,9 +20,12 @@ export class MovieInformationPanelComponent implements OnInit {
@Input() public advancedOptions: boolean; @Input() public advancedOptions: boolean;
public ratings: IMovieRatings; public ratings: IMovieRatings;
public streams: IStreamingData[];
public ngOnInit() { public ngOnInit() {
this.searchService.getRottenMovieRatings(this.movie.title, +this.movie.releaseDate.toString().substring(0,4)) this.searchService.getRottenMovieRatings(this.movie.title, +this.movie.releaseDate.toString().substring(0,4))
.subscribe(x => this.ratings = x); .subscribe(x => this.ratings = x);
this.searchService.getMovieStreams(this.movie.id).subscribe(x => this.streams = x);
} }
} }

@ -5,7 +5,16 @@
<span *ngIf="ratings?.score && ratings?.class"> <span *ngIf="ratings?.score && ratings?.class">
<img class="rating-small" src="{{baseUrl}}/images/{{ratings.class === 'rotten' ? 'rotten-rotten.svg' : 'rotten-fresh.svg'}}"> {{ratings.score}}% <img class="rating-small" src="{{baseUrl}}/images/{{ratings.class === 'rotten' ? 'rotten-rotten.svg' : 'rotten-fresh.svg'}}"> {{ratings.score}}%
</span> </span>
<div *ngIf="streams?.length > 0">
<hr>
<strong>{{'MediaDetails.StreamingOn' | translate }}:</strong>
<div>
<span *ngFor="let stream of streams">
<img class="stream-small" [matTooltip]="stream.streamingProvider" src="https://image.tmdb.org/t/p/original{{stream.logo}}">
</span>
</div>
</div>
<hr> <hr>
<div *ngIf="tv.status"> <div *ngIf="tv.status">
<strong>{{'MediaDetails.Status' | translate }}:</strong> <strong>{{'MediaDetails.Status' | translate }}:</strong>

@ -2,6 +2,7 @@ import { Component, ViewEncapsulation, Input, OnInit } from "@angular/core";
import { ITvRequests } from "../../../../../interfaces"; import { ITvRequests } from "../../../../../interfaces";
import { ITvRatings } from "../../../../../interfaces/IRatings"; import { ITvRatings } from "../../../../../interfaces/IRatings";
import { ISearchTvResultV2 } from "../../../../../interfaces/ISearchTvResultV2"; import { ISearchTvResultV2 } from "../../../../../interfaces/ISearchTvResultV2";
import { IStreamingData } from "../../../../../interfaces/IStreams";
import { SearchV2Service } from "../../../../../services"; import { SearchV2Service } from "../../../../../services";
@Component({ @Component({
@ -19,6 +20,7 @@ export class TvInformationPanelComponent implements OnInit {
@Input() public advancedOptions: boolean; @Input() public advancedOptions: boolean;
public ratings: ITvRatings; public ratings: ITvRatings;
public streams: IStreamingData[];
public seasonCount: number; public seasonCount: number;
public totalEpisodes: number = 0; public totalEpisodes: number = 0;
public nextEpisode: any; public nextEpisode: any;
@ -26,6 +28,8 @@ export class TvInformationPanelComponent implements OnInit {
public ngOnInit(): void { public ngOnInit(): void {
this.searchService.getRottenTvRatings(this.tv.title, +this.tv.firstAired.toString().substring(0,4)) this.searchService.getRottenTvRatings(this.tv.title, +this.tv.firstAired.toString().substring(0,4))
.subscribe(x => this.ratings = x); .subscribe(x => this.ratings = x);
this.searchService.getTvStreams(+this.tv.theTvDbId, this.tv.id).subscribe(x => this.streams = x);
this.tv.seasonRequests.forEach(season => { this.tv.seasonRequests.forEach(season => {
this.totalEpisodes = this.totalEpisodes + season.episodes.length; this.totalEpisodes = this.totalEpisodes + season.episodes.length;
}); });

@ -230,4 +230,10 @@
.rating-small { .rating-small {
width: 1.3em; width: 1.3em;
}
.stream-small {
width: 3em;
border-radius: 1em;
margin-right: 10px;
margin-top: 5px;
} }

@ -4,7 +4,7 @@ import { Injectable, Inject } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { ICheckbox, ICreateWizardUser, IIdentityResult, INotificationPreferences, IResetPasswordToken, IUpdateLocalUser, IUser, IUserDropdown, IWizardUserResult } from "../interfaces"; import { ICheckbox, ICreateWizardUser, IIdentityResult, INotificationPreferences, IResetPasswordToken, IStreamingCountries, IUpdateLocalUser, IUser, IUserDropdown, IWizardUserResult } from "../interfaces";
import { ServiceHelpers } from "./service.helpers"; import { ServiceHelpers } from "./service.helpers";
@Injectable() @Injectable()
@ -83,8 +83,16 @@ export class IdentityService extends ServiceHelpers {
public getNotificationPreferencesForUser(userId: string): Observable<INotificationPreferences[]> { public getNotificationPreferencesForUser(userId: string): Observable<INotificationPreferences[]> {
return this.http.get<INotificationPreferences[]>(`${this.url}notificationpreferences/${userId}`, {headers: this.headers}); return this.http.get<INotificationPreferences[]>(`${this.url}notificationpreferences/${userId}`, {headers: this.headers});
} }
public updateLanguage(lang: string): Observable<null> { public updateLanguage(lang: string): Observable<null> {
return this.http.post<any>(`${this.url}language`, {lang: lang}, {headers: this.headers}); return this.http.post<any>(`${this.url}language`, {lang: lang}, {headers: this.headers});
} }
public getSupportedStreamingCountries(): Observable<string[]> {
return this.http.get<string[]>(`${this.url}streamingcountry`, {headers: this.headers});
}
public updateStreamingCountry(code: string): Observable<null> {
return this.http.post<any>(`${this.url}streamingcountry`, {code: code}, {headers: this.headers});
}
} }

@ -12,6 +12,7 @@ import { ISearchTvResultV2, IMovieCollectionsViewModel, IActorCredits } from "..
import { IArtistSearchResult, IAlbumArt } from "../interfaces/IMusicSearchResultV2"; import { IArtistSearchResult, IAlbumArt } from "../interfaces/IMusicSearchResultV2";
import { SearchFilter } from "../my-nav/SearchFilter"; import { SearchFilter } from "../my-nav/SearchFilter";
import { IMovieRatings, ITvRatings } from "../interfaces/IRatings"; import { IMovieRatings, ITvRatings } from "../interfaces/IRatings";
import { IStreamingData } from "../interfaces/IStreams";
@Injectable() @Injectable()
export class SearchV2Service extends ServiceHelpers { export class SearchV2Service extends ServiceHelpers {
@ -131,4 +132,12 @@ export class SearchV2Service extends ServiceHelpers {
return this.http.get<ITvRatings>(`${this.url}/ratings/tv/${name}/${year}`); return this.http.get<ITvRatings>(`${this.url}/ratings/tv/${name}/${year}`);
} }
public getMovieStreams(theMovieDbId: number): Observable<IStreamingData[]> {
return this.http.get<IStreamingData[]>(`${this.url}/stream/movie/${theMovieDbId}`);
}
public getTvStreams(theTvDbId: number, tvMaze: number): Observable<IStreamingData[]> {
return this.http.get<IStreamingData[]>(`${this.url}/stream/tv/${theTvDbId}/${tvMaze}`);
}
} }

@ -75,7 +75,7 @@ import { EpisodeRequestComponent } from "./episode-request/episode-request.compo
MatSnackBarModule, MatSnackBarModule,
], ],
entryComponents: [ entryComponents: [
EpisodeRequestComponent EpisodeRequestComponent,
], ],
exports: [ exports: [
TranslateModule, TranslateModule,

@ -3,24 +3,50 @@
<hr> <hr>
<div class="row justify-content-md-center top-spacing"> <div class="row top-spacing">
<div class="col-md"> <div class="col-4">
<div> <div>
<mat-form-field> <small>{{'UserPreferences.LanguageDescription' | translate}}</small>
<mat-label [translate]="'UserPreferences.OmbiLanguage'"></mat-label> <br>
<mat-select [(value)]="selectedLang" (selectionChange)="languageSelected();"> <mat-form-field>
<mat-option *ngFor="let lang of availableLanguages" [value]="lang.value"> <mat-label [translate]="'UserPreferences.OmbiLanguage'"></mat-label>
{{lang.display}} <mat-select [(value)]="selectedLang" (selectionChange)="languageSelected();">
</mat-option> <mat-option *ngFor="let lang of availableLanguages" [value]="lang.value">
</mat-select> {{lang.display}}
</mat-form-field> </mat-option>
</div> </mat-select>
<div> </mat-form-field>
</div>
</div> <div>
</div>
</div> </div>
<div class="col-md"> <div class="col-1"></div>
<div class="col-7">
<mat-label [translate]="'UserPreferences.MobileQRCode'"></mat-label>
<qrcode *ngIf="qrCodeEnabled" [qrdata]="qrCode" [size]="256" [level]="'L'"></qrcode> <qrcode *ngIf="qrCodeEnabled" [qrdata]="qrCode" [size]="256" [level]="'L'"></qrcode>
</div> </div>
<div class="col-4">
<div>
<small>{{'UserPreferences.StreamingCountryDescription' | translate}}</small>
<br>
<mat-form-field>
<mat-label [translate]="'UserPreferences.StreamingCountry'"></mat-label>
<mat-select [(value)]="selectedCountry" (selectionChange)="countrySelected();">
<mat-option *ngFor="let value of countries" [value]="value">
{{value}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div>
</div>
</div>
</div> </div>
</div> </div>

@ -3,7 +3,7 @@ import { AuthService } from "../../../auth/auth.service";
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
import { AvailableLanguages, ILanguage } from "./user-preference.constants"; import { AvailableLanguages, ILanguage } from "./user-preference.constants";
import { StorageService } from "../../../shared/storage/storage-service"; import { StorageService } from "../../../shared/storage/storage-service";
import { IdentityService, SettingsService } from "../../../services"; import { IdentityService, NotificationService, SettingsService } from "../../../services";
import { IUser } from "../../../interfaces"; import { IUser } from "../../../interfaces";
@Component({ @Component({
@ -17,12 +17,14 @@ export class UserPreferenceComponent implements OnInit {
public availableLanguages = AvailableLanguages; public availableLanguages = AvailableLanguages;
public qrCode: string; public qrCode: string;
public qrCodeEnabled: boolean; public qrCodeEnabled: boolean;
public countries: string[];
public selectedCountry: string;
private user: IUser; private user: IUser;
constructor(private authService: AuthService, constructor(private authService: AuthService,
private readonly translate: TranslateService, private readonly translate: TranslateService,
private storage: StorageService, private readonly notification: NotificationService,
private readonly identityService: IdentityService, private readonly identityService: IdentityService,
private readonly settingsService: SettingsService) { } private readonly settingsService: SettingsService) { }
@ -33,6 +35,8 @@ export class UserPreferenceComponent implements OnInit {
} }
const customization = await this.settingsService.getCustomization().toPromise(); const customization = await this.settingsService.getCustomization().toPromise();
this.selectedLang = this.translate.currentLang;
const accessToken = await this.identityService.getAccessToken().toPromise(); const accessToken = await this.identityService.getAccessToken().toPromise();
this.qrCode = `${customization.applicationUrl}|${accessToken}`; this.qrCode = `${customization.applicationUrl}|${accessToken}`;
@ -43,14 +47,18 @@ export class UserPreferenceComponent implements OnInit {
} }
this.user = await this.identityService.getUser().toPromise(); this.user = await this.identityService.getUser().toPromise();
if (this.user.language) { this.selectedCountry = this.user.streamingCountry;
this.selectedLang = this.user.language; this.identityService.getSupportedStreamingCountries().subscribe(x => this.countries = x);
}
} }
public languageSelected() { public languageSelected() {
this.identityService.updateLanguage(this.selectedLang).subscribe(); this.identityService.updateLanguage(this.selectedLang).subscribe(x => this.notification.success(this.translate.instant("UserPreferences.Updated")));
this.translate.use(this.selectedLang); this.translate.use(this.selectedLang);
} }
public countrySelected() {
this.identityService.updateStreamingCountry(this.selectedCountry).subscribe(x => this.notification.success(this.translate.instant("UserPreferences.Updated")));
}
} }

@ -18,6 +18,14 @@
<input matInput placeholder="Email Address" type="email" [(ngModel)]="user.emailAddress"> <input matInput placeholder="Email Address" type="email" [(ngModel)]="user.emailAddress">
</mat-form-field> </mat-form-field>
</div> </div>
<mat-form-field>
<mat-label [translate]="'UserPreferences.StreamingCountry'"></mat-label>
<mat-select [(value)]="user.streamingCountry">
<mat-option *ngFor="let value of countries" [value]="value">
{{value}}
</mat-option>
</mat-select>
</mat-form-field>
<div> <div>
<mat-form-field> <mat-form-field>
<input matInput placeholder="Password" type="password" [(ngModel)]="user.password" required> <input matInput placeholder="Password" type="password" [(ngModel)]="user.password" required>

@ -1,5 +1,5 @@
import { Location } from "@angular/common"; import { Location } from "@angular/common";
import { Component, OnInit } from "@angular/core"; import { AfterViewInit, Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { ICheckbox, INotificationAgent, INotificationPreferences, IRadarrProfile, IRadarrRootFolder, ISonarrProfile, ISonarrRootFolder, IUser, UserType } from "../interfaces"; import { ICheckbox, INotificationAgent, INotificationPreferences, IRadarrProfile, IRadarrRootFolder, ISonarrProfile, ISonarrRootFolder, IUser, UserType } from "../interfaces";
@ -25,6 +25,8 @@ export class UserManagementUserComponent implements OnInit {
public NotificationAgent = INotificationAgent; public NotificationAgent = INotificationAgent;
public edit: boolean; public edit: boolean;
public countries: string[];
constructor(private identityService: IdentityService, constructor(private identityService: IdentityService,
private notificationService: MessageService, private notificationService: MessageService,
private router: Router, private router: Router,
@ -45,6 +47,8 @@ export class UserManagementUserComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.identityService.getSupportedStreamingCountries().subscribe(x => this.countries = x);
this.identityService.getAllAvailableClaims().subscribe(x => this.availableClaims = x); this.identityService.getAllAvailableClaims().subscribe(x => this.availableClaims = x);
if(this.edit) { if(this.edit) {
this.identityService.getNotificationPreferencesForUser(this.userId).subscribe(x => this.notificationPreferences = x); this.identityService.getNotificationPreferencesForUser(this.userId).subscribe(x => this.notificationPreferences = x);
@ -74,6 +78,7 @@ export class UserManagementUserComponent implements OnInit {
episodeRequestQuota: null, episodeRequestQuota: null,
movieRequestQuota: null, movieRequestQuota: null,
language: null, language: null,
streamingCountry: "US",
userQualityProfiles: { userQualityProfiles: {
radarrQualityProfile: 0, radarrQualityProfile: 0,
radarrRootPath: 0, radarrRootPath: 0,
@ -172,7 +177,7 @@ export class UserManagementUserComponent implements OnInit {
} }
}); });
} }
public back() { public back() {
this.location.back(); this.location.back();
} }

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Ombi.Api.Plex; using Ombi.Api.Plex;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Attributes; using Ombi.Attributes;
using Ombi.Core.Authentication; using Ombi.Core.Authentication;
using Ombi.Core.Engine; using Ombi.Core.Engine;
@ -159,7 +160,8 @@ namespace Ombi.Controllers.V1
UserName = plexUser.user.username, UserName = plexUser.user.username,
UserType = UserType.PlexUser, UserType = UserType.PlexUser,
Email = plexUser.user.email, Email = plexUser.user.email,
ProviderUserId = plexUser.user.id ProviderUserId = plexUser.user.id,
StreamingCountry = "US" // Default
}; };
await _userManagementSettings.SaveSettingsAsync(new UserManagementSettings await _userManagementSettings.SaveSettingsAsync(new UserManagementSettings
@ -173,7 +175,8 @@ namespace Ombi.Controllers.V1
var userToCreate = new OmbiUser var userToCreate = new OmbiUser
{ {
UserName = user.Username, UserName = user.Username,
UserType = UserType.LocalUser UserType = UserType.LocalUser,
StreamingCountry = "US"
}; };
return await SaveWizardUser(user, userToCreate); return await SaveWizardUser(user, userToCreate);
@ -329,6 +332,32 @@ namespace Ombi.Controllers.V1
return Ok(); return Ok();
} }
/// <summary>
/// Returns the supported country codes that we have streaming data for
/// </summary>
[HttpGet("streamingcountry")]
[Authorize]
public IActionResult GetSupportedStreamingCountries()
{
var resultsProp = typeof(Results).GetProperties();
return Json(resultsProp.Select(x => x.Name));
}
/// <summary>
/// Sets the current users country streaming preference
/// </summary>
[HttpPost("streamingcountry")]
[Authorize]
public async Task<IActionResult> SetCurrentUserCountryStreaming([FromBody] CountryStreamingPreference model)
{
var username = User.Identity.Name.ToUpper();
var user = await UserManager.Users.FirstOrDefaultAsync(x => x.NormalizedUserName == username);
user.StreamingCountry = model.Code;
await UserManager.UpdateAsync(user);
return Ok();
}
/// <summary> /// <summary>
/// Gets the user by the user id. /// Gets the user by the user id.
/// </summary> /// </summary>
@ -358,7 +387,8 @@ namespace Ombi.Controllers.V1
EpisodeRequestLimit = user.EpisodeRequestLimit ?? 0, EpisodeRequestLimit = user.EpisodeRequestLimit ?? 0,
MovieRequestLimit = user.MovieRequestLimit ?? 0, MovieRequestLimit = user.MovieRequestLimit ?? 0,
MusicRequestLimit = user.MusicRequestLimit ?? 0, MusicRequestLimit = user.MusicRequestLimit ?? 0,
Language = user.Language Language = user.Language,
StreamingCountry = user.StreamingCountry
}; };
foreach (var role in userRoles) foreach (var role in userRoles)
@ -437,6 +467,7 @@ namespace Ombi.Controllers.V1
EpisodeRequestLimit = user.EpisodeRequestLimit, EpisodeRequestLimit = user.EpisodeRequestLimit,
MusicRequestLimit = user.MusicRequestLimit, MusicRequestLimit = user.MusicRequestLimit,
UserAccessToken = Guid.NewGuid().ToString("N"), UserAccessToken = Guid.NewGuid().ToString("N"),
StreamingCountry = user.StreamingCountry.HasValue() ? user.StreamingCountry : "US"
}; };
var userResult = await UserManager.CreateAsync(ombiUser, user.Password); var userResult = await UserManager.CreateAsync(ombiUser, user.Password);
@ -594,6 +625,10 @@ namespace Ombi.Controllers.V1
user.MovieRequestLimit = ui.MovieRequestLimit; user.MovieRequestLimit = ui.MovieRequestLimit;
user.EpisodeRequestLimit = ui.EpisodeRequestLimit; user.EpisodeRequestLimit = ui.EpisodeRequestLimit;
user.MusicRequestLimit = ui.MusicRequestLimit; user.MusicRequestLimit = ui.MusicRequestLimit;
if (ui.StreamingCountry.HasValue())
{
user.StreamingCountry = ui.StreamingCountry;
}
var updateResult = await UserManager.UpdateAsync(user); var updateResult = await UserManager.UpdateAsync(user);
if (!updateResult.Succeeded) if (!updateResult.Succeeded)
{ {
@ -739,7 +774,7 @@ namespace Ombi.Controllers.V1
[HttpPost("reset")] [HttpPost("reset")]
[AllowAnonymous] [AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)] [ApiExplorerSettings(IgnoreApi = true)]
public async Task<OmbiIdentityResult> SubmitResetPassword([FromBody]SubmitPasswordReset email) public async Task<OmbiIdentityResult> SubmitResetPassword([FromBody] SubmitPasswordReset email)
{ {
// Check if account exists // Check if account exists
var user = await UserManager.FindByEmailAsync(email.Email); var user = await UserManager.FindByEmailAsync(email.Email);
@ -817,7 +852,7 @@ namespace Ombi.Controllers.V1
[HttpPost("resetpassword")] [HttpPost("resetpassword")]
[AllowAnonymous] [AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)] [ApiExplorerSettings(IgnoreApi = true)]
public async Task<OmbiIdentityResult> ResetPassword([FromBody]ResetPasswordToken token) public async Task<OmbiIdentityResult> ResetPassword([FromBody] ResetPasswordToken token)
{ {
var user = await UserManager.FindByEmailAsync(token.Email); var user = await UserManager.FindByEmailAsync(token.Email);

@ -420,5 +420,20 @@ namespace Ombi.Controllers.V2
return _rottenTomatoesApi.GetTvRatings(name, year); return _rottenTomatoesApi.GetTvRatings(name, year);
} }
[HttpGet("stream/movie/{movieDbId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public Task<IEnumerable<StreamingData>> GetMovieStreams(int movieDBId)
{
return _movieEngineV2.GetStreamInformation(movieDBId, HttpContext.RequestAborted);
}
[HttpGet("stream/tv/{tvdbId}/{tvMaze}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
public Task<IEnumerable<StreamingData>> GetTvStreams(int tvdbId, int tvMaze)
{
return _tvEngineV2.GetStreamInformation(tvdbId, tvMaze, HttpContext.RequestAborted);
}
} }
} }

@ -0,0 +1,7 @@
namespace Ombi.Models.Identity
{
public class CountryStreamingPreference
{
public string Code { get; set; }
}
}

@ -275,7 +275,8 @@
"SonarrConfiguration": "Sonarr Configuration", "SonarrConfiguration": "Sonarr Configuration",
"RadarrConfiguration": "Radarr Configuration", "RadarrConfiguration": "Radarr Configuration",
"RequestOnBehalf": "Request on behalf of", "RequestOnBehalf": "Request on behalf of",
"PleaseSelectUser": "Please select a user" "PleaseSelectUser": "Please select a user",
"StreamingOn": "Streaming On"
}, },
"Discovery": { "Discovery": {
"PopularTab": "Popular", "PopularTab": "Popular",
@ -300,6 +301,11 @@
"UserPreferences": { "UserPreferences": {
"Welcome": "Welcome {{username}}!", "Welcome": "Welcome {{username}}!",
"OmbiLanguage": "Language", "OmbiLanguage": "Language",
"DarkMode": "Dark Mode" "DarkMode": "Dark Mode",
"Updated": "Successfully Updated",
"StreamingCountry":"Streaming Country",
"StreamingCountryDescription": "This is the country code that we will display streaming information for. If you are in the US please select US and you will have US related streaming information.",
"LanguageDescription": "This is the language you would like the Ombi interface to be displayed in.",
"MobileQRCode":"Mobile QR Code"
} }
} }
Loading…
Cancel
Save