mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Simplify local feed downloading.
This commit is contained in:
@@ -713,7 +713,7 @@ private extension CloudKitAccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func storeArticleChanges(new: Set<Article>?, updated: Set<Article>?, deleted: Set<Article>?, completion: @escaping () -> Void) {
|
||||
func storeArticleChanges(new: Set<Article>?, updated: Set<Article>?, deleted: Set<Article>?, completion: (() -> Void)?) {
|
||||
// New records with a read status aren't really new, they just didn't have the read article stored
|
||||
let group = DispatchGroup()
|
||||
if let new = new {
|
||||
@@ -736,7 +736,7 @@ private extension CloudKitAccountDelegate {
|
||||
|
||||
group.notify(queue: DispatchQueue.global(qos: .userInitiated)) {
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -798,15 +798,11 @@ private extension CloudKitAccountDelegate {
|
||||
|
||||
extension CloudKitAccountDelegate: LocalAccountRefresherDelegate {
|
||||
|
||||
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: WebFeed) {
|
||||
refreshProgress.completeTask()
|
||||
}
|
||||
|
||||
func localAccountRefresher(_ refresher: LocalAccountRefresher, articleChanges: ArticleChanges, completion: @escaping () -> Void) {
|
||||
func localAccountRefresher(_ refresher: LocalAccountRefresher, articleChanges: ArticleChanges) {
|
||||
self.storeArticleChanges(new: articleChanges.newArticles,
|
||||
updated: articleChanges.updatedArticles,
|
||||
deleted: articleChanges.deletedArticles,
|
||||
completion: completion)
|
||||
completion: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -217,16 +217,9 @@ final class LocalAccountDelegate: AccountDelegate {
|
||||
}
|
||||
|
||||
extension LocalAccountDelegate: LocalAccountRefresherDelegate {
|
||||
|
||||
|
||||
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: WebFeed) {
|
||||
refreshProgress.completeTask()
|
||||
|
||||
func localAccountRefresher(_ refresher: LocalAccountRefresher, articleChanges: ArticleChanges) {
|
||||
}
|
||||
|
||||
func localAccountRefresher(_ refresher: LocalAccountRefresher, articleChanges: ArticleChanges, completion: @escaping () -> Void) {
|
||||
completion()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension LocalAccountDelegate {
|
||||
|
||||
@@ -14,8 +14,7 @@ import Articles
|
||||
import ArticlesDatabase
|
||||
|
||||
protocol LocalAccountRefresherDelegate {
|
||||
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: WebFeed)
|
||||
func localAccountRefresher(_ refresher: LocalAccountRefresher, articleChanges: ArticleChanges, completion: @escaping () -> Void)
|
||||
func localAccountRefresher(_ refresher: LocalAccountRefresher, articleChanges: ArticleChanges)
|
||||
}
|
||||
|
||||
final class LocalAccountRefresher {
|
||||
@@ -23,18 +22,30 @@ final class LocalAccountRefresher {
|
||||
private var completion: (() -> Void)? = nil
|
||||
private var isSuspended = false
|
||||
var delegate: LocalAccountRefresherDelegate?
|
||||
|
||||
|
||||
private lazy var downloadSession: DownloadSession = {
|
||||
return DownloadSession(delegate: self)
|
||||
}()
|
||||
|
||||
private var urlToFeedDictionary = [String: WebFeed]()
|
||||
|
||||
public func refreshFeeds(_ feeds: Set<WebFeed>, completion: (() -> Void)? = nil) {
|
||||
guard !feeds.isEmpty else {
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
|
||||
urlToFeedDictionary.removeAll()
|
||||
for feed in feeds {
|
||||
urlToFeedDictionary[feed.url] = feed
|
||||
}
|
||||
|
||||
let urls = feeds.compactMap { feed in
|
||||
URL(unicodeString: feed.url)
|
||||
}
|
||||
|
||||
self.completion = completion
|
||||
downloadSession.downloadObjects(feeds as NSSet)
|
||||
downloadSession.download(Set(urls))
|
||||
}
|
||||
|
||||
public func suspend() {
|
||||
@@ -51,100 +62,50 @@ final class LocalAccountRefresher {
|
||||
|
||||
extension LocalAccountRefresher: DownloadSessionDelegate {
|
||||
|
||||
func downloadSession(_ downloadSession: DownloadSession, requestForRepresentedObject representedObject: AnyObject) -> URLRequest? {
|
||||
guard let feed = representedObject as? WebFeed else {
|
||||
return nil
|
||||
}
|
||||
guard let url = URL(string: feed.url) else {
|
||||
return nil
|
||||
}
|
||||
func downloadSession(_ downloadSession: DownloadSession, downloadDidComplete url: URL, response: URLResponse?, data: Data, error: NSError?) {
|
||||
|
||||
return URLRequest(url: url)
|
||||
}
|
||||
|
||||
func downloadSession(_ downloadSession: DownloadSession, downloadDidCompleteForRepresentedObject representedObject: AnyObject, response: URLResponse?, data: Data, error: NSError?, completion: @escaping () -> Void) {
|
||||
let feed = representedObject as! WebFeed
|
||||
|
||||
guard let feed = urlToFeedDictionary[url.absoluteString] else {
|
||||
return
|
||||
}
|
||||
guard !data.isEmpty, !isSuspended else {
|
||||
completion()
|
||||
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
return
|
||||
}
|
||||
|
||||
if let error = error {
|
||||
print("Error downloading \(feed.url) - \(error)")
|
||||
completion()
|
||||
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
if let error {
|
||||
print("Error downloading \(url) - \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
let dataHash = data.md5String
|
||||
if dataHash == feed.contentHash {
|
||||
completion()
|
||||
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
return
|
||||
}
|
||||
|
||||
let parserData = ParserData(url: feed.url, data: data)
|
||||
FeedParser.parse(parserData) { (parsedFeed, error) in
|
||||
|
||||
guard let account = feed.account, let parsedFeed = parsedFeed, error == nil else {
|
||||
completion()
|
||||
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
guard let account = feed.account, let parsedFeed, error == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
account.update(feed, with: parsedFeed) { result in
|
||||
if case .success(let articleChanges) = result {
|
||||
feed.contentHash = dataHash
|
||||
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
self.delegate?.localAccountRefresher(self, articleChanges: articleChanges) {
|
||||
completion()
|
||||
}
|
||||
} else {
|
||||
completion()
|
||||
self.delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
self.delegate?.localAccountRefresher(self, articleChanges: articleChanges)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func downloadSession(_ downloadSession: DownloadSession, shouldContinueAfterReceivingData data: Data, representedObject: AnyObject) -> Bool {
|
||||
let feed = representedObject as! WebFeed
|
||||
guard !isSuspended else {
|
||||
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
func downloadSession(_ downloadSession: DownloadSession, shouldContinueAfterReceivingData data: Data, url: URL) -> Bool {
|
||||
|
||||
guard !data.isDefinitelyNotFeed(), !isSuspended else {
|
||||
return false
|
||||
}
|
||||
|
||||
if data.isEmpty {
|
||||
return true
|
||||
}
|
||||
|
||||
if data.isDefinitelyNotFeed() {
|
||||
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func downloadSession(_ downloadSession: DownloadSession, didReceiveUnexpectedResponse response: URLResponse, representedObject: AnyObject) {
|
||||
let feed = representedObject as! WebFeed
|
||||
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
}
|
||||
|
||||
func downloadSession(_ downloadSession: DownloadSession, didReceiveNotModifiedResponse: URLResponse, representedObject: AnyObject) {
|
||||
let feed = representedObject as! WebFeed
|
||||
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
return true
|
||||
}
|
||||
|
||||
func downloadSession(_ downloadSession: DownloadSession, didDiscardDuplicateRepresentedObject representedObject: AnyObject) {
|
||||
let feed = representedObject as! WebFeed
|
||||
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||
}
|
||||
|
||||
func downloadSessionDidCompleteDownloadObjects(_ downloadSession: DownloadSession) {
|
||||
func downloadSessionDidComplete(_ downloadSession: DownloadSession) {
|
||||
completion?()
|
||||
completion = nil
|
||||
}
|
||||
|
||||
@@ -13,14 +13,9 @@ import Foundation
|
||||
|
||||
public protocol DownloadSessionDelegate {
|
||||
|
||||
func downloadSession(_ downloadSession: DownloadSession, requestForRepresentedObject: AnyObject) -> URLRequest?
|
||||
func downloadSession(_ downloadSession: DownloadSession, downloadDidCompleteForRepresentedObject: AnyObject, response: URLResponse?, data: Data, error: NSError?, completion: @escaping () -> Void)
|
||||
func downloadSession(_ downloadSession: DownloadSession, shouldContinueAfterReceivingData: Data, representedObject: AnyObject) -> Bool
|
||||
func downloadSession(_ downloadSession: DownloadSession, didReceiveUnexpectedResponse: URLResponse, representedObject: AnyObject)
|
||||
func downloadSession(_ downloadSession: DownloadSession, didReceiveNotModifiedResponse: URLResponse, representedObject: AnyObject)
|
||||
func downloadSession(_ downloadSession: DownloadSession, didDiscardDuplicateRepresentedObject: AnyObject)
|
||||
func downloadSessionDidCompleteDownloadObjects(_ downloadSession: DownloadSession)
|
||||
|
||||
func downloadSession(_ downloadSession: DownloadSession, downloadDidComplete: URL, response: URLResponse?, data: Data, error: NSError?)
|
||||
func downloadSession(_ downloadSession: DownloadSession, shouldContinueAfterReceivingData: Data, url: URL) -> Bool
|
||||
func downloadSessionDidComplete(_ downloadSession: DownloadSession)
|
||||
}
|
||||
|
||||
@objc public final class DownloadSession: NSObject {
|
||||
@@ -29,10 +24,10 @@ public protocol DownloadSessionDelegate {
|
||||
private var tasksInProgress = Set<URLSessionTask>()
|
||||
private var tasksPending = Set<URLSessionTask>()
|
||||
private var taskIdentifierToInfoDictionary = [Int: DownloadInfo]()
|
||||
private let representedObjects = NSMutableSet()
|
||||
private var urlsInSession = Set<URL>()
|
||||
private let delegate: DownloadSessionDelegate
|
||||
private var redirectCache = [String: String]()
|
||||
private var queue = [AnyObject]()
|
||||
private var queue = [URL]()
|
||||
private var retryAfterMessages = [String: HTTPResponse429]()
|
||||
|
||||
public init(delegate: DownloadSessionDelegate) {
|
||||
@@ -70,15 +65,12 @@ public protocol DownloadSessionDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
public func downloadObjects(_ objects: NSSet) {
|
||||
for oneObject in objects {
|
||||
if !representedObjects.contains(oneObject) {
|
||||
representedObjects.add(oneObject)
|
||||
addDataTask(oneObject as AnyObject)
|
||||
} else {
|
||||
delegate.downloadSession(self, didDiscardDuplicateRepresentedObject: oneObject as AnyObject)
|
||||
}
|
||||
public func download(_ urls: Set<URL>) {
|
||||
|
||||
for url in urls.subtracting(urlsInSession) {
|
||||
addDataTask(url)
|
||||
}
|
||||
urlsInSession.formUnion(urls)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,16 +82,13 @@ extension DownloadSession: URLSessionTaskDelegate {
|
||||
tasksInProgress.remove(task)
|
||||
|
||||
guard let info = infoForTask(task) else {
|
||||
removeTask(task)
|
||||
return
|
||||
}
|
||||
|
||||
info.error = error
|
||||
|
||||
delegate.downloadSession(self, downloadDidCompleteForRepresentedObject: info.representedObject, response: info.urlResponse, data: info.data as Data, error: error as NSError?) {
|
||||
self.removeTask(task)
|
||||
}
|
||||
delegate.downloadSession(self, downloadDidComplete: info.url, response: info.urlResponse, data: info.data as Data, error: error as NSError?)
|
||||
}
|
||||
|
||||
|
||||
public func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
|
||||
|
||||
if response.statusCode == 301 || response.statusCode == 308 {
|
||||
@@ -124,30 +113,13 @@ extension DownloadSession: URLSessionDataDelegate {
|
||||
if let info = infoForTask(dataTask) {
|
||||
info.urlResponse = response
|
||||
}
|
||||
let statusCode = response.forcedStatusCode
|
||||
|
||||
if statusCode == HTTPResponseCode.notModified {
|
||||
|
||||
if let representedObject = infoForTask(dataTask)?.representedObject {
|
||||
delegate.downloadSession(self, didReceiveNotModifiedResponse: response, representedObject: representedObject)
|
||||
}
|
||||
|
||||
completionHandler(.cancel)
|
||||
removeTask(dataTask)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !response.statusIsOK {
|
||||
|
||||
if let representedObject = infoForTask(dataTask)?.representedObject {
|
||||
delegate.downloadSession(self, didReceiveUnexpectedResponse: response, representedObject: representedObject)
|
||||
}
|
||||
|
||||
completionHandler(.cancel)
|
||||
removeTask(dataTask)
|
||||
|
||||
if statusCode == HTTPResponseCode.tooManyRequests {
|
||||
if response.forcedStatusCode == HTTPResponseCode.tooManyRequests {
|
||||
handle429Response(dataTask, response)
|
||||
}
|
||||
|
||||
@@ -155,7 +127,6 @@ extension DownloadSession: URLSessionDataDelegate {
|
||||
}
|
||||
|
||||
addDataTaskFromQueueIfNecessary()
|
||||
|
||||
completionHandler(.allow)
|
||||
}
|
||||
|
||||
@@ -166,8 +137,8 @@ extension DownloadSession: URLSessionDataDelegate {
|
||||
}
|
||||
info.addData(data)
|
||||
|
||||
if !delegate.downloadSession(self, shouldContinueAfterReceivingData: info.data as Data, representedObject: info.representedObject) {
|
||||
|
||||
if !delegate.downloadSession(self, shouldContinueAfterReceivingData: info.data as Data, url: info.url) {
|
||||
|
||||
info.canceled = true
|
||||
dataTask.cancel()
|
||||
removeTask(dataTask)
|
||||
@@ -179,21 +150,19 @@ extension DownloadSession: URLSessionDataDelegate {
|
||||
|
||||
private extension DownloadSession {
|
||||
|
||||
func addDataTask(_ representedObject: AnyObject) {
|
||||
func addDataTask(_ url: URL) {
|
||||
guard tasksPending.count < 500 else {
|
||||
queue.insert(representedObject, at: 0)
|
||||
return
|
||||
}
|
||||
|
||||
guard let request = delegate.downloadSession(self, requestForRepresentedObject: representedObject) else {
|
||||
queue.insert(url, at: 0)
|
||||
return
|
||||
}
|
||||
|
||||
let request = URLRequest(url: url)
|
||||
var requestToUse = request
|
||||
|
||||
// If received permanent redirect earlier, use that URL.
|
||||
|
||||
if let urlString = request.url?.absoluteString, let redirectedURLString = cachedRedirectForURLString(urlString) {
|
||||
let urlString = url.absoluteString
|
||||
if let redirectedURLString = cachedRedirectForURLString(urlString) {
|
||||
if let redirectedURL = URL(string: redirectedURLString) {
|
||||
requestToUse.url = redirectedURL
|
||||
}
|
||||
@@ -205,7 +174,7 @@ private extension DownloadSession {
|
||||
|
||||
let task = urlSession.dataTask(with: requestToUse)
|
||||
|
||||
let info = DownloadInfo(representedObject, urlRequest: requestToUse)
|
||||
let info = DownloadInfo(url)
|
||||
taskIdentifierToInfoDictionary[task.taskIdentifier] = info
|
||||
|
||||
tasksPending.insert(task)
|
||||
@@ -229,8 +198,8 @@ private extension DownloadSession {
|
||||
addDataTaskFromQueueIfNecessary()
|
||||
|
||||
if tasksInProgress.count + tasksPending.count < 1 {
|
||||
representedObjects.removeAllObjects()
|
||||
delegate.downloadSessionDidCompleteDownloadObjects(self)
|
||||
urlsInSession.removeAll()
|
||||
delegate.downloadSessionDidComplete(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,10 +342,8 @@ extension URLSessionTask {
|
||||
|
||||
private final class DownloadInfo {
|
||||
|
||||
let representedObject: AnyObject
|
||||
let urlRequest: URLRequest
|
||||
let url: URL
|
||||
let data = NSMutableData()
|
||||
var error: Error?
|
||||
var urlResponse: URLResponse?
|
||||
var canceled = false
|
||||
|
||||
@@ -384,10 +351,9 @@ private final class DownloadInfo {
|
||||
return urlResponse?.forcedStatusCode ?? 0
|
||||
}
|
||||
|
||||
init(_ representedObject: AnyObject, urlRequest: URLRequest) {
|
||||
|
||||
self.representedObject = representedObject
|
||||
self.urlRequest = urlRequest
|
||||
init(_ url: URL) {
|
||||
|
||||
self.url = url
|
||||
}
|
||||
|
||||
func addData(_ d: Data) {
|
||||
|
||||
Reference in New Issue
Block a user