Make further progress on favicons. Should be close to a first cut now.
This commit is contained in:
parent
32973c4c85
commit
f8a05badcb
@ -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 = "<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>"; };
|
||||
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>"; };
|
||||
845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleFaviconDownloader.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>"; };
|
||||
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>"; };
|
||||
@ -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 */,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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<NSImage>() // faviconURL: NSImage
|
||||
private var faviconURLCache = ThreadSafeCache<String>() // homePageURL: faviconURL
|
||||
private var singleFaviconDownloaderCache = [String: SingleFaviconDownloader]() // faviconURL: SingleFaviconDownloader
|
||||
private let folder: String
|
||||
private var urlsBeingDownloaded = Set<String>()
|
||||
private var badURLs = Set<String>() // URLs that didn’t work for some reason; don’t try again
|
||||
private let binaryCache: RSBinaryCache
|
||||
private var badImages = Set<String>() // 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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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<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 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
150
Evergreen/Favicons/SingleFaviconDownloader.swift
Normal file
150
Evergreen/Favicons/SingleFaviconDownloader.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user