diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..419d87940 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# These are supported funding model platforms + +github: [tidusjar] +patreon: tidusjar +#open_collective: # Replace with a single Open Collective username +#ko_fi: # Replace with a single Ko-fi username +#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +custom: https://paypal.me/PlexRequestsNet diff --git a/.github/workflows/aspnetcore.yml b/.github/workflows/aspnetcore.yml new file mode 100644 index 000000000..e562216cc --- /dev/null +++ b/.github/workflows/aspnetcore.yml @@ -0,0 +1,18 @@ +name: ASP.NET Core CI + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 2.2.108 + + - name: Build Backend + run: ./build.sh --settings_skipverification=true diff --git a/.github/workflows/test.workflow b/.github/workflows/test.workflow new file mode 100644 index 000000000..7c88813d1 --- /dev/null +++ b/.github/workflows/test.workflow @@ -0,0 +1,9 @@ +workflow "New workflow" { + on = "push" + resolves = [".NET Core CLI"] +} + +action ".NET Core CLI" { + uses = "baruchiro/github-actions@0.0.1" + args = "build src/Ombi.sln" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 18ce95bc1..ae8417bae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,24 +4,129 @@ ### **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** + +- 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** + +- Update appsettings.json. [Jamie] + +- Update CHANGELOG.md. [Jamie] + + +## v3.0.4654 (2019-07-02) + +### **New Features** + - Added further logging into the API's (debug logging) [tidusjar] - Added transactions around all of the CUD operations. [Jamie Rees] -- Update stale.yml. [Jamie] - -- Update README.md. [Dyson Parkes] - -- Added stalebot. [tidusjar] - - Added some validation around the new crons. [Jamie Rees] - Added some defensive coding around when we create an artist for #2915. [tidusjar] -- Update README.md. [Jamie] - -- Update README.md. [Jamie] - - Update JobSetup.cs. [Jamie] - Update JobSetup.cs. [Jamie] @@ -32,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] @@ -314,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) diff --git a/build.cake b/build.cake index 033710a0e..e99bd7aa7 100644 --- a/build.cake +++ b/build.cake @@ -138,9 +138,9 @@ Task("TSLint") }); Task("PrePublish") - .IsDependentOn("SetVersionInfo") - .IsDependentOn("Gulp Publish") - .IsDependentOn("TSLint"); + .IsDependentOn("SetVersionInfo"); + //.IsDependentOn("Gulp Publish") // these are done in the main csproj + //.IsDependentOn("TSLint"); Task("Package") diff --git a/src/Ombi.Api.Emby/EmbyApi.cs b/src/Ombi.Api.Emby/EmbyApi.cs index 43a2badb6..7cb702fbc 100644 --- a/src/Ombi.Api.Emby/EmbyApi.cs +++ b/src/Ombi.Api.Emby/EmbyApi.cs @@ -46,6 +46,17 @@ namespace Ombi.Api.Emby return obj; } + public async Task GetPublicInformation(string baseUrl) + { + var request = new Request("emby/System/Info/public", baseUrl, HttpMethod.Get); + + AddHeaders(request, string.Empty); + + var obj = await Api.Request(request); + + return obj; + } + public async Task 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(mediaId, apiKey, userId, baseUrl); } + public async Task GetEpisodeInformation(string mediaId, string apiKey, string userId, string baseUrl) { return await GetInformation(mediaId, apiKey, userId, baseUrl); diff --git a/src/Ombi.Api.Emby/IEmbyApi.cs b/src/Ombi.Api.Emby/IEmbyApi.cs index b4641ea5f..3c29878b7 100644 --- a/src/Ombi.Api.Emby/IEmbyApi.cs +++ b/src/Ombi.Api.Emby/IEmbyApi.cs @@ -29,5 +29,6 @@ namespace Ombi.Api.Emby Task GetSeriesInformation(string mediaId, string apiKey, string userId, string baseUrl); Task GetMovieInformation(string mediaId, string apiKey, string userId, string baseUrl); Task GetEpisodeInformation(string mediaId, string apiKey, string userId, string baseUrl); + Task GetPublicInformation(string baseUrl); } } \ No newline at end of file diff --git a/src/Ombi.Api.Emby/Models/PublicInfo.cs b/src/Ombi.Api.Emby/Models/PublicInfo.cs new file mode 100644 index 000000000..01432d3c5 --- /dev/null +++ b/src/Ombi.Api.Emby/Models/PublicInfo.cs @@ -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; } + /// + /// Only populated for Jellyfin + /// + public string ProductName { get; set; } + + public bool IsJellyfin => !string.IsNullOrEmpty(ProductName) && ProductName.Contains("Jellyfin"); + + public string OperatingSystem { get; set; } + public string Id { get; set; } + } + +} \ No newline at end of file diff --git a/src/Ombi.Api.Lidarr/ILidarrApi.cs b/src/Ombi.Api.Lidarr/ILidarrApi.cs index 826cfdec3..b542ff0a0 100644 --- a/src/Ombi.Api.Lidarr/ILidarrApi.cs +++ b/src/Ombi.Api.Lidarr/ILidarrApi.cs @@ -20,7 +20,6 @@ namespace Ombi.Api.Lidarr Task MontiorAlbum(int albumId, string apiKey, string baseUrl); Task> GetAllAlbumsByArtistId(int artistId, string apiKey, string baseUrl); Task> GetMetadataProfile(string apiKey, string baseUrl); - Task> GetLanguageProfile(string apiKey, string baseUrl); Task Status(string apiKey, string baseUrl); Task AlbumSearch(int[] albumIds, string apiKey, string baseUrl); Task AlbumInformation(string albumId, string apiKey, string baseUrl); diff --git a/src/Ombi.Api.Lidarr/LidarrApi.cs b/src/Ombi.Api.Lidarr/LidarrApi.cs index 0f03aa1b0..dd589c64d 100644 --- a/src/Ombi.Api.Lidarr/LidarrApi.cs +++ b/src/Ombi.Api.Lidarr/LidarrApi.cs @@ -158,13 +158,6 @@ namespace Ombi.Api.Lidarr return Api.Request>(request); } - public Task> GetLanguageProfile(string apiKey, string baseUrl) - { - var request = new Request($"{ApiVersion}/languageprofile", baseUrl, HttpMethod.Get); - AddHeaders(request, apiKey); - return Api.Request>(request); - } - public Task> GetMetadataProfile(string apiKey, string baseUrl) { var request = new Request($"{ApiVersion}/metadataprofile", baseUrl, HttpMethod.Get); diff --git a/src/Ombi.Api.Lidarr/Models/ArtistAdd.cs b/src/Ombi.Api.Lidarr/Models/ArtistAdd.cs index 65aec3ac8..e292e8905 100644 --- a/src/Ombi.Api.Lidarr/Models/ArtistAdd.cs +++ b/src/Ombi.Api.Lidarr/Models/ArtistAdd.cs @@ -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; } diff --git a/src/Ombi.Api.Lidarr/Models/LanguageProfiles.cs b/src/Ombi.Api.Lidarr/Models/LanguageProfiles.cs deleted file mode 100644 index f503fe33f..000000000 --- a/src/Ombi.Api.Lidarr/Models/LanguageProfiles.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ombi.Api.Lidarr.Models -{ - public class LanguageProfiles - { - public string name { get; set; } - public int id { get; set; } - } -} \ No newline at end of file diff --git a/src/Ombi.Api.Plex/PlexApi.cs b/src/Ombi.Api.Plex/PlexApi.cs index fe6ba23e2..eaafcd75f 100644 --- a/src/Ombi.Api.Plex/PlexApi.cs +++ b/src/Ombi.Api.Plex/PlexApi.cs @@ -227,7 +227,7 @@ namespace Ombi.Api.Plex request.AddQueryString("context[device][environment]", "bundled"); request.AddQueryString("context[device][layout]", "desktop"); request.AddQueryString("context[device][platform]", "Web"); - request.AddQueryString("context[device][device]", "Ombi (Web)"); + request.AddQueryString("context[device][device]", "Ombi"); var s = await GetSettings(); await CheckInstallId(s); @@ -295,7 +295,7 @@ namespace Ombi.Api.Plex request.AddHeader("X-Plex-Client-Identifier", s.InstallId.ToString("N")); request.AddHeader("X-Plex-Product", ApplicationName); request.AddHeader("X-Plex-Version", "3"); - request.AddHeader("X-Plex-Device", "Ombi (Web)"); + 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"); diff --git a/src/Ombi.Core/Engine/MusicSearchEngine.cs b/src/Ombi.Core/Engine/MusicSearchEngine.cs index da41d5bf1..a9af03ecf 100644 --- a/src/Ombi.Core/Engine/MusicSearchEngine.cs +++ b/src/Ombi.Core/Engine/MusicSearchEngine.cs @@ -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; diff --git a/src/Ombi.Core/Rule/Rules/Request/AutoApproveRule.cs b/src/Ombi.Core/Rule/Rules/Request/AutoApproveRule.cs index 685f02b54..6b528e806 100644 --- a/src/Ombi.Core/Rule/Rules/Request/AutoApproveRule.cs +++ b/src/Ombi.Core/Rule/Rules/Request/AutoApproveRule.cs @@ -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 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(); diff --git a/src/Ombi.Core/Rule/Rules/Request/CanRequestRule.cs b/src/Ombi.Core/Rule/Rules/Request/CanRequestRule.cs index 2b316cfc5..b54c8b4fb 100644 --- a/src/Ombi.Core/Rule/Rules/Request/CanRequestRule.cs +++ b/src/Ombi.Core/Rule/Rules/Request/CanRequestRule.cs @@ -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 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) diff --git a/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs b/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs index 8fcc92c1f..343681e29 100644 --- a/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs @@ -70,11 +70,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); } } diff --git a/src/Ombi.Core/Rule/Rules/Specific/SendNotificationRule.cs b/src/Ombi.Core/Rule/Rules/Specific/SendNotificationRule.cs index 3f9e2f159..30ec9b14a 100644 --- a/src/Ombi.Core/Rule/Rules/Specific/SendNotificationRule.cs +++ b/src/Ombi.Core/Rule/Rules/Specific/SendNotificationRule.cs @@ -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 } diff --git a/src/Ombi.Core/Senders/MusicSender.cs b/src/Ombi.Core/Senders/MusicSender.cs index 0e9db9192..04544c6be 100644 --- a/src/Ombi.Core/Senders/MusicSender.cs +++ b/src/Ombi.Core/Senders/MusicSender.cs @@ -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, diff --git a/src/Ombi.Core/Senders/TvSender.cs b/src/Ombi.Core/Senders/TvSender.cs index 9a25ca9c0..3a3e34745 100644 --- a/src/Ombi.Core/Senders/TvSender.cs +++ b/src/Ombi.Core/Senders/TvSender.cs @@ -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 { diff --git a/src/Ombi.Helpers.Tests/StringHelperTests.cs b/src/Ombi.Helpers.Tests/StringHelperTests.cs new file mode 100644 index 000000000..d03f926ff --- /dev/null +++ b/src/Ombi.Helpers.Tests/StringHelperTests.cs @@ -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"); + } + } +} \ No newline at end of file diff --git a/src/Ombi.Helpers/EmbyHelper.cs b/src/Ombi.Helpers/EmbyHelper.cs index b1afe9c99..1d6004d00 100644 --- a/src/Ombi.Helpers/EmbyHelper.cs +++ b/src/Ombi.Helpers/EmbyHelper.cs @@ -2,19 +2,24 @@ { 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()) { - if(!customerServerUrl.EndsWith("/")) + if (!customerServerUrl.EndsWith("/")) { - return $"{customerServerUrl}/#!/itemdetails.html?id={mediaId}"; - } - return $"{customerServerUrl}#!/itemdetails.html?id={mediaId}"; + return $"{customerServerUrl}/#!/{path}.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}"; } } } diff --git a/src/Ombi.Helpers/StringHelper.cs b/src/Ombi.Helpers/StringHelper.cs index 68a29e848..bd9ecb5bb 100644 --- a/src/Ombi.Helpers/StringHelper.cs +++ b/src/Ombi.Helpers/StringHelper.cs @@ -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://"); + } } } \ No newline at end of file diff --git a/src/Ombi.Mapping/Profiles/TvProfile.cs b/src/Ombi.Mapping/Profiles/TvProfile.cs index db6755e01..0e8264a71 100644 --- a/src/Ombi.Mapping/Profiles/TvProfile.cs +++ b/src/Ombi.Mapping/Profiles/TvProfile.cs @@ -24,7 +24,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() @@ -41,7 +41,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)); diff --git a/src/Ombi.Notifications/Agents/MobileNotification.cs b/src/Ombi.Notifications/Agents/MobileNotification.cs index 4e3e55fc4..363029190 100644 --- a/src/Ombi.Notifications/Agents/MobileNotification.cs +++ b/src/Ombi.Notifications/Agents/MobileNotification.cs @@ -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 sn, ILogger log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t, ISettingsService s, IRepository notification, - UserManager um, IRepository sub, IMusicRequestRepository music, + UserManager um, IRepository sub, IMusicRequestRepository music, IRepository issueRepository, IRepository 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 _logger; private readonly IRepository _notifications; private readonly UserManager _userManager; + private readonly IRepository _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 GetUsers(NotificationOptions model, NotificationType type) { var notificationIds = new List(); @@ -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> GetUsersForIssue(NotificationOptions model, int issueId, NotificationType type) + { + var notificationIds = new List(); + + 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(); diff --git a/src/Ombi.Notifications/Agents/SlackNotification.cs b/src/Ombi.Notifications/Agents/SlackNotification.cs index 58b2da651..5b82c1434 100644 --- a/src/Ombi.Notifications/Agents/SlackNotification.cs +++ b/src/Ombi.Notifications/Agents/SlackNotification.cs @@ -79,7 +79,7 @@ namespace Ombi.Notifications.Agents protected override async Task RequestDeclined(NotificationOptions model, SlackNotificationSettings settings) { - await Run(model, settings, NotificationType.RequestAvailable); + await Run(model, settings, NotificationType.RequestDeclined); } protected override async Task RequestApproved(NotificationOptions model, SlackNotificationSettings settings) diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs index f52488daa..ee81892b8 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs @@ -52,13 +52,13 @@ namespace Ombi.Schedule.Jobs.Emby { try { - await StartServerCache(server); + await StartServerCache(server, embySettings); } catch (Exception e) { await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) .SendAsync(NotificationHub.NotificationEvent, "Emby Content Sync Failed"); - _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"); } } @@ -71,7 +71,7 @@ namespace Ombi.Schedule.Jobs.Emby } - private async Task StartServerCache(EmbyServers server) + private async Task StartServerCache(EmbyServers server, EmbySettings settings) { if (!ValidateSettings(server)) return; @@ -146,7 +146,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 }); } diff --git a/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs index 7fed2b4d8..aa88f03d7 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs @@ -696,7 +696,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); @@ -818,7 +818,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); diff --git a/src/Ombi.Schedule/Jobs/Ombi/OmbiAutomaticUpdater.cs b/src/Ombi.Schedule/Jobs/Ombi/OmbiAutomaticUpdater.cs index 4c66b2e2f..82187ed50 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/OmbiAutomaticUpdater.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/OmbiAutomaticUpdater.cs @@ -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; diff --git a/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs index 67ef028b5..c50c63e9b 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs @@ -85,7 +85,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) @@ -98,19 +97,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 contentids) diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs b/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs index 21e1fbbfd..b143b2610 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs @@ -89,7 +89,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) @@ -117,6 +117,7 @@ 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"); } @@ -126,8 +127,7 @@ namespace Ombi.Schedule.Jobs.Plex 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); await Notification.Clients.Clients(NotificationHub.AdminConnectionIds) .SendAsync(NotificationHub.NotificationEvent, recentlyAddedSearch ? "Plex Recently Added Sync Finished" : "Plex Content Sync Finished"); @@ -179,7 +179,8 @@ namespace Ombi.Schedule.Jobs.Plex foreach (var content in allContent) { - if (content.viewGroup.Equals(PlexMediaType.Episode.ToString(), StringComparison.CurrentCultureIgnoreCase)) + Logger.LogDebug($"Got type '{content.viewGroup}' to process"); + if (content.viewGroup.Equals(PlexMediaType.Episode.ToString(), StringComparison.InvariantCultureIgnoreCase)) { Logger.LogDebug("Found some episodes, this must be a recently added sync"); var count = 0; @@ -223,7 +224,7 @@ namespace Ombi.Schedule.Jobs.Plex episodesProcessed.AddRange(episodesAdded.Select(x => x.Id)); } } - if (content.viewGroup.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase)) + if (content.viewGroup.Equals(PlexMediaType.Show.ToString(), StringComparison.InvariantCultureIgnoreCase)) { // Process Shows Logger.LogDebug("Processing TV Shows"); @@ -253,7 +254,7 @@ namespace Ombi.Schedule.Jobs.Plex await Repo.SaveChangesAsync(); } - if (content.viewGroup.Equals(PlexMediaType.Movie.ToString(), StringComparison.CurrentCultureIgnoreCase)) + if (content.viewGroup.Equals(PlexMediaType.Movie.ToString(), StringComparison.InvariantCultureIgnoreCase)) { Logger.LogDebug("Processing Movies"); foreach (var movie in content?.Metadata ?? new Metadata[] { }) diff --git a/src/Ombi.Schedule/Jobs/Radarr/RadarrSync.cs b/src/Ombi.Schedule/Jobs/Radarr/RadarrSync.cs index 6fa4c1bc8..56499aea6 100644 --- a/src/Ombi.Schedule/Jobs/Radarr/RadarrSync.cs +++ b/src/Ombi.Schedule/Jobs/Radarr/RadarrSync.cs @@ -56,7 +56,9 @@ namespace Ombi.Schedule.Jobs.Radarr var movieIds = new List(); 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()) { diff --git a/src/Ombi.Schedule/Ombi.Schedule.csproj b/src/Ombi.Schedule/Ombi.Schedule.csproj index 4e31b6759..b1e7c092c 100644 --- a/src/Ombi.Schedule/Ombi.Schedule.csproj +++ b/src/Ombi.Schedule/Ombi.Schedule.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/Ombi.Settings/Settings/Models/External/EmbySettings.cs b/src/Ombi.Settings/Settings/Models/External/EmbySettings.cs index 595733dc1..3ade5746b 100644 --- a/src/Ombi.Settings/Settings/Models/External/EmbySettings.cs +++ b/src/Ombi.Settings/Settings/Models/External/EmbySettings.cs @@ -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 Servers { get; set; } = new List(); } diff --git a/src/Ombi.Settings/Settings/Models/External/LidarrSettings.cs b/src/Ombi.Settings/Settings/Models/External/LidarrSettings.cs index 3a37b7d43..5f2c5722d 100644 --- a/src/Ombi.Settings/Settings/Models/External/LidarrSettings.cs +++ b/src/Ombi.Settings/Settings/Models/External/LidarrSettings.cs @@ -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; } } diff --git a/src/Ombi.Store/Context/OmbiContext.cs b/src/Ombi.Store/Context/OmbiContext.cs index ea61b253f..28b27107e 100644 --- a/src/Ombi.Store/Context/OmbiContext.cs +++ b/src/Ombi.Store/Context/OmbiContext.cs @@ -101,7 +101,6 @@ namespace Ombi.Store.Context UserName = "Api", UserType = UserType.SystemUser, NormalizedUserName = "API", - }); SaveChanges(); tran.Commit(); diff --git a/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts b/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts index 5a4dd3562..bf000dbaa 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts @@ -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[]; diff --git a/src/Ombi/ClientApp/src/app/login/login.component.ts b/src/Ombi/ClientApp/src/app/login/login.component.ts index 6badf4ec4..534cd08e3 100644 --- a/src/Ombi/ClientApp/src/app/login/login.component.ts +++ b/src/Ombi/ClientApp/src/app/login/login.component.ts @@ -24,6 +24,14 @@ import { MatSnackBar } from "@angular/material"; }) 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; @@ -52,6 +60,8 @@ export class LoginComponent implements OnDestroy, OnInit { private errorValidation: string; private href: string; + private oAuthWindow: Window|null; + constructor(private authService: AuthService, private router: Router, private status: StatusService, private fb: FormBuilder, private settingsService: SettingsService, private images: ImageService, private sanitizer: DomSanitizer, private route: ActivatedRoute, @Inject(APP_BASE_HREF) href:string, private translate: TranslateService, private plexTv: PlexTvService, @@ -142,8 +152,6 @@ export class LoginComponent implements OnDestroy, OnInit { }); } - private oAuthWindow: Window; - public oauth() { this.oAuthWindow = window.open(window.location.toString(), "_blank", `toolbar=0, location=0, @@ -176,7 +184,10 @@ export class LoginComponent implements OnDestroy, OnInit { if (this.authService.loggedIn()) { this.ngOnDestroy(); - this.oAuthWindow.close(); + + if(this.oAuthWindow) { + this.oAuthWindow.close(); + } this.router.navigate(["search"]); return; } diff --git a/src/Ombi/ClientApp/src/app/services/applications/emby.service.ts b/src/Ombi/ClientApp/src/app/services/applications/emby.service.ts index 6c519607c..734568d95 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/emby.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/emby.service.ts @@ -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 { return this.http.post(`${this.url}`, JSON.stringify(settings), {headers: this.headers}); } + public getUsers(): Observable { return this.http.get(`${this.url}users`, {headers: this.headers}); } + + public getPublicInfo(server: IEmbyServer): Observable { + return this.http.post(`${this.url}info`, JSON.stringify(server), {headers: this.headers}); + } } diff --git a/src/Ombi/ClientApp/src/app/services/applications/lidarr.service.ts b/src/Ombi/ClientApp/src/app/services/applications/lidarr.service.ts index b4e4830d3..dfe0e1164 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/lidarr.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/lidarr.service.ts @@ -29,8 +29,5 @@ export class LidarrService extends ServiceHelpers { public getMetadataProfiles(settings: ILidarrSettings): Observable { return this.http.post(`${this.url}/Metadata/`, JSON.stringify(settings), {headers: this.headers}); - } - public getLanguages(settings: ILidarrSettings): Observable { - return this.http.post(`${this.url}/Langauges/`,JSON.stringify(settings), {headers: this.headers}); } } diff --git a/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html b/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html index 66cac8c8b..18d42bb47 100644 --- a/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html +++ b/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html @@ -3,7 +3,7 @@
- Emby Configuration + Emby/Jellyfin Configuration
@@ -71,8 +71,8 @@
- Current URL: "{{server.serverHostname}}/#!/itemdetails.html?id=1" - Current URL: "https://app.emby.media/#!/itemdetails.html?id=1 + Current URL: "{{server.serverHostname}}/#!/{{settings.isJellyfin ? ("itemdetails"): ("item/item")}}.html?id=1" + Current URL: "https://app.emby.media/#!/{{settings.isJellyfin ? ("itemdetails"): ("item/item")}}.html?id=1
@@ -80,6 +80,11 @@
+
+
+ +
+
@@ -88,7 +93,7 @@
- +
@@ -100,4 +105,4 @@ - \ No newline at end of file + diff --git a/src/Ombi/ClientApp/src/app/settings/emby/emby.component.ts b/src/Ombi/ClientApp/src/app/settings/emby/emby.component.ts index c2752a973..bc1d2bbb2 100644 --- a/src/Ombi/ClientApp/src/app/settings/emby/emby.component.ts +++ b/src/Ombi/ClientApp/src/app/settings/emby/emby.component.ts @@ -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 = []; diff --git a/src/Ombi/ClientApp/src/app/settings/lidarr/lidarr.component.html b/src/Ombi/ClientApp/src/app/settings/lidarr/lidarr.component.html index 2db0d4414..633f8dbbd 100644 --- a/src/Ombi/ClientApp/src/app/settings/lidarr/lidarr.component.html +++ b/src/Ombi/ClientApp/src/app/settings/lidarr/lidarr.component.html @@ -72,19 +72,6 @@ *ngIf="rootFoldersRunning" class="fa fa-spinner fa-spin"> -
- - Language Profile - - - {{folder.name}} - - - -
-
-
Metadata Profile diff --git a/src/Ombi/ClientApp/src/app/settings/lidarr/lidarr.component.ts b/src/Ombi/ClientApp/src/app/settings/lidarr/lidarr.component.ts index d1e28285f..45efdb792 100644 --- a/src/Ombi/ClientApp/src/app/settings/lidarr/lidarr.component.ts +++ b/src/Ombi/ClientApp/src/app/settings/lidarr/lidarr.component.ts @@ -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"); diff --git a/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html b/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html index 510d6ce90..0dc8b859a 100644 --- a/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html +++ b/src/Ombi/ClientApp/src/app/settings/settingsmenu.component.html @@ -13,7 +13,7 @@ - + diff --git a/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.html b/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.html index 52d7e3045..c5d99d5a4 100644 --- a/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.html +++ b/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.html @@ -4,7 +4,7 @@
- +
diff --git a/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.ts b/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.ts index 570782e8c..dc9dfca70 100644 --- a/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.ts +++ b/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.ts @@ -29,6 +29,7 @@ export class EmbyComponent implements OnInit { } this.embySettings = { servers: [], + isJellyfin: false, id: 0, enable: true, }; diff --git a/src/Ombi/Controllers/V1/External/EmbyController.cs b/src/Ombi/Controllers/V1/External/EmbyController.cs index 116efaa89..d8ef5c158 100644 --- a/src/Ombi/Controllers/V1/External/EmbyController.cs +++ b/src/Ombi/Controllers/V1/External/EmbyController.cs @@ -4,6 +4,8 @@ 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; using Ombi.Core.Settings.Models.External; @@ -59,6 +61,13 @@ namespace Ombi.Controllers.V1.External return null; } + [HttpPost("info")] + public async Task GetServerInfo([FromBody] EmbyServers server) + { + var result = await EmbyApi.GetPublicInformation(server.FullUri); + return result; + } + /// /// Gets the emby users. /// diff --git a/src/Ombi/Controllers/V1/External/LidarrController.cs b/src/Ombi/Controllers/V1/External/LidarrController.cs index fac7f6dee..33af536d7 100644 --- a/src/Ombi/Controllers/V1/External/LidarrController.cs +++ b/src/Ombi/Controllers/V1/External/LidarrController.cs @@ -60,16 +60,6 @@ namespace Ombi.Controllers.V1.External { return await _lidarrApi.GetMetadataProfile(settings.ApiKey, settings.FullUri); } - /// - /// Gets the Lidarr Langauge profiles. - /// - /// The settings. - /// - [HttpPost("Langauges")] - public async Task> GetLanguageProfiles([FromBody] LidarrSettings settings) - { - return await _lidarrApi.GetLanguageProfile(settings.ApiKey, settings.FullUri); - } /// /// Gets the Lidarr profiles using the saved settings diff --git a/src/Ombi/Controllers/V1/IdentityController.cs b/src/Ombi/Controllers/V1/IdentityController.cs index 6eb28e008..e24bdb87d 100644 --- a/src/Ombi/Controllers/V1/IdentityController.cs +++ b/src/Ombi/Controllers/V1/IdentityController.cs @@ -233,6 +233,8 @@ namespace Ombi.Controllers.V1 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.V1 [Authorize] public async Task 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.V1 [ApiExplorerSettings(IgnoreApi = true)] public async Task 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.V1 [HttpGet("notificationpreferences")] public async Task> 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.V1 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); diff --git a/src/Ombi/Controllers/V1/IssuesController.cs b/src/Ombi/Controllers/V1/IssuesController.cs index 53363c4ec..cca139b89 100644 --- a/src/Ombi/Controllers/V1/IssuesController.cs +++ b/src/Ombi/Controllers/V1/IssuesController.cs @@ -185,7 +185,7 @@ namespace Ombi.Controllers.V1 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; @@ -221,9 +221,10 @@ namespace Ombi.Controllers.V1 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) diff --git a/src/Ombi/Controllers/V1/JobController.cs b/src/Ombi/Controllers/V1/JobController.cs index 3411422ac..a501da2ec 100644 --- a/src/Ombi/Controllers/V1/JobController.cs +++ b/src/Ombi/Controllers/V1/JobController.cs @@ -118,7 +118,7 @@ namespace Ombi.Controllers.V1 [HttpPost("plexrecentlyadded")] public bool StartRecentlyAdded() { - OmbiQuartz.Scheduler.TriggerJob(new JobKey(nameof(IPlexContentSync), "Plex"), new JobDataMap(new Dictionary { { "recentlyAddedSearch", "true" } })); + OmbiQuartz.Scheduler.TriggerJob(new JobKey(nameof(IPlexContentSync) + "RecentlyAdded", "Plex"), new JobDataMap(new Dictionary { { "recentlyAddedSearch", "true" } })); return true; } diff --git a/src/Ombi/Controllers/V1/MobileController.cs b/src/Ombi/Controllers/V1/MobileController.cs index c1b3ee094..a2b1f05e3 100644 --- a/src/Ombi/Controllers/V1/MobileController.cs +++ b/src/Ombi/Controllers/V1/MobileController.cs @@ -40,7 +40,7 @@ namespace Ombi.Controllers.V1 { 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); diff --git a/src/Ombi/Startup.cs b/src/Ombi/Startup.cs index b6d41d934..8eff5486e 100644 --- a/src/Ombi/Startup.cs +++ b/src/Ombi/Startup.cs @@ -2,6 +2,7 @@ using AutoMapper.EquivalencyExpression; using Hangfire; using Hangfire.Dashboard; +using Hangfire.MemoryStorage; using Hangfire.SQLite; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -94,20 +95,15 @@ namespace Ombi services.AddSwagger(); services.AddAppSettingsValues(Configuration); - var i = StoragePathSingleton.Instance; - if (string.IsNullOrEmpty(i.StoragePath)) - { - i.StoragePath = string.Empty; - } - - var sqliteStorage = $"Data Source={Path.Combine(i.StoragePath, "Schedules.db")};"; - services.AddHangfire(x => { - x.UseSQLiteStorage(sqliteStorage); + x.UseMemoryStorage(); x.UseActivator(new IoCJobActivator(services.BuildServiceProvider())); }); + + SQLitePCL.raw.sqlite3_config(2); + services.AddCors(o => o.AddPolicy("MyPolicy", builder => { builder.AllowAnyHeader() diff --git a/src/Ombi/appsettings.json b/src/Ombi/appsettings.json index 7d352f0dc..8a9e38008 100644 --- a/src/Ombi/appsettings.json +++ b/src/Ombi/appsettings.json @@ -2,7 +2,7 @@ "Logging": { "IncludeScopes": false, "LogLevel": { - "Default": "Debug", + "Default": "Information", "System": "Information", "Microsoft": "None", "Hangfire": "None" diff --git a/src/Ombi/wwwroot/translations/fr.json b/src/Ombi/wwwroot/translations/fr.json index 2f98728e5..e2530f95a 100644 --- a/src/Ombi/wwwroot/translations/fr.json +++ b/src/Ombi/wwwroot/translations/fr.json @@ -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 :", diff --git a/src/Ombi/wwwroot/translations/no.json b/src/Ombi/wwwroot/translations/no.json index 51d55b2c5..91e69facb 100644 --- a/src/Ombi/wwwroot/translations/no.json +++ b/src/Ombi/wwwroot/translations/no.json @@ -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" } } diff --git a/src/Ombi/wwwroot/translations/pl.json b/src/Ombi/wwwroot/translations/pl.json index f309e6a86..1e495087d 100644 --- a/src/Ombi/wwwroot/translations/pl.json +++ b/src/Ombi/wwwroot/translations/pl.json @@ -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", diff --git a/src/Ombi/wwwroot/translations/ru.json b/src/Ombi/wwwroot/translations/ru.json index 7cf9f1702..092689584 100644 --- a/src/Ombi/wwwroot/translations/ru.json +++ b/src/Ombi/wwwroot/translations/ru.json @@ -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": "Необходимы голоса" } }