Make a mess of things. Article and ArticleStatus are now immutable structs.
This commit is contained in:
parent
fb121f8a8c
commit
b0cb01a68e
@ -8,40 +8,31 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public final class Article: Hashable {
|
public struct Article: Hashable {
|
||||||
|
|
||||||
weak var account: Account?
|
public let articleID: String // Unique database ID (possibly sync service ID)
|
||||||
|
public let accountID: String
|
||||||
public let articleID: String // Unique database ID
|
|
||||||
public let feedID: String // Likely a URL, but not necessarily
|
public let feedID: String // Likely a URL, but not necessarily
|
||||||
public let uniqueID: String // Unique per feed (RSS guid, for example)
|
public let uniqueID: String // Unique per feed (RSS guid, for example)
|
||||||
public var title: String?
|
public let title: String?
|
||||||
public var contentHTML: String?
|
public let contentHTML: String?
|
||||||
public var contentText: String?
|
public let contentText: String?
|
||||||
public var url: String?
|
public let url: String?
|
||||||
public var externalURL: String?
|
public let externalURL: String?
|
||||||
public var summary: String?
|
public let summary: String?
|
||||||
public var imageURL: String?
|
public let imageURL: String?
|
||||||
public var bannerImageURL: String?
|
public let bannerImageURL: String?
|
||||||
public var datePublished: Date?
|
public let datePublished: Date?
|
||||||
public var dateModified: Date?
|
public let dateModified: Date?
|
||||||
public var authors: [Author]?
|
public let authors: Set<Author>?
|
||||||
public var tags: Set<String>?
|
public let tags: Set<String>?
|
||||||
public var attachments: [Attachment]?
|
public let attachments: Set<Attachment>?
|
||||||
public var accountInfo: [String: Any]? //If account needs to store more data
|
|
||||||
|
|
||||||
public var status: ArticleStatus?
|
|
||||||
public let hashValue: Int
|
public let hashValue: Int
|
||||||
|
|
||||||
public var feed: Feed? {
|
|
||||||
get {
|
|
||||||
return account?.existingFeed(with: feedID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(account: Account, articleID: String?, feedID: String, uniqueID: String, title: String?, contentHTML: String?, contentText: String?, url: String?, externalURL: String?, summary: String?, imageURL: String?, bannerImageURL: String?, datePublished: Date?, dateModified: Date?, authors: [Author]?, tags: Set<String>?, attachments: [Attachment]?, accountInfo: AccountInfo?) {
|
public init(accountID: String, articleID: String?, feedID: String, uniqueID: String, title: String?, contentHTML: String?, contentText: String?, url: String?, externalURL: String?, summary: String?, imageURL: String?, bannerImageURL: String?, datePublished: Date?, dateModified: Date?, authors: Set<Author>?, tags: Set<String>?, attachments: Set<Attachment>?, accountInfo: AccountInfo?) {
|
||||||
|
|
||||||
self.account = account
|
self.accountID = accountID
|
||||||
self.feedID = feedID
|
self.feedID = feedID
|
||||||
self.uniqueID = uniqueID
|
self.uniqueID = uniqueID
|
||||||
self.title = title
|
self.title = title
|
||||||
@ -66,12 +57,12 @@ public final class Article: Hashable {
|
|||||||
self.articleID = databaseIDWithString("\(feedID) \(uniqueID)")
|
self.articleID = databaseIDWithString("\(feedID) \(uniqueID)")
|
||||||
}
|
}
|
||||||
|
|
||||||
self.hashValue = account.hashValue ^ self.articleID.hashValue
|
self.hashValue = accountID.hashValue ^ self.articleID.hashValue
|
||||||
}
|
}
|
||||||
|
|
||||||
public class func ==(lhs: Article, rhs: Article) -> Bool {
|
public class func ==(lhs: Article, rhs: Article) -> Bool {
|
||||||
|
|
||||||
return lhs === rhs
|
return lhs.hashValue == rhs.hashValue && lhs.articleID == rhs.articleID && lhs.accountID == rhs.accountID && lhs.feedID == rhs.feedID && lhs.uniqueID == rhs.uniqueID && lhs.title == rhs.title && lhs.contentHTML == rhs.contentHTML && lhs.url == rhs.url && lhs.externalURL == rhs.externalURL && lhs.summary == rhs.summary && lhs.imageURL == rhs.imageURL && lhs.bannerImageURL == rhs.bannerImageURL && lhs.datePublished == rhs.datePublished && lhs.authors == rhs.authors && lhs.tags == rhs.tags && lhs.attachments == rhs.attachments
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,4 +73,28 @@ public extension Article {
|
|||||||
return (datePublished ?? dateModified) ?? status?.dateArrived
|
return (datePublished ?? dateModified) ?? status?.dateArrived
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Main-thread only accessors.
|
||||||
|
|
||||||
|
public var account: Account? {
|
||||||
|
get {
|
||||||
|
assert(Thread.isMainThread, "article.account is main-thread-only.")
|
||||||
|
return Account.account(with: accountID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var feed: Feed? {
|
||||||
|
get {
|
||||||
|
assert(Thread.isMainThread, "article.feed is main-thread-only.")
|
||||||
|
return account?.existingFeed(with: feedID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var status: ArticleStatus? {
|
||||||
|
get {
|
||||||
|
assert(Thread.isMainThread, "article.status is main-thread-only.")
|
||||||
|
return account?.status(with: articleID)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,14 +15,14 @@ public enum ArticleStatusKey: String {
|
|||||||
case userDeleted = "userDeleted"
|
case userDeleted = "userDeleted"
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class ArticleStatus: Hashable {
|
public struct ArticleStatus: Hashable {
|
||||||
|
|
||||||
public var read = false
|
|
||||||
public var starred = false
|
|
||||||
public var userDeleted = false
|
|
||||||
public var dateArrived: Date
|
|
||||||
public let articleID: String
|
public let articleID: String
|
||||||
public var accountInfo: AccountInfo?
|
public let read = false
|
||||||
|
public let starred = false
|
||||||
|
public let userDeleted = false
|
||||||
|
public let dateArrived: Date
|
||||||
|
public let accountInfo: AccountInfo?
|
||||||
public let hashValue: Int
|
public let hashValue: Int
|
||||||
|
|
||||||
public init(articleID: String, read: Bool, starred: Bool, userDeleted: Bool, dateArrived: Date, accountInfo: AccountInfo?) {
|
public init(articleID: String, read: Bool, starred: Bool, userDeleted: Bool, dateArrived: Date, accountInfo: AccountInfo?) {
|
||||||
@ -59,28 +59,28 @@ public final class ArticleStatus: Hashable {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setBoolStatus(_ status: Bool, forKey key: String) {
|
// public func setBoolStatus(_ status: Bool, forKey key: String) {
|
||||||
|
//
|
||||||
if let articleStatusKey = ArticleStatusKey(rawValue: key) {
|
// if let articleStatusKey = ArticleStatusKey(rawValue: key) {
|
||||||
switch articleStatusKey {
|
// switch articleStatusKey {
|
||||||
case .read:
|
// case .read:
|
||||||
read = status
|
// read = status
|
||||||
case .starred:
|
// case .starred:
|
||||||
starred = status
|
// starred = status
|
||||||
case .userDeleted:
|
// case .userDeleted:
|
||||||
userDeleted = status
|
// userDeleted = status
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
else {
|
// else {
|
||||||
if accountInfo == nil {
|
// if accountInfo == nil {
|
||||||
accountInfo = AccountInfo()
|
// accountInfo = AccountInfo()
|
||||||
}
|
// }
|
||||||
accountInfo![key] = status
|
// accountInfo![key] = status
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
public class func ==(lhs: ArticleStatus, rhs: ArticleStatus) -> Bool {
|
public class func ==(lhs: ArticleStatus, rhs: ArticleStatus) -> Bool {
|
||||||
|
|
||||||
return lhs.articleID == rhs.articleID && lhs.dateArrived == rhs.dateArrived && lhs.read == rhs.read && lhs.starred == rhs.starred
|
return lhs.hashValue == rhs.hashValue && lhs.articleID == rhs.articleID && lhs.dateArrived == rhs.dateArrived && lhs.read == rhs.read && lhs.starred == rhs.starred && lhs.userDeleted == rhs.userDeleted && lhs.accountInfo == rhs.accountInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,6 @@ public struct Attachment: Hashable {
|
|||||||
|
|
||||||
public static func ==(lhs: Attachment, rhs: Attachment) -> Bool {
|
public static func ==(lhs: Attachment, rhs: Attachment) -> Bool {
|
||||||
|
|
||||||
return lhs.sizeInBytes == rhs.sizeInBytes && lhs.url == rhs.url && lhs.mimeType == rhs.mimeType && lhs.title == rhs.title && lhs.durationInSeconds == rhs.durationInSeconds
|
return lhs.hashValue == rhs.hashValue && lhs.attachmentID == rhs.attachmentID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
|
|
||||||
// TODO: update articleCutoffDate as time passes and based on user preferences.
|
// TODO: update articleCutoffDate as time passes and based on user preferences.
|
||||||
private var articleCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 3 * 31)!
|
private var articleCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 3 * 31)!
|
||||||
|
private var maximumArticleCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 4 * 31)!
|
||||||
|
|
||||||
init(name: String, account: Account, queue: RSDatabaseQueue) {
|
init(name: String, account: Account, queue: RSDatabaseQueue) {
|
||||||
|
|
||||||
@ -50,19 +51,19 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
var articles = Set<Article>()
|
var articles = Set<Article>()
|
||||||
|
|
||||||
queue.fetchSync { (database: FMDatabase!) -> Void in
|
queue.fetchSync { (database: FMDatabase!) -> Void in
|
||||||
articles = self.fetchArticlesForFeedID(feedID, database: database)
|
articles = self.fetchArticlesForFeedID(feedID, withLimits: true, database: database)
|
||||||
}
|
}
|
||||||
|
|
||||||
return articleCache.uniquedArticles(articles)
|
return articleCache.uniquedArticles(articles)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchArticlesAsync(_ feed: Feed, _ resultBlock: @escaping ArticleResultBlock) {
|
func fetchArticlesAsync(_ feed: Feed, withLimits: Bool, _ resultBlock: @escaping ArticleResultBlock) {
|
||||||
|
|
||||||
let feedID = feed.feedID
|
let feedID = feed.feedID
|
||||||
|
|
||||||
queue.fetch { (database: FMDatabase!) -> Void in
|
queue.fetch { (database: FMDatabase!) -> Void in
|
||||||
|
|
||||||
let fetchedArticles = self.fetchArticlesForFeedID(feedID, database: database)
|
let fetchedArticles = self.fetchArticlesForFeedID(feedID, withLimits: withLimits, database: database)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let articles = self.articleCache.uniquedArticles(fetchedArticles)
|
let articles = self.articleCache.uniquedArticles(fetchedArticles)
|
||||||
@ -81,22 +82,18 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
func update(_ feed: Feed, _ parsedFeed: ParsedFeed, _ completion: @escaping RSVoidCompletionBlock) {
|
func update(_ feed: Feed, _ parsedFeed: ParsedFeed, _ completion: @escaping RSVoidCompletionBlock) {
|
||||||
|
|
||||||
if parsedFeed.items.isEmpty {
|
if parsedFeed.items.isEmpty {
|
||||||
|
|
||||||
// Once upon a time in an early version of NetNewsWire there was a bug with this issue.
|
|
||||||
// The design, at the time, was that NetNewsWire would always show only what’s currently in a feed —
|
|
||||||
// no more and no less.
|
|
||||||
// This meant that if a feed had zero items, then all the articles for that feed would be deleted.
|
|
||||||
// There were two problems with that:
|
|
||||||
// 1. People didn’t expect articles to just disappear like that.
|
|
||||||
// 2. Technorati (R.I.P.) had a bug where some of its feeds, at seemingly random times, would have zero items.
|
|
||||||
// So this hit people. THEY WERE NOT HAPPY.
|
|
||||||
// These days we just ignore empty feeds. Who cares if they’re empty. It just means less work the app has to do.
|
|
||||||
|
|
||||||
completion()
|
completion()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchArticlesAsync(feed) { (articles) in
|
// 1. Ensure statuses for all the parsedItems.
|
||||||
|
// 2. Fetch all articles for the feed.
|
||||||
|
// 3. For each parsedItem:
|
||||||
|
// - if userDeleted || (!starred && status.dateArrived < cutoff), then ignore
|
||||||
|
// - if matches existing article, then update database with changes between the two
|
||||||
|
// - if new, create article and save in database
|
||||||
|
|
||||||
|
fetchArticlesAsync(feed, withLimits: false) { (articles) in
|
||||||
self.updateArticles(articles.dictionary(), parsedFeed.itemsDictionary(with: feed), feed, completion)
|
self.updateArticles(articles.dictionary(), parsedFeed.itemsDictionary(with: feed), feed, completion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -195,13 +192,13 @@ private extension ArticlesTable {
|
|||||||
return articles
|
return articles
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchArticlesWithWhereClause(_ database: FMDatabase, whereClause: String, parameters: [AnyObject]) -> Set<Article> {
|
func fetchArticlesWithWhereClause(_ database: FMDatabase, whereClause: String, parameters: [AnyObject], withLimits: Bool) -> Set<Article> {
|
||||||
|
|
||||||
// Don’t fetch articles that shouldn’t appear in the UI. The rules:
|
// Don’t fetch articles that shouldn’t appear in the UI. The rules:
|
||||||
// * Must not be deleted.
|
// * Must not be deleted.
|
||||||
// * Must be either 1) starred or 2) dateArrived must be newer than cutoff date.
|
// * Must be either 1) starred or 2) dateArrived must be newer than cutoff date.
|
||||||
|
|
||||||
let sql = "select * from articles natural join statuses where \(whereClause) and userDeleted=0 and (starred=1 or dateArrived>?);"
|
let sql = withLimits ? "select * from articles natural join statuses where \(whereClause) and userDeleted=0 and (starred=1 or dateArrived>?);" : "select * from articles natural join statuses where \(whereClause);"
|
||||||
return articlesWithSQL(sql, parameters + [articleCutoffDate as AnyObject], database)
|
return articlesWithSQL(sql, parameters + [articleCutoffDate as AnyObject], database)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,9 +213,9 @@ private extension ArticlesTable {
|
|||||||
return numberWithSQLAndParameters(sql, [feedID, articleCutoffDate], in: database)
|
return numberWithSQLAndParameters(sql, [feedID, articleCutoffDate], in: database)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchArticlesForFeedID(_ feedID: String, database: FMDatabase) -> Set<Article> {
|
func fetchArticlesForFeedID(_ feedID: String, withLimits: Bool, database: FMDatabase) -> Set<Article> {
|
||||||
|
|
||||||
return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [feedID as AnyObject])
|
return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [feedID as AnyObject], withLimits: withLimits)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchUnreadArticles(_ feedIDs: Set<String>) -> Set<Article> {
|
func fetchUnreadArticles(_ feedIDs: Set<String>) -> Set<Article> {
|
||||||
@ -236,7 +233,7 @@ private extension ArticlesTable {
|
|||||||
let parameters = feedIDs.map { $0 as AnyObject }
|
let parameters = feedIDs.map { $0 as AnyObject }
|
||||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
|
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
|
||||||
let whereClause = "feedID in \(placeholders) and read=0"
|
let whereClause = "feedID in \(placeholders) and read=0"
|
||||||
articles = self.fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
|
articles = self.fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return articleCache.uniquedArticles(articles)
|
return articleCache.uniquedArticles(articles)
|
||||||
@ -254,17 +251,269 @@ private extension ArticlesTable {
|
|||||||
|
|
||||||
func updateArticles(_ articlesDictionary: [String: Article], _ parsedItemsDictionary: [String: ParsedItem], _ feed: Feed, _ completion: @escaping RSVoidCompletionBlock) {
|
func updateArticles(_ articlesDictionary: [String: Article], _ parsedItemsDictionary: [String: ParsedItem], _ feed: Feed, _ completion: @escaping RSVoidCompletionBlock) {
|
||||||
|
|
||||||
let parsedItemArticleIDs = Set(parsedItemsDictionary.keys)
|
// 1. Fetch statuses for parsedItems.
|
||||||
|
// 2. Filter out parsedItems where userDeleted==1 or (arrival date > 4 months and not starred).
|
||||||
|
// (Under no user setting do we retain articles older with an arrival date > 4 months.)
|
||||||
|
// 3. Find parsedItems with no status and no matching article: save them as entirely new articles.
|
||||||
|
// 4. Compare remaining parsedItems with articles, and update database with any changes.
|
||||||
|
|
||||||
queue.update { (database) in
|
assert(Thread.isMainThread)
|
||||||
|
|
||||||
self.statusesTable.ensureStatusesForArticleIDs(parsedItemArticleIDs, database)
|
queue.fetch { (database) in
|
||||||
|
|
||||||
|
let parsedItemArticleIDs = Set(parsedItemsDictionary.keys)
|
||||||
|
let fetchedStatuses = self.statusesTable.fetchStatusesForArticleIDs(parsedItemArticleIDs, database)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
|
||||||
|
// #2. Drop any parsedItems that can be ignored.
|
||||||
|
// If that’s all of them, then great — nothing to do.
|
||||||
|
let filteredParsedItems = self.filterParsedItems(parsedItemsDictionary, fetchedStatuses)
|
||||||
|
if filteredParsedItems.isEmpty {
|
||||||
|
completion()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// #3. Save entirely new parsedItems.
|
||||||
|
let newParsedItems = self.findNewParsedItems(parsedItemsDictionary, fetchedStatuses, articlesDictionary)
|
||||||
|
if !newParsedItems.isEmpty {
|
||||||
|
self.saveNewParsedItems(newParsedItems, feed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// #4. Update existing parsedItems.
|
||||||
|
let parsedItemsToUpdate = self.findExistingParsedItems(parsedItemsDictionary, fetchedStatuses, articlesDictionary)
|
||||||
|
if !parsedItemsToUpdate.isEmpty {
|
||||||
|
self.updateParsedItems(parsedItemsToUpdate, articlesDictionary, feed)
|
||||||
|
}
|
||||||
|
|
||||||
|
completion()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateParsedItems(_ parsedItems: [String: ParsedItem], _ articles: [String: Article], _ feed: Feed) {
|
||||||
|
|
||||||
|
assert(Thread.isMainThread)
|
||||||
|
|
||||||
|
updateRelatedObjects(_ parsedItems: [String: ParsedItem], _ articles: [String: Article])
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateRelatedObjects(_ parsedItems: [String: ParsedItem], _ articles: [String: Article]) {
|
||||||
|
|
||||||
|
// Update the in-memory Articles when needed.
|
||||||
|
// Save only when there are changes, which should be pretty infrequent.
|
||||||
|
|
||||||
|
assert(Thread.isMainThread)
|
||||||
|
|
||||||
|
var articlesWithTagChanges = Set<Article>()
|
||||||
|
var articlesWithAttachmentChanges = Set<Article>()
|
||||||
|
var articlesWithAuthorChanges = Set<Article>()
|
||||||
|
|
||||||
|
for (articleID, parsedItem) in parsedItems {
|
||||||
|
|
||||||
|
guard let article = articles[articleID] else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if article.updateTagsWithParsedTags(parsedItem.tags) {
|
||||||
|
articlesWithTagChanges.insert(article)
|
||||||
|
}
|
||||||
|
if article.updateAttachmentsWithParsedAttachments(parsedItem.attachments) {
|
||||||
|
articlesWithAttachmentChanges.insert(article)
|
||||||
|
}
|
||||||
|
if article.updateAuthorsWithParsedAuthors(parsedItem.authors) {
|
||||||
|
articlesWithAuthorChanges.insert(article)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if articlesWithTagChanges.isEmpty && articlesWithAttachmentChanges.isEmpty && articlesWithAuthorChanges.isEmpty {
|
||||||
|
// Should be pretty common.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We used detachedCopy because the Article objects being updated are main-thread objects.
|
||||||
|
|
||||||
|
articlesWithTagChanges = Set(articlesWithTagChanges.map{ $0.detachedCopy() })
|
||||||
|
articlesWithAttachmentChanges = Set(articlesWithAttachmentChanges.map{ $0.detachedCopy() })
|
||||||
|
articlesWithAuthorChanges = Set(articlesWithAuthorChanges.map{ $0.detachedCopy() })
|
||||||
|
|
||||||
|
queue.update { (database) in
|
||||||
|
if !articlesWithTagChanges.isEmpty {
|
||||||
|
tagsLookupTable.saveRelatedObjects(for: articlesWithTagChanges.databaseObjects(), in: database)
|
||||||
|
}
|
||||||
|
if !articlesWithAttachmentChanges.isEmpty {
|
||||||
|
attachmentsLookupTable.saveRelatedObjects(for: articlesWithAttachmentChanges.databaseObjects(), in: database)
|
||||||
|
}
|
||||||
|
if !articlesWithAuthorChanges.isEmpty {
|
||||||
|
authorsLookupTable.saveRelatedObjects(for: articlesWithAuthorChanges.databaseObjects(), in: database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateRelatedAttachments(_ parsedItems: [String: ParsedItem], _ articles: [String: Article]) {
|
||||||
|
|
||||||
|
var articlesWithChanges = Set<Article>()
|
||||||
|
|
||||||
|
for (articleID, parsedItem) in parsedItems {
|
||||||
|
guard let article = articles[articleID] else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !parsedItemTagsMatchArticlesTag(parsedItem, article) {
|
||||||
|
articlesChanges.insert(article)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if articlesWithChanges.isEmpty {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
queue.update { (database) in
|
||||||
|
tagsLookupTable.saveRelatedObjects(for: articlesWithChanges.databaseObjects(), in: database)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateRelatedTags(_ parsedItems: [String: ParsedItem], _ articles: [String: Article]) {
|
||||||
|
|
||||||
|
var articlesWithChanges = Set<Article>()
|
||||||
|
|
||||||
|
for (articleID, parsedItem) in parsedItems {
|
||||||
|
guard let article = articles[articleID] else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !parsedItemTagsMatchArticlesTag(parsedItem, article) {
|
||||||
|
articlesChanges.insert(article)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if articlesWithChanges.isEmpty {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
queue.update { (database) in
|
||||||
|
tagsLookupTable.saveRelatedObjects(for: articlesWithChanges.databaseObjects(), in: database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsedItemTagsMatchArticlesTag(_ parsedItem: ParsedItem, _ article: Article) -> Bool {
|
||||||
|
|
||||||
|
let parsedItemTags = parsedItem.tags
|
||||||
|
let articleTags = article.tags
|
||||||
|
|
||||||
|
if parsedItemTags == nil && articleTags == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if parsedItemTags != nil && articleTags == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if parsedItemTags == nil && articleTags != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return Set(parsedItemTags!) == articleTags!
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveNewParsedItems(_ parsedItems: [String: ParsedItem], _ feed: Feed) {
|
||||||
|
|
||||||
|
// These parsedItems have no existing status or Article.
|
||||||
|
|
||||||
|
queue.update { (database) in
|
||||||
|
|
||||||
|
let articleIDs = Set(parsedItems.keys)
|
||||||
|
self.statusesTable.ensureStatusesForArticleIDs(articleIDs, database)
|
||||||
|
|
||||||
|
let articles = self.articlesWithParsedItems(Set(parsedItems.values), feed)
|
||||||
|
self.saveUncachedNewArticles(articles, database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func articlesWithParsedItems(_ parsedItems: Set<ParsedItem>, _ feed: Feed) -> Set<Article> {
|
||||||
|
|
||||||
|
// These Articles don’t get cached. Background-queue only.
|
||||||
|
let feedID = feed.feedID
|
||||||
|
return Set(parsedItems.flatMap{ articleWithParsedItem($0, feedID) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func articleWithParsedItem(_ parsedItem: ParsedItem, _ feedID: String) -> Article? {
|
||||||
|
|
||||||
|
guard let account = account else {
|
||||||
|
assertionFailure("account is unexpectedly nil.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return Article(parsedItem: parsedItem, feedID: feedID, account: account)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveUncachedNewArticles(_ articles: Set<Article>, _ database: FMDatabase) {
|
||||||
|
|
||||||
|
saveRelatedObjects(articles, database)
|
||||||
|
|
||||||
|
let databaseDictionaries = articles.map { $0.databaseDictionary() }
|
||||||
|
insertRows(databaseDictionaries, insertType: .orIgnore, in: database)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveRelatedObjects(_ articles: Set<Article>, _ database: FMDatabase) {
|
||||||
|
|
||||||
|
let databaseObjects = articles.databaseObjects()
|
||||||
|
|
||||||
|
authorsLookupTable.saveRelatedObjects(for: databaseObjects, in: database)
|
||||||
|
attachmentsLookupTable.saveRelatedObjects(for: databaseObjects, in: database)
|
||||||
|
tagsLookupTable.saveRelatedObjects(for: databaseObjects, in: database)
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusIndicatesArticleIsIgnorable(_ status: ArticleStatus) -> Bool {
|
||||||
|
|
||||||
|
// Ignorable articles: either userDeleted==1 or (not starred and arrival date > 4 months).
|
||||||
|
|
||||||
|
if status.userDeleted {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if status.starred {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return status.dateArrived < maximumArticleCutoffDate
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterParsedItems(_ parsedItems: [String: ParsedItem], _ statuses: [String: ArticleStatus]) -> [String: ParsedItem] {
|
||||||
|
|
||||||
|
// Drop parsedItems that we can ignore.
|
||||||
|
|
||||||
|
assert(Thread.isMainThread)
|
||||||
|
|
||||||
|
var d = [String: ParsedItem]()
|
||||||
|
|
||||||
|
for (articleID, parsedItem) in parsedItems {
|
||||||
|
|
||||||
|
if let status = statuses[articleID] {
|
||||||
|
if statusIndicatesArticleIsIgnorable(status) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d[articleID] = parsedItem
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func findNewParsedItems(_ parsedItems: [String: ParsedItem], _ statuses: [String: ArticleStatus], _ articles: [String: Article]) -> [String: ParsedItem] {
|
||||||
|
|
||||||
|
// If there’s no existing status or Article, then it’s completely new.
|
||||||
|
|
||||||
|
assert(Thread.isMainThread)
|
||||||
|
|
||||||
|
var d = [String: ParsedItem]()
|
||||||
|
|
||||||
|
for (articleID, parsedItem) in parsedItems {
|
||||||
|
if statuses[articleID] == nil && articles[articleID] == nil {
|
||||||
|
d[articleID] = parsedItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func findExistingParsedItems(_ parsedItems: [String: ParsedItem], _ statuses: [String: ArticleStatus], _ articles: [String: Article]) -> [String: ParsedItem] {
|
||||||
|
|
||||||
|
return [String: ParsedItem]() //TODO
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: -
|
// MARK: -
|
||||||
|
@ -14,7 +14,6 @@ final class AttachmentsTable: DatabaseRelatedObjectsTable {
|
|||||||
|
|
||||||
let name: String
|
let name: String
|
||||||
let databaseIDKey = DatabaseKey.attachmentID
|
let databaseIDKey = DatabaseKey.attachmentID
|
||||||
private let cache = DatabaseObjectCache()
|
|
||||||
|
|
||||||
init(name: String) {
|
init(name: String) {
|
||||||
|
|
||||||
@ -43,14 +42,6 @@ private extension AttachmentsTable {
|
|||||||
guard let attachmentID = row.string(forColumn: DatabaseKey.attachmentID) else {
|
guard let attachmentID = row.string(forColumn: DatabaseKey.attachmentID) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if let cachedAttachment = cache[attachmentID] as? Attachment {
|
return Attachment(attachmentID: attachmentID, row: row)
|
||||||
return cachedAttachment
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let attachment = Attachment(attachmentID: attachmentID, row: row) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cache[attachmentID] = attachment as DatabaseObject
|
|
||||||
return attachment
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,6 @@ final class AuthorsTable: DatabaseRelatedObjectsTable {
|
|||||||
|
|
||||||
let name: String
|
let name: String
|
||||||
let databaseIDKey = DatabaseKey.authorID
|
let databaseIDKey = DatabaseKey.authorID
|
||||||
private let cache = DatabaseObjectCache()
|
|
||||||
|
|
||||||
init(name: String) {
|
init(name: String) {
|
||||||
|
|
||||||
@ -41,7 +40,6 @@ final class AuthorsTable: DatabaseRelatedObjectsTable {
|
|||||||
func save(_ objects: [DatabaseObject], in database: FMDatabase) {
|
func save(_ objects: [DatabaseObject], in database: FMDatabase) {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension AuthorsTable {
|
private extension AuthorsTable {
|
||||||
@ -52,7 +50,7 @@ private extension AuthorsTable {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if let cachedAuthor = cache[authorID] as? Author {
|
if let cachedAuthor = Author.cachedAuthor[authorID] {
|
||||||
return cachedAuthor
|
return cachedAuthor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ public final class Database {
|
|||||||
|
|
||||||
public func fetchArticlesAsync(for feed: Feed, _ resultBlock: @escaping ArticleResultBlock) {
|
public func fetchArticlesAsync(for feed: Feed, _ resultBlock: @escaping ArticleResultBlock) {
|
||||||
|
|
||||||
articlesTable.fetchArticlesAsync(feed, resultBlock)
|
articlesTable.fetchArticlesAsync(feed, withLimits: true, resultBlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetchUnreadArticles(for folder: Folder) -> Set<Article> {
|
public func fetchUnreadArticles(for folder: Folder) -> Set<Article> {
|
||||||
@ -70,19 +70,6 @@ public final class Database {
|
|||||||
public func update(feed: Feed, parsedFeed: ParsedFeed, completion: @escaping RSVoidCompletionBlock) {
|
public func update(feed: Feed, parsedFeed: ParsedFeed, completion: @escaping RSVoidCompletionBlock) {
|
||||||
|
|
||||||
return articlesTable.update(feed, parsedFeed, completion)
|
return articlesTable.update(feed, parsedFeed, completion)
|
||||||
|
|
||||||
// if parsedFeed.items.isEmpty {
|
|
||||||
// completionHandler()
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// let parsedArticlesDictionary = self.articlesDictionary(parsedFeed.items as NSSet) as! [String: ParsedItem]
|
|
||||||
//
|
|
||||||
// fetchArticlesForFeedAsync(feed) { (articles) -> Void in
|
|
||||||
//
|
|
||||||
// let articlesDictionary = self.articlesDictionary(articles as NSSet) as! [String: Article]
|
|
||||||
// self.updateArticles(articlesDictionary, parsedArticles: parsedArticlesDictionary, feed: feed, completionHandler: completionHandler)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Status
|
// MARK: - Status
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import RSDatabase
|
import RSDatabase
|
||||||
import Data
|
import Data
|
||||||
|
import RSParser
|
||||||
|
|
||||||
extension Article {
|
extension Article {
|
||||||
|
|
||||||
@ -35,20 +36,129 @@ extension Article {
|
|||||||
let accountInfo: [String: Any]? = nil // TODO
|
let accountInfo: [String: Any]? = nil // TODO
|
||||||
|
|
||||||
// authors, tags, and attachments are fetched from related tables, after init.
|
// authors, tags, and attachments are fetched from related tables, after init.
|
||||||
let authors: [Author]? = nil
|
|
||||||
let tags: Set<String>? = nil
|
self.init(account: account, articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: nil, tags: nil, attachments: nil, accountInfo: accountInfo)
|
||||||
let attachments: [Attachment]? = nil
|
}
|
||||||
|
|
||||||
self.init(account: account, articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments, accountInfo: accountInfo)
|
convenience init(parsedItem: ParsedItem, feedID: String, account: Account) {
|
||||||
|
|
||||||
|
let authors = Author.authorsWithParsedAuthors(parsedItem.authors)
|
||||||
|
let attachments = Attachment.attachmentsWithParsedAttachments(parsedItem.attachments)
|
||||||
|
let tags = tagSetWithParsedTags(parsedItem.tags)
|
||||||
|
|
||||||
|
self.init(account: account, articleID: parsedItem.syncServiceID, feedID: feedID, uniqueID: parsedItem.uniqueID, title: parsedItem.title, contentHTML: parsedItem.contentHTML, contentText: parsedItem.contentText, url: parsedItem.url, externalURL: parsedItem.externalURL, summary: parsedItem.summary, imageURL: parsedItem.imageURL, bannerImageURL: parsedItem.bannerImageURL, datePublished: parsedItem.datePublished, dateModified: parsedItem.dateModified, authors: authors, tags: tags, attachments: attachments, accountInfo: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func databaseDictionary() -> NSDictionary {
|
func databaseDictionary() -> NSDictionary {
|
||||||
|
|
||||||
let d = NSMutableDictionary()
|
let d = NSMutableDictionary()
|
||||||
|
|
||||||
|
d[DatabaseKey.articleID] = articleID
|
||||||
|
d[DatabaseKey.feedID] = feedID
|
||||||
|
d[DatabaseKey.uniqueID] = uniqueID
|
||||||
|
|
||||||
|
d.addOptionalString(title, DatabaseKey.title)
|
||||||
|
d.addOptionalString(contentHTML, DatabaseKey.contentHTML)
|
||||||
|
d.addOptionalString(url, DatabaseKey.url)
|
||||||
|
d.addOptionalString(externalURL, DatabaseKey.externalURL)
|
||||||
|
d.addOptionalString(summary, DatabaseKey.summary)
|
||||||
|
d.addOptionalString(imageURL, DatabaseKey.imageURL)
|
||||||
|
d.addOptionalString(bannerImageURL, DatabaseKey.bannerImageURL)
|
||||||
|
|
||||||
|
d.addOptionalDate(datePublished, DatabaseKey.datePublished)
|
||||||
|
d.addOptionalDate(dateModified, DatabaseKey.dateModified)
|
||||||
|
|
||||||
|
// TODO: accountInfo
|
||||||
|
|
||||||
return d.copy() as! NSDictionary
|
return d.copy() as! NSDictionary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Updating with ParsedItem
|
||||||
|
|
||||||
|
func updateTagsWithParsedTags(_ parsedTags: [String]?) -> Bool {
|
||||||
|
|
||||||
|
// Return true if there's a change.
|
||||||
|
|
||||||
|
let currentTags = tags
|
||||||
|
|
||||||
|
if parsedTags == nil && currentTags == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if parsedTags != nil && currentTags == nil {
|
||||||
|
tags = Set(parsedItemTags!)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if parsedTags == nil && currentTags != nil {
|
||||||
|
tags = nil
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
let parsedTagSet = Set(parsedTags!)
|
||||||
|
if parsedTagSet == tags! {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
tags = parsedTagSet
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAttachmentsWithParsedAttachments(_ parsedAttachments: [ParsedAttachment]?) -> Bool {
|
||||||
|
|
||||||
|
// Return true if there's a change.
|
||||||
|
|
||||||
|
let currentAttachments = attachments
|
||||||
|
let updatedAttachments = Attachment.attachmentsWithParsedAttachments(parsedAttachments)
|
||||||
|
|
||||||
|
if updatedAttachments == nil && currentAttachments == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if updatedAttachments != nil && currentAttachments == nil {
|
||||||
|
attachments = updatedAttachments
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if updatedAttachments == nil && currentAttachments != nil {
|
||||||
|
attachments = nil
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let currentAttachments = currentAttachments, let updatedAttachments = updatedAttachments else {
|
||||||
|
assertionFailure("currentAttachments and updatedAttachments must both be non-nil.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if currentAttachments != updatedAttachments {
|
||||||
|
attachments = updatedAttachments
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAuthorsWithParsedAuthors(_ parsedAuthors: [ParsedAuthor]?) -> Bool {
|
||||||
|
|
||||||
|
// Return true if there's a change.
|
||||||
|
|
||||||
|
let currentAuthors = authors
|
||||||
|
let updatedAuthors = Author.authorsWithParsedAuthors(parsedAuthors)
|
||||||
|
|
||||||
|
if updatedAuthors == nil && currentAuthors == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if updatedAuthors != nil && currentAuthors == nil {
|
||||||
|
authors = updatedAuthors
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if updatedAuthors == nil && currentAuthors != nil {
|
||||||
|
authors = nil
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let currentAuthors = currentAuthors, let updatedAuthors = updatedAuthors else {
|
||||||
|
assertionFailure("currentAuthors and updatedAuthors must both be non-nil.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if currentAuthors != updatedAuthors {
|
||||||
|
authors = updatedAuthors
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Article: DatabaseObject {
|
extension Article: DatabaseObject {
|
||||||
@ -62,11 +172,6 @@ extension Article: DatabaseObject {
|
|||||||
|
|
||||||
extension Set where Element == Article {
|
extension Set where Element == Article {
|
||||||
|
|
||||||
func withNilProperty<T>(_ keyPath: KeyPath<Article,T?>) -> Set<Article> {
|
|
||||||
|
|
||||||
return Set(filter{ $0[keyPath: keyPath] == nil })
|
|
||||||
}
|
|
||||||
|
|
||||||
func articleIDs() -> Set<String> {
|
func articleIDs() -> Set<String> {
|
||||||
|
|
||||||
return Set<String>(map { $0.databaseID })
|
return Set<String>(map { $0.databaseID })
|
||||||
@ -84,7 +189,7 @@ extension Set where Element == Article {
|
|||||||
|
|
||||||
func missingStatuses() -> Set<Article> {
|
func missingStatuses() -> Set<Article> {
|
||||||
|
|
||||||
return withNilProperty(\Article.status)
|
return Set<Article>(self.filter { $0.status == nil })
|
||||||
}
|
}
|
||||||
|
|
||||||
func statuses() -> Set<ArticleStatus> {
|
func statuses() -> Set<ArticleStatus> {
|
||||||
@ -100,4 +205,26 @@ extension Set where Element == Article {
|
|||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func databaseObjects() -> [DatabaseObject] {
|
||||||
|
|
||||||
|
return self.map{ $0 as DatabaseObject }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension NSMutableDictionary {
|
||||||
|
|
||||||
|
func addOptionalString(_ value: String?, _ key: String) {
|
||||||
|
|
||||||
|
if let value = value {
|
||||||
|
self[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addOptionalDate(_ date: Date?, _ key: String) {
|
||||||
|
|
||||||
|
if let date = date {
|
||||||
|
self[key] = date as NSDate
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Data
|
import Data
|
||||||
import RSDatabase
|
import RSDatabase
|
||||||
|
import RSParser
|
||||||
|
|
||||||
extension Attachment {
|
extension Attachment {
|
||||||
|
|
||||||
@ -26,6 +27,24 @@ extension Attachment {
|
|||||||
self.init(attachmentID: attachmentID, url: url, mimeType: mimeType, title: title, sizeInBytes: sizeInBytes, durationInSeconds: durationInSeconds)
|
self.init(attachmentID: attachmentID, url: url, mimeType: mimeType, title: title, sizeInBytes: sizeInBytes, durationInSeconds: durationInSeconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init?(parsedAttachment: ParsedAttachment) {
|
||||||
|
|
||||||
|
guard let url = parsedAttachment.url else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(attachmentID: nil, url: url, mimeType: parsedAttachment.mimeType, title: parsedAttachment.title, sizeInBytes: parsedAttachment.sizeInBytes, durationInSeconds: parsedAttachment.durationInSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func attachmentsWithParsedAttachments(_ parsedAttachments: [ParsedAttachment]?) -> Set<Attachment>? {
|
||||||
|
|
||||||
|
guard let parsedAttachments = parsedAttachments else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let attachments = parsedAttachments.flatMap{ Attachment(parsedAttachment: $0) }
|
||||||
|
return attachments.isEmpty ? nil : Set(attachments)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func optionalIntForColumn(_ row: FMResultSet, _ columnName: String) -> Int? {
|
private func optionalIntForColumn(_ row: FMResultSet, _ columnName: String) -> Int? {
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Data
|
import Data
|
||||||
import RSDatabase
|
import RSDatabase
|
||||||
|
import RSParser
|
||||||
|
|
||||||
extension Author {
|
extension Author {
|
||||||
|
|
||||||
@ -21,6 +22,21 @@ extension Author {
|
|||||||
|
|
||||||
self.init(authorID: authorID, name: name, url: url, avatarURL: avatarURL, emailAddress: emailAddress)
|
self.init(authorID: authorID, name: name, url: url, avatarURL: avatarURL, emailAddress: emailAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init?(parsedAuthor: ParsedAuthor) {
|
||||||
|
|
||||||
|
self.init(authorID: nil, name: parsedAuthor.name, url: parsedAuthor.url, avatarURL: parsedAuthor.avatarURL, emailAddress: parsedAuthor.emailAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func authorsWithParsedAuthors(_ parsedAuthors: [ParsedAuthor]?) -> Set<Author>? {
|
||||||
|
|
||||||
|
guard let parsedAuthors = parsedAuthors else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let authors = parsedAuthors.flatMap { Author(parsedAuthor: $0) }
|
||||||
|
return authors.isEmpty ? nil : Set(authors)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Author: DatabaseObject {
|
extension Author: DatabaseObject {
|
||||||
|
@ -20,3 +20,12 @@ extension String: DatabaseObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tagSetWithParsedTags(_ parsedTags: [String]?) -> Set<String>? {
|
||||||
|
|
||||||
|
guard let parsedTags = parsedTags, !parsedTags.isEmpty else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return Set(parsedTags)
|
||||||
|
}
|
||||||
|
@ -75,6 +75,36 @@ final class StatusesTable: DatabaseTable {
|
|||||||
createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus, database)
|
createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus, database)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fetchStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) -> [String: ArticleStatus] {
|
||||||
|
|
||||||
|
// Does not create statuses. Checks cache first, then database only if needed.
|
||||||
|
|
||||||
|
var d = [String: ArticleStatus]()
|
||||||
|
var articleIDsMissingCachedStatus = Set<String>()
|
||||||
|
|
||||||
|
for articleID in articleIDs {
|
||||||
|
if let cachedStatus = cache[articleID] as? ArticleStatus {
|
||||||
|
d[articleID] = cachedStatus
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
articleIDsMissingCachedStatus.insert(articleID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if articleIDsMissingCachedStatus.isEmpty {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAndCacheStatusesForArticleIDs(articleIDsMissingCachedStatus, database)
|
||||||
|
for articleID in articleIDsMissingCachedStatus {
|
||||||
|
if let cachedStatus = cache[articleID] as? ArticleStatus {
|
||||||
|
d[articleID] = cachedStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Marking
|
// MARK: Marking
|
||||||
|
|
||||||
func markArticleIDs(_ articleIDs: Set<String>, _ statusKey: String, _ flag: Bool, _ database: FMDatabase) {
|
func markArticleIDs(_ articleIDs: Set<String>, _ statusKey: String, _ flag: Bool, _ database: FMDatabase) {
|
||||||
|
@ -126,7 +126,7 @@ private extension DatabaseLookupTable {
|
|||||||
|
|
||||||
let relatedObjectsToSave = uniqueArrayOfRelatedObjects(with: objectsNeedingUpdate)
|
let relatedObjectsToSave = uniqueArrayOfRelatedObjects(with: objectsNeedingUpdate)
|
||||||
if relatedObjectsToSave.isEmpty {
|
if relatedObjectsToSave.isEmpty {
|
||||||
assertionFailure("updateRelationships: expected related objects to save. This should be unreachable.")
|
assertionFailure("updateRelationships: expected relatedObjectsToSave would not be empty. This should be unreachable.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user