diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index 69b01824c..5d3c845ed 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -21,7 +21,7 @@ 846E77411F6EF6A100A165E2 /* Database.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 846E77211F6EF5D100A165E2 /* Database.framework */; }; 846E77421F6EF6A100A165E2 /* Database.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 846E77211F6EF5D100A165E2 /* Database.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 84702AA41FA27AC0006B8943 /* MarkReadOrUnreadCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkReadOrUnreadCommand.swift */; }; - 848F6AE51FC29CFB002D422E /* FaviconCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848F6AE41FC29CFA002D422E /* FaviconCache.swift */; }; + 848F6AE51FC29CFB002D422E /* FaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */; }; 849A97431ED9EAA9007D329B /* AddFolderWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97421ED9EAA9007D329B /* AddFolderWindowController.swift */; }; 849A97531ED9EAC0007D329B /* AddFeedController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97511ED9EAC0007D329B /* AddFeedController.swift */; }; 849A97541ED9EAC0007D329B /* AddFeedWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97521ED9EAC0007D329B /* AddFeedWindowController.swift */; }; @@ -409,7 +409,7 @@ 846E77161F6EF5D000A165E2 /* Database.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Database.xcodeproj; path = Frameworks/Database/Database.xcodeproj; sourceTree = ""; }; 846E77301F6EF5D600A165E2 /* Account.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Account.xcodeproj; path = Frameworks/Account/Account.xcodeproj; sourceTree = ""; }; 84702AA31FA27AC0006B8943 /* MarkReadOrUnreadCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkReadOrUnreadCommand.swift; sourceTree = ""; }; - 848F6AE41FC29CFA002D422E /* FaviconCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaviconCache.swift; sourceTree = ""; }; + 848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaviconDownloader.swift; sourceTree = ""; }; 849A97421ED9EAA9007D329B /* AddFolderWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFolderWindowController.swift; sourceTree = ""; }; 849A97511ED9EAC0007D329B /* AddFeedController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AddFeedController.swift; path = AddFeed/AddFeedController.swift; sourceTree = ""; }; 849A97521ED9EAC0007D329B /* AddFeedWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AddFeedWindowController.swift; path = AddFeed/AddFeedWindowController.swift; sourceTree = ""; }; @@ -578,7 +578,7 @@ 848F6AE31FC29CFA002D422E /* Favicons */ = { isa = PBXGroup; children = ( - 848F6AE41FC29CFA002D422E /* FaviconCache.swift */, + 848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */, ); name = Favicons; path = Evergreen/Favicons; @@ -1319,7 +1319,7 @@ 849A97791ED9EC04007D329B /* TimelineStringUtilities.swift in Sources */, 84F204CE1FAACB660076E152 /* FeedListViewController.swift in Sources */, 845EE7B11FC2366500854A1F /* StarredFeedDelegate.swift in Sources */, - 848F6AE51FC29CFB002D422E /* FaviconCache.swift in Sources */, + 848F6AE51FC29CFB002D422E /* FaviconDownloader.swift in Sources */, 849A97981ED9EFAA007D329B /* Node-Extensions.swift in Sources */, 849A97531ED9EAC0007D329B /* AddFeedController.swift in Sources */, 849A97831ED9EC63007D329B /* StatusBarView.swift in Sources */, diff --git a/Evergreen/Favicons/FaviconCache.swift b/Evergreen/Favicons/FaviconCache.swift deleted file mode 100644 index 2e036b8e9..000000000 --- a/Evergreen/Favicons/FaviconCache.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// FaviconCache.swift -// Evergreen -// -// Created by Brent Simmons on 11/19/17. -// Copyright © 2017 Ranchero Software. All rights reserved. -// - -import AppKit -import Data - -extension Notification.Name { - - static let FaviconDidDownload = Notification.Name("FaviconDidDownloadNotification") -} - -final class FaviconCache { - - // MARK: - API - - func favicon(for feed: Feed) -> NSImage? { - - return nil - } -} diff --git a/Evergreen/Favicons/FaviconDownloader.swift b/Evergreen/Favicons/FaviconDownloader.swift new file mode 100644 index 000000000..1f885dc2c --- /dev/null +++ b/Evergreen/Favicons/FaviconDownloader.swift @@ -0,0 +1,145 @@ +// +// FaviconDownloader.swift +// Evergreen +// +// Created by Brent Simmons on 11/19/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import AppKit +import Data +import RSCore +import RSWeb + +extension Notification.Name { + + static let FaviconDidBecomeAvailable = Notification.Name("FaviconDidBecomeAvailableNotification") // userInfo keys: homePageURL, faviconURL, image +} + +final class FaviconDownloader { + + private var cache = ThreadSafeCache() // faviconURL: NSImage + private var faviconURLCache = ThreadSafeCache() // homePageURL: faviconURL + private let folder: String + private var urlsBeingDownloaded = Set() + private let binaryCache: RSBinaryCache + private var badImages = Set() // keys for images on disk that NSImage can’t handle + private let queue: DispatchQueue + + public struct UserInfoKey { + static let homePageURL = "homePageURL" + static let faviconURL = "faviconURL" + static let image = "image" // NSImage + } + + init(folder: String) { + + self.folder = folder + self.binaryCache = RSBinaryCache(folder: folder) + self.queue = DispatchQueue(label: "FaviconCache serial queue - \(folder)") + } + + // MARK: - API + + func favicon(for feed: Feed) -> NSImage? { + + assert(Thread.isMainThread) + + if let faviconURL = faviconURL(for: feed) { + + if let cachedFavicon = cache[faviconURL] { + return cachedFavicon + } + if shouldDownloadFaviconURL(faviconURL) { + downloadFavicon(faviconURL) + return nil + } + } + + return nil + } +} + +private extension FaviconDownloader { + + func shouldDownloadFaviconURL(_ faviconURL: String) -> Bool { + + return !urlsBeingDownloaded.contains(faviconURL) + } + + func downloadFavicon(_ faviconURL: String) { + + guard let url = URL(string: faviconURL) else { + return + } + + urlsBeingDownloaded.insert(faviconURL) + + download(url) { (data, response, error) in + + self.urlsBeingDownloaded.remove(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) { + 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 + } + + func keyFor(_ faviconURL: String) -> String { + + return (faviconURL as NSString).rs_md5Hash() + } +}