2017-11-19 23:59:04 -08:00
|
|
|
|
//
|
|
|
|
|
// FaviconDownloader.swift
|
2018-08-28 22:18:24 -07:00
|
|
|
|
// NetNewsWire
|
2017-11-19 23:59:04 -08:00
|
|
|
|
//
|
|
|
|
|
// Created by Brent Simmons on 11/19/17.
|
|
|
|
|
// Copyright © 2017 Ranchero Software. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
2019-04-11 17:53:03 -05:00
|
|
|
|
import Foundation
|
2018-07-23 18:29:08 -07:00
|
|
|
|
import Articles
|
2018-07-28 12:16:14 -07:00
|
|
|
|
import Account
|
2017-11-19 23:59:04 -08:00
|
|
|
|
import RSCore
|
|
|
|
|
|
|
|
|
|
extension Notification.Name {
|
|
|
|
|
|
2017-11-24 13:12:18 -08:00
|
|
|
|
static let FaviconDidBecomeAvailable = Notification.Name("FaviconDidBecomeAvailableNotification") // userInfo key: FaviconDownloader.UserInfoKey.faviconURL
|
2017-11-19 23:59:04 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final class FaviconDownloader {
|
|
|
|
|
|
2019-10-31 14:04:34 -05:00
|
|
|
|
private static let saveQueue = CoalescingQueue(name: "Cache Save Queue", interval: 1.0)
|
|
|
|
|
|
2017-11-19 23:59:04 -08:00
|
|
|
|
private let folder: String
|
2017-11-24 13:12:18 -08:00
|
|
|
|
private let diskCache: BinaryDiskCache
|
2017-12-02 21:27:25 -08:00
|
|
|
|
private var singleFaviconDownloaderCache = [String: SingleFaviconDownloader]() // faviconURL: SingleFaviconDownloader
|
2019-10-31 14:04:34 -05:00
|
|
|
|
|
2017-12-02 21:27:25 -08:00
|
|
|
|
private var homePageToFaviconURLCache = [String: String]() //homePageURL: faviconURL
|
2019-10-31 14:04:34 -05:00
|
|
|
|
private var homePageToFaviconURLCachePath: String
|
|
|
|
|
private var homePageToFaviconURLCacheDirty = false {
|
|
|
|
|
didSet {
|
|
|
|
|
queueSaveHomePageToFaviconURLCacheIfNeeded()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var homePageURLsWithNoFaviconURLCache = Set<String>()
|
|
|
|
|
private var homePageURLsWithNoFaviconURLCachePath: String
|
|
|
|
|
private var homePageURLsWithNoFaviconURLCacheDirty = false {
|
|
|
|
|
didSet {
|
|
|
|
|
queueSaveHomePageURLsWithNoFaviconURLCacheIfNeeded()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-19 23:59:04 -08:00
|
|
|
|
private let queue: DispatchQueue
|
2019-11-05 18:05:57 -06:00
|
|
|
|
private var cache = [Feed: IconImage]() // faviconURL: RSImage
|
2017-11-19 23:59:04 -08:00
|
|
|
|
|
2017-11-24 13:12:18 -08:00
|
|
|
|
struct UserInfoKey {
|
2017-11-19 23:59:04 -08:00
|
|
|
|
static let faviconURL = "faviconURL"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init(folder: String) {
|
|
|
|
|
|
|
|
|
|
self.folder = folder
|
2017-11-24 13:12:18 -08:00
|
|
|
|
self.diskCache = BinaryDiskCache(folder: folder)
|
2017-11-23 14:15:28 -08:00
|
|
|
|
self.queue = DispatchQueue(label: "FaviconDownloader serial queue - \(folder)")
|
2017-11-24 13:12:18 -08:00
|
|
|
|
|
2019-10-31 14:04:34 -05:00
|
|
|
|
self.homePageToFaviconURLCachePath = (folder as NSString).appendingPathComponent("HomePageToFaviconURLCache.plist")
|
|
|
|
|
self.homePageURLsWithNoFaviconURLCachePath = (folder as NSString).appendingPathComponent("HomePageURLsWithNoFaviconURLCache.plist")
|
|
|
|
|
loadHomePageToFaviconURLCache()
|
|
|
|
|
loadHomePageURLsWithNoFaviconURLCache()
|
|
|
|
|
|
2017-11-24 13:12:18 -08:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(didLoadFavicon(_:)), name: .DidLoadFavicon, object: nil)
|
2017-11-19 23:59:04 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - API
|
|
|
|
|
|
2019-06-14 15:33:13 -05:00
|
|
|
|
func resetCache() {
|
2019-11-05 18:05:57 -06:00
|
|
|
|
cache = [Feed: IconImage]()
|
2019-06-14 15:33:13 -05:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-05 18:05:57 -06:00
|
|
|
|
func favicon(for feed: Feed) -> IconImage? {
|
2017-11-19 23:59:04 -08:00
|
|
|
|
|
|
|
|
|
assert(Thread.isMainThread)
|
2017-11-24 10:45:22 -08:00
|
|
|
|
|
|
|
|
|
if let faviconURL = feed.faviconURL {
|
2017-11-24 13:12:18 -08:00
|
|
|
|
return favicon(with: faviconURL)
|
2017-11-24 10:45:22 -08:00
|
|
|
|
}
|
|
|
|
|
|
2017-12-13 19:45:12 -08:00
|
|
|
|
var homePageURL = feed.homePageURL
|
|
|
|
|
if homePageURL == nil {
|
|
|
|
|
// Base homePageURL off feedURL if needed. Won’t always be accurate, but is good enough.
|
|
|
|
|
if let feedURL = URL(string: feed.url), let scheme = feedURL.scheme, let host = feedURL.host {
|
|
|
|
|
homePageURL = scheme + "://" + host + "/"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let homePageURL = homePageURL {
|
|
|
|
|
return favicon(withHomePageURL: homePageURL)
|
2017-11-20 13:29:20 -08:00
|
|
|
|
}
|
2017-12-13 19:45:12 -08:00
|
|
|
|
|
|
|
|
|
return nil
|
2017-11-24 10:45:22 -08:00
|
|
|
|
}
|
2019-06-14 15:33:13 -05:00
|
|
|
|
|
2019-11-05 18:05:57 -06:00
|
|
|
|
func faviconAsIcon(for feed: Feed) -> IconImage? {
|
2019-06-14 15:33:13 -05:00
|
|
|
|
|
|
|
|
|
if let image = cache[feed] {
|
|
|
|
|
return image
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-05 18:05:57 -06:00
|
|
|
|
if let iconImage = favicon(for: feed), let imageData = iconImage.image.dataRepresentation() {
|
|
|
|
|
if let scaledImage = RSImage.scaledForIcon(imageData) {
|
|
|
|
|
let scaledIconImage = IconImage(scaledImage)
|
|
|
|
|
cache[feed] = scaledIconImage
|
|
|
|
|
return scaledIconImage
|
2019-06-14 15:33:13 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2017-11-24 10:45:22 -08:00
|
|
|
|
|
2019-11-05 18:05:57 -06:00
|
|
|
|
func favicon(with faviconURL: String) -> IconImage? {
|
2017-11-24 10:45:22 -08:00
|
|
|
|
|
2017-11-24 13:12:18 -08:00
|
|
|
|
let downloader = faviconDownloader(withURL: faviconURL)
|
2019-11-05 18:05:57 -06:00
|
|
|
|
return downloader.iconImage
|
2017-11-24 10:45:22 -08:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-05 18:05:57 -06:00
|
|
|
|
func favicon(withHomePageURL homePageURL: String) -> IconImage? {
|
2017-11-20 13:29:20 -08:00
|
|
|
|
|
2018-09-09 12:42:39 -07:00
|
|
|
|
let url = homePageURL.rs_normalizedURL()
|
2019-10-31 14:04:34 -05:00
|
|
|
|
if homePageURLsWithNoFaviconURLCache.contains(url) {
|
2017-11-24 13:12:18 -08:00
|
|
|
|
return nil
|
2017-11-24 10:45:22 -08:00
|
|
|
|
}
|
2017-12-02 21:27:25 -08:00
|
|
|
|
|
|
|
|
|
if let faviconURL = homePageToFaviconURLCache[url] {
|
|
|
|
|
return favicon(with: faviconURL)
|
|
|
|
|
}
|
2017-11-24 10:45:22 -08:00
|
|
|
|
|
2017-12-30 13:16:40 -08:00
|
|
|
|
findFaviconURL(with: url) { (faviconURL) in
|
2017-12-02 21:27:25 -08:00
|
|
|
|
if let faviconURL = faviconURL {
|
|
|
|
|
self.homePageToFaviconURLCache[url] = faviconURL
|
2019-10-31 14:04:34 -05:00
|
|
|
|
self.homePageToFaviconURLCacheDirty = true
|
2017-12-02 21:27:25 -08:00
|
|
|
|
let _ = self.favicon(with: faviconURL)
|
|
|
|
|
}
|
|
|
|
|
else {
|
2019-10-31 14:04:34 -05:00
|
|
|
|
self.homePageURLsWithNoFaviconURLCache.insert(url)
|
|
|
|
|
self.homePageURLsWithNoFaviconURLCacheDirty = true
|
2017-12-02 21:27:25 -08:00
|
|
|
|
}
|
2017-11-23 14:15:28 -08:00
|
|
|
|
}
|
2017-12-02 21:27:25 -08:00
|
|
|
|
|
|
|
|
|
return nil
|
2017-11-23 14:15:28 -08:00
|
|
|
|
}
|
|
|
|
|
|
2017-12-02 21:27:25 -08:00
|
|
|
|
// MARK: - Notifications
|
|
|
|
|
|
2017-11-24 13:12:18 -08:00
|
|
|
|
@objc func didLoadFavicon(_ note: Notification) {
|
2017-11-23 14:15:28 -08:00
|
|
|
|
|
2017-11-24 13:12:18 -08:00
|
|
|
|
guard let singleFaviconDownloader = note.object as? SingleFaviconDownloader else {
|
|
|
|
|
return
|
2017-11-20 13:29:20 -08:00
|
|
|
|
}
|
2019-11-05 18:05:57 -06:00
|
|
|
|
guard let _ = singleFaviconDownloader.iconImage else {
|
2017-11-24 13:12:18 -08:00
|
|
|
|
return
|
2017-11-19 23:59:04 -08:00
|
|
|
|
}
|
|
|
|
|
|
2017-11-24 13:12:18 -08:00
|
|
|
|
postFaviconDidBecomeAvailableNotification(singleFaviconDownloader.faviconURL)
|
2017-11-19 23:59:04 -08:00
|
|
|
|
}
|
2019-10-31 14:04:34 -05:00
|
|
|
|
|
|
|
|
|
@objc func saveHomePageToFaviconURLCacheIfNeeded() {
|
|
|
|
|
if homePageToFaviconURLCacheDirty {
|
|
|
|
|
saveHomePageToFaviconURLCache()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func saveHomePageURLsWithNoFaviconURLCacheIfNeeded() {
|
|
|
|
|
if homePageURLsWithNoFaviconURLCacheDirty {
|
|
|
|
|
saveHomePageURLsWithNoFaviconURLCache()
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-11-19 23:59:04 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extension FaviconDownloader {
|
|
|
|
|
|
2017-12-02 21:27:25 -08:00
|
|
|
|
static let localeForLowercasing = Locale(identifier: "en_US")
|
|
|
|
|
|
|
|
|
|
func findFaviconURL(with homePageURL: String, _ completion: @escaping (String?) -> Void) {
|
|
|
|
|
|
|
|
|
|
guard let url = URL(string: homePageURL) else {
|
|
|
|
|
completion(nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FaviconURLFinder.findFaviconURL(homePageURL) { (faviconURL) in
|
|
|
|
|
|
|
|
|
|
if let faviconURL = faviconURL {
|
|
|
|
|
completion(faviconURL)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard let scheme = url.scheme, let host = url.host else {
|
|
|
|
|
completion(nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let defaultFaviconURL = "\(scheme)://\(host)/favicon.ico".lowercased(with: FaviconDownloader.localeForLowercasing)
|
|
|
|
|
completion(defaultFaviconURL)
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-11-25 16:11:24 -08:00
|
|
|
|
|
2017-11-24 13:12:18 -08:00
|
|
|
|
func faviconDownloader(withURL faviconURL: String) -> SingleFaviconDownloader {
|
2017-11-19 23:59:04 -08:00
|
|
|
|
|
2017-11-24 13:12:18 -08:00
|
|
|
|
if let downloader = singleFaviconDownloaderCache[faviconURL] {
|
|
|
|
|
downloader.downloadFaviconIfNeeded()
|
|
|
|
|
return downloader
|
2017-11-19 23:59:04 -08:00
|
|
|
|
}
|
|
|
|
|
|
2017-11-24 13:12:18 -08:00
|
|
|
|
let downloader = SingleFaviconDownloader(faviconURL: faviconURL, diskCache: diskCache, queue: queue)
|
|
|
|
|
singleFaviconDownloaderCache[faviconURL] = downloader
|
|
|
|
|
return downloader
|
2017-11-19 23:59:04 -08:00
|
|
|
|
}
|
|
|
|
|
|
2017-11-24 13:12:18 -08:00
|
|
|
|
func postFaviconDidBecomeAvailableNotification(_ faviconURL: String) {
|
2017-11-23 14:15:28 -08:00
|
|
|
|
|
2018-01-04 21:20:09 -08:00
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
let userInfo: [AnyHashable: Any] = [UserInfoKey.faviconURL: faviconURL]
|
|
|
|
|
NotificationCenter.default.post(name: .FaviconDidBecomeAvailable, object: self, userInfo: userInfo)
|
|
|
|
|
}
|
2017-11-23 14:15:28 -08:00
|
|
|
|
}
|
2019-10-31 14:04:34 -05:00
|
|
|
|
|
|
|
|
|
func loadHomePageToFaviconURLCache() {
|
|
|
|
|
let url = URL(fileURLWithPath: homePageToFaviconURLCachePath)
|
|
|
|
|
guard let data = try? Data(contentsOf: url) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
let decoder = PropertyListDecoder()
|
|
|
|
|
homePageToFaviconURLCache = (try? decoder.decode([String: String].self, from: data)) ?? [String: String]()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loadHomePageURLsWithNoFaviconURLCache() {
|
|
|
|
|
let url = URL(fileURLWithPath: homePageURLsWithNoFaviconURLCachePath)
|
|
|
|
|
guard let data = try? Data(contentsOf: url) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
let decoder = PropertyListDecoder()
|
|
|
|
|
let decoded = (try? decoder.decode([String].self, from: data)) ?? [String]()
|
|
|
|
|
homePageURLsWithNoFaviconURLCache = Set(decoded)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func queueSaveHomePageToFaviconURLCacheIfNeeded() {
|
|
|
|
|
FaviconDownloader.saveQueue.add(self, #selector(saveHomePageToFaviconURLCacheIfNeeded))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func queueSaveHomePageURLsWithNoFaviconURLCacheIfNeeded() {
|
|
|
|
|
FaviconDownloader.saveQueue.add(self, #selector(saveHomePageURLsWithNoFaviconURLCacheIfNeeded))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func saveHomePageToFaviconURLCache() {
|
|
|
|
|
homePageToFaviconURLCacheDirty = false
|
|
|
|
|
|
|
|
|
|
let encoder = PropertyListEncoder()
|
|
|
|
|
encoder.outputFormat = .binary
|
|
|
|
|
let url = URL(fileURLWithPath: homePageToFaviconURLCachePath)
|
|
|
|
|
do {
|
|
|
|
|
let data = try encoder.encode(homePageToFaviconURLCache)
|
|
|
|
|
try data.write(to: url)
|
|
|
|
|
} catch {
|
|
|
|
|
assertionFailure(error.localizedDescription)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func saveHomePageURLsWithNoFaviconURLCache() {
|
|
|
|
|
homePageURLsWithNoFaviconURLCacheDirty = false
|
|
|
|
|
|
|
|
|
|
let encoder = PropertyListEncoder()
|
|
|
|
|
encoder.outputFormat = .binary
|
|
|
|
|
let url = URL(fileURLWithPath: homePageURLsWithNoFaviconURLCachePath)
|
|
|
|
|
do {
|
|
|
|
|
let data = try encoder.encode(Array(homePageURLsWithNoFaviconURLCache))
|
|
|
|
|
try data.write(to: url)
|
|
|
|
|
} catch {
|
|
|
|
|
assertionFailure(error.localizedDescription)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-19 23:59:04 -08:00
|
|
|
|
}
|