diff --git a/Frameworks/Account/FeedFinder/FeedFinder.swift b/Frameworks/Account/FeedFinder/FeedFinder.swift index 89ef24602..c6c2f660e 100644 --- a/Frameworks/Account/FeedFinder/FeedFinder.swift +++ b/Frameworks/Account/FeedFinder/FeedFinder.swift @@ -11,38 +11,54 @@ import RSParser import RSWeb import RSCore -protocol FeedFinderDelegate: class { - - func feedFinder(_: FeedFinder, didFindFeeds: Set) -} - class FeedFinder { + + static func find(url: URL, completion: @escaping (Result, Error>) -> Void) { - private weak var delegate: FeedFinderDelegate? - private var feedSpecifiers = [String: FeedSpecifier]() - private var didNotifyDelegate = false - - var initialDownloadError: Error? - var initialDownloadStatusCode = -1 - - init(url: URL, delegate: FeedFinderDelegate) { - - self.delegate = delegate - - DispatchQueue.main.async() { () -> Void in - - self.findFeeds(url) + downloadUsingCache(url) { (data, response, error) in + + if response?.forcedStatusCode == 404 { + completion(.failure(AccountError.createErrorNotFound)) + return + } + + if let error = error { + completion(.failure(error)) + return + } + + guard let data = data, let response = response else { + completion(.failure(AccountError.createErrorNotFound)) + return + } + + if !response.statusIsOK || data.isEmpty { + completion(.failure(AccountError.createErrorNotFound)) + return + } + + if FeedFinder.isFeed(data, url.absoluteString) { + let feedSpecifier = FeedSpecifier(title: nil, urlString: url.absoluteString, source: .UserEntered) + completion(.success(Set([feedSpecifier]))) + return + } + + if !FeedFinder.isHTML(data) { + completion(.failure(AccountError.createErrorNotFound)) + return + } + + FeedFinder.findFeedsInHTMLPage(htmlData: data, urlString: url.absoluteString, completion: completion) + } + } - deinit { - notifyDelegateIfNeeded() - } } private extension FeedFinder { - func addFeedSpecifier(_ feedSpecifier: FeedSpecifier) { + static func addFeedSpecifier(_ feedSpecifier: FeedSpecifier, feedSpecifiers: inout [String: FeedSpecifier]) { // If there’s an existing feed specifier, merge the two so that we have the best data. If one has a title and one doesn’t, use that non-nil title. Use the better source. @@ -55,7 +71,7 @@ private extension FeedFinder { } } - func findFeedsInHTMLPage(htmlData: Data, urlString: String) { + static func findFeedsInHTMLPage(htmlData: Data, urlString: String, completion: @escaping (Result, Error>) -> Void) { // Feeds in the section we automatically assume are feeds. // If there are none from the section, @@ -63,31 +79,35 @@ private extension FeedFinder { // and added once we determine they are feeds. let possibleFeedSpecifiers = possibleFeedsInHTMLPage(htmlData: htmlData, urlString: urlString) + var feedSpecifiers = [String: FeedSpecifier]() var feedSpecifiersToDownload = Set() var didFindFeedInHTMLHead = false for oneFeedSpecifier in possibleFeedSpecifiers { if oneFeedSpecifier.source == .HTMLHead { - addFeedSpecifier(oneFeedSpecifier) + addFeedSpecifier(oneFeedSpecifier, feedSpecifiers: &feedSpecifiers) didFindFeedInHTMLHead = true } else { - if !feedSpecifiersContainsURLString(oneFeedSpecifier.urlString) { + if feedSpecifiers[oneFeedSpecifier.urlString] == nil { feedSpecifiersToDownload.insert(oneFeedSpecifier) } } } - if didFindFeedInHTMLHead || feedSpecifiersToDownload.isEmpty { - stopFinding() - } - else { - downloadFeedSpecifiers(feedSpecifiersToDownload) + if didFindFeedInHTMLHead { + completion(.success(Set(feedSpecifiers.values))) + return + } else if feedSpecifiersToDownload.isEmpty { + completion(.failure(AccountError.createErrorNotFound)) + return + } else { + downloadFeedSpecifiers(feedSpecifiersToDownload, feedSpecifiers: feedSpecifiers, completion: completion) } } - func possibleFeedsInHTMLPage(htmlData: Data, urlString: String) -> Set { + static func possibleFeedsInHTMLPage(htmlData: Data, urlString: String) -> Set { let parserData = ParserData(url: urlString, data: htmlData) var feedSpecifiers = HTMLFeedFinder(parserData: parserData).feedSpecifiers @@ -109,105 +129,42 @@ private extension FeedFinder { return feedSpecifiers } - func feedSpecifiersContainsURLString(_ urlString: String) -> Bool { - - if let _ = feedSpecifiers[urlString] { - return true - } - return false - } - - func isHTML(_ data: Data) -> Bool { - + static func isHTML(_ data: Data) -> Bool { return (data as NSData).rs_dataIsProbablyHTML() } - func findFeeds(_ initialURL: URL) { + static func downloadFeedSpecifiers(_ downloadFeedSpecifiers: Set, feedSpecifiers: [String: FeedSpecifier], completion: @escaping (Result, Error>) -> Void) { - downloadInitialFeed(initialURL) - } + var resultFeedSpecifiers = feedSpecifiers + let group = DispatchGroup() + + for downloadFeedSpecifier in downloadFeedSpecifiers { - func downloadInitialFeed(_ initialURL: URL) { - - downloadUsingCache(initialURL) { (data, response, error) in - - self.initialDownloadStatusCode = response?.forcedStatusCode ?? -1 - - if let error = error { - self.initialDownloadError = error - self.stopFinding() - return - } - guard let data = data, let response = response else { - self.stopFinding() - return - } - - if !response.statusIsOK || data.isEmpty { - self.stopFinding() - return - } - - if self.isFeed(data, initialURL.absoluteString) { - let feedSpecifier = FeedSpecifier(title: nil, urlString: initialURL.absoluteString, source: .UserEntered) - self.addFeedSpecifier(feedSpecifier) - self.stopFinding() - return - } - - if !self.isHTML(data) { - self.stopFinding() - return - } - - self.findFeedsInHTMLPage(htmlData: data, urlString: initialURL.absoluteString) - } - } - - func downloadFeedSpecifiers(_ feedSpecifiers: Set) { - - var pendingDownloads = feedSpecifiers - - for oneFeedSpecifier in feedSpecifiers { - - guard let url = URL(string: oneFeedSpecifier.urlString) else { - pendingDownloads.remove(oneFeedSpecifier) + guard let url = URL(string: downloadFeedSpecifier.urlString) else { continue } - + + group.enter() downloadUsingCache(url) { (data, response, error) in - - pendingDownloads.remove(oneFeedSpecifier) - if let data = data, let response = response, response.statusIsOK, error == nil { - if self.isFeed(data, oneFeedSpecifier.urlString) { - self.addFeedSpecifier(oneFeedSpecifier) + if self.isFeed(data, downloadFeedSpecifier.urlString) { + addFeedSpecifier(downloadFeedSpecifier, feedSpecifiers: &resultFeedSpecifiers) } } - - if pendingDownloads.isEmpty { - self.stopFinding() - } + group.leave() } + } - } - func stopFinding() { - - notifyDelegateIfNeeded() - } - - func notifyDelegateIfNeeded() { - - if !didNotifyDelegate { - delegate?.feedFinder(self, didFindFeeds: Set(feedSpecifiers.values)) - didNotifyDelegate = true + group.notify(queue: DispatchQueue.main) { + completion(.success(Set(resultFeedSpecifiers.values))) } + } - func isFeed(_ data: Data, _ urlString: String) -> Bool { - + static func isFeed(_ data: Data, _ urlString: String) -> Bool { let parserData = ParserData(url: urlString, data: data) return FeedParser.canParse(parserData) } + } diff --git a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift index d0d4c5435..aab4c1f56 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift @@ -26,12 +26,6 @@ final class LocalAccountDelegate: AccountDelegate { var credentials: Credentials? var accountMetadata: AccountMetadata? - private weak var account: Account? - private var feedFinder: FeedFinder? - private var createFeedName: String? - private var createFeedContainer: Container? - private var createFeedCompletion: ((Result) -> Void)? - private let refresher = LocalAccountRefresher() var refreshProgress: DownloadProgress { @@ -99,11 +93,42 @@ final class LocalAccountDelegate: AccountDelegate { return } - self.account = account - createFeedName = name - createFeedContainer = container - createFeedCompletion = completion - feedFinder = FeedFinder(url: url, delegate: self) + FeedFinder.find(url: url) { result in + + switch result { + case .success(let feedSpecifiers): + + guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers), + let url = URL(string: bestFeedSpecifier.urlString) else { + completion(.failure(AccountError.createErrorNotFound)) + return + } + + if account.hasFeed(withURL: bestFeedSpecifier.urlString) { + completion(.failure(AccountError.createErrorAlreadySubscribed)) + return + } + + let feed = account.createFeed(with: nil, url: url.absoluteString, feedID: url.absoluteString, homePageURL: nil) + + InitialFeedDownloader.download(url) { parsedFeed in + + if let parsedFeed = parsedFeed { + account.update(feed, with: parsedFeed, {}) + } + + feed.editedName = name + + container.addFeed(feed) + completion(.success(feed)) + + } + + case .failure(let error): + completion(.failure(error)) + } + + } } @@ -168,49 +193,3 @@ final class LocalAccountDelegate: AccountDelegate { } } - -extension LocalAccountDelegate: FeedFinderDelegate { - - // MARK: FeedFinderDelegate - - public func feedFinder(_ feedFinder: FeedFinder, didFindFeeds feedSpecifiers: Set) { - - if let error = feedFinder.initialDownloadError { - if feedFinder.initialDownloadStatusCode == 404 { - createFeedCompletion!(.failure(AccountError.createErrorNotFound)) - } else { - createFeedCompletion!(.failure(error)) - } - return - } - - guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers), - let url = URL(string: bestFeedSpecifier.urlString), - let account = account else { - createFeedCompletion!(.failure(AccountError.createErrorNotFound)) - return - } - - if account.hasFeed(withURL: bestFeedSpecifier.urlString) { - createFeedCompletion!(.failure(AccountError.createErrorAlreadySubscribed)) - return - } - - let feed = account.createFeed(with: nil, url: url.absoluteString, feedID: url.absoluteString, homePageURL: nil) - - InitialFeedDownloader.download(url) { parsedFeed in - - if let parsedFeed = parsedFeed { - account.update(feed, with: parsedFeed, {}) - } - - feed.editedName = self.createFeedName - - self.createFeedContainer?.addFeed(feed) - self.createFeedCompletion?(.success(feed)) - - } - - } - -}