Separate FeedDownload into UpdateScheduler & WebFeed

This commit is contained in:
relikd
2019-08-11 20:23:32 +02:00
parent a1f191789d
commit 48578ea211
13 changed files with 317 additions and 228 deletions

View File

@@ -1,61 +0,0 @@
//
// The MIT License (MIT)
// Copyright (c) 2018 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import <Cocoa/Cocoa.h>
#import <RSXML/RSXML.h>
@class Feed;
@interface FeedDownload : NSObject
@property (class, readonly) NSDate *dateScheduled;
@property (class, readonly) BOOL allowNetworkConnection;
@property (class, readonly) BOOL isUpdating;
@property (class, setter=setPaused:) BOOL isPaused;
// Register for network change notifications
+ (void)registerNetworkChangeNotification;
+ (void)unregisterNetworkChangeNotification;
// Scheduling
+ (void)scheduleUpdateForUpcomingFeeds;
+ (void)forceUpdateAllFeeds;
// Downloading
+ (void)newFeed:(NSString *)urlStr askUser:(nonnull NSString*(^)(RSHTMLMetadata *meta))askUser block:(nonnull void(^)(RSParsedFeed *parsed, NSError *error, NSHTTPURLResponse *response))block;
+ (void)autoDownloadAndParseURL:(NSString*)urlStr addAnyway:(BOOL)flag modify:(nullable void(^)(Feed *feed))block;
+ (void)autoDownloadAndParseUpdateURL;
+ (void)batchDownloadFeeds:(NSArray<Feed*> *)list favicons:(BOOL)fav showErrorAlert:(BOOL)alert finally:(nullable os_block_t)block;
// Favicon image download
+ (void)downloadFavicon:(NSString*)urlStr finished:(void(^)(NSImage * _Nullable img))block;
+ (void)downloadImage:(NSString*)url finished:(void(^)(NSImage * _Nullable img))block;
+ (nullable NSString*)faviconUrlForMetadata:(RSHTMLMetadata*)meta;
@end
/*
Developer Tip, error logs see:
Task <..> HTTP load failed (error code: -1003 [12:8])
Task <..> finished with error - code: -1003
==> NSURLErrorCannotFindHost in #import <Foundation/NSURLError.h>
TIC TCP Conn Failed [21:0x1d417fb00]: 1:65 Err(65)
==> EHOSTUNREACH in #import <sys/errno.h>
*/

View File

@@ -1,584 +0,0 @@
//
// The MIT License (MIT)
// Copyright (c) 2018 Oleg Geier
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#import "FeedDownload.h"
#import "Constants.h"
#import "StoreCoordinator.h"
#import "Feed+Ext.h"
#import "FeedMeta+Ext.h"
#import "FeedGroup+Ext.h"
#import "NSDate+Ext.h"
#import <SystemConfiguration/SystemConfiguration.h>
static NSTimer *_timer;
static SCNetworkReachabilityRef _reachability = NULL;
static BOOL _isReachable = NO;
static BOOL _isUpdating = NO;
static BOOL _updatePaused = NO;
static BOOL _nextUpdateIsForced = NO;
@implementation FeedDownload
#pragma mark - User Interaction -
/// @return Date when background update will fire. If updates are paused, date is @c distantFuture.
+ (NSDate *)dateScheduled { return _timer.fireDate; }
/// @return @c YES if current network state is reachable and updates are not paused by user.
+ (BOOL)allowNetworkConnection { return (_isReachable && !_updatePaused); }
/// @return @c YES if batch update is running
+ (BOOL)isUpdating { return _isUpdating; }
/// @return @c YES if update is paused by user.
+ (BOOL)isPaused { return _updatePaused; }
/// Set paused flag and cancel timer regardless of network connectivity.
+ (void)setPaused:(BOOL)flag {
_updatePaused = flag;
if (_updatePaused)
[self pauseUpdates];
else
[self resumeUpdates];
}
/// Cancel current timer and stop any updates until enabled again.
+ (void)pauseUpdates {
[self scheduleTimer:nil];
}
/// Start normal (non forced) schedule if network is reachable.
+ (void)resumeUpdates {
if (_isReachable)
[self scheduleUpdateForUpcomingFeeds];
}
#pragma mark - Update Feed Timer -
/**
Get date of next up feed and start the timer.
*/
+ (void)scheduleUpdateForUpcomingFeeds {
if (![self allowNetworkConnection]) // timer will restart once connection exists
return;
NSDate *nextTime = [StoreCoordinator nextScheduledUpdate]; // if nextTime = nil, then no feeds to update
if (nextTime && [nextTime timeIntervalSinceNow] < 1) { // mostly, if app was closed for a long time
nextTime = [NSDate dateWithTimeIntervalSinceNow:1];
}
[self scheduleTimer:nextTime];
}
/**
Start download of all feeds (immediatelly) regardless of @c .scheduled property.
*/
+ (void)forceUpdateAllFeeds {
if (![self allowNetworkConnection]) // timer will restart once connection exists
return;
_nextUpdateIsForced = YES;
[self scheduleTimer:[NSDate dateWithTimeIntervalSinceNow:0.05]];
}
/**
Set new @c .fireDate and @c .tolerance for update timer.
@param nextTime If @c nil timer will be disabled with a @c .fireDate very far in the future.
*/
+ (void)scheduleTimer:(NSDate*)nextTime {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_timer = [NSTimer timerWithTimeInterval:NSTimeIntervalSince1970 target:[self class] selector:@selector(updateTimerCallback) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
});
if (!nextTime)
nextTime = [NSDate distantFuture];
NSTimeInterval tolerance = [nextTime timeIntervalSinceNow] * 0.15;
_timer.tolerance = (tolerance < 1 ? 1 : tolerance); // at least 1 sec
_timer.fireDate = nextTime;
}
/**
Called when schedule timer runs out (earliest @c .schedule date). Or if forced by user request.
*/
+ (void)updateTimerCallback {
#ifdef DEBUG
NSLog(@"fired");
#endif
BOOL updateAll = _nextUpdateIsForced;
_nextUpdateIsForced = NO;
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
NSArray<Feed*> *list = [StoreCoordinator getListOfFeedsThatNeedUpdate:updateAll inContext:moc];
//NSAssert(list.count > 0, @"ERROR: Something went wrong, timer fired too early.");
if (![self allowNetworkConnection]) {
[moc reset];
return;
}
[self batchDownloadFeeds:list favicons:updateAll showErrorAlert:NO finally:^{
[StoreCoordinator saveContext:moc andParent:YES]; // save parents too ...
[moc reset];
[self resumeUpdates]; // always reset the timer
}];
}
#pragma mark - Request Generator -
/// @return Base URL part. E.g., https://stackoverflow.com/a/15897956/10616114 ==> https://stackoverflow.com/
+ (NSURL*)hostURL:(NSString*)urlStr {
return [[NSURL URLWithString:@"/" relativeToURL:[self fixURL:urlStr]] absoluteURL];
}
/// Check if any scheme is set. If not, prepend 'http://'.
+ (NSURL*)fixURL:(NSString*)urlStr {
NSURL *url = [NSURL URLWithString:urlStr];
if (!url.scheme) {
url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@", urlStr]]; // usually will redirect to https if necessary
}
return url;
}
/// @return New request with no caching policy and timeout interval of 30 seconds.
+ (NSMutableURLRequest*)newRequestURL:(NSString*)urlStr {
return [NSMutableURLRequest requestWithURL:[self fixURL:urlStr]];
}
/// @return New request with etag and modified headers set (or not, if @c flag @c == @c YES ).
+ (NSURLRequest*)newRequest:(FeedMeta*)meta ignoreCache:(BOOL)flag {
NSMutableURLRequest *req = [self newRequestURL:meta.url];
if (!flag) {
if (meta.etag.length > 0)
[req setValue:meta.etag forHTTPHeaderField:@"If-None-Match"]; // ETag
else if (meta.modified.length > 0)
[req setValue:meta.modified forHTTPHeaderField:@"If-Modified-Since"];
}
if (!_nextUpdateIsForced) // any request that is not forced, is a background update
req.networkServiceType = NSURLNetworkServiceTypeBackground;
return req;
}
+ (NSURLSession*)nonCachingSession {
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *conf = [NSURLSessionConfiguration defaultSessionConfiguration];
conf.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicyNever;
conf.HTTPShouldSetCookies = NO;
conf.HTTPCookieStorage = nil; // disables '~/Library/Cookies/'
conf.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
conf.URLCache = nil; // disables '~/Library/Caches/de.relikd.baRSS/'
conf.HTTPAdditionalHeaders = @{ @"User-Agent": @"baRSS (macOS)",
@"Accept-Encoding": @"gzip" };
session = [NSURLSession sessionWithConfiguration:conf];
});
return session; // [NSURLSession sharedSession];
}
/// Helper method to start new @c NSURLSession. If @c (http.statusCode==304) then set @c data @c = @c nil.
+ (void)asyncRequest:(NSURLRequest*)request block:(nonnull void(^)(NSData * _Nullable data, NSError * _Nullable error, NSHTTPURLResponse *response))block {
[[[self nonCachingSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
NSInteger status = [httpResponse statusCode];
if (error || status == 304) { // 304 Not Modified
data = nil;
} else if (status >= 500 && status < 600) { // 5xx Server Error
NSString *reason = [NSString stringWithFormat:NSLocalizedString(@"Server HTTP error %ld.\n\n%@", nil),
status, [self extractReadableHTML:data]];
error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorBadServerResponse userInfo:@{NSLocalizedDescriptionKey: reason}];
data = nil;
}
block(data, error, httpResponse); // if status == 304, data & error nil
}] resume];
}
/// Helper method to extract readable text from HTML
+ (NSString*)extractReadableHTML:(NSData*)data {
NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
// replace all <tags> with (presumably) non-used character
str = [[NSRegularExpression regularExpressionWithPattern:@"<[^>]*>\\s*" options:kNilOptions error:nil]
stringByReplacingMatchesInString:str options:kNilOptions range:NSMakeRange(0, str.length) withTemplate:@"◊"];
// then replace multiple occurences of that character with a single new line
str = [[NSRegularExpression regularExpressionWithPattern:@"◊+" options:kNilOptions error:nil]
stringByReplacingMatchesInString:str options:kNilOptions range:NSMakeRange(0, str.length) withTemplate:@"\n"];
// finally trim whitespace at start and end
return [str stringByTrimmingCharactersInSet: NSCharacterSet.whitespaceAndNewlineCharacterSet];
}
#pragma mark - Download RSS Feed -
/**
Start download session of RSS or Atom feed, parse feed and return result on the main thread.
@param xmlBlock Called immediately after @c RSXMLData is initialized. E.g., to use this data as HTML parser.
Return @c YES to to exit without calling @c feedBlock.
If @c NO and @c err @c != @c nil skip feed parsing and call @c feedBlock(nil,err,response).
@param feedBlock Called when parsing finished or an @c NSURL error occured.
If content did not change (status code 304) both, error and result will be @c nil.
Will be called on main thread.
*/
+ (void)parseFeedRequest:(NSURLRequest*)request xmlBlock:(nullable BOOL(^)(RSXMLData *xml, NSError **err))xmlBlock feedBlock:(nonnull void(^)(RSParsedFeed *rss, NSError *error, NSHTTPURLResponse *response))feedBlock {
[self asyncRequest:request block:^(NSData * _Nullable data, NSError * _Nullable error, NSHTTPURLResponse *response) {
RSParsedFeed *result = nil;
if (data) { // data = nil if (error || 304)
RSXMLData *xml = [[RSXMLData alloc] initWithData:data url:response.URL];
if (xmlBlock && xmlBlock(xml, &error)) {
return;
}
if (!error) { // metaBlock may set error
RSFeedParser *parser = [RSFeedParser parserWithXMLData:xml];
parser.dontStopOnLowerAsciiBytes = YES;
result = [parser parseSync:&error];
}
}
dispatch_async(dispatch_get_main_queue(), ^{
feedBlock(result, error, response);
});
}];
}
/**
Perform feed download request from URL alone. Not updating any @c Feed item.
@note @c askUser will not be called if url is XML already.
@param urlStr XML URL or HTTP URL that will be parsed to find feed URLs.
@param askUser Use @c list to present user a list of detected feed URLs.
@param block Called after webpage has been fully parsed (including html autodetect).
*/
+ (void)newFeed:(NSString *)urlStr askUser:(nonnull NSString*(^)(RSHTMLMetadata *meta))askUser block:(nonnull void(^)(RSParsedFeed *parsed, NSError *error, NSHTTPURLResponse *response))block {
[self parseFeedRequest:[self newRequestURL:urlStr] xmlBlock:^BOOL(RSXMLData *xml, NSError **err) {
if (![xml.parserClass isHTMLParser])
return NO;
RSHTMLMetadataParser *parser = [RSHTMLMetadataParser parserWithXMLData:xml];
RSHTMLMetadata *parsedMeta = [parser parseSync:err];
if (*err)
return NO;
if (!parsedMeta || parsedMeta.feedLinks.count == 0) {
*err = RSXMLMakeErrorWrongParser(RSXMLErrorExpectingFeed, RSXMLErrorExpectingHTML, xml.url);
return NO;
}
__block NSString *chosenURL = nil;
dispatch_sync(dispatch_get_main_queue(), ^{ // sync! (thread is already in background)
chosenURL = askUser(parsedMeta);
});
if (!chosenURL || chosenURL.length == 0) { // User canceled operation, show appropriate error message
NSString *reason = NSLocalizedString(@"Operation canceled.", nil);
*err = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:@{NSLocalizedDescriptionKey: reason}];
return NO;
}
[self parseFeedRequest:[self newRequestURL:chosenURL] xmlBlock:nil feedBlock:block];
return YES;
} feedBlock:block];
}
/**
Start download request with existing @c Feed object. Reuses etag and modified headers (unless articles count is 0).
@note Will post a @c kNotificationFeedUpdated notification if download was successful and @b not status code 304.
@param alert If @c YES display Error Popup to user.
@param block Parameter @c success is only @c YES if download was successful or if status code is 304 (not modified).
*/
+ (void)backgroundUpdateFeed:(Feed*)feed showErrorAlert:(BOOL)alert finally:(nullable void(^)(BOOL success))block {
NSManagedObjectID *oid = feed.objectID;
NSManagedObjectContext *moc = feed.managedObjectContext;
NSURLRequest *req = [self newRequest:feed.meta ignoreCache:(feed.articles.count == 0)];
NSString *reqURL = req.URL.absoluteString;
[self parseFeedRequest:req xmlBlock:nil feedBlock:^(RSParsedFeed *rss, NSError *error, NSHTTPURLResponse *response) {
Feed *f = [moc objectWithID:oid];
BOOL success = NO;
BOOL needsNotification = NO;
if (error) {
if (alert) {
NSAlert *alertPopup = [NSAlert alertWithError:error];
alertPopup.informativeText = [NSString stringWithFormat:@"Error loading source: %@", reqURL];
[alertPopup runModal];
}
[f.meta setErrorAndPostponeSchedule];
} else {
success = YES;
[f.meta setSucessfulWithResponse:response];
if (rss && rss.articles.count > 0) {
[f updateWithRSS:rss postUnreadCountChange:YES];
needsNotification = YES;
}
}
[StoreCoordinator saveContext:moc andParent:YES];
if (needsNotification)
PostNotification(kNotificationFeedUpdated, oid);
if (block) block(success);
}];
}
/**
Download feed at url and append to persistent store in root folder.
On error present user modal alert.
Creates new @c FeedGroup, @c Feed, @c FeedMeta and @c FeedArticle instances and saves them to the persistent store.
Update duration is set to the default of 30 minutes.
*/
+ (void)autoDownloadAndParseURL:(NSString*)url addAnyway:(BOOL)flag modify:(nullable void(^)(Feed *feed))block {
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
Feed *f = [Feed appendToRootWithDefaultIntervalInContext:moc];
f.meta.url = url;
[self backgroundUpdateBoth:f favicon:YES alert:!flag finally:^(BOOL successful){
if (!flag && !successful) {
[moc deleteObject:f.group];
} else if (block) {
block(f); // only on success
}
[StoreCoordinator saveContext:moc andParent:YES];
[moc reset];
if (successful) {
PostNotification(kNotificationGroupInserted, f.group.objectID);
[self scheduleUpdateForUpcomingFeeds];
}
}];
}
/// Insert Github URL for version releases with update interval 2 days and rename @c FeedGroup item.
+ (void)autoDownloadAndParseUpdateURL {
[self autoDownloadAndParseURL:versionUpdateURL addAnyway:YES modify:^(Feed *feed) {
feed.group.name = NSLocalizedString(@"baRSS releases", nil);
[feed.meta setRefreshAndSchedule:2 * TimeUnitDays];
}];
}
/**
Start download of feed xml, then continue with favicon download (optional).
@param fav If @c YES continue with favicon download after xml download finished.
@param alert If @c YES display Error Popup to user.
@param block Parameter @c success is @c YES if xml download succeeded (regardless of favicon result).
*/
+ (void)backgroundUpdateBoth:(Feed*)feed favicon:(BOOL)fav alert:(BOOL)alert finally:(nullable void(^)(BOOL success))block {
[self backgroundUpdateFeed:feed showErrorAlert:alert finally:^(BOOL success) {
if (fav && success) {
[self backgroundUpdateFavicon:feed replaceExisting:NO finally:^{
if (block) block(YES);
}];
} else {
if (block) block(success);
}
}];
}
/**
Start download of all feeds in list. Either with or without favicons.
@param list Download list using @c feed.meta.url as download url. (while reusing etag and modified headers)
@param fav If @c YES continue with favicon download after xml download finished.
@param alert If @c YES display Error Popup to user.
@param block Called after all downloads finished.
*/
+ (void)batchDownloadFeeds:(NSArray<Feed*> *)list favicons:(BOOL)fav showErrorAlert:(BOOL)alert finally:(nullable os_block_t)block {
_isUpdating = YES;
PostNotification(kNotificationBackgroundUpdateInProgress, @(list.count));
dispatch_group_t group = dispatch_group_create();
for (Feed *f in list) {
dispatch_group_enter(group);
[self backgroundUpdateBoth:f favicon:fav alert:alert finally:^(BOOL success){
dispatch_group_leave(group);
}];
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
if (block) block();
_isUpdating = NO;
PostNotification(kNotificationBackgroundUpdateInProgress, @(0));
});
}
#pragma mark - Download Favicon -
/**
Start favicon download request on existing @c Feed object.
@note Will post a @c kNotificationFeedIconUpdated notification if icon was updated.
@param overwrite If @c YES and icon is present already, @c block will return immediatelly.
*/
+ (void)backgroundUpdateFavicon:(Feed*)feed replaceExisting:(BOOL)overwrite finally:(nullable os_block_t)block {
if (!overwrite && feed.icon != nil) {
if (block) block();
return; // skip existing icons if replace == NO
}
NSManagedObjectID *oid = feed.objectID;
NSManagedObjectContext *moc = feed.managedObjectContext;
NSString *faviconURL = (feed.link.length > 0 ? feed.link : feed.meta.url);
[self downloadFavicon:faviconURL finished:^(NSImage *img) {
Feed *f = [moc objectWithID:oid];
if (f && [f setIconImage:img]) {
[StoreCoordinator saveContext:moc andParent:YES];
PostNotification(kNotificationFeedIconUpdated, oid);
}
if (block) block();
}];
}
/// Download favicon located at http://.../ @c favicon.ico. Callback @c block will be called on main thread.
+ (void)downloadFavicon:(NSString*)urlStr finished:(void(^)(NSImage * _Nullable img))block {
NSURL *host = [self hostURL:urlStr];
NSString *hostURL = host.absoluteString;
NSString *favURL = [host URLByAppendingPathComponent:@"favicon.ico"].absoluteString;
[self downloadImage:favURL finished:^(NSImage * _Nullable img) {
if (img) {
block(img); // is on main already (from downloadImage:)
} else {
[self downloadFaviconByParsingHTML:hostURL finished:block];
}
}];
}
/// Download html page and parse all icon urls. Starting a successive request on the url of the smallest icon.
+ (void)downloadFaviconByParsingHTML:(NSString*)hostURL finished:(void(^)(NSImage * _Nullable img))block {
[self asyncRequest:[self newRequestURL:hostURL] block:^(NSData * _Nullable htmlData, NSError * _Nullable error, NSHTTPURLResponse *response) {
if (htmlData) {
// TODO: use session delegate to stop downloading after <head>
RSXMLData *xml = [[RSXMLData alloc] initWithData:htmlData url:response.URL];
RSHTMLMetadataParser *parser = [RSHTMLMetadataParser parserWithXMLData:xml];
RSHTMLMetadata *meta = [parser parseSync:&error];
if (error) meta = nil;
NSString *iconURL = [self faviconUrlForMetadata:meta];
if (iconURL) {
// if everything went well we can finally start a request on the url we found.
[self downloadImage:iconURL finished:block];
return;
}
}
dispatch_async(dispatch_get_main_queue(), ^{ block(nil); }); // on failure
}];
}
/// Extract favicon URL from parsed HTML metadata.
+ (nullable NSString*)faviconUrlForMetadata:(RSHTMLMetadata*)meta {
if (meta) {
if (meta.faviconLink.length > 0) {
return meta.faviconLink;
}
else if (meta.iconLinks.count > 0) {
// at least any url (even if all items in list have size 0)
NSString *iconURL = meta.iconLinks.firstObject.link;
double best = DBL_MAX;
for (RSHTMLMetadataIconLink *icon in meta.iconLinks) {
CGSize size = [icon getSize];
CGFloat area = size.width * size.height;
if (area > 0) {
// find icon with closest matching size 32x32
double match = fabs(log10(area) - log10(32*32));
if (match < best) {
best = match;
iconURL = icon.link;
}
}
}
if (iconURL && iconURL.length > 0)
return iconURL;
}
}
return nil;
}
/// Download image in a background thread and notify once finished.
+ (void)downloadImage:(NSString*)url finished:(void(^)(NSImage * _Nullable img))block {
[self asyncRequest:[self newRequestURL:url] block:^(NSData * _Nullable data, NSError * _Nullable e, NSHTTPURLResponse *r) {
NSImage *img = [[NSImage alloc] initWithData:data];
if (!img || ![img isValid])
img = nil;
// if (img.size.width > 16 || img.size.height > 16) {
// NSImage *smallImage = [NSImage imageWithSize:NSMakeSize(16, 16) flipped:NO drawingHandler:^BOOL(NSRect dstRect) {
// [img drawInRect:dstRect];
// return YES;
// }];
// if (img.TIFFRepresentation.length > smallImage.TIFFRepresentation.length)
// img = smallImage;
// }
dispatch_async(dispatch_get_main_queue(), ^{ block(img); });
}];
}
#pragma mark - Network Connection & Reachability -
/// Set callback on @c self to listen for network reachability changes.
+ (void)registerNetworkChangeNotification {
// https://stackoverflow.com/questions/11240196/notification-when-wifi-connected-os-x
if (_reachability != NULL) return;
_reachability = SCNetworkReachabilityCreateWithName(NULL, "1.1.1.1");
if (_reachability == NULL) return;
// If reachability information is available now, we don't get a callback later
SCNetworkConnectionFlags flags;
if (SCNetworkReachabilityGetFlags(_reachability, &flags))
networkReachabilityCallback(_reachability, flags, NULL);
if (!SCNetworkReachabilitySetCallback(_reachability, networkReachabilityCallback, NULL) ||
!SCNetworkReachabilityScheduleWithRunLoop(_reachability, [[NSRunLoop currentRunLoop] getCFRunLoop], kCFRunLoopCommonModes))
{
CFRelease(_reachability);
_reachability = NULL;
}
}
/// Remove @c self callback (network reachability changes).
+ (void)unregisterNetworkChangeNotification {
if (_reachability != NULL) {
SCNetworkReachabilitySetCallback(_reachability, nil, nil);
SCNetworkReachabilitySetDispatchQueue(_reachability, nil);
CFRelease(_reachability);
_reachability = NULL;
}
}
/// Called when network interface or reachability changes.
static void networkReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkConnectionFlags flags, void *object) {
if (_reachability == NULL) return;
_isReachable = [FeedDownload hasConnectivity:flags];
PostNotification(kNotificationNetworkStatusChanged, @(_isReachable));
if (_isReachable) {
[FeedDownload resumeUpdates];
} else {
[FeedDownload pauseUpdates];
}
}
/// @return @c YES if network connection established.
+ (BOOL)hasConnectivity:(SCNetworkReachabilityFlags)flags {
if ((flags & kSCNetworkReachabilityFlagsReachable) == 0)
return NO;
if ((flags & kSCNetworkReachabilityFlagsConnectionRequired) == 0)
return YES;
if ((flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0 &&
((flags & kSCNetworkReachabilityFlagsConnectionOnDemand) != 0 ||
(flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0))
return YES; // no-intervention AND ( on-demand OR on-traffic )
return NO;
}
@end