diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index 941c60af1..104d939fa 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 8426118A1FCB67AA0086A189 /* FeedIconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611891FCB67AA0086A189 /* FeedIconDownloader.swift */; }; 8426119E1FCB6ED40086A189 /* HTMLMetadataDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8426119D1FCB6ED40086A189 /* HTMLMetadataDownloader.swift */; }; + 842611A01FCB72600086A189 /* FeaturedImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8426119F1FCB72600086A189 /* FeaturedImageDownloader.swift */; }; + 842611A21FCB769D0086A189 /* RSHTMLData+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611A11FCB769D0086A189 /* RSHTMLData+Extension.swift */; }; 842E45CE1ED8C308000A8B52 /* AppNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45CD1ED8C308000A8B52 /* AppNotifications.swift */; }; 842E45DD1ED8C54B000A8B52 /* Browser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45DC1ED8C54B000A8B52 /* Browser.swift */; }; 842E45E31ED8C681000A8B52 /* KeyboardDelegateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45E21ED8C681000A8B52 /* KeyboardDelegateProtocol.swift */; }; @@ -407,6 +409,8 @@ /* Begin PBXFileReference section */ 842611891FCB67AA0086A189 /* FeedIconDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedIconDownloader.swift; sourceTree = ""; }; 8426119D1FCB6ED40086A189 /* HTMLMetadataDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadataDownloader.swift; sourceTree = ""; }; + 8426119F1FCB72600086A189 /* FeaturedImageDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedImageDownloader.swift; sourceTree = ""; }; + 842611A11FCB769D0086A189 /* RSHTMLData+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSHTMLData+Extension.swift"; sourceTree = ""; }; 842E45CD1ED8C308000A8B52 /* AppNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppNotifications.swift; path = Evergreen/AppNotifications.swift; sourceTree = ""; }; 842E45DC1ED8C54B000A8B52 /* Browser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Browser.swift; path = Evergreen/Browser.swift; sourceTree = ""; }; 842E45E21ED8C681000A8B52 /* KeyboardDelegateProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardDelegateProtocol.swift; sourceTree = ""; }; @@ -581,6 +585,8 @@ 845213221FCA5B10003B6E93 /* ImageDownloader.swift */, 84E850851FCB60CE0072EA88 /* AuthorAvatarDownloader.swift */, 842611891FCB67AA0086A189 /* FeedIconDownloader.swift */, + 8426119F1FCB72600086A189 /* FeaturedImageDownloader.swift */, + 842611A11FCB769D0086A189 /* RSHTMLData+Extension.swift */, ); name = Images; path = Evergreen/Images; @@ -1372,6 +1378,7 @@ 849A975C1ED9EB0D007D329B /* DefaultFeedsImporter.swift in Sources */, 849A97891ED9ECEF007D329B /* ArticleStyle.swift in Sources */, 84FF69B11FC3793300DC198E /* FaviconURLFinder.swift in Sources */, + 842611A21FCB769D0086A189 /* RSHTMLData+Extension.swift in Sources */, 849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */, 849A97791ED9EC04007D329B /* TimelineStringUtilities.swift in Sources */, 84F204CE1FAACB660076E152 /* FeedListViewController.swift in Sources */, @@ -1391,6 +1398,7 @@ 849A978D1ED9EE4D007D329B /* FeedListWindowController.swift in Sources */, 849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */, 84B99C6B1FAE370B00ECDEDB /* FeedListFeed.swift in Sources */, + 842611A01FCB72600086A189 /* FeaturedImageDownloader.swift in Sources */, 849A97781ED9EC04007D329B /* TimelineCellLayout.swift in Sources */, 849A976C1ED9EBC8007D329B /* TimelineTableRowView.swift in Sources */, 849A977B1ED9EC04007D329B /* UnreadIndicatorView.swift in Sources */, diff --git a/Evergreen/AppDelegate.swift b/Evergreen/AppDelegate.swift index f43c31226..efb9b6c80 100644 --- a/Evergreen/AppDelegate.swift +++ b/Evergreen/AppDelegate.swift @@ -24,6 +24,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, var faviconDownloader: FaviconDownloader! var imageDownloader: ImageDownloader! var authorAvatarDownloader: AuthorAvatarDownloader! + var feedIconDownloader: FeedIconDownloader! var appName: String! var pseudoFeeds = [PseudoFeed]() @@ -144,7 +145,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, imageDownloader = ImageDownloader(folder: imagesFolder) authorAvatarDownloader = AuthorAvatarDownloader(imageDownloader: imageDownloader) - + feedIconDownloader = FeedIconDownloader(imageDownloader: imageDownloader) + let todayFeed = SmartFeed(delegate: TodayFeedDelegate()) let unreadFeed = UnreadFeed() let starredFeed = SmartFeed(delegate: StarredFeedDelegate()) diff --git a/Evergreen/Images/FeaturedImageDownloader.swift b/Evergreen/Images/FeaturedImageDownloader.swift new file mode 100644 index 000000000..3a043b811 --- /dev/null +++ b/Evergreen/Images/FeaturedImageDownloader.swift @@ -0,0 +1,88 @@ +// +// FeaturedImageDownloader.swift +// Evergreen +// +// Created by Brent Simmons on 11/26/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import Cocoa +import Data +import RSParser + +final class FeaturedImageDownloader { + + private let imageDownloader: ImageDownloader + private var articleURLToFeaturedImageURLCache = [String: String]() + private var articleURLsWithNoFeaturedImage = Set() + + init(imageDownloader: ImageDownloader) { + + self.imageDownloader = imageDownloader + } + + func image(for article: Article) -> NSImage? { + + if let url = article.imageURL { + return image(forFeaturedImageURL: url) + } + if let articleURL = article.url { + return image(forArticleURL: articleURL) + } + return nil + } + + func image(forArticleURL articleURL: String) -> NSImage? { + + if articleURLsWithNoFeaturedImage.contains(articleURL) { + return nil + } + + if let featuredImageURL = cachedURL(for: articleURL) { + return image(forFeaturedImageURL: featuredImageURL) + } + findFeaturedImageURL(for: articleURL) + return nil + } + + func image(forFeaturedImageURL featuredImageURL: String) -> NSImage? { + + return imageDownloader.image(for: featuredImageURL) + } +} + +private extension FeaturedImageDownloader { + + func cachedURL(for articleURL: String) -> String? { + + return articleURLToFeaturedImageURLCache[articleURL] + } + + func cacheURL(for articleURL: String, _ featuredImageURL: String) { + + articleURLsWithNoFeaturedImage.remove(articleURL) + articleURLToFeaturedImageURLCache[articleURL] = featuredImageURL + } + + func findFeaturedImageURL(for articleURL: String) { + + HTMLMetadataDownloader.downloadMetadata(for: articleURL) { (metadata) in + + guard let metadata = metadata else { + return + } + self.pullFeaturedImageURL(from: metadata, articleURL: articleURL) + } + } + + func pullFeaturedImageURL(from metadata: RSHTMLMetadata, articleURL: String) { + + if let url = metadata.bestFeaturedImageURL() { + cacheURL(for: articleURL, url) + let _ = image(forFeaturedImageURL: url) + return + } + + articleURLsWithNoFeaturedImage.insert(articleURL) + } +} diff --git a/Evergreen/Images/FeedIconDownloader.swift b/Evergreen/Images/FeedIconDownloader.swift index 16a67b812..b2504f048 100644 --- a/Evergreen/Images/FeedIconDownloader.swift +++ b/Evergreen/Images/FeedIconDownloader.swift @@ -15,6 +15,8 @@ public final class FeedIconDownloader { private let imageDownloader: ImageDownloader private var homePageToIconURLCache = [String: String]() + private var homePagesWithNoIconURL = Set() + private var homePageDownloadsInProgress = Set() init(imageDownloader: ImageDownloader) { @@ -36,6 +38,10 @@ public final class FeedIconDownloader { func icon(forHomePageURL homePageURL: String) -> NSImage? { + if homePagesWithNoIconURL.contains(homePageURL) { + return nil + } + if let iconURL = cachedIconURL(for: homePageURL) { return icon(forURL: iconURL) } @@ -59,14 +65,20 @@ private extension FeedIconDownloader { func cacheIconURL(for homePageURL: String, _ iconURL: String) { + homePagesWithNoIconURL.remove(homePageURL) homePageToIconURLCache[homePageURL] = iconURL - let _ = icon(forURL: iconURL) } func findIconURLForHomePageURL(_ homePageURL: String) { + guard !homePageDownloadsInProgress.contains(homePageURL) else { + return + } + homePageDownloadsInProgress.insert(homePageURL) + HTMLMetadataDownloader.downloadMetadata(for: homePageURL) { (metadata) in + self.homePageDownloadsInProgress.remove(homePageURL) guard let metadata = metadata else { return } @@ -76,34 +88,12 @@ private extension FeedIconDownloader { func pullIconURL(from metadata: RSHTMLMetadata, homePageURL: String) { - if let openGraphImageURL = largestOpenGraphImageURL(from: metadata) { - cacheIconURL(for: homePageURL, openGraphImageURL) + if let url = metadata.bestWebsiteIconURL() { + cacheIconURL(for: homePageURL, url) + let _ = icon(forURL: url) return } - if let twitterImageURL = metadata.twitterProperties.imageURL { - cacheIconURL(for: homePageURL, twitterImageURL) - } - } - - func largestOpenGraphImageURL(from metadata: RSHTMLMetadata) -> String? { - - guard let openGraphImages = metadata.openGraphProperties?.images else { - return nil - } - - var bestImage: RSHTMLOpenGraphImage? = nil - - for image in openGraphImages { - if bestImage == nil { - bestImage = image - continue - } - if image.height > bestImage!.height && image.width > bestImage!.width { - bestImage = image - } - } - - return bestImage?.secureURL ?? bestImage?.url + homePagesWithNoIconURL.insert(homePageURL) } } diff --git a/Evergreen/Images/RSHTMLData+Extension.swift b/Evergreen/Images/RSHTMLData+Extension.swift new file mode 100644 index 000000000..5a2df11f1 --- /dev/null +++ b/Evergreen/Images/RSHTMLData+Extension.swift @@ -0,0 +1,64 @@ +// +// RSHTMLData+Extension.swift +// Evergreen +// +// Created by Brent Simmons on 11/26/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import Foundation +import RSParser + +extension RSHTMLMetadata { + + func largestOpenGraphImageURL() -> String? { + + guard let openGraphImages = openGraphProperties?.images, !openGraphImages.isEmpty else { + return nil + } + + var bestImage: RSHTMLOpenGraphImage? = nil + + for image in openGraphImages { + if bestImage == nil { + bestImage = image + continue + } + if image.height > bestImage!.height && image.width > bestImage!.width { + bestImage = image + } + } + + guard let url = bestImage?.secureURL ?? bestImage?.url else { + return nil + } + + // Bad ones we should ignore. + let badURLs = Set(["https://s0.wp.com/i/blank.jpg"]) + guard !badURLs.contains(url) else { + return nil + } + + return url + } + + func bestWebsiteIconURL() -> String? { + + // TODO: metadata icons — sometimes they’re large enough to use here. + + if let openGraphImageURL = largestOpenGraphImageURL() { + return openGraphImageURL + } + + return twitterProperties.imageURL + } + + func bestFeaturedImageURL() -> String? { + + if let openGraphImageURL = largestOpenGraphImageURL() { + return openGraphImageURL + } + + return twitterProperties.imageURL + } +} diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController.swift b/Evergreen/MainWindow/Timeline/TimelineViewController.swift index 0338dc4ed..e2f07a338 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController.swift @@ -443,11 +443,7 @@ extension TimelineViewController: NSTableViewDelegate { // TODO: make Feed know about its authors. // https://github.com/brentsimmons/Evergreen/issues/212 - if let iconURL = feed.iconURL { - return appDelegate.imageDownloader.image(for: iconURL) - } - - return nil + return appDelegate.feedIconDownloader.icon(for: feed) } private func avatarForAuthor(_ author: Author) -> NSImage? {