2017-11-20 08:59:04 +01:00
|
|
|
|
//
|
|
|
|
|
// 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 {
|
|
|
|
|
|
2017-11-23 23:15:28 +01:00
|
|
|
|
private var seekingFaviconCache: [String: SeekingFavicon]() // homePageURL: SeekingFavicon
|
2017-11-20 08:59:04 +01:00
|
|
|
|
private var cache = ThreadSafeCache<NSImage>() // faviconURL: NSImage
|
|
|
|
|
private var faviconURLCache = ThreadSafeCache<String>() // homePageURL: faviconURL
|
|
|
|
|
private let folder: String
|
|
|
|
|
private var urlsBeingDownloaded = Set<String>()
|
2017-11-20 22:29:20 +01:00
|
|
|
|
private var badURLs = Set<String>() // URLs that didn’t work for some reason; don’t try again
|
2017-11-20 08:59:04 +01:00
|
|
|
|
private let binaryCache: RSBinaryCache
|
|
|
|
|
private var badImages = Set<String>() // 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)
|
2017-11-23 23:15:28 +01:00
|
|
|
|
self.queue = DispatchQueue(label: "FaviconDownloader serial queue - \(folder)")
|
2017-11-20 08:59:04 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - API
|
|
|
|
|
|
|
|
|
|
func favicon(for feed: Feed) -> NSImage? {
|
|
|
|
|
|
|
|
|
|
assert(Thread.isMainThread)
|
2017-11-20 22:29:20 +01:00
|
|
|
|
guard let homePageURL = feed.homePageURL else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-23 23:15:28 +01:00
|
|
|
|
if let favicon = cachedInMemoryFavicon(for: feed) {
|
|
|
|
|
return favicon
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
findFavicon(for: feed)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func findFavicon(for feed: Feed) {
|
|
|
|
|
|
|
|
|
|
if let faviconMetadata = cachedFaviconMetadata
|
2017-11-20 08:59:04 +01:00
|
|
|
|
if let faviconURL = faviconURL(for: feed) {
|
|
|
|
|
|
2017-11-23 23:15:28 +01:00
|
|
|
|
// It might be on disk.
|
2017-11-20 22:29:20 +01:00
|
|
|
|
|
2017-11-23 23:15:28 +01:00
|
|
|
|
readFaviconFromDisk(faviconURL) { (image) in
|
2017-11-20 22:29:20 +01:00
|
|
|
|
|
2017-11-23 23:15:28 +01:00
|
|
|
|
if let image = image {
|
|
|
|
|
self.cache[faviconURL] = image
|
|
|
|
|
self.postFaviconDidBecomeAvailableNotification(homePageURL: homePageURL, faviconURL: faviconURL, image: image)
|
|
|
|
|
return
|
|
|
|
|
}
|
2017-11-20 22:29:20 +01:00
|
|
|
|
|
2017-11-23 23:15:28 +01:00
|
|
|
|
// Download it (probably).
|
|
|
|
|
|
|
|
|
|
if !self.shouldDownloadFaviconURL(faviconURL) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
2017-11-20 22:29:20 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-11-23 23:15:28 +01:00
|
|
|
|
|
2017-11-20 22:29:20 +01:00
|
|
|
|
// 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.
|
|
|
|
|
}
|
2017-11-20 08:59:04 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extension FaviconDownloader {
|
|
|
|
|
|
2017-11-23 23:15:28 +01:00
|
|
|
|
func cachedInMemoryFavicon(for feed: Feed) -> NSImage? {
|
|
|
|
|
|
|
|
|
|
guard let faviconURL = faviconURL(for: feed), let cachedFavicon = cache[faviconURL] else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return cachedFavicon
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-20 08:59:04 +01:00
|
|
|
|
func shouldDownloadFaviconURL(_ faviconURL: String) -> Bool {
|
|
|
|
|
|
2017-11-20 22:29:20 +01:00
|
|
|
|
return !urlsBeingDownloaded.contains(faviconURL) && !badURLs.contains(faviconURL)
|
2017-11-20 08:59:04 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-11-20 22:29:20 +01:00
|
|
|
|
func downloadFavicon(_ faviconURL: String, _ homePageURL: String) {
|
2017-11-20 08:59:04 +01:00
|
|
|
|
|
|
|
|
|
guard let url = URL(string: faviconURL) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
urlsBeingDownloaded.insert(faviconURL)
|
|
|
|
|
|
2017-11-23 19:29:00 +01:00
|
|
|
|
downloadUsingCache(url) { (data, response, error) in
|
2017-11-20 08:59:04 +01:00
|
|
|
|
|
|
|
|
|
self.urlsBeingDownloaded.remove(faviconURL)
|
2017-11-20 22:29:20 +01:00
|
|
|
|
if response == nil || !response!.statusIsOK {
|
|
|
|
|
self.badURLs.insert(faviconURL)
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-20 08:59:04 +01:00
|
|
|
|
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()
|
|
|
|
|
}
|
2017-11-23 23:15:28 +01:00
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2017-11-20 08:59:04 +01:00
|
|
|
|
}
|