diff --git a/.gitignore b/.gitignore index 3c4efe2..fa24b41 100644 --- a/.gitignore +++ b/.gitignore @@ -258,4 +258,8 @@ paket-files/ # Python Tools for Visual Studio (PTVS) __pycache__/ -*.pyc \ No newline at end of file +*.pyc + + +# Data folder for artifacts +Data/** diff --git a/RadarrAPI/Config.cs b/RadarrAPI/Config.cs index 7690f95..bd7a311 100644 --- a/RadarrAPI/Config.cs +++ b/RadarrAPI/Config.cs @@ -9,5 +9,9 @@ public class Config public string AppVeyorApiKey { get; set; } public string ApiKey { get; set; } + + public string TeamcityUser { get; set; } + + public string TeamcityPass { get; set; } } } diff --git a/RadarrAPI/Database/Models/UpdateEntity.cs b/RadarrAPI/Database/Models/UpdateEntity.cs index d55bcbe..4420ef3 100644 --- a/RadarrAPI/Database/Models/UpdateEntity.cs +++ b/RadarrAPI/Database/Models/UpdateEntity.cs @@ -62,5 +62,6 @@ public string FixedStr get { return JsonConvert.SerializeObject(Fixed); } set { Fixed = JsonConvert.DeserializeObject>(value); } } + } } diff --git a/RadarrAPI/RadarrAPI.csproj b/RadarrAPI/RadarrAPI.csproj index ed39bfe..5bc5fc5 100644 --- a/RadarrAPI/RadarrAPI.csproj +++ b/RadarrAPI/RadarrAPI.csproj @@ -37,4 +37,8 @@ + + + + diff --git a/RadarrAPI/Release/ReleaseService.cs b/RadarrAPI/Release/ReleaseService.cs index 08d9abc..6f57cfc 100644 --- a/RadarrAPI/Release/ReleaseService.cs +++ b/RadarrAPI/Release/ReleaseService.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Threading.Tasks; using NLog; +using RadarrAPI.Release.Teamcity; using RadarrAPI.Release.AppVeyor; using RadarrAPI.Release.Github; using RadarrAPI.Update; @@ -18,7 +19,7 @@ public ReleaseService(IServiceProvider serviceProvider) { _releaseBranches = new ConcurrentDictionary(); _releaseBranches.TryAdd(Branch.Develop, new GithubReleaseSource(serviceProvider, Branch.Develop)); - _releaseBranches.TryAdd(Branch.Nightly, new AppVeyorReleaseSource(serviceProvider, Branch.Nightly)); + _releaseBranches.TryAdd(Branch.Nightly, new TeamcityReleaseSource(serviceProvider, Branch.Nightly)); } public void UpdateReleases(Branch branch) diff --git a/RadarrAPI/Release/Teamcity/Responses/TeamcityResponse.cs b/RadarrAPI/Release/Teamcity/Responses/TeamcityResponse.cs new file mode 100644 index 0000000..9e0bd15 --- /dev/null +++ b/RadarrAPI/Release/Teamcity/Responses/TeamcityResponse.cs @@ -0,0 +1,79 @@ +using System; +using Newtonsoft.Json; +using System.Collections; +using System.Collections.Generic; +namespace RadarrAPI.Release.Teamcity.Responses +{ + public class File + { + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("href")] + public string Href { get; set; } + } + + public class Change + { + + [JsonProperty("comment")] + public string Comment { get; set; } + } + + public class Changes + { + + [JsonProperty("change")] + public IList Change { get; set; } + + [JsonProperty("count")] + public int Count { get; set; } + } + + public class Artifacts + { + + [JsonProperty("file")] + public IList File { get; set; } + } + + public class Build + { + + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("number")] + public string Number { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("state")] + public string State { get; set; } + + [JsonProperty("branchName")] + public string BranchName { get; set; } + + [JsonProperty("defaultBranch")] + public bool DefaultBranch { get; set; } + + [JsonProperty("finishDate")] + public string FinishDate { get; set; } + + [JsonProperty("changes")] + public Changes Changes { get; set; } + + [JsonProperty("artifacts")] + public Artifacts Artifacts { get; set; } + } + + public class TeamcityResponse + { + + [JsonProperty("build")] + public IList Build { get; set; } + } + +} diff --git a/RadarrAPI/Release/Teamcity/TeamcityReleaseSource.cs b/RadarrAPI/Release/Teamcity/TeamcityReleaseSource.cs new file mode 100644 index 0000000..db64447 --- /dev/null +++ b/RadarrAPI/Release/Teamcity/TeamcityReleaseSource.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using NLog; +using RadarrAPI.Database; +using RadarrAPI.Release.Teamcity.Responses; +using RadarrAPI.Update; +using Microsoft.EntityFrameworkCore; +using RadarrAPI.Database.Models; +using System.Globalization; + +namespace RadarrAPI.Release.Teamcity +{ + public class TeamcityReleaseSource : ReleaseSourceBase + { + private const string AccountName = "galli-leo"; + private const string ProjectSlug = "radarr-usby1"; + private const string ServerURL = "https://builds.radarr.video"; + + private readonly HttpClient _httpClient; + + private readonly HttpClient _downloadHttpClient; + + private int? _lastBuildId; + + public TeamcityReleaseSource(IServiceProvider serviceProvider, Branch branch) : base(serviceProvider, branch) + { + var config = serviceProvider.GetService>().Value; + + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{config.TeamcityUser}:{config.TeamcityPass}"))); + + _downloadHttpClient = new HttpClient(); + _downloadHttpClient.DefaultRequestHeaders.Authorization = _httpClient.DefaultRequestHeaders.Authorization; + } + + protected override async Task DoFetchReleasesAsync() + { + //https://builds.radarr.video/httpAuth/app/rest/builds/?locator=buildType:Radarr_Build,count:10&fields=build(id,number,status,branchName,defaultBranch,state,artifacts(file)) + var buildUrl = $"{ServerURL}/httpAuth/app/rest/builds/?locator=buildType:Radarr_Build,count:10&fields=build(id,number,status,finishDate,branchName,defaultBranch,state,artifacts(file),changes(count,change(comment)))"; + + var buildData = await _httpClient.GetStringAsync(buildUrl); + var builds = JsonConvert.DeserializeObject(buildData); + + // Store here temporarily so we don't break on not processed builds. + var lastBuild = _lastBuildId; + var database = ServiceProvider.GetService(); + + foreach (var build in builds.Build) + { + if (lastBuild.HasValue && + lastBuild.Value >= build.Id) break; + + // Make sure we dont distribute; + // - pull requests, + // - unsuccesful builds, + // - tagged builds (duplicate). + if (!build.DefaultBranch + || build.Status != "SUCCESS" + || build.State != "finished" + || build.Changes.Count == 0) continue; + + // Get an updateEntity + var updateEntity = database.UpdateEntities + .Include(x => x.UpdateFiles) + .FirstOrDefault(x => x.Version.Equals(build.Number) && x.Branch.Equals(ReleaseBranch)); + + if (updateEntity == null) + { + // Create update object + List changeList = new List(string.Join("\n", build.Changes.Change.Select(c => string.Join("\n", c.Comment.Split('\n').Where(t => !string.IsNullOrEmpty(t))))).Split('\n')); + List fixes = changeList.Where(c => c.ToLower().Contains("fix")).ToList(); + List newStuff = changeList.Where(c => !c.ToLower().Contains("fix")).ToList(); + + updateEntity = new UpdateEntity + { + Version = build.Number, + ReleaseDate = DateTime.ParseExact(build.FinishDate, "yyyyMMddTHHmmss+0000", CultureInfo.InvariantCulture), + Branch = ReleaseBranch, + New = newStuff, + Fixed = fixes + }; + + // Start tracking this object + await database.AddAsync(updateEntity); + } + + // Process artifacts + foreach (var artifact in build.Artifacts.File) + { + // Detect target operating system. + OperatingSystem operatingSystem; + + if (artifact.Name.Contains("windows.")) + { + operatingSystem = OperatingSystem.Windows; + } + else if (artifact.Name.Contains("linux.")) + { + operatingSystem = OperatingSystem.Linux; + } + else if (artifact.Name.Contains("osx.")) + { + operatingSystem = OperatingSystem.Osx; + } + else + { + continue; + } + + // Check if exists in database. + var updateFileEntity = database.UpdateFileEntities + .FirstOrDefault(x => + x.UpdateEntityId == updateEntity.UpdateEntityId && + x.OperatingSystem == operatingSystem); + + if (updateFileEntity != null) continue; + + // Calculate the hash of the zip file. + var releaseDownloadUrl = $"{ServerURL}{artifact.Href.Replace("metadata", "content")}"; + var releaseFileName = artifact.Name; + var releaseZip = Path.Combine(Config.DataDirectory, ReleaseBranch.ToString(), releaseFileName); + string releaseHash; + + if (!System.IO.File.Exists(releaseZip)) + { + Directory.CreateDirectory(Path.GetDirectoryName(releaseZip)); + System.IO.File.WriteAllBytes(releaseZip, await _downloadHttpClient.GetByteArrayAsync(releaseDownloadUrl)); + } + + using (var stream = System.IO.File.OpenRead(releaseZip)) + { + using (var sha = SHA256.Create()) + { + releaseHash = BitConverter.ToString(sha.ComputeHash(stream)).Replace("-", "").ToLower(); + } + } + + //File.Delete(releaseZip); + + // Add to database. + updateEntity.UpdateFiles.Add(new UpdateFileEntity + { + OperatingSystem = operatingSystem, + Filename = releaseFileName, + Url = releaseDownloadUrl, + Hash = releaseHash + }); + } + + // Save all changes to the database. + await database.SaveChangesAsync(); + + // Make sure we atleast skip this build next time. + if (_lastBuildId == null || + _lastBuildId.Value < build.Id) + { + _lastBuildId = build.Id; + } + } + } + } +}