Automatic feed url detection (let user choose from many)

This commit is contained in:
relikd
2019-01-15 23:46:27 +01:00
parent 55850832d8
commit e4a25a9637
5 changed files with 91 additions and 32 deletions

View File

@@ -1 +1 @@
github "relikd/RSXML" "22189e65048487f31a4db7cec91a0a9a1af88140" github "relikd/RSXML" "7fa835427e61d2745c02d8d779e0223d0ec2effa"

View File

@@ -115,7 +115,6 @@
newOnes += 1; newOnes += 1;
if (hasGapBetweenNewArticles && lastInserted) { // gap with at least one article inbetween if (hasGapBetweenNewArticles && lastInserted) { // gap with at least one article inbetween
lastInserted.unread = NO; lastInserted.unread = NO;
NSLog(@"Ghost item: %@", lastInserted.title);
newOnes -= 1; newOnes -= 1;
} }
hasGapBetweenNewArticles = NO; hasGapBetweenNewArticles = NO;
@@ -125,7 +124,6 @@
} }
if (hasGapBetweenNewArticles && lastInserted) { if (hasGapBetweenNewArticles && lastInserted) {
lastInserted.unread = NO; lastInserted.unread = NO;
NSLog(@"Ghost item: %@", lastInserted.title);
newOnes -= 1; newOnes -= 1;
} }
if (newOnes > 0) if (newOnes > 0)

View File

@@ -33,7 +33,7 @@
+ (void)scheduleUpdateForUpcomingFeeds; + (void)scheduleUpdateForUpcomingFeeds;
+ (void)forceUpdateAllFeeds; + (void)forceUpdateAllFeeds;
// Downloading // Downloading
+ (void)newFeed:(NSString *)urlStr block:(void(^)(RSParsedFeed *feed, NSError *error, NSHTTPURLResponse *response))block; + (void)newFeed:(NSString *)urlStr askUser:(nonnull NSString*(^)(NSArray<RSHTMLMetadataFeedLink*> *list))askUser block:(nonnull void(^)(RSParsedFeed *parsed, NSError *error, NSHTTPURLResponse *response))block;
+ (void)autoDownloadAndParseURL:(NSString*)urlStr; + (void)autoDownloadAndParseURL:(NSString*)urlStr;
+ (void)batchDownloadRSSAndFavicons:(NSArray<Feed*> *)list showErrorAlert:(BOOL)flag rssFinished:(void(^)(NSArray<Feed*> *successful, BOOL *cancelFavicons))blockXml finally:(void(^)(BOOL successful))blockFavicon; + (void)batchDownloadRSSAndFavicons:(NSArray<Feed*> *)list showErrorAlert:(BOOL)flag rssFinished:(void(^)(NSArray<Feed*> *successful, BOOL *cancelFavicons))blockXml finally:(void(^)(BOOL successful))blockFavicon;
+ (void)downloadFavicon:(NSString*)urlStr finished:(void(^)(NSImage * _Nullable img))block ; + (void)downloadFavicon:(NSString*)urlStr finished:(void(^)(NSImage * _Nullable img))block ;

View File

@@ -190,29 +190,13 @@ static BOOL _nextUpdateIsForced = NO;
return req; return req;
} }
/** /// Helper method to start new @c NSURLSession. If @c (http.statusCode==304) then set @c data @c = @c nil.
Start download session of RSS or Atom feed, parse feed and return result on the main thread. + (void)asyncRequest:(NSURLRequest*)request block:(nonnull void(^)(NSData * _Nullable data, NSError * _Nullable error, NSHTTPURLResponse *response))block {
@param block 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 block:(nonnull void(^)(RSParsedFeed *rss, NSError *error, NSHTTPURLResponse *response))block {
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response; NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
if (error || [httpResponse statusCode] == 304) { if (error || [httpResponse statusCode] == 304)
dispatch_async(dispatch_get_main_queue(), ^{ data = nil;
block(nil, error, httpResponse); // error = nil if status == 304 block(data, error, httpResponse); // if status == 304, data & error nil
});
} else {
RSXMLData *xml = [[RSXMLData alloc] initWithData:data urlString:httpResponse.URL.absoluteString];
RSFeedParser *parser = [RSFeedParser parserWithXMLData:xml];
[parser parseAsync:^(RSParsedFeed * _Nullable parsedFeed, NSError * _Nullable err) {
dispatch_async(dispatch_get_main_queue(), ^{
block(parsedFeed, err, httpResponse);
});
}];
}
}] resume]; }] resume];
} }
@@ -221,10 +205,64 @@ static BOOL _nextUpdateIsForced = NO;
/** /**
Perform feed download request from URL alone. Not updating any @c Feed item. 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)newFeed:(NSString *)urlStr block:(void(^)(RSParsedFeed *parsed, NSError *error, NSHTTPURLResponse *response))block { + (void)parseFeedRequest:(NSURLRequest*)request xmlBlock:(nullable BOOL(^)(RSXMLData *xml, NSError **err))xmlBlock feedBlock:(nonnull void(^)(RSParsedFeed *rss, NSError *error, NSHTTPURLResponse *response))feedBlock {
[self parseFeedRequest:[self newRequestURL:urlStr] block:block]; [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 urlString:response.URL.absoluteString];
if (xmlBlock && xmlBlock(xml, &error)) {
return;
}
if (!error) { // metaBlock may set error
RSFeedParser *parser = [RSFeedParser parserWithXMLData:xml];
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. Always before @c block.
@param block Called after webpage has been fully parsed (including html autodetect).
*/
+ (void)newFeed:(NSString *)urlStr askUser:(nonnull NSString*(^)(NSArray<RSHTMLMetadataFeedLink*> *list))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);
return NO;
}
__block NSString *chosenURL = nil;
dispatch_sync(dispatch_get_main_queue(), ^{ // sync! (thread is already in background)
chosenURL = askUser(parsedMeta.feedLinks);
});
if (!chosenURL || chosenURL.length == 0)
return NO;
[self parseFeedRequest:[self newRequestURL:chosenURL] xmlBlock:nil feedBlock:block];
return YES;
} feedBlock:block];
} }
/** /**
@@ -246,7 +284,7 @@ static BOOL _nextUpdateIsForced = NO;
return; return;
} }
dispatch_group_enter(group); dispatch_group_enter(group);
[self parseFeedRequest:[self newRequest:feed.meta] block:^(RSParsedFeed *rss, NSError *error, NSHTTPURLResponse *response) { [self parseFeedRequest:[self newRequest:feed.meta] xmlBlock:nil feedBlock:^(RSParsedFeed *rss, NSError *error, NSHTTPURLResponse *response) {
if (error) { if (error) {
if (alert) [NSApp presentError:error]; if (alert) [NSApp presentError:error];
[feed.meta setErrorAndPostponeSchedule]; [feed.meta setErrorAndPostponeSchedule];
@@ -382,6 +420,7 @@ static BOOL _nextUpdateIsForced = NO;
+ (void)downloadFavicon:(NSString*)urlStr finished:(void(^)(NSImage * _Nullable img))block { + (void)downloadFavicon:(NSString*)urlStr finished:(void(^)(NSImage * _Nullable img))block {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSURL *favURL = [[self hostURL:urlStr] URLByAppendingPathComponent:@"favicon.ico"]; NSURL *favURL = [[self hostURL:urlStr] URLByAppendingPathComponent:@"favicon.ico"];
// TODO: fix anonymous session. initWithContentsOfURL: will set cookie in ~/Library/Cookies/
NSImage *img = [[NSImage alloc] initWithContentsOfURL:favURL]; NSImage *img = [[NSImage alloc] initWithContentsOfURL:favURL];
if (!img || ![img isValid]) if (!img || ![img isValid])
img = nil; img = nil;
@@ -437,10 +476,8 @@ static void networkReachabilityCallback(SCNetworkReachabilityRef target, SCNetwo
_isReachable = [FeedDownload hasConnectivity:flags]; _isReachable = [FeedDownload hasConnectivity:flags];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationNetworkStatusChanged object:@(_isReachable)]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationNetworkStatusChanged object:@(_isReachable)];
if (_isReachable) { if (_isReachable) {
NSLog(@"reachable");
[FeedDownload resumeUpdates]; [FeedDownload resumeUpdates];
} else { } else {
NSLog(@"not reachable");
[FeedDownload pauseUpdates]; [FeedDownload pauseUpdates];
} }
} }

View File

@@ -158,7 +158,9 @@
return; return;
[self preDownload]; [self preDownload];
// TODO: parse webpage to find feed links instead (automatic link detection) // TODO: parse webpage to find feed links instead (automatic link detection)
[FeedDownload newFeed:self.previousURL block:^(RSParsedFeed *result, NSError *error, NSHTTPURLResponse* response) { [FeedDownload newFeed:self.previousURL askUser:^NSString *(NSArray<RSHTMLMetadataFeedLink *> *list) {
return [self letUserChooseXmlUrlFromList:list];
} block:^(RSParsedFeed *result, NSError *error, NSHTTPURLResponse* response) {
if (self.modalSheet.didCloseAndCancel) if (self.modalSheet.didCloseAndCancel)
return; return;
self.didDownloadFeed = YES; self.didDownloadFeed = YES;
@@ -170,6 +172,28 @@
}]; }];
} }
/**
If entered URL happens to be a normal webpage, @c RSXML will parse all suitable feed links.
Present this list to the user and let her decide which one it should be.
@return Either URL string or @c nil if user canceled the selection.
*/
- (NSString*)letUserChooseXmlUrlFromList:(NSArray<RSHTMLMetadataFeedLink*> *)list {
if (list.count == 1) // nothing to choose
return list.firstObject.link;
NSMenu *menu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Choose feed menu", nil)];
menu.autoenablesItems = NO;
for (RSHTMLMetadataFeedLink *fl in list) {
[menu addItemWithTitle:fl.title action:nil keyEquivalent:@""];
}
NSPoint belowURL = NSMakePoint(0,self.url.frame.size.height);
if ([menu popUpMenuPositioningItem:nil atLocation:belowURL inView:self.url]) {
NSInteger idx = [menu indexOfItem:menu.highlightedItem];
return [list objectAtIndex:(NSUInteger)idx].link;
}
return nil; // user selection canceled
}
/** /**
Update UI TextFields with downloaded values. Update UI TextFields with downloaded values.
Title will be updated if TextField is empty. URL on redirect. Title will be updated if TextField is empty. URL on redirect.