NetNewsWire/Shared/Favicons/FaviconDownloader.swift

308 lines
9.1 KiB
Swift
Raw Normal View History

2017-11-20 08:59:04 +01:00
//
// FaviconDownloader.swift
2018-08-29 07:18:24 +02:00
// NetNewsWire
2017-11-20 08:59:04 +01:00
//
// Created by Brent Simmons on 11/19/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
import CoreServices
import Articles
import Account
2017-11-20 08:59:04 +01:00
import RSCore
extension Notification.Name {
static let FaviconDidBecomeAvailable = Notification.Name("FaviconDidBecomeAvailableNotification") // userInfo key: FaviconDownloader.UserInfoKey.faviconURL
2017-11-20 08:59:04 +01:00
}
final class FaviconDownloader {
2019-10-31 20:04:34 +01:00
private static let saveQueue = CoalescingQueue(name: "Cache Save Queue", interval: 1.0)
2017-11-20 08:59:04 +01:00
private let folder: String
private let diskCache: BinaryDiskCache
private var singleFaviconDownloaderCache = [String: SingleFaviconDownloader]() // faviconURL: SingleFaviconDownloader
private var remainingFaviconURLs = [String: ArraySlice<String>]() // homePageURL: array of faviconURLs that haven't been checked yet
private var currentHomePageHasOnlyFaviconICO = false
private var homePageToFaviconURLCache = [String: String]() //homePageURL: faviconURL
2019-10-31 20:04:34 +01: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-20 08:59:04 +01:00
private let queue: DispatchQueue
private var cache = [WebFeed: IconImage]() // faviconURL: RSImage
2017-11-20 08:59:04 +01:00
struct UserInfoKey {
2017-11-20 08:59:04 +01:00
static let faviconURL = "faviconURL"
}
init(folder: String) {
self.folder = folder
self.diskCache = BinaryDiskCache(folder: folder)
2017-11-23 23:15:28 +01:00
self.queue = DispatchQueue(label: "FaviconDownloader serial queue - \(folder)")
2019-10-31 20:04:34 +01:00
self.homePageToFaviconURLCachePath = (folder as NSString).appendingPathComponent("HomePageToFaviconURLCache.plist")
self.homePageURLsWithNoFaviconURLCachePath = (folder as NSString).appendingPathComponent("HomePageURLsWithNoFaviconURLCache.plist")
loadHomePageToFaviconURLCache()
loadHomePageURLsWithNoFaviconURLCache()
FaviconURLFinder.ignoredTypes = [kUTTypeScalableVectorGraphics as String]
NotificationCenter.default.addObserver(self, selector: #selector(didLoadFavicon(_:)), name: .DidLoadFavicon, object: nil)
2017-11-20 08:59:04 +01:00
}
// MARK: - API
func resetCache() {
cache = [WebFeed: IconImage]()
}
func favicon(for webFeed: WebFeed) -> IconImage? {
2017-11-20 08:59:04 +01:00
assert(Thread.isMainThread)
2017-11-24 19:45:22 +01:00
2019-11-27 21:08:52 +01:00
var homePageURL = webFeed.homePageURL
if let faviconURL = webFeed.faviconURL {
return favicon(with: faviconURL, homePageURL: homePageURL)
2017-11-24 19:45:22 +01:00
}
if homePageURL == nil {
// Base homePageURL off feedURL if needed. Wont always be accurate, but is good enough.
if let feedURL = URL(string: webFeed.url), let scheme = feedURL.scheme, let host = feedURL.host {
homePageURL = scheme + "://" + host + "/"
}
}
if let homePageURL = homePageURL {
return favicon(withHomePageURL: homePageURL)
}
return nil
2017-11-24 19:45:22 +01:00
}
func faviconAsIcon(for webFeed: WebFeed) -> IconImage? {
if let image = cache[webFeed] {
return image
}
if let iconImage = favicon(for: webFeed), let imageData = iconImage.image.dataRepresentation() {
if let scaledImage = RSImage.scaledForIcon(imageData) {
let scaledIconImage = IconImage(scaledImage)
cache[webFeed] = scaledIconImage
return scaledIconImage
}
}
return nil
}
2017-11-24 19:45:22 +01:00
2019-11-27 21:08:52 +01:00
func favicon(with faviconURL: String, homePageURL: String?) -> IconImage? {
let downloader = faviconDownloader(withURL: faviconURL, homePageURL: homePageURL)
return downloader.iconImage
2017-11-24 19:45:22 +01:00
}
func favicon(withHomePageURL homePageURL: String) -> IconImage? {
let url = homePageURL.normalizedURL
if let url = URL(string: homePageURL) {
if url.host == "nnw.ranchero.com" {
return IconImage.appIcon
}
}
2019-10-31 20:04:34 +01:00
if homePageURLsWithNoFaviconURLCache.contains(url) {
return nil
2017-11-24 19:45:22 +01:00
}
if let faviconURL = homePageToFaviconURLCache[url] {
return favicon(with: faviconURL, homePageURL: url)
}
2017-11-24 19:45:22 +01:00
findFaviconURLs(with: url) { (faviconURLs) in
if let faviconURLs = faviconURLs {
2020-02-15 15:22:59 +01:00
// If the site explicitly specifies favicon.ico, it will appear twice.
self.currentHomePageHasOnlyFaviconICO = faviconURLs.count == 1
if let firstIconURL = faviconURLs.first {
let _ = self.favicon(with: firstIconURL, homePageURL: url)
self.remainingFaviconURLs[url] = faviconURLs.dropFirst()
}
}
2017-11-23 23:15:28 +01:00
}
return nil
2017-11-23 23:15:28 +01:00
}
// MARK: - Notifications
@objc func didLoadFavicon(_ note: Notification) {
2017-11-23 23:15:28 +01:00
guard let singleFaviconDownloader = note.object as? SingleFaviconDownloader else {
return
}
guard let homePageURL = singleFaviconDownloader.homePageURL else {
return
}
guard let _ = singleFaviconDownloader.iconImage else {
if let faviconURLs = remainingFaviconURLs[homePageURL] {
if let nextIconURL = faviconURLs.first {
let _ = favicon(with: nextIconURL, homePageURL: singleFaviconDownloader.homePageURL)
remainingFaviconURLs[homePageURL] = faviconURLs.dropFirst();
} else {
remainingFaviconURLs[homePageURL] = nil
if currentHomePageHasOnlyFaviconICO {
self.homePageURLsWithNoFaviconURLCache.insert(homePageURL)
self.homePageURLsWithNoFaviconURLCacheDirty = true
}
}
}
return
2017-11-20 08:59:04 +01:00
}
remainingFaviconURLs[homePageURL] = nil
if self.homePageToFaviconURLCache[homePageURL] == nil {
self.homePageToFaviconURLCache[homePageURL] = singleFaviconDownloader.faviconURL
self.homePageToFaviconURLCacheDirty = true
}
postFaviconDidBecomeAvailableNotification(singleFaviconDownloader.faviconURL)
2017-11-20 08:59:04 +01:00
}
2019-10-31 20:04:34 +01:00
@objc func saveHomePageToFaviconURLCacheIfNeeded() {
if homePageToFaviconURLCacheDirty {
saveHomePageToFaviconURLCache()
}
}
@objc func saveHomePageURLsWithNoFaviconURLCacheIfNeeded() {
if homePageURLsWithNoFaviconURLCacheDirty {
saveHomePageURLsWithNoFaviconURLCache()
}
}
2017-11-20 08:59:04 +01:00
}
private extension FaviconDownloader {
static let localeForLowercasing = Locale(identifier: "en_US")
func findFaviconURLs(with homePageURL: String, _ completion: @escaping ([String]?) -> Void) {
guard let url = URL(string: homePageURL) else {
completion(nil)
return
}
FaviconURLFinder.findFaviconURLs(with: homePageURL) { (faviconURLs) in
var defaultFaviconURL: String? = nil
if let scheme = url.scheme, let host = url.host {
defaultFaviconURL = "\(scheme)://\(host)/favicon.ico".lowercased(with: FaviconDownloader.localeForLowercasing)
}
if var faviconURLs = faviconURLs {
if let defaultFaviconURL = defaultFaviconURL {
faviconURLs.append(defaultFaviconURL)
}
completion(faviconURLs)
return
}
completion(defaultFaviconURL != nil ? [defaultFaviconURL!] : nil)
}
}
func faviconDownloader(withURL faviconURL: String, homePageURL: String?) -> SingleFaviconDownloader {
2017-11-20 08:59:04 +01:00
if let downloader = singleFaviconDownloaderCache[faviconURL] {
downloader.downloadFaviconIfNeeded()
return downloader
2017-11-20 08:59:04 +01:00
}
let downloader = SingleFaviconDownloader(faviconURL: faviconURL, homePageURL: homePageURL, diskCache: diskCache, queue: queue)
singleFaviconDownloaderCache[faviconURL] = downloader
return downloader
2017-11-20 08:59:04 +01:00
}
func postFaviconDidBecomeAvailableNotification(_ faviconURL: String) {
2017-11-23 23:15:28 +01:00
DispatchQueue.main.async {
let userInfo: [AnyHashable: Any] = [UserInfoKey.faviconURL: faviconURL]
NotificationCenter.default.post(name: .FaviconDidBecomeAvailable, object: self, userInfo: userInfo)
}
2017-11-23 23:15:28 +01:00
}
2019-10-31 20:04:34 +01: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-20 08:59:04 +01:00
}