Make progress on favicons.

This commit is contained in:
Brent Simmons 2017-11-23 14:15:28 -08:00
parent 3282f0ec09
commit 9e3e093bcd
6 changed files with 238 additions and 13 deletions

View File

@ -13,6 +13,10 @@
842E45E51ED8C6B7000A8B52 /* MainWindowSplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45E41ED8C6B7000A8B52 /* MainWindowSplitView.swift */; }; 842E45E51ED8C6B7000A8B52 /* MainWindowSplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45E41ED8C6B7000A8B52 /* MainWindowSplitView.swift */; };
842E45E71ED8C747000A8B52 /* DB5.plist in Resources */ = {isa = PBXBuildFile; fileRef = 842E45E61ED8C747000A8B52 /* DB5.plist */; }; 842E45E71ED8C747000A8B52 /* DB5.plist in Resources */ = {isa = PBXBuildFile; fileRef = 842E45E61ED8C747000A8B52 /* DB5.plist */; };
84513F901FAA63950023A1A9 /* FeedListControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84513F8F1FAA63950023A1A9 /* FeedListControlsView.swift */; }; 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 */; };
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 */; }; 845EE7B11FC2366500854A1F /* StarredFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845EE7B01FC2366500854A1F /* StarredFeedDelegate.swift */; };
845EE7C11FC2488C00854A1F /* SmartFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845EE7C01FC2488C00854A1F /* SmartFeed.swift */; }; 845EE7C11FC2488C00854A1F /* SmartFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845EE7C01FC2488C00854A1F /* SmartFeed.swift */; };
845F52ED1FB2B9FC00C10BF0 /* FeedPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845F52EC1FB2B9FC00C10BF0 /* FeedPasteboardWriter.swift */; }; 845F52ED1FB2B9FC00C10BF0 /* FeedPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845F52EC1FB2B9FC00C10BF0 /* FeedPasteboardWriter.swift */; };
@ -403,6 +407,10 @@
842E45E41ED8C6B7000A8B52 /* MainWindowSplitView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainWindowSplitView.swift; sourceTree = "<group>"; }; 842E45E41ED8C6B7000A8B52 /* MainWindowSplitView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainWindowSplitView.swift; sourceTree = "<group>"; };
842E45E61ED8C747000A8B52 /* DB5.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = DB5.plist; path = Evergreen/Resources/DB5.plist; sourceTree = "<group>"; }; 842E45E61ED8C747000A8B52 /* DB5.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = DB5.plist; path = Evergreen/Resources/DB5.plist; sourceTree = "<group>"; };
84513F8F1FAA63950023A1A9 /* FeedListControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListControlsView.swift; sourceTree = "<group>"; }; 84513F8F1FAA63950023A1A9 /* FeedListControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListControlsView.swift; sourceTree = "<group>"; };
845A29081FC74B8E007B49E3 /* FaviconMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconMetadata.swift; sourceTree = "<group>"; };
845A29181FC7563E007B49E3 /* FaviconCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconCache.swift; sourceTree = "<group>"; };
845A291A1FC75AA6007B49E3 /* SeekingFavicon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeekingFavicon.swift; sourceTree = "<group>"; };
845A291C1FC75F49007B49E3 /* ImageDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = "<group>"; };
845B14A51FC2299E0013CF92 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; }; 845B14A51FC2299E0013CF92 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
845EE7B01FC2366500854A1F /* StarredFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarredFeedDelegate.swift; sourceTree = "<group>"; }; 845EE7B01FC2366500854A1F /* StarredFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarredFeedDelegate.swift; sourceTree = "<group>"; };
845EE7C01FC2488C00854A1F /* SmartFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartFeed.swift; sourceTree = "<group>"; }; 845EE7C01FC2488C00854A1F /* SmartFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartFeed.swift; sourceTree = "<group>"; };
@ -581,6 +589,10 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */, 848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */,
845A291C1FC75F49007B49E3 /* ImageDownloader.swift */,
845A291A1FC75AA6007B49E3 /* SeekingFavicon.swift */,
845A29081FC74B8E007B49E3 /* FaviconMetadata.swift */,
845A29181FC7563E007B49E3 /* FaviconCache.swift */,
84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */, 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */,
); );
name = Favicons; name = Favicons;
@ -1308,6 +1320,7 @@
84F2D5371FC22FCC00998D64 /* PseudoFeed.swift in Sources */, 84F2D5371FC22FCC00998D64 /* PseudoFeed.swift in Sources */,
845EE7C11FC2488C00854A1F /* SmartFeed.swift in Sources */, 845EE7C11FC2488C00854A1F /* SmartFeed.swift in Sources */,
84702AA41FA27AC0006B8943 /* MarkReadOrUnreadCommand.swift in Sources */, 84702AA41FA27AC0006B8943 /* MarkReadOrUnreadCommand.swift in Sources */,
845A291B1FC75AA6007B49E3 /* SeekingFavicon.swift in Sources */,
849A979F1ED9F130007D329B /* SidebarCell.swift in Sources */, 849A979F1ED9F130007D329B /* SidebarCell.swift in Sources */,
849A97651ED9EB96007D329B /* SidebarTreeControllerDelegate.swift in Sources */, 849A97651ED9EB96007D329B /* SidebarTreeControllerDelegate.swift in Sources */,
849A97671ED9EB96007D329B /* UnreadCountView.swift in Sources */, 849A97671ED9EB96007D329B /* UnreadCountView.swift in Sources */,
@ -1329,9 +1342,11 @@
849A97831ED9EC63007D329B /* StatusBarView.swift in Sources */, 849A97831ED9EC63007D329B /* StatusBarView.swift in Sources */,
84F2D5381FC22FCC00998D64 /* TodayFeedDelegate.swift in Sources */, 84F2D5381FC22FCC00998D64 /* TodayFeedDelegate.swift in Sources */,
849A97431ED9EAA9007D329B /* AddFolderWindowController.swift in Sources */, 849A97431ED9EAA9007D329B /* AddFolderWindowController.swift in Sources */,
845A29191FC7563E007B49E3 /* FaviconCache.swift in Sources */,
849A97921ED9EF65007D329B /* IndeterminateProgressWindowController.swift in Sources */, 849A97921ED9EF65007D329B /* IndeterminateProgressWindowController.swift in Sources */,
849A97801ED9EC42007D329B /* DetailViewController.swift in Sources */, 849A97801ED9EC42007D329B /* DetailViewController.swift in Sources */,
849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */, 849A976E1ED9EBC8007D329B /* TimelineViewController.swift in Sources */,
845A291D1FC75F49007B49E3 /* ImageDownloader.swift in Sources */,
849A978D1ED9EE4D007D329B /* FeedListWindowController.swift in Sources */, 849A978D1ED9EE4D007D329B /* FeedListWindowController.swift in Sources */,
849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */, 849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */,
84B99C6B1FAE370B00ECDEDB /* FeedListFeed.swift in Sources */, 84B99C6B1FAE370B00ECDEDB /* FeedListFeed.swift in Sources */,
@ -1345,6 +1360,7 @@
84B99C691FAE36B800ECDEDB /* FeedListFolder.swift in Sources */, 84B99C691FAE36B800ECDEDB /* FeedListFolder.swift in Sources */,
84F204DE1FAACB8B0076E152 /* FeedListTimelineViewController.swift in Sources */, 84F204DE1FAACB8B0076E152 /* FeedListTimelineViewController.swift in Sources */,
849A97A31ED9F180007D329B /* FolderTreeControllerDelegate.swift in Sources */, 849A97A31ED9F180007D329B /* FolderTreeControllerDelegate.swift in Sources */,
845A29091FC74B8E007B49E3 /* FaviconMetadata.swift in Sources */,
849A97851ED9ECCD007D329B /* PreferencesWindowController.swift in Sources */, 849A97851ED9ECCD007D329B /* PreferencesWindowController.swift in Sources */,
849A977A1ED9EC04007D329B /* TimelineTableCellView.swift in Sources */, 849A977A1ED9EC04007D329B /* TimelineTableCellView.swift in Sources */,
849A97761ED9EC04007D329B /* TimelineCellAppearance.swift in Sources */, 849A97761ED9EC04007D329B /* TimelineCellAppearance.swift in Sources */,

View File

@ -0,0 +1,29 @@
//
// 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
}
}

View File

@ -18,6 +18,7 @@ extension Notification.Name {
final class FaviconDownloader { final class FaviconDownloader {
private var seekingFaviconCache: [String: SeekingFavicon]() // homePageURL: SeekingFavicon
private var cache = ThreadSafeCache<NSImage>() // faviconURL: NSImage private var cache = ThreadSafeCache<NSImage>() // faviconURL: NSImage
private var faviconURLCache = ThreadSafeCache<String>() // homePageURL: faviconURL private var faviconURLCache = ThreadSafeCache<String>() // homePageURL: faviconURL
private let folder: String private let folder: String
@ -37,7 +38,7 @@ final class FaviconDownloader {
self.folder = folder self.folder = folder
self.binaryCache = RSBinaryCache(folder: folder) self.binaryCache = RSBinaryCache(folder: folder)
self.queue = DispatchQueue(label: "FaviconCache serial queue - \(folder)") self.queue = DispatchQueue(label: "FaviconDownloader serial queue - \(folder)")
} }
// MARK: - API // MARK: - API
@ -45,27 +46,43 @@ final class FaviconDownloader {
func favicon(for feed: Feed) -> NSImage? { func favicon(for feed: Feed) -> NSImage? {
assert(Thread.isMainThread) assert(Thread.isMainThread)
guard let homePageURL = feed.homePageURL else { guard let homePageURL = feed.homePageURL else {
return nil return nil
} }
if let favicon = cachedInMemoryFavicon(for: feed) {
return favicon
}
findFavicon(for: feed)
}
func findFavicon(for feed: Feed) {
if let faviconMetadata = cachedFaviconMetadata
if let faviconURL = faviconURL(for: feed) { if let faviconURL = faviconURL(for: feed) {
if let cachedFavicon = cache[faviconURL] { // It might be on disk.
return cachedFavicon
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
}
} }
// TODO: read from disk and return if present.
if shouldDownloadFaviconURL(faviconURL) {
downloadFavicon(faviconURL, homePageURL)
return nil
}
return nil
} }
// Try to find the faviconURL. It might be in the web page. // Try to find the faviconURL. It might be in the web page.
FaviconURLFinder.findFaviconURL(homePageURL) { (faviconURL) in FaviconURLFinder.findFaviconURL(homePageURL) { (faviconURL) in
@ -84,6 +101,14 @@ final class FaviconDownloader {
private extension FaviconDownloader { private extension FaviconDownloader {
func cachedInMemoryFavicon(for feed: Feed) -> NSImage? {
guard let faviconURL = faviconURL(for: feed), let cachedFavicon = cache[faviconURL] else {
return nil
}
return cachedFavicon
}
func shouldDownloadFaviconURL(_ faviconURL: String) -> Bool { func shouldDownloadFaviconURL(_ faviconURL: String) -> Bool {
return !urlsBeingDownloaded.contains(faviconURL) && !badURLs.contains(faviconURL) return !urlsBeingDownloaded.contains(faviconURL) && !badURLs.contains(faviconURL)
@ -168,4 +193,10 @@ private extension FaviconDownloader {
return (faviconURL as NSString).rs_md5Hash() 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]
NotificationCenter.default.post(name: .FaviconDidBecomeAvailable, object: self, userInfo: userInfo)
}
} }

View File

@ -0,0 +1,26 @@
//
// Favicon.swift
// Evergreen
//
// Created by Brent Simmons on 11/23/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import AppKit
final class FaviconMetadata {
enum DiskStatus {
case unknown, notOnDisk, onDisk
}
let faviconURL: String
var lastDownloadAttemptDate: Date?
var diskStatus = DiskStatus.unknown
var image: NSImage?
init?(faviconURL: String) {
self.faviconURL = faviconURL
}
}

View File

@ -0,0 +1,57 @@
//
// 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<String>()
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 ImageDownload {
func shouldDownloadImage(_ url: String) -> Bool {
if urlsBeingDownloaded.contains(url) {
return false
}
if let lastAttemptDate = lastAttemptDates[url], Date().timeIntervalSince(lastAttemptDate) < minimumAttemptInterval {
return false
}
return true
}
}

View File

@ -0,0 +1,66 @@
//
// SeekingFavicon.swift
// Evergreen
//
// Created by Brent Simmons on 11/23/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
extension Notification.Name {
static let SeekingFaviconDidFindFaviconURL = Notification.Name("SeekingFaviconDidFindFaviconURLNotification")
static let SeekingFaviconDidNotFindFaviconURL = Notification.Name("SeekingFaviconDidNotFindFaviconURLNotification")
}
final class SeekingFavicon {
// At first, when looking for a favicon, we only know the homePageURL.
// The faviconURL may be specified by metadata in the home page,
// 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
}
private static let localeForLowercasing = Locale(identifier: "en_US")
init?(homePageURL: String) {
guard let url = URL(string: homePageURL), let scheme = url.scheme, let host = url.host else {
return nil
}
self.homePageURL = homePageURL
self.defaultFaviconURL = "\(scheme)://\(host)/favicon.ico".lowercased(with: SeekingFavicon.localeForLowercasing)
findFaviconURL()
}
}
private extension SeekingFavicon {
func findFaviconURL() {
FaviconURLFinder.findFaviconURL(homePageURL) { (faviconURL) in
self.didAttemptToLookAtHomePageMetadata = true
self.foundFaviconURL = faviconURL
if let _ = faviconURL {
NotificationCenter.default.post(name: .SeekingFaviconDidFindFaviconURL, object: self)
}
else {
NotificationCenter.default.post(name: .SeekingFaviconDidNotFindFaviconURL, object: self)
}
}
}
}