Merge pull request #3200 from tidusjar/develop

Develop
pull/3201/head v3.0.4817
Jamie 5 years ago committed by GitHub
commit cd8a89800a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,34 +1,131 @@
# Changelog
## (unreleased)
### **New Features**
- Added better support for Jellyfin, we will now auto detect if it's a jellyfin server after pressing the discover button. [tidusjar]
- Update aspnetcore.yml. [Jamie]
- Update aspnetcore.yml. [Jamie]
- Update aspnetcore.yml. [Jamie]
- Update aspnetcore.yml. [Jamie]
- Update aspnetcore.yml. [Jamie]
- Update and rename .github/workflows to .github/.github/workflows/test.workflow. [Jamie]
- Update aspnetcore.yml. [Jamie]
- Update aspnetcore.yml. [Jamie]
- Update aspnetcore.yml. [Jamie]
- Added a bit more logging into the recently added scan. [tidusjar]
- Update emby.component.html. [sorano]
- Update EmbyHelper.cs. [sorano]
- Update CHANGELOG.md. [Jamie]
### **Fixes**
- Fixed #3078. [tidusjar]
- Fixes issue #3195 The new string extension method ToHttpsUrl ensures that URLs starting with "https" are no longer turned into "httpss" The commit also replaces all occurances of the error prone .Replace("http", "https") in the whole solution. [msdeibel]
- Create test.workflow. [Jamie]
- Delete test.workflow. [Jamie]
- New translations en.json (Norwegian) [Jamie]
- New translations en.json (Norwegian) [Jamie]
- Fix for #3183. [tidusjar]
- Fixed an issue where running the recently added sync via the UI was running the full sync. [tidusjar]
- Fixed #3143. [Jamie Rees]
- New translations en.json (French) [Jamie]
- New translations en.json (French) [Jamie]
- New translations en.json (Russian) [Jamie]
- New translations en.json (Russian) [Jamie]
- New translations en.json (Russian) [Jamie]
- New translations en.json (French) [Jamie]
- New translations en.json (Russian) [Jamie]
- New translations en.json (Russian) [Jamie]
- New translations en.json (Russian) [Jamie]
- New translations en.json (Russian) [Jamie]
- New translations en.json (Russian) [Jamie]
- New translations en.json (Polish) [Jamie]
- Fixed the issue when we are logging errors in the logs incorrectly. [Jamie]
- Removed the lanuage profile from the Lidarr integration. [tidusjar]
- Try and clear up the issue #2998. [tidusjar]
- Fixed an issue where shows that have no aired, episodes are not marked as monitored in Sonarr. [tidusjar]
- Fixed an error when finishing the content sync. [tidusjar]
- Fixed issue where using the API to request a movie/tv show would throw an exception when only using the API Key #3091. [tidusjar]
- Put "Ombi" back as the product name for Plex oAuth. [tidusjar]
## v3.0.4680 (2019-07-17)
### **New Features**
- Update CHANGELOG.md. [Jamie]
### **Fixes**
- Fixed the database lock issues - [TidusJar]
- Fixed the issue with [Plex OAuth](https://forums.plex.tv/t/plex-oauth-not-working-with-tautulli-ombi-etc/433945) - [TidusJar]
- Fix Plex's (intentional) mistake #3073. [Jamie Rees]
- #2994 Fixed the startup issue. [tidusjar]
- #2994 - enable multithreading in the sql config. [Jamie Rees]
## v3.0.4659 (2019-07-02)
### **New Features**
- Added further logging into the API's (debug logging) [tidusjar]
- Update appsettings.json. [Jamie]
- Added transactions around all of the CUD operations. [Jamie Rees]
- Update CHANGELOG.md. [Jamie]
- Update stale.yml. [Jamie]
- Update README.md. [Dyson Parkes]
## v3.0.4654 (2019-07-02)
- Added stalebot. [tidusjar]
### **New Features**
- Added some validation around the new crons. [Jamie Rees]
- Added further logging into the API's (debug logging) [tidusjar]
- Added some defensive coding around when we create an artist for #2915. [tidusjar]
- Added transactions around all of the CUD operations. [Jamie Rees]
- Update README.md. [Jamie]
- Added some validation around the new crons. [Jamie Rees]
- Update README.md. [Jamie]
- Added some defensive coding around when we create an artist for #2915. [tidusjar]
- Update JobSetup.cs. [Jamie]
@ -40,10 +137,28 @@
- Update dependancies. [TidusJar]
- Update stale.yml. [Jamie]
- Update README.md. [Dyson Parkes]
- Update README.md. [Jamie]
- Update README.md. [Jamie]
- Update CHANGELOG.md. [Jamie]
- Added stalebot. [tidusjar]
### **Fixes**
- Add back in the login time. [tidusjar]
- New translations en.json (Spanish) [Jamie]
- New translations en.json (Spanish) [Jamie]
- New translations en.json (Spanish) [Jamie]
- New translations en.json (Spanish) [Jamie]
- New translations en.json (Swedish) [Jamie]
@ -322,6 +437,12 @@
- Converted the Plex Jobs to use Quartz. [Jamie]
- Remove the need for the schedules.db #2994. [tidusjar]
- Create FUNDING.yml. [Jamie]
- Logging and slight change to the string matching now not dependant on Thread Culture #2866. [tidusjar]
## v3.0.4256 (2019-02-19)

@ -46,6 +46,17 @@ namespace Ombi.Api.Emby
return obj;
}
public async Task<PublicInfo> GetPublicInformation(string baseUrl)
{
var request = new Request("emby/System/Info/public", baseUrl, HttpMethod.Get);
AddHeaders(request, string.Empty);
var obj = await Api.Request<PublicInfo>(request);
return obj;
}
public async Task<EmbyUser> LogIn(string username, string password, string apiKey, string baseUri)
{
var request = new Request("emby/users/authenticatebyname", baseUri, HttpMethod.Post);
@ -124,6 +135,7 @@ namespace Ombi.Api.Emby
{
return await GetInformation<MovieInformation>(mediaId, apiKey, userId, baseUrl);
}
public async Task<EpisodeInformation> GetEpisodeInformation(string mediaId, string apiKey, string userId, string baseUrl)
{
return await GetInformation<EpisodeInformation>(mediaId, apiKey, userId, baseUrl);

@ -29,5 +29,6 @@ namespace Ombi.Api.Emby
Task<SeriesInformation> GetSeriesInformation(string mediaId, string apiKey, string userId, string baseUrl);
Task<MovieInformation> GetMovieInformation(string mediaId, string apiKey, string userId, string baseUrl);
Task<EpisodeInformation> GetEpisodeInformation(string mediaId, string apiKey, string userId, string baseUrl);
Task<PublicInfo> GetPublicInformation(string baseUrl);
}
}

@ -0,0 +1,19 @@
namespace Ombi.Api.Emby.Models
{
public class PublicInfo
{
public string LocalAddress { get; set; }
public string ServerName { get; set; }
public string Version { get; set; }
/// <summary>
/// Only populated for Jellyfin
/// </summary>
public string ProductName { get; set; }
public bool IsJellyfin => !string.IsNullOrEmpty(ProductName) && ProductName.Contains("Jellyfin");
public string OperatingSystem { get; set; }
public string Id { get; set; }
}
}

@ -20,7 +20,6 @@ namespace Ombi.Api.Lidarr
Task<AlbumResponse> MontiorAlbum(int albumId, string apiKey, string baseUrl);
Task<List<AlbumResponse>> GetAllAlbumsByArtistId(int artistId, string apiKey, string baseUrl);
Task<List<MetadataProfile>> GetMetadataProfile(string apiKey, string baseUrl);
Task<List<LanguageProfiles>> GetLanguageProfile(string apiKey, string baseUrl);
Task<LidarrStatus> Status(string apiKey, string baseUrl);
Task<CommandResult> AlbumSearch(int[] albumIds, string apiKey, string baseUrl);
Task<AlbumByForeignId> AlbumInformation(string albumId, string apiKey, string baseUrl);

@ -158,13 +158,6 @@ namespace Ombi.Api.Lidarr
return Api.Request<List<AlbumResponse>>(request);
}
public Task<List<LanguageProfiles>> GetLanguageProfile(string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/languageprofile", baseUrl, HttpMethod.Get);
AddHeaders(request, apiKey);
return Api.Request<List<LanguageProfiles>>(request);
}
public Task<List<MetadataProfile>> GetMetadataProfile(string apiKey, string baseUrl)
{
var request = new Request($"{ApiVersion}/metadataprofile", baseUrl, HttpMethod.Get);

@ -17,7 +17,6 @@ namespace Ombi.Api.Lidarr.Models
public Image[] images { get; set; }
public string remotePoster { get; set; }
public int qualityProfileId { get; set; }
public int languageProfileId { get; set; }
public int metadataProfileId { get; set; }
public bool albumFolder { get; set; }
public bool monitored { get; set; }

@ -1,8 +0,0 @@
namespace Ombi.Api.Lidarr.Models
{
public class LanguageProfiles
{
public string name { get; set; }
public int id { get; set; }
}
}

@ -223,11 +223,11 @@ namespace Ombi.Api.Plex
await AddHeaders(request);
request.AddQueryString("code", code);
request.AddQueryString("context[device][product]", "Plex Web"); // Note this is to work around Plex's shit https://forums.plex.tv/t/plex-oauth-not-working-with-tautulli-ombi-etc/433945
request.AddQueryString("context[device][product]", ApplicationName);
request.AddQueryString("context[device][environment]", "bundled");
request.AddQueryString("context[device][layout]", "desktop");
request.AddQueryString("context[device][platform]", "Web");
request.AddQueryString("context[device][device]", "Plex");
request.AddQueryString("context[device][device]", "Ombi");
var s = await GetSettings();
await CheckInstallId(s);
@ -293,9 +293,9 @@ namespace Ombi.Api.Plex
var s = await GetSettings();
await CheckInstallId(s);
request.AddHeader("X-Plex-Client-Identifier", s.InstallId.ToString("N"));
request.AddHeader("X-Plex-Product", "Plex Web");
request.AddHeader("X-Plex-Product", ApplicationName);
request.AddHeader("X-Plex-Version", "3");
request.AddHeader("X-Plex-Device", "Plex");
request.AddHeader("X-Plex-Device", "Ombi");
request.AddHeader("X-Plex-Platform", "Web");
request.AddContentHeader("Content-Type", request.ContentType == ContentType.Json ? "application/json" : "application/xml");
request.AddHeader("Accept", "application/json");

@ -17,6 +17,7 @@ using Ombi.Api.Lidarr.Models;
using Ombi.Core.Authentication;
using Ombi.Core.Settings;
using Ombi.Helpers;
using Ombi.Core.Helpers;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.External;
using Ombi.Store.Entities;
@ -166,7 +167,7 @@ namespace Ombi.Core.Engine
Rating = a.ratings?.value ?? 0m,
ReleaseDate = a.releaseDate,
Title = a.title,
Disk = a.images?.FirstOrDefault(x => x.coverType.Equals("disc"))?.url?.Replace("http", "https"),
Disk = a.images?.FirstOrDefault(x => x.coverType.Equals("disc"))?.url?.ToHttpsUrl(),
Genres = a.genres,
AlbumType = a.albumType,
ArtistName = a.artist.artistName,
@ -187,7 +188,7 @@ namespace Ombi.Core.Engine
//vm.ArtistName = a.artist?.artistName;
}
vm.Cover = a.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url?.Replace("http", "https");
vm.Cover = a.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url?.ToHttpsUrl();
await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum);
@ -205,7 +206,7 @@ namespace Ombi.Core.Engine
Rating = a.ratings?.value ?? 0m,
ReleaseDate = a.releaseDate,
Title = a.title,
Disk = a.images?.FirstOrDefault(x => x.coverType.Equals("disc"))?.url?.Replace("http", "https"),
Disk = a.images?.FirstOrDefault(x => x.coverType.Equals("disc"))?.url?.ToHttpsUrl(),
Genres = a.genres
};
if (a.artistId > 0)
@ -223,7 +224,7 @@ namespace Ombi.Core.Engine
vm.ArtistName = a.artist?.artistName;
}
vm.Cover = a.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url?.Replace("http", "https");
vm.Cover = a.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url?.ToHttpsUrl();
if (vm.Cover.IsNullOrEmpty())
{
vm.Cover = a.remoteCover;

@ -1,4 +1,5 @@
using System.Security.Principal;
using System;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Ombi.Core.Authentication;
@ -23,8 +24,8 @@ namespace Ombi.Core.Rule.Rules.Request
public async Task<RuleResult> Execute(BaseRequest obj)
{
var user = await _manager.Users.FirstOrDefaultAsync(x => x.UserName == User.Identity.Name);
if (await _manager.IsInRoleAsync(user, OmbiRoles.Admin))
var user = await _manager.Users.FirstOrDefaultAsync(x => x.UserName.Equals(User.Identity.Name, StringComparison.InvariantCultureIgnoreCase));
if (await _manager.IsInRoleAsync(user, OmbiRoles.Admin) || user.IsSystemUser)
{
obj.Approved = true;
return Success();

@ -1,3 +1,4 @@
using System;
using Ombi.Store.Entities;
using System.IO;
using System.Security.Claims;
@ -25,8 +26,8 @@ namespace Ombi.Core.Rule.Rules.Request
public async Task<RuleResult> Execute(BaseRequest obj)
{
var user = await _manager.Users.FirstOrDefaultAsync(x => x.UserName == User.Identity.Name);
if (await _manager.IsInRoleAsync(user, OmbiRoles.Admin))
var user = await _manager.Users.FirstOrDefaultAsync(x => x.UserName.Equals(User.Identity.Name, StringComparison.InvariantCultureIgnoreCase));
if (await _manager.IsInRoleAsync(user, OmbiRoles.Admin) || user.IsSystemUser)
return Success();
if (obj.RequestType == RequestType.Movie)

@ -68,11 +68,11 @@ namespace Ombi.Core.Rule.Rules.Search
var server = s.Servers.FirstOrDefault(x => x.ServerHostname != null);
if ((server?.ServerHostname ?? string.Empty).HasValue())
{
obj.EmbyUrl = $"{server.ServerHostname}#!/itemdetails.html?id={item.EmbyId}";
obj.EmbyUrl = EmbyHelper.GetEmbyMediaUrl(item.EmbyId, server?.ServerHostname, s.IsJellyfin);
}
else
{
obj.EmbyUrl = $"https://app.emby.media/#!/itemdetails.html?id={item.EmbyId}";
obj.EmbyUrl = EmbyHelper.GetEmbyMediaUrl(item.EmbyId, null, s.IsJellyfin);
}
if (obj.Type == RequestType.TvShow)

@ -50,7 +50,7 @@ namespace Ombi.Core.Rule.Rules.Specific
}
}
if (await UserManager.IsInRoleAsync(requestedUser, OmbiRoles.Admin))
if (await UserManager.IsInRoleAsync(requestedUser, OmbiRoles.Admin) || requestedUser.IsSystemUser)
{
sendNotification = false; // Don't bother sending a notification if the user is an admin
}

@ -110,7 +110,6 @@ namespace Ombi.Core.Senders
artistName = model.ArtistName,
cleanName = model.ArtistName.ToLowerInvariant().RemoveSpaces(),
images = new Image[] { },
languageProfileId = settings.LanguageProfileId,
links = new Link[] {},
metadataProfileId = settings.MetadataProfileId,
qualityProfileId = qualityToUse,

@ -346,6 +346,11 @@ namespace Ombi.Core.Senders
existingSeason.monitored = true;
seriesChanges = true;
}
// Now update the episodes that need updating
foreach (var epToUpdate in episodesToUpdate.Where(x => x.seasonNumber == season.SeasonNumber))
{
await SonarrApi.UpdateEpisode(epToUpdate, s.ApiKey, s.FullUri);
}
}
else
{

@ -0,0 +1,44 @@
using NUnit.Framework;
namespace Ombi.Helpers.Tests
{
[TestFixture]
public class StringHelperTests
{
[Test]
public void ToHttpsUrl_ShouldReturnsHttpsUrl_HttpUrl()
{
var sourceUrl = "http://www.test.url";
var expectedUrl = "https://www.test.url";
Assert.AreEqual(expectedUrl, sourceUrl.ToHttpsUrl(), "Should return the source URL as https");
}
[Test]
public void ToHttpsUrl_ShouldReturnsUnchangedUrl_HttpsUrl()
{
var sourceUrl = "https://www.test.url";
var expectedUrl = "https://www.test.url";
Assert.AreEqual(expectedUrl, sourceUrl.ToHttpsUrl(), "Should return the unchanged https URL");
}
[Test]
public void ToHttpsUrl_ShouldReturnsUnchangedUrl_NonHttpUrl()
{
var sourceUrl = "ftp://www.test.url";
var expectedUrl = "ftp://www.test.url";
Assert.AreEqual(expectedUrl, sourceUrl.ToHttpsUrl(), "Should return the unchanged non-http URL");
}
[Test]
public void ToHttpsUrl_ShouldReturnsUnchangedUrl_InvalidUrl()
{
var sourceUrl = "http:/www.test.url";
var expectedUrl = "http:/www.test.url";
Assert.AreEqual(expectedUrl, sourceUrl.ToHttpsUrl(), "Should return the unchanged invalid URL");
}
}
}

@ -1,21 +1,21 @@
using System;
using System.Globalization;
using System.Collections.Generic;
using System.Text;
namespace Ombi.Helpers
namespace Ombi.Helpers
{
public class EmbyHelper
{
public static string GetEmbyMediaUrl(string mediaId, string customerServerUrl = null)
public static string GetEmbyMediaUrl(string mediaId, string customerServerUrl = null, bool isJellyfin = false)
{
string path = "item/item";
if (isJellyfin)
{
path = "itemdetails";
}
if (customerServerUrl.HasValue())
{
return $"{customerServerUrl}#!/itemdetails.html?id={mediaId}";
return $"{customerServerUrl}#!/{path}.html?id={mediaId}";
}
else
{
return $"https://app.emby.media/#!/itemdetails.html?id={mediaId}";
return $"https://app.emby.media/#!/{path}.html?id={mediaId}";
}
}
}

@ -128,5 +128,10 @@ namespace Ombi.Helpers
{
return string.Concat(str.Where(c => !chars.Contains(c)));
}
public static string ToHttpsUrl(this string currentUrl)
{
return currentUrl.Replace("http://", "https://");
}
}
}

@ -29,7 +29,7 @@ namespace Ombi.Mapping.Profiles
.ForMember(dest => dest.Runtime, opts => opts.MapFrom(src => src.show.runtime.ToString()))
.ForMember(dest => dest.SeriesId, opts => opts.MapFrom(src => src.show.id))
.ForMember(dest => dest.Title, opts => opts.MapFrom(src => src.show.name))
.ForMember(dest => dest.Banner, opts => opts.MapFrom(src => !string.IsNullOrEmpty(src.show.image.medium) ? src.show.image.medium.Replace("http", "https") : string.Empty))
.ForMember(dest => dest.Banner, opts => opts.MapFrom(src => !string.IsNullOrEmpty(src.show.image.medium) ? src.show.image.medium.ToHttpsUrl() : string.Empty))
.ForMember(dest => dest.Status, opts => opts.MapFrom(src => src.show.status));
CreateMap<TvMazeShow, SearchTvShowViewModel>()
@ -46,7 +46,7 @@ namespace Ombi.Mapping.Profiles
.ForMember(dest => dest.Title, opts => opts.MapFrom(src => src.name))
.ForMember(dest => dest.Banner,
opts => opts.MapFrom(src => !string.IsNullOrEmpty(src.image.medium)
? src.image.medium.Replace("http", "https")
? src.image.medium.ToHttpsUrl()
: string.Empty))
.ForMember(dest => dest.Status, opts => opts.MapFrom(src => src.status));

@ -12,6 +12,7 @@ using Ombi.Notifications.Models;
using Ombi.Settings.Settings.Models;
using Ombi.Settings.Settings.Models.Notifications;
using Ombi.Store.Entities;
using Ombi.Store.Entities.Requests;
using Ombi.Store.Repository;
using Ombi.Store.Repository.Requests;
@ -21,13 +22,14 @@ namespace Ombi.Notifications.Agents
{
public MobileNotification(IOneSignalApi api, ISettingsService<MobileNotificationSettings> sn, ILogger<MobileNotification> log, INotificationTemplatesRepository r,
IMovieRequestRepository m, ITvRequestRepository t, ISettingsService<CustomizationSettings> s, IRepository<NotificationUserId> notification,
UserManager<OmbiUser> um, IRepository<RequestSubscription> sub, IMusicRequestRepository music,
UserManager<OmbiUser> um, IRepository<RequestSubscription> sub, IMusicRequestRepository music, IRepository<Issues> issueRepository,
IRepository<UserNotificationPreferences> userPref) : base(sn, r, m, t, s, log, sub, music, userPref)
{
_api = api;
_logger = log;
_notifications = notification;
_userManager = um;
_issueRepository = issueRepository;
}
public override string NotificationName => "MobileNotification";
@ -36,6 +38,7 @@ namespace Ombi.Notifications.Agents
private readonly ILogger<MobileNotification> _logger;
private readonly IRepository<NotificationUserId> _notifications;
private readonly UserManager<OmbiUser> _userManager;
private readonly IRepository<Issues> _issueRepository;
protected override bool ValidateConfiguration(MobileNotificationSettings settings)
{
@ -95,8 +98,9 @@ namespace Ombi.Notifications.Agents
var isAdmin = bool.Parse(isAdminString);
if (isAdmin)
{
model.Substitutes.TryGetValue("IssueId", out var issueId);
// Send to user
var playerIds = GetUsers(model, NotificationType.IssueComment);
var playerIds = await GetUsersForIssue(model, int.Parse(issueId), NotificationType.IssueComment);
await Send(playerIds, notification, settings, model);
}
else
@ -250,6 +254,7 @@ namespace Ombi.Notifications.Agents
}
return playerIds;
}
private List<string> GetUsers(NotificationOptions model, NotificationType type)
{
var notificationIds = new List<NotificationUserId>();
@ -261,14 +266,36 @@ namespace Ombi.Notifications.Agents
}
if (model.UserId.HasValue() && (!notificationIds?.Any() ?? true))
{
var user= _userManager.Users.Include(x => x.NotificationUserIds).FirstOrDefault(x => x.Id == model.UserId);
var user = _userManager.Users.Include(x => x.NotificationUserIds).FirstOrDefault(x => x.Id == model.UserId);
notificationIds = user.NotificationUserIds;
}
if (!notificationIds?.Any() ?? true)
{
_logger.LogInformation(
$"there are no admins to send a notification for {type}, for agent {NotificationAgent.Mobile}");
$"there are no users to send a notification for {type}, for agent {NotificationAgent.Mobile}");
return null;
}
var playerIds = notificationIds.Select(x => x.PlayerId).ToList();
return playerIds;
}
private async Task<List<string>> GetUsersForIssue(NotificationOptions model, int issueId, NotificationType type)
{
var notificationIds = new List<NotificationUserId>();
var issue = await _issueRepository.GetAll()
.FirstOrDefaultAsync(x => x.Id == issueId);
// Get the user that raised the issue to send the notification to
var userRaised = await _userManager.Users.Include(x => x.NotificationUserIds).FirstOrDefaultAsync(x => x.Id == issue.UserReportedId);
notificationIds = userRaised.NotificationUserIds;
if (!notificationIds?.Any() ?? true)
{
_logger.LogInformation(
$"there are no users to send a notification for {type}, for agent {NotificationAgent.Mobile}");
return null;
}
var playerIds = notificationIds.Select(x => x.PlayerId).ToList();

@ -45,11 +45,11 @@ namespace Ombi.Schedule.Jobs.Emby
{
try
{
await StartServerCache(server);
await StartServerCache(server, embySettings);
}
catch (Exception e)
{
_logger.LogError(e, "Exception when caching Emby for server {0}", server.Name);
_logger.LogError(e, "Exception when caching {1} for server {0}", server.Name, embySettings.IsJellyfin ? "Jellyfin" : "Emby");
}
}
@ -60,7 +60,7 @@ namespace Ombi.Schedule.Jobs.Emby
}
private async Task StartServerCache(EmbyServers server)
private async Task StartServerCache(EmbyServers server, EmbySettings settings)
{
if (!ValidateSettings(server))
return;
@ -135,7 +135,7 @@ namespace Ombi.Schedule.Jobs.Emby
Title = tvShow.Name,
Type = EmbyMediaType.Series,
EmbyId = tvShow.Id,
Url = EmbyHelper.GetEmbyMediaUrl(tvShow.Id, server.ServerHostname),
Url = EmbyHelper.GetEmbyMediaUrl(tvShow.Id, server.ServerHostname, settings.IsJellyfin),
AddedAt = DateTime.UtcNow
});
}

@ -682,7 +682,7 @@ namespace Ombi.Schedule.Jobs.Ombi
var banner = info.image?.original;
if (!string.IsNullOrEmpty(banner))
{
banner = banner.Replace("http", "https"); // Always use the Https banners
banner = banner.ToHttpsUrl(); // Always use the Https banners
}
var tvInfo = await _movieApi.GetTVInfo(t.TheMovieDbId);
@ -804,7 +804,7 @@ namespace Ombi.Schedule.Jobs.Ombi
var banner = info.image?.original;
if (!string.IsNullOrEmpty(banner))
{
banner = banner.Replace("http", "https"); // Always use the Https banners
banner = banner.ToHttpsUrl(); // Always use the Https banners
}
var tvInfo = await _movieApi.GetTVInfo(t.TheMovieDbId);

@ -21,6 +21,7 @@ using Ombi.Store.Entities;
using Ombi.Store.Repository;
using Ombi.Updater;
using Quartz;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Readers.Tar;

@ -72,7 +72,6 @@ namespace Ombi.Schedule.Jobs.Ombi
{
_log.LogInformation("Starting the Metadata refresh from RecentlyAddedSync");
var plexSettings = await _plexSettings.GetSettingsAsync();
var embySettings = await _embySettings.GetSettingsAsync();
try
{
if (plexSettings.Enable)
@ -85,19 +84,6 @@ namespace Ombi.Schedule.Jobs.Ombi
_log.LogError(e, "Exception when refreshing the Plex Metadata");
throw;
}
finally
{
if (plexSettings.Enable)
{
await OmbiQuartz.TriggerJob(nameof(IPlexAvailabilityChecker), "Plex");
}
if (embySettings.Enable)
{
await OmbiQuartz.TriggerJob(nameof(IEmbyAvaliabilityChecker), "Emby");
}
}
}
private async Task StartPlexWithKnownContent(IEnumerable<int> contentids)

@ -83,7 +83,7 @@ namespace Ombi.Schedule.Jobs.Plex
return;
}
var processedContent = new ProcessedContent();
Logger.LogInformation("Starting Plex Content Cacher");
Logger.LogInformation($"Starting Plex Content Cacher {(recentlyAddedSearch ? "Recently Added Scan" : "")}");
try
{
if (recentlyAddedSearch)
@ -109,17 +109,12 @@ namespace Ombi.Schedule.Jobs.Plex
if ((processedContent?.HasProcessedContent ?? false) && recentlyAddedSearch)
{
Logger.LogInformation("Starting Metadata refresh");
// Just check what we send it
await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System");
}
if ((processedContent?.HasProcessedContent ?? false) && recentlyAddedSearch)
{
await OmbiQuartz.TriggerJob(nameof(IPlexAvailabilityChecker), "Plex");
}
Logger.LogInformation("Finished Plex Content Cacher, with processed content: {0}, episodes: {0}", processedContent.Content.Count(), processedContent.Episodes.Count());
Logger.LogInformation("Finished Plex Content Cacher, with processed content: {0}, episodes: {1}. Recently Added Scan: {2}", processedContent?.Content?.Count() ?? 0, processedContent?.Episodes?.Count() ?? 0, recentlyAddedSearch);
}
private async Task<ProcessedContent> StartTheCache(PlexSettings plexSettings, bool recentlyAddedSearch)

@ -56,7 +56,9 @@ namespace Ombi.Schedule.Jobs.Radarr
var movieIds = new List<RadarrCache>();
foreach (var m in movies)
{
if (m.tmdbId > 0 && m.monitored)
if(m.monitored)
{
if (m.tmdbId > 0)
{
movieIds.Add(new RadarrCache
{
@ -69,6 +71,7 @@ namespace Ombi.Schedule.Jobs.Radarr
Logger.LogError("TMDBId is not > 0 for movie {0}", m.title);
}
}
}
using (var tran = await _ctx.Database.BeginTransactionAsync())
{

@ -18,7 +18,7 @@
<PackageReference Include="Hangfire.SQLite" Version="1.4.2" />
<PackageReference Include="Serilog" Version="2.7.1" />
<PackageReference Include="Quartz" Version="3.0.7" />
<PackageReference Include="SharpCompress" Version="0.18.2" />
<PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="System.Diagnostics.Process" Version="4.3.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.6.13" />
<PackageReference Include="Markdig" Version="0.14.8" />

@ -6,6 +6,7 @@ namespace Ombi.Core.Settings.Models.External
public sealed class EmbySettings : Ombi.Settings.Settings.Models.Settings
{
public bool Enable { get; set; }
public bool IsJellyfin { get; set; }
public List<EmbyServers> Servers { get; set; } = new List<EmbyServers>();
}

@ -9,7 +9,6 @@ namespace Ombi.Settings.Settings.Models.External
public string DefaultQualityProfile { get; set; }
public string DefaultRootPath { get; set; }
public bool AlbumFolder { get; set; }
public int LanguageProfileId { get; set; }
public int MetadataProfileId { get; set; }
public bool AddOnly { get; set; }
}

@ -101,7 +101,6 @@ namespace Ombi.Store.Context
UserName = "Api",
UserType = UserType.SystemUser,
NormalizedUserName = "API",
});
SaveChanges();
tran.Commit();

@ -33,6 +33,7 @@ export interface IUpdateSettings extends ISettings {
export interface IEmbySettings extends ISettings {
enable: boolean;
isJellyfin: boolean;
servers: IEmbyServer[];
}
@ -44,6 +45,11 @@ export interface IEmbyServer extends IExternalSettings {
serverHostname: string;
}
export interface IPublicInfo {
serverName: string;
isJellyfin: boolean;
}
export interface IPlexSettings extends ISettings {
enable: boolean;
servers: IPlexServer[];

@ -22,6 +22,14 @@ import { fadeInOutAnimation } from "../animations/fadeinout";
})
export class LoginComponent implements OnDestroy, OnInit {
public get appName(): string {
if (this.customizationSettings.applicationName) {
return this.customizationSettings.applicationName;
} else {
return "Ombi";
}
}
public form: FormGroup;
public customizationSettings: ICustomizationSettings;
public authenticationSettings: IAuthenticationSettings;
@ -32,20 +40,14 @@ export class LoginComponent implements OnDestroy, OnInit {
public loginWithOmbi: boolean;
public pinTimer: any;
public get appName(): string {
if (this.customizationSettings.applicationName) {
return this.customizationSettings.applicationName;
} else {
return "Ombi";
}
}
private timer: any;
private clientId: string;
private errorBody: string;
private errorValidation: string;
private oAuthWindow: Window|null;
constructor(private authService: AuthService, private router: Router, private notify: NotificationService, private status: StatusService,
private fb: FormBuilder, private settingsService: SettingsService, private images: ImageService, private sanitizer: DomSanitizer,
private route: ActivatedRoute, private location: PlatformLocation, private translate: TranslateService, private plexTv: PlexTvService) {
@ -127,7 +129,7 @@ export class LoginComponent implements OnDestroy, OnInit {
}
public oauth() {
const oAuthWindow = window.open(window.location.toString(), "_blank", `toolbar=0,
this.oAuthWindow = window.open(window.location.toString(), "_blank", `toolbar=0,
location=0,
status=0,
menubar=0,
@ -138,7 +140,7 @@ export class LoginComponent implements OnDestroy, OnInit {
this.plexTv.GetPin(this.clientId, this.appName).subscribe((pin: any) => {
this.authService.login({ usePlexOAuth: true, password: "", rememberMe: true, username: "", plexTvPin: pin }).subscribe(x => {
oAuthWindow!.location.replace(x.url);
this.oAuthWindow!.location.replace(x.url);
this.pinTimer = setInterval(() => {
this.notify.info("Authenticating", "Loading... Please Wait");
@ -155,6 +157,9 @@ export class LoginComponent implements OnDestroy, OnInit {
if (this.authService.loggedIn()) {
this.ngOnDestroy();
if(this.oAuthWindow) {
this.oAuthWindow.close();
}
this.router.navigate(["search"]);
return;
}

@ -5,7 +5,7 @@ import { Observable } from "rxjs";
import { ServiceHelpers } from "../service.helpers";
import { IEmbySettings, IUsersModel } from "../../interfaces";
import { IEmbyServer, IEmbySettings, IPublicInfo, IUsersModel } from "../../interfaces";
@Injectable()
export class EmbyService extends ServiceHelpers {
@ -16,8 +16,13 @@ export class EmbyService extends ServiceHelpers {
public logIn(settings: IEmbySettings): Observable<IEmbySettings> {
return this.http.post<IEmbySettings>(`${this.url}`, JSON.stringify(settings), {headers: this.headers});
}
public getUsers(): Observable<IUsersModel[]> {
return this.http.get<IUsersModel[]>(`${this.url}users`, {headers: this.headers});
}
public getPublicInfo(server: IEmbyServer): Observable<IPublicInfo> {
return this.http.post<IPublicInfo>(`${this.url}info`, JSON.stringify(server), {headers: this.headers});
}
}

@ -29,8 +29,5 @@ export class LidarrService extends ServiceHelpers {
public getMetadataProfiles(settings: ILidarrSettings): Observable<IProfiles[]> {
return this.http.post<IProfiles[]>(`${this.url}/Metadata/`, JSON.stringify(settings), {headers: this.headers});
}
public getLanguages(settings: ILidarrSettings): Observable<IProfiles[]> {
return this.http.post<IProfiles[]>(`${this.url}/Langauges/`,JSON.stringify(settings), {headers: this.headers});
}
}

@ -3,7 +3,7 @@
<div *ngIf="settings">
<fieldset>
<legend>
Emby Configuration
Emby/Jellyfin Configuration
</legend>
<div class="row">
@ -71,8 +71,8 @@
</label>
<div>
<input type="text" class="form-control-custom form-control" id="authToken" [(ngModel)]="server.serverHostname" placeholder="e.g. https://jellyfin.server.com/" value="{{server.serverHostname}}">
<small><span *ngIf="server.serverHostname">Current URL: "{{server.serverHostname}}/#!/itemdetails.html?id=1"</span>
<span *ngIf="!server.serverHostname">Current URL: "https://app.emby.media/#!/itemdetails.html?id=1</span></small>
<small><span *ngIf="server.serverHostname">Current URL: "{{server.serverHostname}}/#!/{{settings.isJellyfin ? ("itemdetails"): ("item/item")}}.html?id=1"</span>
<span *ngIf="!server.serverHostname">Current URL: "https://app.emby.media/#!/{{settings.isJellyfin ? ("itemdetails"): ("item/item")}}.html?id=1</span></small>
</div>
</div>
<div class="form-group">
@ -80,6 +80,11 @@
<button id="testEmby" type="button" (click)="test(server)" class="btn btn-primary-outline">Test Connectivity <div id="spinner"></div></button>
</div>
</div>
<div class="form-group">
<div>
<button id="discover" type="button" (click)="discoverServerInfo(server)" class="btn btn-primary-outline">Discover Server Information <div id="spinner"></div></button>
</div>
</div>
</div>
</ng-template>
</ngb-tab>
@ -88,7 +93,7 @@
<div class="col-md-1">
<div class="form-group">
<div>
<button (click)="save()" type="submit" id="save" class="btn btn-primary-outline">Submit</button>
<button [disabled]="!hasDiscovered" (click)="save()" type="submit" id="save" class="btn btn-primary-outline">Submit</button>
</div>
</div>
</div>
@ -100,4 +105,4 @@
</div>
</div>
</fieldset>
</div>
</div>

@ -1,7 +1,7 @@
import { Component, OnInit } from "@angular/core";
import { IEmbyServer, IEmbySettings } from "../../interfaces";
import { JobService, NotificationService, SettingsService, TesterService } from "../../services";
import { EmbyService, JobService, NotificationService, SettingsService, TesterService } from "../../services";
@Component({
templateUrl: "./emby.component.html",
@ -9,16 +9,25 @@ import { JobService, NotificationService, SettingsService, TesterService } from
export class EmbyComponent implements OnInit {
public settings: IEmbySettings;
public hasDiscovered: boolean;
constructor(private settingsService: SettingsService,
private notificationService: NotificationService,
private testerService: TesterService,
private jobService: JobService) { }
private jobService: JobService,
private embyService: EmbyService) { }
public ngOnInit() {
this.settingsService.getEmby().subscribe(x => this.settings = x);
}
public async discoverServerInfo(server: IEmbyServer) {
const result = await this.embyService.getPublicInfo(server).toPromise();
this.settings.isJellyfin = result.isJellyfin;
server.name = result.serverName;
this.hasDiscovered = true;
}
public addTab() {
if (this.settings.servers == null) {
this.settings.servers = [];

@ -84,22 +84,6 @@
</div>
</div>
<div class="form-group">
<label for="languageProfileId" class="control-label">Language Profile
<i *ngIf="form.get('languageProfileId').hasError('required')" class="fa fa-exclamation-circle error-text" pTooltip="Language Profile is required"></i>
</label>
<div id="languageProfileId">
<select formControlName="languageProfileId" class="form-control form-control-custom col-md-5 form-half" [ngClass]="{'form-error': form.get('languageProfileId').hasError('required')}">
<option *ngFor="let folder of languageProfiles" value="{{folder.id}}" >{{folder.name}}</option>
</select>
<button (click)="getLanguageProfiles(form)" type="button" class="btn btn-primary-outline col-md-4 col-md-push-1">Get Languages <span *ngIf="rootFoldersRunning" class="fa fa-spinner fa-spin"></span></button>
</div>
</div>
<div class="form-group">
<label for="metadataProfileId" class="control-label">Metadata Profile

@ -12,14 +12,12 @@ import { SettingsService } from "../../services";
export class LidarrComponent implements OnInit {
public qualities: IRadarrProfile[];
public languageProfiles: IProfiles[];
public metadataProfiles: IProfiles[];
public rootFolders: IRadarrRootFolder[];
public minimumAvailabilityOptions: IMinimumAvailability[];
public profilesRunning: boolean;
public rootFoldersRunning: boolean;
public metadataRunning: boolean;
public languageRunning: boolean;
public advanced = false;
public form: FormGroup;
@ -43,7 +41,6 @@ export class LidarrComponent implements OnInit {
ip: [x.ip, [Validators.required]],
port: [x.port, [Validators.required]],
albumFolder: [x.albumFolder],
languageProfileId: [x.languageProfileId, [Validators.required]],
metadataProfileId: [x.metadataProfileId, [Validators.required]],
addOnly: [x.addOnly],
});
@ -54,9 +51,6 @@ export class LidarrComponent implements OnInit {
if (x.defaultRootPath) {
this.getRootFolders(this.form);
}
if (x.languageProfileId) {
this.getLanguageProfiles(this.form);
}
if (x.metadataProfileId) {
this.getMetadataProfiles(this.form);
}
@ -68,9 +62,6 @@ export class LidarrComponent implements OnInit {
this.rootFolders = [];
this.rootFolders.push({ path: "Please Select", id: -1 });
this.languageProfiles = [];
this.languageProfiles.push({ name: "Please Select", id: -1 });
this.metadataProfiles = [];
this.metadataProfiles.push({ name: "Please Select", id: -1 });
}
@ -108,17 +99,6 @@ export class LidarrComponent implements OnInit {
});
}
public getLanguageProfiles(form: FormGroup) {
this.languageRunning = true;
this.lidarrService.getLanguages(form.value).subscribe(x => {
this.languageProfiles = x;
this.languageProfiles.unshift({ name: "Please Select", id: -1 });
this.languageRunning = false;
this.notificationService.success("Successfully retrieved the Language profiles");
});
}
public test(form: FormGroup) {
if (form.invalid) {
this.notificationService.error("Please check your entered values");

@ -22,7 +22,7 @@
</a>
<ul class="dropdown-menu">
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Plex']">Plex</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Emby']">Emby</a></li>
<li [routerLinkActive]="['active']"><a [routerLink]="['/Settings/Emby']">Emby/Jellyfin</a></li>
</ul>
</li>

@ -3,7 +3,7 @@
<div class="landing-block shadow">
<div class="media">
<div id="contentBody" class="media-body">
<h4 class="media-heading landing-title">Emby Authentication</h4>
<h4 class="media-heading landing-title">Emby/Jellyfin Authentication</h4>
<div *ngIf="embySettings">
<div *ngIf="embySettings.servers">
<div *ngFor="let server of embySettings.servers">
@ -26,6 +26,11 @@
<input type="checkbox" [(ngModel)]="server.ssl" id="Ssl" name="Ssl"><label for="Ssl">SSL</label>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<input type="checkbox" [(ngModel)]="embySettings.isJellyfin" id="isJellyfin" name="isJellyfin"><label for="isJellyfin">Jellyfin Install</label>
</div>
</div>
<div class="form-group">
<label for="username" class="control-label">Api Key</label>
<div>

@ -28,6 +28,7 @@ export class EmbyComponent implements OnInit {
}
this.embySettings = {
servers: [],
isJellyfin: false,
id: 0,
enable: true,
};

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ombi.Api.Emby;
using Ombi.Api.Emby.Models;
using Ombi.Api.Plex;
using Ombi.Attributes;
using Ombi.Core.Settings;
@ -60,6 +61,13 @@ namespace Ombi.Controllers.External
return null;
}
[HttpPost("info")]
public async Task<PublicInfo> GetServerInfo([FromBody] EmbyServers server)
{
var result = await EmbyApi.GetPublicInformation(server.FullUri);
return result;
}
/// <summary>
/// Gets the emby users.
/// </summary>

@ -60,16 +60,6 @@ namespace Ombi.Controllers.External
{
return await _lidarrApi.GetMetadataProfile(settings.ApiKey, settings.FullUri);
}
/// <summary>
/// Gets the Lidarr Langauge profiles.
/// </summary>
/// <param name="settings">The settings.</param>
/// <returns></returns>
[HttpPost("Langauges")]
public async Task<IEnumerable<LanguageProfiles>> GetLanguageProfiles([FromBody] LidarrSettings settings)
{
return await _lidarrApi.GetLanguageProfile(settings.ApiKey, settings.FullUri);
}
/// <summary>
/// Gets the Lidarr profiles using the saved settings

@ -233,6 +233,8 @@ namespace Ombi.Controllers
await CreateRole(OmbiRoles.AutoApproveMovie);
await CreateRole(OmbiRoles.Admin);
await CreateRole(OmbiRoles.AutoApproveTv);
await CreateRole(OmbiRoles.AutoApproveMusic);
await CreateRole(OmbiRoles.RequestMusic);
await CreateRole(OmbiRoles.PowerUser);
await CreateRole(OmbiRoles.RequestMovie);
await CreateRole(OmbiRoles.RequestTv);
@ -279,7 +281,7 @@ namespace Ombi.Controllers
[Authorize]
public async Task<UserViewModel> GetCurrentUser()
{
var user = await UserManager.Users.FirstOrDefaultAsync(x => x.UserName == User.Identity.Name);
var user = await UserManager.Users.FirstOrDefaultAsync(x => x.UserName.Equals(User.Identity.Name, StringComparison.InvariantCultureIgnoreCase));
return await GetUserWithRoles(user);
}
@ -873,7 +875,7 @@ namespace Ombi.Controllers
[ApiExplorerSettings(IgnoreApi = true)]
public async Task<string> GetUserAccessToken()
{
var user = await UserManager.Users.FirstOrDefaultAsync(x => x.UserName == User.Identity.Name);
var user = await UserManager.Users.FirstOrDefaultAsync(x => x.UserName.Equals(User.Identity.Name, StringComparison.InvariantCultureIgnoreCase));
if (user == null)
{
return Guid.Empty.ToString("N");
@ -895,7 +897,7 @@ namespace Ombi.Controllers
[HttpGet("notificationpreferences")]
public async Task<List<UserNotificationPreferences>> GetUserPreferences()
{
var user = await UserManager.Users.FirstOrDefaultAsync(x => x.UserName == User.Identity.Name);
var user = await UserManager.Users.FirstOrDefaultAsync(x => x.UserName.Equals(User.Identity.Name, StringComparison.InvariantCultureIgnoreCase));
return await GetPreferences(user);
}
@ -948,7 +950,7 @@ namespace Ombi.Controllers
return NotFound();
}
// Check if we are editing a different user than ourself, if we are then we need to power user role
var me = await UserManager.Users.FirstOrDefaultAsync(x => x.UserName == User.Identity.Name);
var me = await UserManager.Users.FirstOrDefaultAsync(x => x.UserName.Equals(User.Identity.Name, StringComparison.InvariantCultureIgnoreCase));
if (!me.Id.Equals(user.Id, StringComparison.InvariantCultureIgnoreCase))
{
var isPowerUser = await UserManager.IsInRoleAsync(me, OmbiRoles.PowerUser);

@ -187,7 +187,7 @@ namespace Ombi.Controllers
Comment = c.Comment,
Date = c.Date,
Username = c.User.UserAlias,
AdminComment = roles.Contains(OmbiRoles.PowerUser) || roles.Contains(OmbiRoles.Admin)
AdminComment = roles.Contains(OmbiRoles.PowerUser) || roles.Contains(OmbiRoles.Admin) || c.User.IsSystemUser
});
}
return vm;
@ -223,9 +223,10 @@ namespace Ombi.Controllers
UserId = user.Id
};
var isAdmin = await _userManager.IsInRoleAsync(user, OmbiRoles.Admin);
var isAdmin = await _userManager.IsInRoleAsync(user, OmbiRoles.Admin) || user.IsSystemUser;
AddIssueNotificationSubstitutes(notificationModel, issue, issue.UserReported.UserAlias);
notificationModel.Substitutes.Add("NewIssueComment", comment.Comment);
notificationModel.Substitutes.Add("IssueId", comment.IssueId.ToString());
notificationModel.Substitutes.Add("AdminComment", isAdmin.ToString());
if (isAdmin)

@ -120,7 +120,7 @@ namespace Ombi.Controllers
[HttpPost("plexrecentlyadded")]
public bool StartRecentlyAdded()
{
OmbiQuartz.Scheduler.TriggerJob(new JobKey(nameof(IPlexContentSync), "Plex"), new JobDataMap(new Dictionary<string, string> { { "recentlyAddedSearch", "true" } }));
OmbiQuartz.Scheduler.TriggerJob(new JobKey(nameof(IPlexContentSync) + "RecentlyAdded", "Plex"), new JobDataMap(new Dictionary<string, string> { { "recentlyAddedSearch", "true" } }));
return true;
}

@ -40,7 +40,7 @@ namespace Ombi.Controllers
{
if (body?.PlayerId.HasValue() ?? false)
{
var user = await _userManager.Users.FirstOrDefaultAsync(x => x.UserName == User.Identity.Name);
var user = await _userManager.Users.FirstOrDefaultAsync(x => x.UserName.Equals(User.Identity.Name, StringComparison.InvariantCultureIgnoreCase));
// Check if we already have this notification id
var alreadyExists = await _notification.GetAll().AnyAsync(x => x.PlayerId == body.PlayerId && x.UserId == user.Id);

@ -45,7 +45,7 @@
},
"NavigationBar": {
"Search": "Rechercher",
"Requests": "Demandes",
"Requests": "En attente",
"UserManagement": "Gestion des utilisateurs",
"Issues": "Problèmes",
"Vote": "Vote",
@ -64,7 +64,7 @@
"Title": "Rechercher",
"Paragraph": "Vous voulez regarder quelque chose qui n'est pas disponible actuellement ? Pas de problème, recherchez-le ci-dessous et demandez-le !",
"MoviesTab": "Films",
"TvTab": "TV",
"TvTab": "Séries",
"MusicTab": "Musique",
"Suggestions": "Suggestions",
"NoResults": "Désolé, nous n'avons trouvé aucun résultat !",
@ -104,7 +104,7 @@
"Title": "Demandes",
"Paragraph": "Vous pouvez voir ci-dessous vos demandes et celles des autres, ainsi que leur statut de téléchargement et d'approbation.",
"MoviesTab": "Films",
"TvTab": "Émissions",
"TvTab": "Séries",
"MusicTab": "Musique",
"RequestedBy": "Demandé par :",
"Status": "Statut :",

@ -12,7 +12,7 @@
"Common": {
"ContinueButton": "Gå videre",
"Available": "Tilgjengelig",
"PartiallyAvailable": "Partially Available",
"PartiallyAvailable": "Delvis tilgjengelig",
"Monitored": "Overvåket",
"NotAvailable": "Ikke tilgjengelig",
"ProcessingRequest": "Behandler forespørsel",
@ -74,8 +74,8 @@
"ViewOnEmby": "Spill av på Emby",
"RequestAdded": "Forespørsel om {{title}} er lagt til",
"Similar": "Lignende",
"Refine": "Refine",
"SearchBarPlaceholder": "Type Here to Search",
"Refine": "Spesifiser",
"SearchBarPlaceholder": "Angi nøkkelord for søk",
"Movies": {
"PopularMovies": "Populære filmer",
"UpcomingMovies": "Kommende filmer",
@ -86,7 +86,7 @@
},
"TvShows": {
"Popular": "Populært",
"Trending": "Trending",
"Trending": "På vei opp",
"MostWatched": "Mest sett",
"MostAnticipated": "Mest etterlengtede",
"Results": "Resultater",
@ -111,8 +111,8 @@
"RequestStatus": "Status for forespørsel:",
"Denied": " Avslått:",
"TheatricalRelease": "Kinopremiere: {{date}}",
"ReleaseDate": "Released: {{date}}",
"TheatricalReleaseSort": "Theatrical Release",
"ReleaseDate": "Utgitt: {{date}}",
"TheatricalReleaseSort": "Kinopremiere",
"DigitalRelease": "Digital utgivelse: {{date}}",
"RequestDate": "Dato for forespørsel:",
"QualityOverride": "Overstyr kvalitet:",
@ -133,16 +133,16 @@
"SeasonNumberHeading": "Sesong: {seasonNumber}",
"SortTitleAsc": "Tittel ▲",
"SortTitleDesc": "Tittel ▼",
"SortRequestDateAsc": "Request Date ▲",
"SortRequestDateDesc": "Request Date ▼",
"SortRequestDateAsc": "Dato for forespørsel ▲",
"SortRequestDateDesc": "Dato for forespørsel ▼",
"SortStatusAsc": "Status ▲",
"SortStatusDesc": "Status ▼",
"Remaining": {
"Quota": "{{remaining}}/{{total}} requests remaining",
"NextDays": "Another request will be added in {{time}} days",
"NextHours": "Another request will be added in {{time}} hours",
"NextMinutes": "Another request will be added in {{time}} minutes",
"NextMinute": "Another request will be added in {{time}} minute"
"Quota": "{{remaining}}/{{total}} forespørsler igjen",
"NextDays": "En ny foresøprel vil bli lagt til om {{time}} dager",
"NextHours": "En ny foresøprel vil bli lagt til om {{time}} timer",
"NextMinutes": "En ny foresøprel vil bli lagt til om {{time}} minutter",
"NextMinute": "En ny foresøprel vil bli lagt til om {{time}} minutt"
}
},
"Issues": {
@ -181,6 +181,6 @@
},
"Votes": {
"CompletedVotesTab": "Stemt",
"VotesTab": "Votes Needed"
"VotesTab": "Stemmer som trengs"
}
}

@ -62,7 +62,7 @@
},
"Search": {
"Title": "Szukaj",
"Paragraph": "Chcesz obejrzeć coś, co nie jest obecnie dostępne? Żaden problem, po prostu wyszukaj poniżej i dodaj zgłoszenie!",
"Paragraph": "Chcesz obejrzeć coś, co nie jest obecnie dostępne? Żaden problem! Po prostu wyszukaj poniżej i dodaj zgłoszenie!",
"MoviesTab": "Filmy",
"TvTab": "Seriale",
"MusicTab": "Muzyka",

@ -16,171 +16,171 @@
"Monitored": "Мониторинг",
"NotAvailable": "Недоступно",
"ProcessingRequest": "Обработка запроса",
"PendingApproval": "Ожидание утверждения",
"PendingApproval": "В ожидании одобрения",
"RequestDenied": "Запрос отклонен",
"NotRequested": "Не запрошено",
"Requested": "Запрос отправлен",
"Request": "Запрос",
"Denied": "Запрещено",
"Approve": "Утвердить",
"PartlyAvailable": "Partly Available",
"Requested": "Запрошено",
"Request": "Запросить",
"Denied": "Отказано",
"Approve": "Одобрить",
"PartlyAvailable": "Частично доступно",
"Errors": {
"Validation": "Please check your entered values"
"Validation": "Пожалуйста, проверьте введенные значения"
}
},
"PasswordReset": {
"EmailAddressPlaceholder": "Email Address",
"ResetPasswordButton": "Reset Password"
"EmailAddressPlaceholder": "Адрес эл. почты",
"ResetPasswordButton": "Сбросить пароль"
},
"LandingPage": {
"OnlineHeading": "Currently Online",
"OnlineParagraph": "The media server is currently online",
"PartiallyOnlineHeading": "Partially Online",
"PartiallyOnlineParagraph": "The media server is partially online.",
"MultipleServersUnavailable": "There are {{serversUnavailable}} servers offline out of {{totalServers}}.",
"SingleServerUnavailable": "There is {{serversUnavailable}} server offline out of {{totalServers}}.",
"OfflineHeading": "Currently Offline",
"OfflineParagraph": "The media server is currently offline.",
"CheckPageForUpdates": "Check this page for continuous site updates."
"OnlineHeading": "Сейчас в сети",
"OnlineParagraph": "Медиа-сервер в настоящее время в сети",
"PartiallyOnlineHeading": "Частично в сети",
"PartiallyOnlineParagraph": "Медиа-сервер частично в сети.",
"MultipleServersUnavailable": "В сети нет {{serversUnavailable}} серверов из {{totalServers}}.",
"SingleServerUnavailable": "В сети нет {{serversUnavailable}} серверов из {{totalServers}}.",
"OfflineHeading": "В настоящее время в offline",
"OfflineParagraph": "Медиа-сервер в настоящее время не в сети.",
"CheckPageForUpdates": "Проверьте эту страницу для получения последних новостей сайта."
},
"NavigationBar": {
"Search": "Search",
"Requests": "Requests",
"UserManagement": "User Management",
"Issues": "Issues",
"Vote": "Vote",
"Donate": "Donate!",
"DonateLibraryMaintainer": "Donate to Library Maintainer",
"DonateTooltip": "This is how I convince my wife to let me spend my spare time developing Ombi ;)",
"UpdateAvailableTooltip": "Update Available!",
"Settings": "Settings",
"Welcome": "Welcome {{username}}",
"UpdateDetails": "Update Details",
"Logout": "Logout",
"OpenMobileApp": "Open Mobile App",
"RecentlyAdded": "Recently Added"
"Search": "Поиск",
"Requests": "Запросы",
"UserManagement": "Управление пользователями",
"Issues": "Проблемы",
"Vote": "Голосование",
"Donate": "Поддержать!",
"DonateLibraryMaintainer": "Поддержать библиотекаря",
"DonateTooltip": "Так я убедил свою жену позволить мне тратить своё свободное время на разработку Ombi ;)",
"UpdateAvailableTooltip": "Доступно обновление!",
"Settings": "Настройки",
"Welcome": "Добро пожаловать, {{username}}",
"UpdateDetails": "Обновить детали",
"Logout": "Выйти",
"OpenMobileApp": "Открыть моб. приложение",
"RecentlyAdded": "Недавно добавленные"
},
"Search": {
"Title": "Search",
"Paragraph": "Want to watch something that is not currently available? No problem, just search for it below and request it!",
"MoviesTab": "Movies",
"TvTab": "TV Shows",
"MusicTab": "Music",
"Suggestions": "Suggestions",
"NoResults": "Sorry, we didn't find any results!",
"DigitalDate": "Digital Release: {{date}}",
"TheatricalRelease": "Theatrical Release: {{date}}",
"ViewOnPlex": "View On Plex",
"ViewOnEmby": "View On Emby",
"RequestAdded": "Request for {{title}} has been added successfully",
"Similar": "Similar",
"Refine": "Refine",
"SearchBarPlaceholder": "Type Here to Search",
"Title": "Поиск",
"Paragraph": "Хотите посмотреть что-то, чего нет в доступе? Нет проблем, просто вбейте название и запросите!",
"MoviesTab": "Фильмы",
"TvTab": "Сериалы",
"MusicTab": "Музыка",
"Suggestions": "Рекомендации",
"NoResults": "Извините, мы ничего не нашли!",
"DigitalDate": "Дигитальный релиз: {{date}}",
"TheatricalRelease": "Релиз в кинотеатрах: {{date}}",
"ViewOnPlex": "Смотреть в Plex",
"ViewOnEmby": "Смотреть в Emby",
"RequestAdded": "Запрос на {{title}} успешно добавлен",
"Similar": "Похожие",
"Refine": "Уточнить",
"SearchBarPlaceholder": "Поиск...",
"Movies": {
"PopularMovies": "Popular Movies",
"UpcomingMovies": "Upcoming Movies",
"TopRatedMovies": "Top Rated Movies",
"NowPlayingMovies": "Now Playing Movies",
"HomePage": "Home Page",
"Trailer": "Trailer"
"PopularMovies": "Популярные фильмы",
"UpcomingMovies": "В скором времени",
"TopRatedMovies": "Фильмы с высоким рейтингом",
"NowPlayingMovies": "Сейчас в кинотеатрах",
"HomePage": "Главная страница",
"Trailer": "Трейлер"
},
"TvShows": {
"Popular": "Popular",
"Trending": "Trending",
"MostWatched": "Most Watched",
"MostAnticipated": "Most Anticipated",
"Results": "Results",
"AirDate": "Air Date:",
"AllSeasons": "All Seasons",
"FirstSeason": "First Season",
"LatestSeason": "Latest Season",
"Select": "Select ...",
"SubmitRequest": "Submit Request",
"Season": "Season: {{seasonNumber}}",
"SelectAllInSeason": "Select All in Season {{seasonNumber}}"
"Popular": "Популярное",
"Trending": "Сейчас смотрят",
"MostWatched": "Самые просматриваемые",
"MostAnticipated": "Самые ожидаемые",
"Results": "Результаты",
"AirDate": "Дата выхода:",
"AllSeasons": "Все сезоны",
"FirstSeason": "Первый сезон",
"LatestSeason": "Последний сезон",
"Select": "Выбрать...",
"SubmitRequest": "Подать запрос",
"Season": "Сезон: {{seasonNumber}}",
"SelectAllInSeason": "Выбрать все в сезоне {{seasonNumber}}"
}
},
"Requests": {
"Title": "Requests",
"Paragraph": "Below you can see yours and all other requests, as well as their download and approval status.",
"MoviesTab": "Movies",
"TvTab": "TV Shows",
"MusicTab": "Music",
"RequestedBy": "Requested By:",
"Status": "Status:",
"RequestStatus": "Request status:",
"Denied": " Denied:",
"TheatricalRelease": "Theatrical Release: {{date}}",
"ReleaseDate": "Released: {{date}}",
"TheatricalReleaseSort": "Theatrical Release",
"DigitalRelease": "Digital Release: {{date}}",
"RequestDate": "Request Date:",
"QualityOverride": "Quality Override:",
"RootFolderOverride": "Root Folder Override:",
"ChangeRootFolder": "Root Folder",
"ChangeQualityProfile": "Quality Profile",
"MarkUnavailable": "Mark Unavailable",
"MarkAvailable": "Mark Available",
"Remove": "Remove",
"Deny": "Deny",
"Season": "Season:",
"GridTitle": "Title",
"AirDate": "AirDate",
"GridStatus": "Status",
"ReportIssue": "Report Issue",
"Filter": "Filter",
"Sort": "Sort",
"SeasonNumberHeading": "Season: {seasonNumber}",
"SortTitleAsc": "Title ▲",
"SortTitleDesc": "Title ▼",
"SortRequestDateAsc": "Request Date ▲",
"SortRequestDateDesc": "Request Date ▼",
"SortStatusAsc": "Status ▲",
"SortStatusDesc": "Status ▼",
"Title": "Запросы",
"Paragraph": "Ниже вы можете увидеть ваши и все другие запросы, а также их статус загрузки и одобрения.",
"MoviesTab": "Фильмы",
"TvTab": "Сериалы",
"MusicTab": "Музыка",
"RequestedBy": "Автор запроса:",
"Status": "Статус:",
"RequestStatus": "Статус запроса:",
"Denied": " Отказано:",
"TheatricalRelease": "Релиз в кинотеатрах: {{date}}",
"ReleaseDate": "Дата выхода: {{date}}",
"TheatricalReleaseSort": "Релиз в кинотеатрах",
"DigitalRelease": "Дигитальный релиз: {{date}}",
"RequestDate": "Дата запроса:",
"QualityOverride": "Переопределение качества:",
"RootFolderOverride": "Переопределение корневой папки:",
"ChangeRootFolder": "Корневая папка",
"ChangeQualityProfile": "Профиль качества",
"MarkUnavailable": "Отметить недоступным",
"MarkAvailable": "Отметить доступным",
"Remove": "Удалить",
"Deny": "Отклонить",
"Season": "Сезон:",
"GridTitle": "Название",
"AirDate": "Дата",
"GridStatus": "Статус",
"ReportIssue": "Сообщить о проблеме",
"Filter": "Фильтр",
"Sort": "Сортировать",
"SeasonNumberHeading": "Сезон: {seasonNumber}",
"SortTitleAsc": "Название ▲",
"SortTitleDesc": "Название ▼",
"SortRequestDateAsc": "Дата запроса ▲",
"SortRequestDateDesc": "Дата запроса ▼",
"SortStatusAsc": "Статус ▲",
"SortStatusDesc": "Статус ▼",
"Remaining": {
"Quota": "{{remaining}}/{{total}} requests remaining",
"NextDays": "Another request will be added in {{time}} days",
"NextHours": "Another request will be added in {{time}} hours",
"NextMinutes": "Another request will be added in {{time}} minutes",
"NextMinute": "Another request will be added in {{time}} minute"
"Quota": "Осталось запросов: {{remaining}}/{{total}}",
"NextDays": "Следующий запрос будет добавлен через {{time}} дней",
"NextHours": "Следующий запрос будет добавлен через {{time}} часов",
"NextMinutes": "Следующий запрос будет добавлен через {{time}} минут",
"NextMinute": "Следующий запрос будет добавлен через {{time}} минуту"
}
},
"Issues": {
"Title": "Issues",
"PendingTitle": "Pending Issues",
"InProgressTitle": "In Progress Issues",
"ResolvedTitle": "Resolved Issues",
"ColumnTitle": "Title",
"Category": "Category",
"Status": "Status",
"Details": "Details",
"Description": "Description",
"NoComments": "No Comments!",
"MarkInProgress": "Mark In Progress",
"MarkResolved": "Mark Resolved",
"SendMessageButton": "Send",
"Subject": "Subject",
"Comments": "Comments",
"WriteMessagePlaceholder": "Write your message here...",
"ReportedBy": "Reported By"
"Title": "Проблемы",
"PendingTitle": "Проблемы в ожидании",
"InProgressTitle": "Проблемы в процессе",
"ResolvedTitle": "Решенные проблемы",
"ColumnTitle": "Название",
"Category": "Категория",
"Status": "Статус",
"Details": "Подробная информация",
"Description": "Описание",
"NoComments": "Нет комментариев!",
"MarkInProgress": "Отметить в процессе",
"MarkResolved": "Отметить как решенное",
"SendMessageButton": "Отправить",
"Subject": "Тема",
"Comments": "Комментарии",
"WriteMessagePlaceholder": "Введите текст сообщения здесь...",
"ReportedBy": "Жалоба поступила от"
},
"Filter": {
"ClearFilter": "Clear Filter",
"FilterHeaderAvailability": "Availability",
"FilterHeaderRequestStatus": "Status",
"Approved": "Approved",
"PendingApproval": "Pending Approval"
"ClearFilter": "Сбросить фильтр",
"FilterHeaderAvailability": "Доступность",
"FilterHeaderRequestStatus": "Статус",
"Approved": "Одобрено",
"PendingApproval": "В ожидании одобрения"
},
"UserManagment": {
"TvRemaining": "TV: {{remaining}}/{{total}} remaining",
"MovieRemaining": "Movies: {{remaining}}/{{total}} remaining",
"MusicRemaining": "Music: {{remaining}}/{{total}} remaining",
"TvDue": "TV: {{date}}",
"MovieDue": "Movie: {{date}}",
"MusicDue": "Music: {{date}}"
"TvRemaining": "Сериалы: {{remaining}}/{{total}} осталось",
"MovieRemaining": "Фильмы: {{remaining}}/{{total}} осталось",
"MusicRemaining": "Музыка: {{remaining}}/{{total}} осталось",
"TvDue": "Сериалы: {{date}}",
"MovieDue": "Фильм: {{date}}",
"MusicDue": "Музыка: {{date}}"
},
"Votes": {
"CompletedVotesTab": "Voted",
"VotesTab": "Votes Needed"
"CompletedVotesTab": "Проголосовано",
"VotesTab": "Необходимы голоса"
}
}

Loading…
Cancel
Save