@@ -14,10 +14,16 @@ public class PeriodicTasks : BackgroundService
1414 private readonly IServiceScopeFactory _scopeFactory ;
1515 private readonly ILogger < PeriodicTasks > _logger ;
1616 private static HttpClient _httpClient = new HttpClient ( ) ;
17- private static string _orcaHelloDetectionsUri = "https://aifororcasdetections.azurewebsites.net/api/detections?Page=1&SortBy=timestamp&SortOrder=desc&Timeframe=24h&Location=all&RecordsPerPage=1000" ;
18- private static string _orcasiteDetectionsUri = "https://live.orcasound.net/api/json/detections?sort=-timestamp&filter[category]=whale&filter[source]=machine&page[limit]=1" ;
17+
18+ // We really only need to get all that haven't been submitted already,
19+ // but for now we will just get up to 1000 detections from the last week.
20+ // TODO: make URI configurable.
21+ //private static string _orcaHelloGetDetectionsUri = "https://aifororcasdetections.azurewebsites.net/api/detections?Page=1&SortBy=timestamp&SortOrder=desc&Timeframe=24h&Location=all&RecordsPerPage=1000";
22+ private static string _orcaHelloGetDetectionsUri = "https://aifororcasdetections.azurewebsites.net/api/detections?Page=1&SortBy=timestamp&SortOrder=desc&Timeframe=1w&Location=all&RecordsPerPage=1000" ;
23+
24+ private static string _orcasiteGetDetectionsUri = "https://beta.orcasound.net/api/json/detections?sort=-timestamp&filter[category]=whale&filter[source]=machine&page[limit]=10&fields[detection]=id,source_ip,playlist_timestamp,player_offset,listener_count,timestamp,description,visible,source,category,candidate_id,feed_id" ;
1925 private static string _orcasitePostDetectionUri = "https://beta.orcasound.net/api/json/detections?fields%5Bdetection%5D=id%2Csource_ip%2Cplaylist_timestamp%2Cplayer_offset%2Clistener_count%2Ctimestamp%2Cdescription%2Cvisible%2Csource%2Ccategory%2Ccandidate_id%2Cfeed_id" ;
20- private static string _orcasiteFeedsUri = "https://beta.orcasound.net/api/json/feeds?fields%5Bfeed%5D=id%2Cname%2Cnode_name%2Cslug%2Clocation_point%2Cintro_html%2Cimage_url%2Cvisible%2Cbucket%2Cbucket_region%2Ccloudfront_url%2Cdataplicity_id%2Corcahello_id" ;
26+ private static string _orcasiteGetFeedsUri = "https://beta.orcasound.net/api/json/feeds?fields%5Bfeed%5D=id%2Cname%2Cnode_name%2Cslug%2Clocation_point%2Cintro_html%2Cimage_url%2Cvisible%2Cbucket%2Cbucket_region%2Ccloudfront_url%2Cdataplicity_id%2Corcahello_id" ;
2127 private static string ? _orcasiteApiKey ;
2228
2329 public PeriodicTasks ( IServiceScopeFactory scopeFactory , ILogger < PeriodicTasks > logger )
@@ -80,6 +86,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
8086 return arrayElement ;
8187 } catch ( Exception e )
8288 {
89+ _logger . LogError ( $ "Error: { e . Message } ") ;
8390 return null ;
8491 }
8592 }
@@ -99,6 +106,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
99106 }
100107 catch ( Exception e )
101108 {
109+ _logger . LogError ( $ "Error: { e . Message } ") ;
102110 return null ;
103111 }
104112 }
@@ -111,25 +119,23 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
111119
112120 try
113121 {
114- JsonElement ? orcasiteFeedsArray = await GetDataArrayAsync ( _orcasiteFeedsUri ) ;
122+ JsonElement ? orcasiteFeedsArray = await GetDataArrayAsync ( _orcasiteGetFeedsUri ) ;
115123 if ( orcasiteFeedsArray == null )
116124 {
117125 _logger . LogError ( "Failed to retrieve orcasite feeds." ) ;
118126 return ;
119127 }
120128
121- // Query detections from OrcaHello over the past 24 hours.
122- // We really only need to get all that haven't been submitted already,
123- // but for now we will just get up to 1000 detections from the last 24 hours.
124- JsonElement ? orcaHelloDetectionsArray = await GetRawArrayAsync ( _orcaHelloDetectionsUri ) ;
129+ // Query the most recent detections from OrcaHello.
130+ JsonElement ? orcaHelloDetectionsArray = await GetRawArrayAsync ( _orcaHelloGetDetectionsUri ) ;
125131 if ( orcaHelloDetectionsArray == null )
126132 {
127133 _logger . LogError ( "Failed to retrieve orcahello detections." ) ;
128134 return ;
129135 }
130136
131- // Query the last 100 detections already on Orcasite.
132- JsonElement ? orcasiteDetectionsArray = await GetDataArrayAsync ( _orcasiteDetectionsUri ) ;
137+ // Query the most recent detection(s) already on Orcasite.
138+ JsonElement ? orcasiteDetectionsArray = await GetDataArrayAsync ( _orcasiteGetDetectionsUri ) ;
133139 if ( orcasiteDetectionsArray == null )
134140 {
135141 _logger . LogError ( "Failed to retrieve orcasite detections." ) ;
@@ -192,6 +198,11 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
192198 _logger . LogError ( $ "Invalid timestamp kind in ExecuteTask: { timestamp . ValueKind } ") ;
193199 continue ;
194200 }
201+ if ( ! DateTime . TryParse ( timestamp . GetString ( ) , out DateTime dateTime ) )
202+ {
203+ _logger . LogError ( $ "Invalid timestamp ExecuteTask: { timestamp } ") ;
204+ continue ;
205+ }
195206
196207 // Get comments from OrcaHello.
197208 if ( ! orcaHelloDetection . TryGetProperty ( "comments" , out var comments ) )
@@ -201,6 +212,12 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
201212 }
202213 string ? commentsString = comments . ValueKind == JsonValueKind . String ? comments . GetString ( ) : null ;
203214
215+ // See if the detection is already on orcasite.
216+ if ( OrcasiteAlreadyHasDetection ( orcasiteDetectionsArray , dateTime , feedId , commentsString ) )
217+ {
218+ continue ;
219+ }
220+
204221 // Compose a detections post.
205222 JsonObject newDetection = new JsonObject
206223 {
@@ -241,18 +258,18 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
241258 // Optionally handle response
242259 if ( response . IsSuccessStatusCode )
243260 {
244- Console . WriteLine ( "Detection posted successfully!" ) ;
261+ _logger . LogInformation ( $ "Detection for { timestamp } posted successfully!") ;
245262 }
246263 else
247264 {
248265 string message = await response . Content . ReadAsStringAsync ( ) ;
249- Console . WriteLine ( $ "Error: { response . StatusCode } - { message } ") ;
266+ _logger . LogError ( $ "Error: { response . StatusCode } - { message } ") ;
250267 }
251268 }
252269 }
253270 catch ( Exception e )
254271 {
255-
272+ _logger . LogError ( $ "Error: { e . Message } " ) ;
256273 }
257274#if false
258275 OrcanodeMonitorContext context = scope . ServiceProvider . GetRequiredService < OrcanodeMonitorContext > ( ) ;
@@ -269,8 +286,111 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
269286#endif
270287 }
271288
289+ /// <summary>
290+ /// Check whether Orcasite already has a detection with the same timestamp, feed_id, and comments.
291+ /// </summary>
292+ /// <param name="orcasiteDetectionsArray">Most recent detections already on Orcasite</param>
293+ /// <param name="dateTime">Timestamp of OrcaHello detection</param>
294+ /// <param name="feedId">Feed ID of OrcaHello detection</param>
295+ /// <param name="comments">Comments from OrcaHello detection</param>
296+ /// <returns></returns>
297+ private bool OrcasiteAlreadyHasDetection ( JsonElement ? orcasiteDetectionsArray , DateTime dateTime , string feedId , string ? comments )
298+ {
299+ if ( orcasiteDetectionsArray == null )
300+ {
301+ return false ;
302+ }
303+
304+ // Compare against each detection known to orcasite, with newest first.
305+ foreach ( JsonElement detection in orcasiteDetectionsArray . Value . EnumerateArray ( ) )
306+ {
307+ if ( ! detection . TryGetProperty ( "attributes" , out var attributes ) )
308+ {
309+ _logger . LogError ( $ "Missing attributes in Orcasite result") ;
310+ continue ;
311+ }
312+ if ( attributes . ValueKind != JsonValueKind . Object )
313+ {
314+ _logger . LogError ( $ "Invalid attributes kind in ExecuteTask: { attributes . ValueKind } ") ;
315+ continue ;
316+ }
317+
318+ // Get timestamp according to Orcasite.
319+ if ( ! attributes . TryGetProperty ( "timestamp" , out var orcasiteTimestamp ) )
320+ {
321+ _logger . LogError ( $ "Missing timestamp in Orcasite result") ;
322+ continue ;
323+ }
324+ if ( orcasiteTimestamp . ValueKind != JsonValueKind . String )
325+ {
326+ _logger . LogError ( $ "Invalid timestamp kind in ExecuteTask: { orcasiteTimestamp . ValueKind } ") ;
327+ continue ;
328+ }
329+ string orcasiteTimestampString = orcasiteTimestamp . GetString ( ) ?? "" ;
330+ DateTime orcasiteDateTime = DateTime . Parse ( orcasiteTimestampString ) ;
331+ if ( orcasiteDateTime != dateTime )
332+ {
333+ if ( dateTime < orcasiteDateTime )
334+ {
335+ // The new detection is older than the latest on orcasite,
336+ // so just assume that orcasite already has it.
337+ return true ;
338+ }
339+ continue ;
340+ }
341+
342+ // Get description according to Orcasite.
343+ if ( ! attributes . TryGetProperty ( "description" , out var description ) )
344+ {
345+ _logger . LogError ( $ "Missing description in Orcasite result") ;
346+ continue ;
347+ }
348+ if ( description . ValueKind != JsonValueKind . String )
349+ {
350+ _logger . LogError ( $ "Invalid description kind in ExecuteTask: { description . ValueKind } ") ;
351+ continue ;
352+ }
353+ string ? orcasiteDescriptionString = description . GetString ( ) ;
354+ if ( orcasiteDescriptionString != comments )
355+ {
356+ continue ;
357+ }
358+
359+ // Get feed_id according to Orcasite.
360+ if ( ! attributes . TryGetProperty ( "feed_id" , out var orcasiteFeedId ) )
361+ {
362+ _logger . LogError ( $ "Missing feed_id in Orcasite result") ;
363+ continue ;
364+ }
365+ if ( orcasiteFeedId . ValueKind != JsonValueKind . String )
366+ {
367+ _logger . LogError ( $ "Invalid feed_id kind in ExecuteTask: { orcasiteFeedId . ValueKind } ") ;
368+ continue ;
369+ }
370+ string orcasiteFeedIdString = orcasiteFeedId . GetString ( ) ?? "" ;
371+ if ( orcasiteFeedIdString != feedId )
372+ {
373+ continue ;
374+ }
375+
376+ return true ;
377+ }
378+ return false ;
379+ }
380+
272381 private string ? GetFeedId ( string nameToFind , JsonElement feedsArray )
273382 {
383+ // Implement some aliases for backwards compatibility since OrcaHello uses
384+ // its own strings.
385+ if ( nameToFind == "North SJC" )
386+ {
387+ nameToFind = "North San Juan Channel" ;
388+ }
389+ else if ( nameToFind == "Haro Strait" )
390+ {
391+ nameToFind = "Orcasound Lab" ;
392+ }
393+
274394 foreach ( JsonElement feed in feedsArray . EnumerateArray ( ) )
275395 {
276396 if ( ! feed . TryGetProperty ( "attributes" , out var attributes ) )
0 commit comments