1+ using System ;
2+ using System . Collections . Generic ;
3+ using System . IO ;
4+ using System . Linq ;
5+ using System . Net . Http ;
6+ using System . Net . Http . Headers ;
7+ using System . Security . Cryptography ;
8+ using System . Threading . Tasks ;
9+ using LidarrAPI . Database ;
10+ using LidarrAPI . Database . Models ;
11+ using LidarrAPI . Release . AppVeyor . Responses ;
12+ using LidarrAPI . Update ;
13+ using Microsoft . EntityFrameworkCore ;
14+ using Microsoft . Extensions . Options ;
15+ using Newtonsoft . Json ;
16+ using OperatingSystem = LidarrAPI . Update . OperatingSystem ;
17+
18+ namespace LidarrAPI . Release . AppVeyor
19+ {
20+ public class AppVeyorReleaseSource : ReleaseSourceBase
21+ {
22+ private const string AccountName = "lidarr" ;
23+ private const string ProjectSlug = "lidarr" ;
24+
25+ private static int ? _lastBuildId ;
26+
27+ private readonly Config _config ;
28+
29+ private readonly DatabaseContext _database ;
30+
31+ private readonly HttpClient _downloadHttpClient ;
32+
33+ private readonly HttpClient _httpClient ;
34+
35+ public AppVeyorReleaseSource ( DatabaseContext database , IOptions < Config > config )
36+ {
37+ _database = database ;
38+ _config = config . Value ;
39+
40+ _httpClient = new HttpClient ( ) ;
41+ _httpClient . DefaultRequestHeaders . Accept . Add ( new MediaTypeWithQualityHeaderValue ( "application/json" ) ) ;
42+ _httpClient . DefaultRequestHeaders . Authorization =
43+ new AuthenticationHeaderValue ( "Bearer" , _config . AppVeyorApiKey ) ;
44+
45+ _downloadHttpClient = new HttpClient ( ) ;
46+ }
47+
48+ protected override async Task DoFetchReleasesAsync ( )
49+ {
50+ if ( ReleaseBranch == Branch . Unknown )
51+ {
52+ throw new ArgumentException ( "ReleaseBranch must not be unknown when fetching releases." ) ;
53+ }
54+
55+ var historyUrl =
56+ $ "https://ci.appveyor.com/api/projects/{ AccountName } /{ ProjectSlug } /history?recordsNumber=10&branch=develop";
57+
58+ var historyData = await _httpClient . GetStringAsync ( historyUrl ) ;
59+ var history = JsonConvert . DeserializeObject < AppVeyorProjectHistory > ( historyData ) ;
60+
61+ // Store here temporarily so we don't break on not processed builds.
62+ var lastBuild = _lastBuildId ;
63+
64+ foreach ( var build in history . Builds . Take ( 5 ) . ToList ( ) ) // Only take last 5.
65+ {
66+ if ( lastBuild . HasValue &&
67+ lastBuild . Value >= build . BuildId ) break ;
68+
69+ // Make sure we dont distribute;
70+ // - pull requests,
71+ // - unsuccesful builds,
72+ // - tagged builds (duplicate).
73+ if ( build . PullRequestId . HasValue ||
74+ build . IsTag ) continue ;
75+
76+ var buildExtendedData = await _httpClient . GetStringAsync (
77+ $ "https://ci.appveyor.com/api/projects/{ AccountName } /{ ProjectSlug } /build/{ build . Version } ") ;
78+ var buildExtended = JsonConvert . DeserializeObject < AppVeyorProjectLastBuild > ( buildExtendedData ) . Build ;
79+
80+ // Filter out incomplete builds
81+ var buildJob = buildExtended . Jobs . FirstOrDefault ( ) ;
82+ if ( buildJob == null ||
83+ buildJob . ArtifactsCount == 0 ||
84+ ! buildExtended . Started . HasValue ) continue ;
85+
86+ // Grab artifacts
87+ var artifactsPath = $ "https://ci.appveyor.com/api/buildjobs/{ buildJob . JobId } /artifacts";
88+ var artifactsData = await _httpClient . GetStringAsync ( artifactsPath ) ;
89+ var artifacts = JsonConvert . DeserializeObject < AppVeyorArtifact [ ] > ( artifactsData ) ;
90+
91+ // Get an updateEntity
92+ var updateEntity = _database . UpdateEntities
93+ . Include ( x => x . UpdateFiles )
94+ . FirstOrDefault ( x => x . Version . Equals ( buildExtended . Version ) && x . Branch . Equals ( ReleaseBranch ) ) ;
95+
96+ if ( updateEntity == null )
97+ {
98+ // Create update object
99+ updateEntity = new UpdateEntity
100+ {
101+ Version = buildExtended . Version ,
102+ ReleaseDate = buildExtended . Started . Value . UtcDateTime ,
103+ Branch = ReleaseBranch ,
104+ New = new List < string >
105+ {
106+ build . Message
107+ }
108+ } ;
109+
110+ // Add extra message
111+ if ( ! string . IsNullOrWhiteSpace ( build . MessageExtended ) )
112+ {
113+ updateEntity . New . Add ( build . MessageExtended ) ;
114+ }
115+
116+ // Start tracking this object
117+ await _database . AddAsync ( updateEntity ) ;
118+ }
119+
120+ // Process artifacts
121+ foreach ( var artifact in artifacts )
122+ {
123+ // Detect target operating system.
124+ OperatingSystem operatingSystem ;
125+
126+ if ( artifact . FileName . Contains ( "windows." ) )
127+ {
128+ operatingSystem = OperatingSystem . Windows ;
129+ }
130+ else if ( artifact . FileName . Contains ( "linux." ) )
131+ {
132+ operatingSystem = OperatingSystem . Linux ;
133+ }
134+ else if ( artifact . FileName . Contains ( "osx." ) )
135+ {
136+ operatingSystem = OperatingSystem . Osx ;
137+ }
138+ else
139+ {
140+ continue ;
141+ }
142+
143+ // Check if exists in database.
144+ var updateFileEntity = _database . UpdateFileEntities
145+ . FirstOrDefault ( x =>
146+ x . UpdateEntityId == updateEntity . UpdateEntityId &&
147+ x . OperatingSystem == operatingSystem ) ;
148+
149+ if ( updateFileEntity != null ) continue ;
150+
151+ // Calculate the hash of the zip file.
152+ var releaseDownloadUrl = $ "{ artifactsPath } /{ artifact . FileName } ";
153+ var releaseFileName = artifact . FileName . Split ( '/' ) . Last ( ) ;
154+ var releaseZip = Path . Combine ( _config . DataDirectory , ReleaseBranch . ToString ( ) , releaseFileName ) ;
155+ string releaseHash ;
156+
157+ if ( ! File . Exists ( releaseZip ) )
158+ {
159+ Directory . CreateDirectory ( Path . GetDirectoryName ( releaseZip ) ) ;
160+ File . WriteAllBytes ( releaseZip , await _downloadHttpClient . GetByteArrayAsync ( releaseDownloadUrl ) ) ;
161+ }
162+
163+ using ( var stream = File . OpenRead ( releaseZip ) )
164+ {
165+ using ( var sha = SHA256 . Create ( ) )
166+ {
167+ releaseHash = BitConverter . ToString ( sha . ComputeHash ( stream ) ) . Replace ( "-" , "" ) . ToLower ( ) ;
168+ }
169+ }
170+
171+ File . Delete ( releaseZip ) ;
172+
173+ // Add to database.
174+ updateEntity . UpdateFiles . Add ( new UpdateFileEntity
175+ {
176+ OperatingSystem = operatingSystem ,
177+ Filename = releaseFileName ,
178+ Url = releaseDownloadUrl ,
179+ Hash = releaseHash
180+ } ) ;
181+ }
182+
183+ // Save all changes to the database.
184+ await _database . SaveChangesAsync ( ) ;
185+
186+ // Make sure we atleast skip this build next time.
187+ if ( _lastBuildId == null ||
188+ _lastBuildId . Value < build . BuildId )
189+ {
190+ _lastBuildId = build . BuildId ;
191+ }
192+ }
193+ }
194+ }
195+ }
0 commit comments