diff --git a/README.md b/README.md index 6959079b7b977..8222a98ad3299 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ Developer note: The software implements the [Torznab](https://torznab.github.io/ A third-party Golang SDK for Jackett is available from [webtor-io/go-jackett](https://github.com/webtor-io/go-jackett) #### Supported Systems -* Windows 7 SP1 or greater -* Linux [supported operating systems here](https://github.com/dotnet/core/blob/main/release-notes/6.0/supported-os.md#linux) -* macOS 10.15+ or greater +* Windows 10 Version 1607+ or greater [supported operating systems here](https://github.com/dotnet/core/blob/main/release-notes/8.0/supported-os.md#windows) +* Linux [supported operating systems here](https://github.com/dotnet/core/blob/main/release-notes/8.0/supported-os.md#linux) +* macOS 12.0+ (Monterey) or greater [supported operating systems here](https://github.com/dotnet/core/blob/main/release-notes/8.0/supported-os.md#macos)
Supported Public Trackers @@ -725,7 +725,7 @@ We recommend you install Jackett as a Windows service using the supplied install To get started with using the installer for Jackett, follow the steps below: -1. Check if you need any .NET prerequisites installed, see https://docs.microsoft.com/en-us/dotnet/core/install/windows?tabs=net60#dependencies +1. Check if you need any .NET prerequisites installed, see https://docs.microsoft.com/en-us/dotnet/core/install/windows?tabs=net80#dependencies 2. Download the latest version of the Windows installer, "Jackett.Installer.Windows.exe" from the [releases](https://github.com/Jackett/Jackett/releases/latest) page. 3. When prompted if you would like this app to make changes to your computer, select "yes". 4. If you would like to install Jackett as a Windows Service, make sure the "Install as Windows Service" checkbox is filled. @@ -802,7 +802,7 @@ On an Ubuntu 16 system: [chrisjohnson00.jackett](https://galaxy.ansible.com/chri ## Installation on macOS ### Prerequisites -macOS 10.15+ or greater +macOS 12.0+ (Monterey) or greater ### Install as service 1. Download and extract the latest `Jackett.Binaries.macOS.tar.gz` or `Jackett.Binaries.macOSARM64.tar.gz` release from the [releases](https://github.com/Jackett/Jackett/releases/latest) page. @@ -948,8 +948,8 @@ git clone https://github.com/Jackett/Jackett.git cd Jackett/src # dotnet core version -dotnet publish Jackett.Server -f net6.0 --self-contained -r osx-x64 -c Debug # takes care of everything -./Jackett.Server/bin/Debug/net6.0/osx-x64/jackett # run jackett +dotnet publish Jackett.Server -f net8.0 --self-contained -r osx-x64 -c Debug # takes care of everything +./Jackett.Server/bin/Debug/net8.0/osx-x64/jackett # run jackett ``` ### Linux @@ -961,8 +961,8 @@ git clone https://github.com/Jackett/Jackett.git cd Jackett/src # dotnet core version -dotnet publish Jackett.Server -f net6.0 --self-contained -r linux-x64 -c Debug # takes care of everything -./Jackett.Server/bin/Debug/net6.0/linux-x64/jackett # run jackett +dotnet publish Jackett.Server -f net8.0 --self-contained -r linux-x64 -c Debug # takes care of everything +./Jackett.Server/bin/Debug/net8.0/linux-x64/jackett # run jackett ``` ## Screenshots diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fed73123235ff..6f9bc68349cce 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -2,12 +2,12 @@ name: $(majorVersion).$(minorVersion).$(patchVersion) variables: majorVersion: 0 - minorVersion: 21 + minorVersion: 22 patchVersion: $[counter(variables['minorVersion'], 1)] # this will reset when we bump minor jackettVersion: $(majorVersion).$(minorVersion).$(patchVersion) buildConfiguration: Release - netCoreFramework: net6.0 - netCoreSdkVersion: 6.0.x + netCoreFramework: net8.0 + netCoreSdkVersion: 8.0.x # system.debug: true trigger: @@ -129,7 +129,7 @@ stages: displayName: Build DateTimeRoutines # this task is not mandatory since DateTimeRoutines is build in the next task, but the purpose is to fix: # error MSB4018: System.IO.IOException: The process cannot access the file - # '/home/vsts/work/1/net6.0-linux-musl-arm/src/DateTimeRoutines/bin/Release/netstandard2.0/DateTimeRoutines.deps.json' + # '/home/vsts/work/1/src/DateTimeRoutines/bin/Release/netstandard2.0/DateTimeRoutines.deps.json' # because it is being used by another process. inputs: command: build @@ -500,7 +500,7 @@ stages: - task: PublishPipelineArtifact@1 condition: and(succeeded(), startsWith(variables['runtime'], 'win')) inputs: - targetPath: $(Build.SourcesDirectory)/coverlet/reports/coverage.cobertura.Windows.net6.0.xml + targetPath: $(Build.SourcesDirectory)/coverlet/reports/coverage.cobertura.Windows.net8.0.xml - stage: IntegrationTestJackett displayName: Integration Tests diff --git a/src/Jackett.Common/Indexers/BaseIndexer.cs b/src/Jackett.Common/Indexers/BaseIndexer.cs index 39937d4a801ad..afde8420c1ef8 100644 --- a/src/Jackett.Common/Indexers/BaseIndexer.cs +++ b/src/Jackett.Common/Indexers/BaseIndexer.cs @@ -661,7 +661,7 @@ protected async Task FollowIfRedirect(WebResult response, string referrer = null break; } - await DoFollowIfRedirect(response, referrer, overrideRedirectUrl, overrideCookies, accumulateCookies); + response = await DoFollowIfRedirect(response, referrer, overrideRedirectUrl, overrideCookies, accumulateCookies); if (accumulateCookies) { @@ -701,7 +701,7 @@ protected virtual void UpdateCookieHeader(string newCookies, string cookieOverri } } - private async Task DoFollowIfRedirect(WebResult incomingResponse, string referrer = null, string overrideRedirectUrl = null, string overrideCookies = null, bool accumulateCookies = false) + private async Task DoFollowIfRedirect(WebResult incomingResponse, string referrer = null, string overrideRedirectUrl = null, string overrideCookies = null, bool accumulateCookies = false) { if (incomingResponse.IsRedirect) { @@ -722,8 +722,11 @@ private async Task DoFollowIfRedirect(WebResult incomingResponse, string referre Cookies = redirRequestCookies, Encoding = Encoding }); - MapperUtil.Mapper.Map(redirectedResponse, incomingResponse); + + return redirectedResponse; } + + return incomingResponse; } protected List GetAllTrackerCategories() => diff --git a/src/Jackett.Common/Jackett.Common.csproj b/src/Jackett.Common/Jackett.Common.csproj index 6b63a97342c01..2ff328b10e1b5 100644 --- a/src/Jackett.Common/Jackett.Common.csproj +++ b/src/Jackett.Common/Jackett.Common.csproj @@ -13,7 +13,6 @@ - @@ -27,10 +26,10 @@ - - - - + + + + diff --git a/src/Jackett.Common/Models/Config/RuntimeSettings.cs b/src/Jackett.Common/Models/Config/RuntimeSettings.cs index f98c2e1bb5724..db255a1e8881c 100644 --- a/src/Jackett.Common/Models/Config/RuntimeSettings.cs +++ b/src/Jackett.Common/Models/Config/RuntimeSettings.cs @@ -35,13 +35,13 @@ public string DataFolder return CustomDataFolder; } - if (System.Environment.OSVersion.Platform == PlatformID.Unix) + if (Environment.OSVersion.Platform == PlatformID.Unix) { - return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "Jackett"); + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.DoNotVerify), "Jackett"); } else { - return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Jackett"); + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData, Environment.SpecialFolderOption.DoNotVerify), "Jackett"); } } } diff --git a/src/Jackett.Common/Models/TrackerCacheResult.cs b/src/Jackett.Common/Models/TrackerCacheResult.cs index c65bf09ac6234..dc0e338522494 100644 --- a/src/Jackett.Common/Models/TrackerCacheResult.cs +++ b/src/Jackett.Common/Models/TrackerCacheResult.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; namespace Jackett.Common.Models { @@ -10,5 +11,49 @@ public class TrackerCacheResult : ReleaseInfo public string TrackerType { get; set; } public string CategoryDesc { get; set; } public Uri BlackholeLink { get; set; } + + public TrackerCacheResult(ReleaseInfo releaseInfo) + { + Title = releaseInfo.Title; + Guid = releaseInfo.Guid; + Link = releaseInfo.Link; + Details = releaseInfo.Details; + PublishDate = releaseInfo.PublishDate; + Category = releaseInfo.Category; + Size = releaseInfo.Size; + Files = releaseInfo.Files; + Grabs = releaseInfo.Grabs; + Description = releaseInfo.Description; + RageID = releaseInfo.RageID; + TVDBId = releaseInfo.TVDBId; + Imdb = releaseInfo.Imdb; + TMDb = releaseInfo.TMDb; + TVMazeId = releaseInfo.TVMazeId; + TraktId = releaseInfo.TraktId; + DoubanId = releaseInfo.DoubanId; + Genres = releaseInfo.Genres; + Year = releaseInfo.Year; + Author = releaseInfo.Author; + BookTitle = releaseInfo.BookTitle; + Publisher = releaseInfo.Publisher; + Artist = releaseInfo.Artist; + Album = releaseInfo.Album; + Label = releaseInfo.Label; + Track = releaseInfo.Track; + Seeders = releaseInfo.Seeders; + Peers = releaseInfo.Peers; + Poster = releaseInfo.Poster; + InfoHash = releaseInfo.InfoHash; + MagnetUri = releaseInfo.MagnetUri; + MinimumRatio = releaseInfo.MinimumRatio; + MinimumSeedTime = releaseInfo.MinimumSeedTime; + DownloadVolumeFactor = releaseInfo.DownloadVolumeFactor; + UploadVolumeFactor = releaseInfo.UploadVolumeFactor; + + CategoryDesc = Category != null ? string.Join(", ", Category.Select(TorznabCatType.GetCatDesc).Where(x => !string.IsNullOrEmpty(x))) : string.Empty; + + // Use peers as leechers + Peers -= Seeders; + } } } diff --git a/src/Jackett.Common/Services/CacheService.cs b/src/Jackett.Common/Services/CacheService.cs index 03f641ac44b7c..4b5e568ca9065 100644 --- a/src/Jackett.Common/Services/CacheService.cs +++ b/src/Jackett.Common/Services/CacheService.cs @@ -69,7 +69,7 @@ public void CacheResults(IIndexer indexer, TorznabQuery query, List var trackerCacheQuery = new TrackerCacheQuery { Created = DateTime.Now, - Results = releases + Results = releases.Select(r => (ReleaseInfo)r.Clone()).ToList() }; var trackerCache = _cache[indexer.Id]; @@ -123,27 +123,23 @@ public IReadOnlyList GetCachedResults() PruneCacheByTtl(); // remove expired results - var results = new List(); - foreach (var trackerCache in _cache.Values) - { - var trackerResults = new List(); - foreach (var query in trackerCache.Queries.Values.OrderByDescending(q => q.Created)) // newest first - { - foreach (var release in query.Results) - { - var item = MapperUtil.Mapper.Map(release); - item.FirstSeen = query.Created; - item.Tracker = trackerCache.TrackerName; - item.TrackerId = trackerCache.TrackerId; - item.TrackerType = trackerCache.TrackerType; - item.Peers -= item.Seeders; // Use peers as leechers - trackerResults.Add(item); - } - } - trackerResults = trackerResults.GroupBy(r => r.Guid).Select(y => y.First()).Take(300).ToList(); - results.AddRange(trackerResults); - } - var result = results.OrderByDescending(i => i.PublishDate).Take(3000).ToList(); + var result = _cache.Values.SelectMany( + trackerCache => trackerCache.Queries.Values + .OrderByDescending(q => q.Created) + .SelectMany( + query => query.Results.Select(release => + new TrackerCacheResult(release) + { + FirstSeen = query.Created, + Tracker = trackerCache.TrackerName, + TrackerId = trackerCache.TrackerId, + TrackerType = trackerCache.TrackerType + })) + .GroupBy(r => r.Guid) + .Select(y => y.First()) + .Take(300)) + .OrderByDescending(i => i.PublishDate) + .Take(3000).ToList(); _logger.Debug($"CACHE GetCachedResults / Results: {result.Count} (cache may contain more results)"); PrintCacheStatus(); diff --git a/src/Jackett.Common/Services/ConfigurationService.cs b/src/Jackett.Common/Services/ConfigurationService.cs index 657365f16d21b..11650616b4ecb 100644 --- a/src/Jackett.Common/Services/ConfigurationService.cs +++ b/src/Jackett.Common/Services/ConfigurationService.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; using Jackett.Common.Models.Config; @@ -11,7 +13,6 @@ namespace Jackett.Common.Services { - public class ConfigurationService : IConfigurationService { private readonly ISerializeService serializeService; @@ -34,10 +35,23 @@ public void CreateOrMigrateSettings() { try { + // Migrate app data to 'Library/Application Support' on .NET 8 for OSX + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var userAppDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify), ".config", "Jackett"); + var appDataFolder = GetAppDataFolder(); + + // Check for existing json configuration files due to LogManager creating log.txt early in Program.cs + if (Directory.Exists(userAppDataFolder) && (!Directory.Exists(appDataFolder) || !Directory.EnumerateFiles(appDataFolder, "*.json", SearchOption.AllDirectories).Any())) + { + PerformMigration(userAppDataFolder); + } + } + if (!Directory.Exists(GetAppDataFolder())) { var dir = Directory.CreateDirectory(GetAppDataFolder()); - if (System.Environment.OSVersion.Platform != PlatformID.Unix) + if (Environment.OSVersion.Platform != PlatformID.Unix) { var access = dir.GetAccessControl(); var directorySecurity = new DirectorySecurity(GetAppDataFolder(), AccessControlSections.All); @@ -112,7 +126,7 @@ public void PerformMigration(string oldDirectory) if (!Directory.Exists(destFolder)) { var dir = Directory.CreateDirectory(destFolder); - if (System.Environment.OSVersion.Platform != PlatformID.Unix) + if (Environment.OSVersion.Platform != PlatformID.Unix) { var directorySecurity = new DirectorySecurity(destFolder, AccessControlSections.All); directorySecurity.AddAccessRule(new FileSystemAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), FileSystemRights.FullControl, InheritanceFlags.ObjectInherit | InheritanceFlags.ContainerInherit, PropagationFlags.None, AccessControlType.Allow)); @@ -123,7 +137,7 @@ public void PerformMigration(string oldDirectory) { File.Copy(file, destPath); // The old files were created when running as admin so make sure they are editable by normal users / services. - if (System.Environment.OSVersion.Platform != PlatformID.Unix) + if (Environment.OSVersion.Platform != PlatformID.Unix) { var fileInfo = new FileInfo(destFolder); var fileSecurity = new FileSecurity(destPath, AccessControlSections.All); @@ -200,7 +214,7 @@ public List GetCardigannDefinitionsFolders() { var dirs = new List(); - if (System.Environment.OSVersion.Platform == PlatformID.Unix) + if (Environment.OSVersion.Platform == PlatformID.Unix) { dirs.Add(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "cardigann/definitions/")); dirs.Add("/etc/xdg/cardigan/definitions/"); diff --git a/src/Jackett.Common/Utils/MapperUtil.cs b/src/Jackett.Common/Utils/MapperUtil.cs deleted file mode 100644 index c6f770f923065..0000000000000 --- a/src/Jackett.Common/Utils/MapperUtil.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Linq; -using AutoMapper; -using Jackett.Common.Models; -using Jackett.Common.Utils.Clients; - -namespace Jackett.Common.Utils -{ - public static class MapperUtil - { - public static Mapper Mapper = new Mapper( - new MapperConfiguration( - cfg => - { - cfg.CreateMap(); - - cfg.CreateMap(); - - cfg.CreateMap().AfterMap((r, t) => - { - t.CategoryDesc = r.Category != null - ? string.Join(", ", r.Category.Select(x => TorznabCatType.GetCatDesc(x)).Where(x => !string.IsNullOrEmpty(x))) - : ""; - }); - })); - } -} diff --git a/src/Jackett.IntegrationTests/Jackett.IntegrationTests.csproj b/src/Jackett.IntegrationTests/Jackett.IntegrationTests.csproj index 7b280692906b8..29b3b73338edf 100644 --- a/src/Jackett.IntegrationTests/Jackett.IntegrationTests.csproj +++ b/src/Jackett.IntegrationTests/Jackett.IntegrationTests.csproj @@ -1,7 +1,7 @@ - net6.0;net462 + net8.0;net462 false false diff --git a/src/Jackett.Server/Controllers/ResultsController.cs b/src/Jackett.Server/Controllers/ResultsController.cs index f48b5d76fa040..87ba9874efa75 100644 --- a/src/Jackett.Server/Controllers/ResultsController.cs +++ b/src/Jackett.Server/Controllers/ResultsController.cs @@ -308,14 +308,11 @@ public async Task Results([FromQuery] ApiSearch requestt) var searchResults = t.Result.Releases; var indexer = t.Result.Indexer; - return searchResults.Select(result => + return searchResults.Select(result => new TrackerCacheResult(result) { - var item = MapperUtil.Mapper.Map(result); - item.Tracker = indexer.Name; - item.TrackerId = indexer.Id; - item.TrackerType = indexer.Type; - item.Peers = item.Peers - item.Seeders; // Use peers as leechers - return item; + Tracker = indexer.Name, + TrackerId = indexer.Id, + TrackerType = indexer.Type }); }).OrderByDescending(d => d.PublishDate).ToList(); @@ -447,7 +444,7 @@ public async Task Torznab([FromQuery] TorznabRequest request) Link = new Uri(CurrentIndexer.SiteLink) }); - var proxiedReleases = result.Releases.Select(r => MapperUtil.Mapper.Map(r)).Select(r => + var proxiedReleases = result.Releases.Select(r => { r.Link = serverService.ConvertToProxyLink(r.Link, serverUrl, r.Origin.Id, "dl", r.Title); r.Poster = serverService.ConvertToProxyLink(r.Poster, serverUrl, r.Origin.Id, "img", "poster"); @@ -481,7 +478,7 @@ public async Task Torznab([FromQuery] TorznabRequest request) if (retryAfter > 0) { - HttpContext.Response.Headers.Add("Retry-After", $"{retryAfter}"); + HttpContext.Response.Headers.Append("Retry-After", $"{retryAfter}"); } } @@ -568,9 +565,8 @@ public async Task Potato([FromQuery] TorrentPotatoRequest logger.Info($"Potato search in {CurrentIndexer.Name} for {CurrentQuery.GetQueryString()} => Found {result.Releases.Count()} releases{cacheStr}"); var serverUrl = serverService.GetServerUrl(Request); - var potatoReleases = result.Releases.Where(r => r.Link != null || r.MagnetUri != null).Select(r => + var potatoReleases = result.Releases.Where(r => r.Link != null || r.MagnetUri != null).Select(release => { - var release = MapperUtil.Mapper.Map(r); release.Link = serverService.ConvertToProxyLink(release.Link, serverUrl, CurrentIndexer.Id, "dl", release.Title); // Poster is not used in Potato response //release.Poster = serverService.ConvertToProxyLink(release.Poster, serverUrl, CurrentIndexer.Id, "img", "poster"); @@ -589,7 +585,7 @@ public async Task Potato([FromQuery] TorrentPotatoRequest size = (long)release.Size / (1024 * 1024), // This is in MB leechers = (release.Peers ?? -1) - (release.Seeders ?? 0), seeders = release.Seeders ?? -1, - publish_date = r.PublishDate == DateTime.MinValue ? null : release.PublishDate.ToUniversalTime().ToString("s") + publish_date = release.PublishDate == DateTime.MinValue ? null : release.PublishDate.ToUniversalTime().ToString("s") }; return item; }); diff --git a/src/Jackett.Server/Jackett.Server.csproj b/src/Jackett.Server/Jackett.Server.csproj index c2cedd3c02d35..39f1374fd643d 100644 --- a/src/Jackett.Server/Jackett.Server.csproj +++ b/src/Jackett.Server/Jackett.Server.csproj @@ -2,7 +2,7 @@ $(MSBuildProjectName) - net6.0;net462 + net8.0;net462 jackett.ico Exe @@ -10,12 +10,12 @@ ISLINUXMUSL - + false - + false @@ -33,11 +33,11 @@ - - - - - + + + + + @@ -55,13 +55,12 @@ - - + - - + + diff --git a/src/Jackett.Service/Jackett.Service.csproj b/src/Jackett.Service/Jackett.Service.csproj index 0737914d3314a..eab1fd27353cd 100644 --- a/src/Jackett.Service/Jackett.Service.csproj +++ b/src/Jackett.Service/Jackett.Service.csproj @@ -1,7 +1,7 @@ - net6.0-windows + net8.0-windows WinExe JackettService jackett.ico diff --git a/src/Jackett.Test/Jackett.Test.csproj b/src/Jackett.Test/Jackett.Test.csproj index 655b8457e5969..4fc29f07c9a7b 100644 --- a/src/Jackett.Test/Jackett.Test.csproj +++ b/src/Jackett.Test/Jackett.Test.csproj @@ -1,7 +1,7 @@ - net6.0;net462 + net8.0;net462 false @@ -25,14 +25,14 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + - + diff --git a/src/Jackett.Test/Server/Services/RuntimeSettingsTests.cs b/src/Jackett.Test/Server/Services/RuntimeSettingsTests.cs index eee5dff294b3a..dad32262807f1 100644 --- a/src/Jackett.Test/Server/Services/RuntimeSettingsTests.cs +++ b/src/Jackett.Test/Server/Services/RuntimeSettingsTests.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using Jackett.Common.Models.Config; using Jackett.Test.TestHelpers; using NUnit.Framework; @@ -14,9 +15,12 @@ public void Default_data_folder_is_correct() var runtimeSettings = new RuntimeSettings(); var dataFolder = runtimeSettings.DataFolder; - if (System.Environment.OSVersion.Platform == PlatformID.Unix) + if (Environment.OSVersion.Platform == PlatformID.Unix) { - var expectedUnixPath = Environment.GetEnvironmentVariable("HOME") + "/.config/Jackett"; + var expectedUnixPath = Environment.GetEnvironmentVariable("HOME") + + (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) + ? "/Library/Application Support" + : "/.config") + "/Jackett"; Assert.AreEqual(expectedUnixPath, dataFolder); } else diff --git a/src/Jackett.Tray/Jackett.Tray.csproj b/src/Jackett.Tray/Jackett.Tray.csproj index cfd7acfd8bda2..8a8253e938b61 100644 --- a/src/Jackett.Tray/Jackett.Tray.csproj +++ b/src/Jackett.Tray/Jackett.Tray.csproj @@ -1,7 +1,7 @@ - net6.0-windows + net8.0-windows WinExe true JackettTray diff --git a/src/Jackett.Updater/Jackett.Updater.csproj b/src/Jackett.Updater/Jackett.Updater.csproj index 5000e750da8db..79a4474be721f 100644 --- a/src/Jackett.Updater/Jackett.Updater.csproj +++ b/src/Jackett.Updater/Jackett.Updater.csproj @@ -1,7 +1,7 @@ - net6.0;net462 + net8.0;net462 jackett.ico JackettUpdater Exe