NetNewsWire/Evergreen/Favicons/FaviconDownloader.swift

172 lines
3.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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<NSImage>() // faviconURL: NSImage
private var faviconURLCache = ThreadSafeCache<String>() // homePageURL: faviconURL
private let folder: String
private var urlsBeingDownloaded = Set<String>()
private var badURLs = Set<String>() // URLs that didnt work for some reason; dont try again
private let binaryCache: RSBinaryCache
private var badImages = Set<String>() // keys for images on disk that NSImage cant 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)
guard let homePageURL = feed.homePageURL else {
return nil
}
if let faviconURL = faviconURL(for: feed) {
if let cachedFavicon = cache[faviconURL] {
return cachedFavicon
}
// 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.
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
}
}
private extension FaviconDownloader {
func shouldDownloadFaviconURL(_ faviconURL: String) -> Bool {
return !urlsBeingDownloaded.contains(faviconURL) && !badURLs.contains(faviconURL)
}
func downloadFavicon(_ faviconURL: String, _ homePageURL: String) {
guard let url = URL(string: faviconURL) else {
return
}
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) {
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()
}
}