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

@ -26,5 +26,6 @@ namespace Ombi.Core.Engine.Interfaces
int ResultLimit { get; set; }
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;
namespace Ombi.Core
@ -7,5 +9,6 @@ namespace Ombi.Core
{
Task<SearchFullInfoTvShowViewModel> GetShowInformation(int tvdbid);
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;
}
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(
IEnumerable<MovieSearchResult> movies)
{

@ -19,6 +19,8 @@ using Ombi.Core.Settings;
using Ombi.Store.Repository;
using TraktSharp.Entities;
using Microsoft.EntityFrameworkCore;
using System.Threading;
using Ombi.Api.TheMovieDb;
namespace Ombi.Core.Engine.V2
{
@ -27,15 +29,17 @@ namespace Ombi.Core.Engine.V2
private readonly ITvMazeApi _tvMaze;
private readonly IMapper _mapper;
private readonly ITraktApi _traktApi;
private readonly IMovieDbApi _movieApi;
public TvSearchEngineV2(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper,
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)
{
_tvMaze = tvMaze;
_mapper = mapper;
_traktApi = trakt;
_movieApi = movieApi;
}
@ -106,6 +110,39 @@ namespace Ombi.Core.Engine.V2
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)
{
var retVal = new List<SearchTvShowViewModel>();
@ -141,7 +178,7 @@ namespace Ombi.Core.Engine.V2
{
item.Images.Medium = item.Images.Medium.ToHttpsUrl();
}
if (item.Cast?.Any() ?? false)
{
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 int MovieRequestLimit { get; set; }
public int EpisodeRequestLimit { get; set; }
public string StreamingCountry { get; set; }
public RequestQuotaCountModel EpisodeRequestQuota { get; set; }
public RequestQuotaCountModel MovieRequestQuota { get; set; }
public RequestQuotaCountModel MusicRequestQuota { get; set; }

@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.Identity;
using Newtonsoft.Json;
using Ombi.Helpers;
namespace Ombi.Store.Entities
{
@ -21,6 +21,12 @@ namespace Ombi.Store.Entities
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? EpisodeRequestLimit { get; set; }
public int? MusicRequestLimit { get; set; }
@ -40,14 +46,14 @@ namespace Ombi.Store.Entities
public bool EmailLogin { get; set; }
[NotMapped] public bool IsSystemUser => UserType == UserType.SystemUser;
[JsonIgnore]
public override string PasswordHash
{
get => base.PasswordHash;
set => base.PasswordHash = value;
}
[JsonIgnore]
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"
```
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:
```

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

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

@ -13,7 +13,7 @@ namespace Ombi.Api.TheMovieDb
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>> 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>> Upcoming(string languageCode, int? page = null);
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<List<Keyword>> SearchKeyword(string searchTerm);
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.Core.Settings;
using Ombi.Core.Settings.Models.External;
using Ombi.Helpers;
using Ombi.TheMovieDbApi.Models;
namespace Ombi.Api.TheMovieDb
@ -24,7 +25,7 @@ namespace Ombi.Api.TheMovieDb
}
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 IApi Api { get; }
private AsyncLazy<TheMovieDbSettings> Settings { get; }
@ -107,11 +108,15 @@ namespace Ombi.Api.TheMovieDb
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);
request.AddQueryString("api_key", ApiToken);
request.AddQueryString("query", searchTerm);
if (year.HasValue())
{
request.AddQueryString("first_air_date_year", year);
}
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<SearchResult>>(request);
@ -126,7 +131,7 @@ namespace Ombi.Api.TheMovieDb
return await Api.Request<TvExternals>(request);
}
public async Task<List<MovieSearchResult>> SimilarMovies(int movieId, string langCode)
{
var request = new Request($"movie/{movieId}/similar", BaseUri, HttpMethod.Get);
@ -165,7 +170,7 @@ namespace Ombi.Api.TheMovieDb
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);
}
@ -299,6 +304,32 @@ namespace Ombi.Api.TheMovieDb
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)
{
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)
{
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;
language: string;
userQualityProfiles: IUserQualityProfiles;
streamingCountry: string;
// FOR UI
episodeRequestQuota: IRemainingRequests | null;
@ -35,7 +36,7 @@ export interface IUserQualityProfiles {
sonarrRootPath: number;
sonarrQualityProfile: number;
radarrRootPath: number;
radarrQualityProfile: number;
radarrQualityProfile: number;
}
export interface ICreateWizardUser {
@ -49,6 +50,10 @@ export interface IWizardUserResult {
errors: string[];
}
export interface IStreamingCountries {
code: string;
}
export enum UserType {
LocalUser = 1,
PlexUser = 2,

@ -1,13 +1,27 @@
<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
</span>
<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 *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>
<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>
<div>
<strong>{{'MediaDetails.Status' | translate }}:</strong>
@ -59,48 +73,48 @@
<hr>
<strong>{{'MediaDetails.TheatricalRelease' | translate }}:</strong>
{{movie.releaseDate | date: 'mediumDate'}}
{{movie.releaseDate | date: 'mediumDate'}}
<div *ngIf="movie.digitalReleaseDate">
<strong>{{'MediaDetails.DigitalRelease' | translate }}:</strong>
{{movie.digitalReleaseDate | date: 'mediumDate'}}
</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>
<div *ngIf="movie.digitalReleaseDate">
<strong>{{'MediaDetails.DigitalRelease' | translate }}:</strong>
{{movie.digitalReleaseDate | date: 'mediumDate'}}
</div>
<hr />
<div *ngIf="movie.genres">
<strong>{{'MediaDetails.Genres' | translate }}:</strong>
<div>
<mat-chip-list>
<mat-chip color="accent" selected *ngFor="let genre of movie.genres">
{{genre.name}}
</mat-chip>
</mat-chip-list>
</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?.keywords?.keywordsValue?.length > 0">
<strong>{{'MediaDetails.Keywords' | translate }}:</strong>
<hr />
<div *ngIf="movie.genres">
<strong>{{'MediaDetails.Genres' | translate }}:</strong>
<div>
<mat-chip-list>
<mat-chip color="accent" selected *ngFor="let keyword of movie.keywords.keywordsValue">
{{keyword.name}}
<mat-chip color="accent" selected *ngFor="let genre of movie.genres">
{{genre.name}}
</mat-chip>
</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 { IMovieRatings } from "../../../../interfaces/IRatings";
import { APP_BASE_HREF } from "@angular/common";
import { IStreamingData } from "../../../../interfaces/IStreams";
@Component({
templateUrl: "./movie-information-panel.component.html",
styleUrls: ["../../../media-details.component.scss"],
@ -19,9 +20,12 @@ export class MovieInformationPanelComponent implements OnInit {
@Input() public advancedOptions: boolean;
public ratings: IMovieRatings;
public streams: IStreamingData[];
public ngOnInit() {
this.searchService.getRottenMovieRatings(this.movie.title, +this.movie.releaseDate.toString().substring(0,4))
.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">
<img class="rating-small" src="{{baseUrl}}/images/{{ratings.class === 'rotten' ? 'rotten-rotten.svg' : 'rotten-fresh.svg'}}"> {{ratings.score}}%
</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>
<div *ngIf="tv.status">
<strong>{{'MediaDetails.Status' | translate }}:</strong>

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

@ -230,4 +230,10 @@
.rating-small {
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 { 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";
@Injectable()
@ -83,8 +83,16 @@ export class IdentityService extends ServiceHelpers {
public getNotificationPreferencesForUser(userId: string): Observable<INotificationPreferences[]> {
return this.http.get<INotificationPreferences[]>(`${this.url}notificationpreferences/${userId}`, {headers: this.headers});
}
public updateLanguage(lang: string): Observable<null> {
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 { SearchFilter } from "../my-nav/SearchFilter";
import { IMovieRatings, ITvRatings } from "../interfaces/IRatings";
import { IStreamingData } from "../interfaces/IStreams";
@Injectable()
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}`);
}
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,
],
entryComponents: [
EpisodeRequestComponent
EpisodeRequestComponent,
],
exports: [
TranslateModule,

@ -3,24 +3,50 @@
<hr>
<div class="row justify-content-md-center top-spacing">
<div class="col-md">
<div class="row top-spacing">
<div class="col-4">
<div>
<mat-form-field>
<mat-label [translate]="'UserPreferences.OmbiLanguage'"></mat-label>
<mat-select [(value)]="selectedLang" (selectionChange)="languageSelected();">
<mat-option *ngFor="let lang of availableLanguages" [value]="lang.value">
{{lang.display}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div>
</div>
<small>{{'UserPreferences.LanguageDescription' | translate}}</small>
<br>
<mat-form-field>
<mat-label [translate]="'UserPreferences.OmbiLanguage'"></mat-label>
<mat-select [(value)]="selectedLang" (selectionChange)="languageSelected();">
<mat-option *ngFor="let lang of availableLanguages" [value]="lang.value">
{{lang.display}}
</mat-option>
</mat-select>
</mat-form-field>
</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>
</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>

@ -3,7 +3,7 @@ import { AuthService } from "../../../auth/auth.service";
import { TranslateService } from "@ngx-translate/core";
import { AvailableLanguages, ILanguage } from "./user-preference.constants";
import { StorageService } from "../../../shared/storage/storage-service";
import { IdentityService, SettingsService } from "../../../services";
import { IdentityService, NotificationService, SettingsService } from "../../../services";
import { IUser } from "../../../interfaces";
@Component({
@ -17,12 +17,14 @@ export class UserPreferenceComponent implements OnInit {
public availableLanguages = AvailableLanguages;
public qrCode: string;
public qrCodeEnabled: boolean;
public countries: string[];
public selectedCountry: string;
private user: IUser;
constructor(private authService: AuthService,
private readonly translate: TranslateService,
private storage: StorageService,
private readonly notification: NotificationService,
private readonly identityService: IdentityService,
private readonly settingsService: SettingsService) { }
@ -33,6 +35,8 @@ export class UserPreferenceComponent implements OnInit {
}
const customization = await this.settingsService.getCustomization().toPromise();
this.selectedLang = this.translate.currentLang;
const accessToken = await this.identityService.getAccessToken().toPromise();
this.qrCode = `${customization.applicationUrl}|${accessToken}`;
@ -43,14 +47,18 @@ export class UserPreferenceComponent implements OnInit {
}
this.user = await this.identityService.getUser().toPromise();
if (this.user.language) {
this.selectedLang = this.user.language;
}
this.selectedCountry = this.user.streamingCountry;
this.identityService.getSupportedStreamingCountries().subscribe(x => this.countries = x);
}
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);
}
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">
</mat-form-field>
</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>
<mat-form-field>
<input matInput placeholder="Password" type="password" [(ngModel)]="user.password" required>

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

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Api.Plex;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Attributes;
using Ombi.Core.Authentication;
using Ombi.Core.Engine;
@ -159,7 +160,8 @@ namespace Ombi.Controllers.V1
UserName = plexUser.user.username,
UserType = UserType.PlexUser,
Email = plexUser.user.email,
ProviderUserId = plexUser.user.id
ProviderUserId = plexUser.user.id,
StreamingCountry = "US" // Default
};
await _userManagementSettings.SaveSettingsAsync(new UserManagementSettings
@ -173,7 +175,8 @@ namespace Ombi.Controllers.V1
var userToCreate = new OmbiUser
{
UserName = user.Username,
UserType = UserType.LocalUser
UserType = UserType.LocalUser,
StreamingCountry = "US"
};
return await SaveWizardUser(user, userToCreate);
@ -329,6 +332,32 @@ namespace Ombi.Controllers.V1
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>
/// Gets the user by the user id.
/// </summary>
@ -358,7 +387,8 @@ namespace Ombi.Controllers.V1
EpisodeRequestLimit = user.EpisodeRequestLimit ?? 0,
MovieRequestLimit = user.MovieRequestLimit ?? 0,
MusicRequestLimit = user.MusicRequestLimit ?? 0,
Language = user.Language
Language = user.Language,
StreamingCountry = user.StreamingCountry
};
foreach (var role in userRoles)
@ -437,6 +467,7 @@ namespace Ombi.Controllers.V1
EpisodeRequestLimit = user.EpisodeRequestLimit,
MusicRequestLimit = user.MusicRequestLimit,
UserAccessToken = Guid.NewGuid().ToString("N"),
StreamingCountry = user.StreamingCountry.HasValue() ? user.StreamingCountry : "US"
};
var userResult = await UserManager.CreateAsync(ombiUser, user.Password);
@ -594,6 +625,10 @@ namespace Ombi.Controllers.V1
user.MovieRequestLimit = ui.MovieRequestLimit;
user.EpisodeRequestLimit = ui.EpisodeRequestLimit;
user.MusicRequestLimit = ui.MusicRequestLimit;
if (ui.StreamingCountry.HasValue())
{
user.StreamingCountry = ui.StreamingCountry;
}
var updateResult = await UserManager.UpdateAsync(user);
if (!updateResult.Succeeded)
{
@ -739,7 +774,7 @@ namespace Ombi.Controllers.V1
[HttpPost("reset")]
[AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)]
public async Task<OmbiIdentityResult> SubmitResetPassword([FromBody]SubmitPasswordReset email)
public async Task<OmbiIdentityResult> SubmitResetPassword([FromBody] SubmitPasswordReset email)
{
// Check if account exists
var user = await UserManager.FindByEmailAsync(email.Email);
@ -817,7 +852,7 @@ namespace Ombi.Controllers.V1
[HttpPost("resetpassword")]
[AllowAnonymous]
[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);

@ -420,5 +420,20 @@ namespace Ombi.Controllers.V2
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",
"RadarrConfiguration": "Radarr Configuration",
"RequestOnBehalf": "Request on behalf of",
"PleaseSelectUser": "Please select a user"
"PleaseSelectUser": "Please select a user",
"StreamingOn": "Streaming On"
},
"Discovery": {
"PopularTab": "Popular",
@ -300,6 +301,11 @@
"UserPreferences": {
"Welcome": "Welcome {{username}}!",
"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