diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index aea0aff0d..698cdb76c 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -13,10 +13,8 @@ 842E45E51ED8C6B7000A8B52 /* MainWindowSplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45E41ED8C6B7000A8B52 /* MainWindowSplitView.swift */; }; 842E45E71ED8C747000A8B52 /* DB5.plist in Resources */ = {isa = PBXBuildFile; fileRef = 842E45E61ED8C747000A8B52 /* DB5.plist */; }; 84513F901FAA63950023A1A9 /* FeedListControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84513F8F1FAA63950023A1A9 /* FeedListControlsView.swift */; }; - 845A29091FC74B8E007B49E3 /* FaviconMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29081FC74B8E007B49E3 /* FaviconMetadata.swift */; }; - 845A29191FC7563E007B49E3 /* FaviconCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29181FC7563E007B49E3 /* FaviconCache.swift */; }; + 845A29091FC74B8E007B49E3 /* SingleFaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */; }; 845A291B1FC75AA6007B49E3 /* SeekingFavicon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A291A1FC75AA6007B49E3 /* SeekingFavicon.swift */; }; - 845A291D1FC75F49007B49E3 /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A291C1FC75F49007B49E3 /* ImageDownloader.swift */; }; 845EE7B11FC2366500854A1F /* StarredFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845EE7B01FC2366500854A1F /* StarredFeedDelegate.swift */; }; 845EE7C11FC2488C00854A1F /* SmartFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845EE7C01FC2488C00854A1F /* SmartFeed.swift */; }; 845F52ED1FB2B9FC00C10BF0 /* FeedPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845F52EC1FB2B9FC00C10BF0 /* FeedPasteboardWriter.swift */; }; @@ -407,10 +405,8 @@ 842E45E41ED8C6B7000A8B52 /* MainWindowSplitView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainWindowSplitView.swift; sourceTree = ""; }; 842E45E61ED8C747000A8B52 /* DB5.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = DB5.plist; path = Evergreen/Resources/DB5.plist; sourceTree = ""; }; 84513F8F1FAA63950023A1A9 /* FeedListControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListControlsView.swift; sourceTree = ""; }; - 845A29081FC74B8E007B49E3 /* FaviconMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconMetadata.swift; sourceTree = ""; }; - 845A29181FC7563E007B49E3 /* FaviconCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconCache.swift; sourceTree = ""; }; + 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleFaviconDownloader.swift; sourceTree = ""; }; 845A291A1FC75AA6007B49E3 /* SeekingFavicon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeekingFavicon.swift; sourceTree = ""; }; - 845A291C1FC75F49007B49E3 /* ImageDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = ""; }; 845B14A51FC2299E0013CF92 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 845EE7B01FC2366500854A1F /* StarredFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarredFeedDelegate.swift; sourceTree = ""; }; 845EE7C01FC2488C00854A1F /* SmartFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartFeed.swift; sourceTree = ""; }; @@ -589,10 +585,8 @@ isa = PBXGroup; children = ( 848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */, - 845A291C1FC75F49007B49E3 /* ImageDownloader.swift */, + 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */, 845A291A1FC75AA6007B49E3 /* SeekingFavicon.swift */, - 845A29081FC74B8E007B49E3 /* FaviconMetadata.swift */, - 845A29181FC7563E007B49E3 /* FaviconCache.swift */, 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */, ); name = Favicons; @@ -1342,11 +1336,9 @@ 849A97831ED9EC63007D329B /* StatusBarView.swift in Sources */, 84F2D5381FC22FCC00998D64 /* TodayFeedDelegate.swift in Sources */, 849A97431ED9EAA9007D329B /* AddFolderWindowController.swift in Sources */, - 845A29191FC7563E007B49E3 /* FaviconCache.swift in Sources */, 849A97921ED9EF65007D329B /* IndeterminateProgressWindowController.swift in Sources */, 849A97801ED9EC42007D329B /* DetailViewController.swift in Sources */, 849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */, - 845A291D1FC75F49007B49E3 /* ImageDownloader.swift in Sources */, 849A978D1ED9EE4D007D329B /* FeedListWindowController.swift in Sources */, 849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */, 84B99C6B1FAE370B00ECDEDB /* FeedListFeed.swift in Sources */, @@ -1360,7 +1352,7 @@ 84B99C691FAE36B800ECDEDB /* FeedListFolder.swift in Sources */, 84F204DE1FAACB8B0076E152 /* FeedListTimelineViewController.swift in Sources */, 849A97A31ED9F180007D329B /* FolderTreeControllerDelegate.swift in Sources */, - 845A29091FC74B8E007B49E3 /* FaviconMetadata.swift in Sources */, + 845A29091FC74B8E007B49E3 /* SingleFaviconDownloader.swift in Sources */, 849A97851ED9ECCD007D329B /* PreferencesWindowController.swift in Sources */, 849A977A1ED9EC04007D329B /* TimelineTableCellView.swift in Sources */, 849A97761ED9EC04007D329B /* TimelineCellAppearance.swift in Sources */, diff --git a/Evergreen/Favicons/FaviconCache.swift b/Evergreen/Favicons/FaviconCache.swift deleted file mode 100644 index e340ff8d0..000000000 --- a/Evergreen/Favicons/FaviconCache.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// FaviconCache.swift -// Evergreen -// -// Created by Brent Simmons on 11/23/17. -// Copyright © 2017 Ranchero Software. All rights reserved. -// - -import Foundation - -final class FaviconCache { - - static var cache = [String: Favicon]() - - class func cachedFavicon(_ homePageURL: String) -> Favicon? { - - return cache[homePageURL] - } - - class func cacheFavicon(_ homePageURL: String, _ favicon: Favicon) { - - cache[homePageURL] = favicon - } - - class func removeFavicon(_ homePageURL: String) { - - cache[homePageURL] = nil - } -} diff --git a/Evergreen/Favicons/FaviconDownloader.swift b/Evergreen/Favicons/FaviconDownloader.swift index 032946c63..f59a74f52 100644 --- a/Evergreen/Favicons/FaviconDownloader.swift +++ b/Evergreen/Favicons/FaviconDownloader.swift @@ -9,37 +9,32 @@ import AppKit import Data import RSCore -import RSWeb extension Notification.Name { - static let FaviconDidBecomeAvailable = Notification.Name("FaviconDidBecomeAvailableNotification") // userInfo keys, one or more of which will be present: homePageURL, faviconURL + static let FaviconDidBecomeAvailable = Notification.Name("FaviconDidBecomeAvailableNotification") // userInfo key: FaviconDownloader.UserInfoKey.faviconURL } final class FaviconDownloader { - private var imageCache = [String: NSImage]() private var seekingFaviconCache = [String: SeekingFavicon]() // homePageURL: SeekingFavicon - private var cache = ThreadSafeCache() // faviconURL: NSImage - private var faviconURLCache = ThreadSafeCache() // homePageURL: faviconURL + private var singleFaviconDownloaderCache = [String: SingleFaviconDownloader]() // faviconURL: SingleFaviconDownloader private let folder: String - private var urlsBeingDownloaded = Set() - private var badURLs = Set() // URLs that didn’t work for some reason; don’t try again - private let binaryCache: RSBinaryCache - private var badImages = Set() // keys for images on disk that NSImage can’t handle + private let diskCache: BinaryDiskCache private let queue: DispatchQueue - public struct UserInfoKey { - static let homePageURL = "homePageURL" + struct UserInfoKey { static let faviconURL = "faviconURL" - static let image = "image" // NSImage } init(folder: String) { self.folder = folder - self.binaryCache = RSBinaryCache(folder: folder) + self.diskCache = BinaryDiskCache(folder: folder) self.queue = DispatchQueue(label: "FaviconDownloader serial queue - \(folder)") + + NotificationCenter.default.addObserver(self, selector: #selector(seekingFaviconDidSeek(_:)), name: .SeekingFaviconSeekDidComplete, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didLoadFavicon(_:)), name: .DidLoadFavicon, object: nil) } // MARK: - API @@ -49,9 +44,7 @@ final class FaviconDownloader { assert(Thread.isMainThread) if let faviconURL = feed.faviconURL { - // JSON Feeds may include the faviconURL in the feed, - // so we don’t have to hunt for it. - return favicon(withURL: faviconURL) + return favicon(with: faviconURL) } guard let homePageURL = feed.homePageURL else { @@ -60,181 +53,82 @@ final class FaviconDownloader { return favicon(withHomePageURL: homePageURL) } - func favicon(withURL faviconURL: String) -> NSImage? { + func favicon(with faviconURL: String) -> NSImage? { - if let cachedImage = imageCache[faviconURL] { - return cachedImage - } - - let controller = faviconController(withURL: faviconURL) - return favicon(withController: controller) + let downloader = faviconDownloader(withURL: faviconURL) + return downloader.image } - func faviconController(withURL faviconURL: String) -> FaviconController { + func favicon(withHomePageURL homePageURL: String) -> NSImage? { - if let controller = faviconControllerCache[faviconURL] { - return controller + guard let seekingFavicon = seekingFavicon(with: homePageURL) else { + return nil } - let controller = FaviconController(faviconURL: faviconURL) - faviconControllerCache[faviconURL] = controller - return controller + return favicon(withSeekingFavicon: seekingFavicon) } - func favicon(withController controller: FaviconController) -> NSImage? { + // MARK: - Notifications - if let image = controller.image { - return image + @objc func seekingFaviconDidSeek(_ note: Notification) { + + guard let seekingFavicon = note.object as? SeekingFavicon else { + return } - - controller.readFromDisk(binaryCache) { (image) in - - if let image = image { - post - } - } - + favicon(withSeekingFavicon: seekingFavicon) } - func findFavicon(for feed: Feed) { + @objc func didLoadFavicon(_ note: Notification) { -// if let faviconMetadata = cachedFaviconMetadata - if let faviconURL = faviconURL(for: feed) { - - // It might be on disk. - - readFaviconFromDisk(faviconURL) { (image) in - - if let image = image { - self.cache[faviconURL] = image - self.postFaviconDidBecomeAvailableNotification(homePageURL: homePageURL, faviconURL: faviconURL, image: image) - return - } - - // Download it (probably). - - if !self.shouldDownloadFaviconURL(faviconURL) { - return - } - - - } + guard let singleFaviconDownloader = note.object as? SingleFaviconDownloader else { + return + } + guard let _ = singleFaviconDownloader.image else { + return } - - // Try to find the faviconURL. It might be in the web page. - FaviconURLFinder.findFaviconURL(homePageURL) { (faviconURL) in - - if let faviconURL = faviconURL { - print(faviconURL) // cache it; then download favicon - } - else { - // Try appending /favicon.ico - // It often works. - } - } - - return nil + postFaviconDidBecomeAvailableNotification(singleFaviconDownloader.faviconURL) } } private extension FaviconDownloader { - func cachedInMemoryFavicon(for feed: Feed) -> NSImage? { + @discardableResult + func favicon(withSeekingFavicon seekingFavicon: SeekingFavicon) -> NSImage? { - guard let faviconURL = faviconURL(for: feed), let cachedFavicon = cache[faviconURL] else { + guard let faviconURL = seekingFavicon.faviconURL else { return nil } - return cachedFavicon + return favicon(with: faviconURL) } - func shouldDownloadFaviconURL(_ faviconURL: String) -> Bool { + func faviconDownloader(withURL faviconURL: String) -> SingleFaviconDownloader { - return !urlsBeingDownloaded.contains(faviconURL) && !badURLs.contains(faviconURL) + if let downloader = singleFaviconDownloaderCache[faviconURL] { + downloader.downloadFaviconIfNeeded() + return downloader + } + + let downloader = SingleFaviconDownloader(faviconURL: faviconURL, diskCache: diskCache, queue: queue) + singleFaviconDownloaderCache[faviconURL] = downloader + return downloader } - func downloadFavicon(_ faviconURL: String, _ homePageURL: String) { + func seekingFavicon(with homePageURL: String) -> SeekingFavicon? { - guard let url = URL(string: faviconURL) else { - return + if let seekingFavicon = seekingFaviconCache[homePageURL] { + return seekingFavicon } - urlsBeingDownloaded.insert(faviconURL) - - downloadUsingCache(url) { (data, response, error) in - - self.urlsBeingDownloaded.remove(faviconURL) - if response == nil || !response!.statusIsOK { - self.badURLs.insert(faviconURL) - } - - if let data = data { - self.queue.async { - let _ = NSImage(data: data) - } - } - } - } - - func faviconURL(for feed: Feed) -> String? { - - if let faviconURL = feed.faviconURL { - return faviconURL - } - - if let homePageURL = feed.homePageURL { - return faviconURLCache[homePageURL] - } - return nil - } - - func readFaviconFromDisk(_ faviconURL: String, _ callback: @escaping (NSImage?) -> Void) { - - queue.async { - let image = self.tryToInstantiateNSImageFromDisk(faviconURL) - DispatchQueue.main.async { - callback(image) - } - } - } - - func tryToInstantiateNSImageFromDisk(_ faviconURL: String) -> NSImage? { - - // Call on serial queue. - - if badImages.contains(faviconURL) { + guard let seekingFavicon = SeekingFavicon(homePageURL: homePageURL) else { return nil } - - let key = keyFor(faviconURL) - var data: Data? - - do { - data = try binaryCache.binaryData(forKey: key) - } - catch { - return nil - } - - if data == nil { - return nil - } - - guard let image = NSImage(data: data!) else { - badImages.insert(faviconURL) - return nil - } - - return image + seekingFaviconCache[homePageURL] = seekingFavicon + return seekingFavicon } - func keyFor(_ faviconURL: String) -> String { + func postFaviconDidBecomeAvailableNotification(_ faviconURL: String) { - return (faviconURL as NSString).rs_md5Hash() - } - - func postFaviconDidBecomeAvailableNotification(homePageURL: String, faviconURL: String, image: NSImage) { - - let userInfo: [AnyHashable: Any] = [UserInfoKey.homePageURL: homePageURL, UserInfoKey.faviconURL: faviconURL, UserInfoKey.image: image] + let userInfo: [AnyHashable: Any] = [UserInfoKey.faviconURL: faviconURL] NotificationCenter.default.post(name: .FaviconDidBecomeAvailable, object: self, userInfo: userInfo) } } diff --git a/Evergreen/Favicons/FaviconMetadata.swift b/Evergreen/Favicons/FaviconMetadata.swift deleted file mode 100644 index 953071c25..000000000 --- a/Evergreen/Favicons/FaviconMetadata.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// Favicon.swift -// Evergreen -// -// Created by Brent Simmons on 11/23/17. -// Copyright © 2017 Ranchero Software. All rights reserved. -// - -import AppKit -import RSCore - -final class FaviconController { - - enum DiskStatus { - case unknown, notOnDisk, onDisk - } - - let faviconURL: String - var lastDownloadAttemptDate: Date? - var diskStatus = DiskStatus.unknown - let diskCache: RSBinaryCache - var image: NSImage? - - init?(faviconURL: String, _ diskCache: RSBinaryCache) { - - self.faviconURL = faviconURL - self.diskCache = diskCache - findFavicon() - } -} - -private extension FaviconController { - - func findFavicon() { - - readFromDisk { (image) in - self.image = image - } - - } - - func readFromDisk(_ callback: (NSImage?) -> Void) { - - if diskStatus == .notOnDisk { - callback(nil) - return - } - - } - -} diff --git a/Evergreen/Favicons/ImageDownloader.swift b/Evergreen/Favicons/ImageDownloader.swift deleted file mode 100644 index 375fea74c..000000000 --- a/Evergreen/Favicons/ImageDownloader.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// FaviconImageDownloader.swift -// Evergreen -// -// Created by Brent Simmons on 11/23/17. -// Copyright © 2017 Ranchero Software. All rights reserved. -// - -import AppKit -import RSWeb -import RSCore - -// Downloads using cache. Enforces a minimum time interval between attempts. - -final class ImageDownloader { - - private var urlsBeingDownloaded = Set() - private var lastAttemptDates = [String: Date]() - private let minimumAttemptInterval: TimeInterval = 5 * 60 - - func downloadImage(_ url: String, _ callback: @escaping (NSImage?) -> Void) { - - guard shouldDownloadImage(url) else { - callback(nil) - return - } - - urlsBeingDownloaded.insert(url) - lastAttemptDates[url] = Date() - - downloadUsingCache(url) { (data, response, error) in - - urlsBeingDownloaded.remove(url) - - if let data = data, let response = response, response.statusIsOK, error == nil { - NSImage.rs_image(with: data, imageResultBlock: callback) - return - } - callback(nil) - } - } -} - -private extension ImageDownloader { - - func shouldDownloadImage(_ url: String) -> Bool { - - if urlsBeingDownloaded.contains(url) { - return false - } - if let lastAttemptDate = lastAttemptDates[url], Date().timeIntervalSince(lastAttemptDate) < minimumAttemptInterval { - return false - } - - return true - } -} diff --git a/Evergreen/Favicons/SeekingFavicon.swift b/Evergreen/Favicons/SeekingFavicon.swift index 809686eb6..00276c9b7 100644 --- a/Evergreen/Favicons/SeekingFavicon.swift +++ b/Evergreen/Favicons/SeekingFavicon.swift @@ -10,8 +10,7 @@ import Foundation extension Notification.Name { - static let SeekingFaviconDidFindFaviconURL = Notification.Name("SeekingFaviconDidFindFaviconURLNotification") - static let SeekingFaviconDidNotFindFaviconURL = Notification.Name("SeekingFaviconDidNotFindFaviconURLNotification") + static let SeekingFaviconSeekDidComplete = Notification.Name("SeekingFaviconSeekDidCompleteNotification") } final class SeekingFavicon { @@ -21,16 +20,14 @@ final class SeekingFavicon { // or it might be at /favicon.ico, // or it might not exist (or be unfindable, which is the same thing). - let homePageURL: String - let defaultFaviconURL: String // /favicon.ico - var didAttemptToLookAtHomePageMetadata = false - var foundFaviconURL: String? - - var shouldUseDefaultFaviconURL: Bool { - - return didAttemptToLookAtHomePageMetadata && foundFaviconURL == nil + var didSeek = false + var faviconURL: String? { + return didSeek ? (foundFaviconURL ?? defaultFaviconURL) : nil } + private let homePageURL: String + private var foundFaviconURL: String? + private let defaultFaviconURL: String // /favicon.ico private static let localeForLowercasing = Locale(identifier: "en_US") init?(homePageURL: String) { @@ -52,15 +49,10 @@ private extension SeekingFavicon { FaviconURLFinder.findFaviconURL(homePageURL) { (faviconURL) in - self.didAttemptToLookAtHomePageMetadata = true self.foundFaviconURL = faviconURL + self.didSeek = true - if let _ = faviconURL { - NotificationCenter.default.post(name: .SeekingFaviconDidFindFaviconURL, object: self) - } - else { - NotificationCenter.default.post(name: .SeekingFaviconDidNotFindFaviconURL, object: self) - } + NotificationCenter.default.post(name: .SeekingFaviconSeekDidComplete, object: self) } } } diff --git a/Evergreen/Favicons/SingleFaviconDownloader.swift b/Evergreen/Favicons/SingleFaviconDownloader.swift new file mode 100644 index 000000000..77ccf719c --- /dev/null +++ b/Evergreen/Favicons/SingleFaviconDownloader.swift @@ -0,0 +1,150 @@ +// +// SingleFaviconDownloader.swift +// Evergreen +// +// Created by Brent Simmons on 11/23/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import AppKit +import RSCore +import RSWeb + +// The image may be on disk already. If not, download it. +// Post .DidLoadFavicon notification once it’s in memory. + +extension Notification.Name { + + static let DidLoadFavicon = Notification.Name("DidLoadFaviconNotification") +} + +final class SingleFaviconDownloader { + + enum DiskStatus { + case unknown, notOnDisk, onDisk + } + + let faviconURL: String + var image: NSImage? + + private var lastDownloadAttemptDate: Date + private var diskStatus = DiskStatus.unknown + private var diskCache: BinaryDiskCache + private let queue: DispatchQueue + + private var diskKey: String { + return (faviconURL as NSString).rs_md5Hash() + } + + init(faviconURL: String, diskCache: BinaryDiskCache, queue: DispatchQueue) { + + self.faviconURL = faviconURL + self.diskCache = diskCache + self.queue = queue + self.lastDownloadAttemptDate = Date() + + findFavicon() + } + + func downloadFaviconIfNeeded() { + + // If we don’t have an image, and lastDownloadAttemptDate is a while ago, try again. + + if let _ = image { + return + } + + let retryInterval: TimeInterval = 30 * 60 // 30 minutes + if Date().timeIntervalSince(lastDownloadAttemptDate) < retryInterval { + return + } + + lastDownloadAttemptDate = Date() + findFavicon() + } +} + +private extension SingleFaviconDownloader { + + func findFavicon() { + + readFromDisk { (image) in + + if let image = image { + self.diskStatus = .onDisk + self.image = image + self.postDidLoadFaviconNotification() + return + } + + self.diskStatus = .notOnDisk + + self.downloadFavicon { (image) in + + if let image = image { + self.image = image + self.postDidLoadFaviconNotification() + } + } + } + } + + func readFromDisk(_ callback: @escaping (NSImage?) -> Void) { + + guard diskStatus != .notOnDisk else { + callback(nil) + return + } + + queue.async { + + if let data = self.diskCache[self.diskKey], !data.isEmpty { + NSImage.rs_image(with: data, imageResultBlock: callback) + return + } + + DispatchQueue.main.async { + callback(nil) + } + } + } + + func saveToDisk(_ data: Data) { + + queue.async { + + do { + try self.diskCache.setData(data, forKey: self.diskKey) + DispatchQueue.main.async { + self.diskStatus = .onDisk + } + } + catch {} + } + } + + func downloadFavicon(_ callback: @escaping (NSImage?) -> Void) { + + guard let url = URL(string: faviconURL) else { + callback(nil) + return + } + + downloadUsingCache(url) { (data, response, error) in + + if let data = data, !data.isEmpty, let response = response, response.statusIsOK, error == nil { + self.saveToDisk(data) + NSImage.rs_image(with: data, imageResultBlock: callback) + return + } + + callback(nil) + } + } + + func postDidLoadFaviconNotification() { + + assert(Thread.isMainThread) + NotificationCenter.default.post(name: .DidLoadFavicon, object: self) + } +}