Merge branch 'mac-candidate'
This commit is contained in:
commit
d1c9fc02d8
@ -7,7 +7,6 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import RSCore
|
|
||||||
import RSDatabase
|
import RSDatabase
|
||||||
import RSParser
|
import RSParser
|
||||||
import Articles
|
import Articles
|
||||||
@ -23,12 +22,9 @@ public typealias UpdateArticlesWithFeedCompletionBlock = (Set<Article>?, Set<Art
|
|||||||
|
|
||||||
public final class ArticlesDatabase {
|
public final class ArticlesDatabase {
|
||||||
|
|
||||||
private let accountID: String
|
|
||||||
private let articlesTable: ArticlesTable
|
private let articlesTable: ArticlesTable
|
||||||
|
|
||||||
public init(databaseFilePath: String, accountID: String) {
|
public init(databaseFilePath: String, accountID: String) {
|
||||||
self.accountID = accountID
|
|
||||||
|
|
||||||
let queue = RSDatabaseQueue(filepath: databaseFilePath, excludeFromBackup: false)
|
let queue = RSDatabaseQueue(filepath: databaseFilePath, excludeFromBackup: false)
|
||||||
self.articlesTable = ArticlesTable(name: DatabaseTableName.articles, accountID: accountID, queue: queue)
|
self.articlesTable = ArticlesTable(name: DatabaseTableName.articles, accountID: accountID, queue: queue)
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
self.attachmentsLookupTable = DatabaseLookupTable(name: DatabaseTableName.attachmentsLookup, objectIDKey: DatabaseKey.articleID, relatedObjectIDKey: DatabaseKey.attachmentID, relatedTable: attachmentsTable, relationshipName: RelationshipName.attachments)
|
self.attachmentsLookupTable = DatabaseLookupTable(name: DatabaseTableName.attachmentsLookup, objectIDKey: DatabaseKey.articleID, relatedObjectIDKey: DatabaseKey.attachmentID, relatedTable: attachmentsTable, relationshipName: RelationshipName.attachments)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Fetch Articles for Feed
|
// MARK: - Fetching Articles for Feed
|
||||||
|
|
||||||
func fetchArticles(_ feedID: String) -> Set<Article> {
|
func fetchArticles(_ feedID: String) -> Set<Article> {
|
||||||
return fetchArticles{ self.fetchArticlesForFeedID(feedID, withLimits: true, $0) }
|
return fetchArticles{ self.fetchArticlesForFeedID(feedID, withLimits: true, $0) }
|
||||||
@ -59,7 +59,7 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [feedID as AnyObject], withLimits: withLimits)
|
return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [feedID as AnyObject], withLimits: withLimits)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Fetch Articles by articleID
|
// MARK: - Fetching Articles by articleID
|
||||||
|
|
||||||
func fetchArticles(articleIDs: Set<String>) -> Set<Article> {
|
func fetchArticles(articleIDs: Set<String>) -> Set<Article> {
|
||||||
return fetchArticles{ self.fetchArticles(articleIDs: articleIDs, $0) }
|
return fetchArticles{ self.fetchArticles(articleIDs: articleIDs, $0) }
|
||||||
@ -79,7 +79,7 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
|
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Fetch Unread Articles
|
// MARK: - Fetching Unread Articles
|
||||||
|
|
||||||
func fetchUnreadArticles(_ feedIDs: Set<String>) -> Set<Article> {
|
func fetchUnreadArticles(_ feedIDs: Set<String>) -> Set<Article> {
|
||||||
return fetchArticles{ self.fetchUnreadArticles(feedIDs, $0) }
|
return fetchArticles{ self.fetchUnreadArticles(feedIDs, $0) }
|
||||||
@ -100,7 +100,7 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true)
|
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Fetch Today Articles
|
// MARK: - Fetching Today Articles
|
||||||
|
|
||||||
func fetchTodayArticles(_ feedIDs: Set<String>) -> Set<Article> {
|
func fetchTodayArticles(_ feedIDs: Set<String>) -> Set<Article> {
|
||||||
return fetchArticles{ self.fetchTodayArticles(feedIDs, $0) }
|
return fetchArticles{ self.fetchTodayArticles(feedIDs, $0) }
|
||||||
@ -124,7 +124,7 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
|
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Fetch Starred Articles
|
// MARK: - Fetching Starred Articles
|
||||||
|
|
||||||
func fetchStarredArticles(_ feedIDs: Set<String>) -> Set<Article> {
|
func fetchStarredArticles(_ feedIDs: Set<String>) -> Set<Article> {
|
||||||
return fetchArticles{ self.fetchStarredArticles(feedIDs, $0) }
|
return fetchArticles{ self.fetchStarredArticles(feedIDs, $0) }
|
||||||
@ -145,7 +145,7 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
|
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Fetch Search Articles
|
// MARK: - Fetching Search Articles
|
||||||
|
|
||||||
func fetchArticlesMatching(_ searchString: String, _ feedIDs: Set<String>) -> Set<Article> {
|
func fetchArticlesMatching(_ searchString: String, _ feedIDs: Set<String>) -> Set<Article> {
|
||||||
var articles: Set<Article> = Set<Article>()
|
var articles: Set<Article> = Set<Article>()
|
||||||
@ -180,7 +180,7 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
return articles.filter{ feedIDs.contains($0.feedID) }
|
return articles.filter{ feedIDs.contains($0.feedID) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Fetch Articles for Indexer
|
// MARK: - Fetching Articles for Indexer
|
||||||
|
|
||||||
func fetchArticleSearchInfos(_ articleIDs: Set<String>, in database: FMDatabase) -> Set<ArticleSearchInfo>? {
|
func fetchArticleSearchInfos(_ articleIDs: Set<String>, in database: FMDatabase) -> Set<ArticleSearchInfo>? {
|
||||||
let parameters = articleIDs.map { $0 as AnyObject }
|
let parameters = articleIDs.map { $0 as AnyObject }
|
||||||
@ -207,10 +207,9 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Updating
|
// MARK: - Updating
|
||||||
|
|
||||||
func update(_ feedID: String, _ parsedItems: Set<ParsedItem>, _ read: Bool, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) {
|
func update(_ feedID: String, _ parsedItems: Set<ParsedItem>, _ read: Bool, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) {
|
||||||
|
|
||||||
if parsedItems.isEmpty {
|
if parsedItems.isEmpty {
|
||||||
completion(nil, nil)
|
completion(nil, nil)
|
||||||
return
|
return
|
||||||
@ -228,7 +227,6 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
let articleIDs = Set(parsedItems.map { $0.articleID })
|
let articleIDs = Set(parsedItems.map { $0.articleID })
|
||||||
|
|
||||||
self.queue.update { (database) in
|
self.queue.update { (database) in
|
||||||
|
|
||||||
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1
|
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1
|
||||||
assert(statusesDictionary.count == articleIDs.count)
|
assert(statusesDictionary.count == articleIDs.count)
|
||||||
|
|
||||||
@ -264,7 +262,7 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
if articleIDs.isEmpty {
|
if articleIDs.isEmpty {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async() {
|
DispatchQueue.main.async {
|
||||||
self.searchTable.ensureIndexedArticles(for: articleIDs)
|
self.searchTable.ensureIndexedArticles(for: articleIDs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -278,10 +276,9 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Unread Counts
|
// MARK: - Unread Counts
|
||||||
|
|
||||||
func fetchUnreadCounts(_ feedIDs: Set<String>, _ completion: @escaping UnreadCountCompletionBlock) {
|
func fetchUnreadCounts(_ feedIDs: Set<String>, _ completion: @escaping UnreadCountCompletionBlock) {
|
||||||
|
|
||||||
if feedIDs.isEmpty {
|
if feedIDs.isEmpty {
|
||||||
completion(UnreadCountDictionary())
|
completion(UnreadCountDictionary())
|
||||||
return
|
return
|
||||||
@ -290,19 +287,17 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
var unreadCountDictionary = UnreadCountDictionary()
|
var unreadCountDictionary = UnreadCountDictionary()
|
||||||
|
|
||||||
queue.fetch { (database) in
|
queue.fetch { (database) in
|
||||||
|
|
||||||
for feedID in feedIDs {
|
for feedID in feedIDs {
|
||||||
unreadCountDictionary[feedID] = self.fetchUnreadCount(feedID, database)
|
unreadCountDictionary[feedID] = self.fetchUnreadCount(feedID, database)
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async() {
|
DispatchQueue.main.async {
|
||||||
completion(unreadCountDictionary)
|
completion(unreadCountDictionary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchUnreadCount(_ feedIDs: Set<String>, _ since: Date, _ callback: @escaping (Int) -> Void) {
|
func fetchUnreadCount(_ feedIDs: Set<String>, _ since: Date, _ callback: @escaping (Int) -> Void) {
|
||||||
|
|
||||||
// Get unread count for today, for instance.
|
// Get unread count for today, for instance.
|
||||||
|
|
||||||
if feedIDs.isEmpty {
|
if feedIDs.isEmpty {
|
||||||
@ -311,7 +306,6 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
queue.fetch { (database) in
|
queue.fetch { (database) in
|
||||||
|
|
||||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
|
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
|
||||||
let sql = "select count(*) from articles natural join statuses where feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?)) and read=0 and userDeleted=0;"
|
let sql = "select count(*) from articles natural join statuses where feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?)) and read=0 and userDeleted=0;"
|
||||||
|
|
||||||
@ -322,24 +316,22 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
|
|
||||||
let unreadCount = self.numberWithSQLAndParameters(sql, parameters, in: database)
|
let unreadCount = self.numberWithSQLAndParameters(sql, parameters, in: database)
|
||||||
|
|
||||||
DispatchQueue.main.async() {
|
DispatchQueue.main.async {
|
||||||
callback(unreadCount)
|
callback(unreadCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAllUnreadCounts(_ completion: @escaping UnreadCountCompletionBlock) {
|
func fetchAllUnreadCounts(_ completion: @escaping UnreadCountCompletionBlock) {
|
||||||
|
|
||||||
// Returns only where unreadCount > 0.
|
// Returns only where unreadCount > 0.
|
||||||
|
|
||||||
let cutoffDate = articleCutoffDate
|
let cutoffDate = articleCutoffDate
|
||||||
|
|
||||||
queue.fetch { (database) in
|
queue.fetch { (database) in
|
||||||
|
|
||||||
let sql = "select distinct feedID, count(*) from articles natural join statuses where read=0 and userDeleted=0 and (starred=1 or dateArrived>?) group by feedID;"
|
let sql = "select distinct feedID, count(*) from articles natural join statuses where read=0 and userDeleted=0 and (starred=1 or dateArrived>?) group by feedID;"
|
||||||
|
|
||||||
guard let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate]) else {
|
guard let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate]) else {
|
||||||
DispatchQueue.main.async() {
|
DispatchQueue.main.async {
|
||||||
completion(UnreadCountDictionary())
|
completion(UnreadCountDictionary())
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -353,34 +345,32 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async() {
|
DispatchQueue.main.async {
|
||||||
completion(d)
|
completion(d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchStarredAndUnreadCount(_ feedIDs: Set<String>, _ callback: @escaping (Int) -> Void) {
|
func fetchStarredAndUnreadCount(_ feedIDs: Set<String>, _ callback: @escaping (Int) -> Void) {
|
||||||
|
|
||||||
if feedIDs.isEmpty {
|
if feedIDs.isEmpty {
|
||||||
callback(0)
|
callback(0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
queue.fetch { (database) in
|
queue.fetch { (database) in
|
||||||
|
|
||||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
|
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
|
||||||
let sql = "select count(*) from articles natural join statuses where feedID in \(placeholders) and read=0 and starred=1 and userDeleted=0;"
|
let sql = "select count(*) from articles natural join statuses where feedID in \(placeholders) and read=0 and starred=1 and userDeleted=0;"
|
||||||
let parameters = Array(feedIDs) as [Any]
|
let parameters = Array(feedIDs) as [Any]
|
||||||
|
|
||||||
let unreadCount = self.numberWithSQLAndParameters(sql, parameters, in: database)
|
let unreadCount = self.numberWithSQLAndParameters(sql, parameters, in: database)
|
||||||
|
|
||||||
DispatchQueue.main.async() {
|
DispatchQueue.main.async {
|
||||||
callback(unreadCount)
|
callback(unreadCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Status
|
// MARK: - Statuses
|
||||||
|
|
||||||
func fetchUnreadArticleIDs(_ callback: @escaping (Set<String>) -> Void) {
|
func fetchUnreadArticleIDs(_ callback: @escaping (Set<String>) -> Void) {
|
||||||
statusesTable.fetchUnreadArticleIDs(callback)
|
statusesTable.fetchUnreadArticleIDs(callback)
|
||||||
@ -402,7 +392,7 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
return statuses
|
return statuses
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Indexing
|
// MARK: - Indexing
|
||||||
|
|
||||||
func indexUnindexedArticles() {
|
func indexUnindexedArticles() {
|
||||||
queue.fetch { (database) in
|
queue.fetch { (database) in
|
||||||
@ -427,7 +417,7 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
|
|
||||||
private extension ArticlesTable {
|
private extension ArticlesTable {
|
||||||
|
|
||||||
// MARK: Fetching
|
// MARK: - Fetching
|
||||||
|
|
||||||
private func fetchArticles(_ fetchMethod: @escaping ArticlesFetchMethod) -> Set<Article> {
|
private func fetchArticles(_ fetchMethod: @escaping ArticlesFetchMethod) -> Set<Article> {
|
||||||
var articles = Set<Article>()
|
var articles = Set<Article>()
|
||||||
@ -447,7 +437,6 @@ private extension ArticlesTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func articlesWithResultSet(_ resultSet: FMResultSet, _ database: FMDatabase) -> Set<Article> {
|
func articlesWithResultSet(_ resultSet: FMResultSet, _ database: FMDatabase) -> Set<Article> {
|
||||||
|
|
||||||
// 1. Create DatabaseArticles without related objects.
|
// 1. Create DatabaseArticles without related objects.
|
||||||
// 2. Then fetch the related objects, given the set of articleIDs.
|
// 2. Then fetch the related objects, given the set of articleIDs.
|
||||||
// 3. Then create set of Articles with DatabaseArticles and related objects and return it.
|
// 3. Then create set of Articles with DatabaseArticles and related objects and return it.
|
||||||
@ -485,7 +474,6 @@ private extension ArticlesTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func makeDatabaseArticles(with resultSet: FMResultSet) -> Set<DatabaseArticle> {
|
func makeDatabaseArticles(with resultSet: FMResultSet) -> Set<DatabaseArticle> {
|
||||||
|
|
||||||
let articles = resultSet.mapToSet { (row) -> DatabaseArticle? in
|
let articles = resultSet.mapToSet { (row) -> DatabaseArticle? in
|
||||||
|
|
||||||
// The resultSet is a result of a JOIN query with the statuses table,
|
// The resultSet is a result of a JOIN query with the statuses table,
|
||||||
@ -527,7 +515,6 @@ private extension ArticlesTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fetchArticlesWithWhereClause(_ database: FMDatabase, whereClause: String, parameters: [AnyObject], withLimits: Bool) -> 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.
|
||||||
@ -543,7 +530,6 @@ private extension ArticlesTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fetchUnreadCount(_ feedID: String, _ database: FMDatabase) -> Int {
|
func fetchUnreadCount(_ feedID: String, _ database: FMDatabase) -> Int {
|
||||||
|
|
||||||
// Count only the articles that would appear in the UI.
|
// Count only the articles that would appear in the UI.
|
||||||
// * Must be unread.
|
// * Must be unread.
|
||||||
// * Must not be deleted.
|
// * Must not be deleted.
|
||||||
@ -587,33 +573,28 @@ private extension ArticlesTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func articlesWithSQL(_ sql: String, _ parameters: [AnyObject], _ database: FMDatabase) -> Set<Article> {
|
func articlesWithSQL(_ sql: String, _ parameters: [AnyObject], _ database: FMDatabase) -> Set<Article> {
|
||||||
|
|
||||||
guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else {
|
guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else {
|
||||||
return Set<Article>()
|
return Set<Article>()
|
||||||
}
|
}
|
||||||
return articlesWithResultSet(resultSet, database)
|
return articlesWithResultSet(resultSet, database)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Saving Parsed Items
|
// MARK: - Saving Parsed Items
|
||||||
|
|
||||||
|
|
||||||
func callUpdateArticlesCompletionBlock(_ newArticles: Set<Article>?, _ updatedArticles: Set<Article>?, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) {
|
func callUpdateArticlesCompletionBlock(_ newArticles: Set<Article>?, _ updatedArticles: Set<Article>?, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) {
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
completion(newArticles, updatedArticles)
|
completion(newArticles, updatedArticles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Save New Articles
|
// MARK: - Saving New Articles
|
||||||
|
|
||||||
func findNewArticles(_ incomingArticles: Set<Article>, _ fetchedArticlesDictionary: [String: Article]) -> Set<Article>? {
|
func findNewArticles(_ incomingArticles: Set<Article>, _ fetchedArticlesDictionary: [String: Article]) -> Set<Article>? {
|
||||||
|
|
||||||
let newArticles = Set(incomingArticles.filter { fetchedArticlesDictionary[$0.articleID] == nil })
|
let newArticles = Set(incomingArticles.filter { fetchedArticlesDictionary[$0.articleID] == nil })
|
||||||
return newArticles.isEmpty ? nil : newArticles
|
return newArticles.isEmpty ? nil : newArticles
|
||||||
}
|
}
|
||||||
|
|
||||||
func findAndSaveNewArticles(_ incomingArticles: Set<Article>, _ fetchedArticlesDictionary: [String: Article], _ database: FMDatabase) -> Set<Article>? { //5
|
func findAndSaveNewArticles(_ incomingArticles: Set<Article>, _ fetchedArticlesDictionary: [String: Article], _ database: FMDatabase) -> Set<Article>? { //5
|
||||||
|
|
||||||
guard let newArticles = findNewArticles(incomingArticles, fetchedArticlesDictionary) else {
|
guard let newArticles = findNewArticles(incomingArticles, fetchedArticlesDictionary) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -622,7 +603,6 @@ private extension ArticlesTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func saveNewArticles(_ articles: Set<Article>, _ database: FMDatabase) {
|
func saveNewArticles(_ articles: Set<Article>, _ database: FMDatabase) {
|
||||||
|
|
||||||
saveRelatedObjectsForNewArticles(articles, database)
|
saveRelatedObjectsForNewArticles(articles, database)
|
||||||
|
|
||||||
if let databaseDictionaries = articles.databaseDictionaries() {
|
if let databaseDictionaries = articles.databaseDictionaries() {
|
||||||
@ -631,17 +611,15 @@ private extension ArticlesTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func saveRelatedObjectsForNewArticles(_ articles: Set<Article>, _ database: FMDatabase) {
|
func saveRelatedObjectsForNewArticles(_ articles: Set<Article>, _ database: FMDatabase) {
|
||||||
|
|
||||||
let databaseObjects = articles.databaseObjects()
|
let databaseObjects = articles.databaseObjects()
|
||||||
|
|
||||||
authorsLookupTable.saveRelatedObjects(for: databaseObjects, in: database)
|
authorsLookupTable.saveRelatedObjects(for: databaseObjects, in: database)
|
||||||
attachmentsLookupTable.saveRelatedObjects(for: databaseObjects, in: database)
|
attachmentsLookupTable.saveRelatedObjects(for: databaseObjects, in: database)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Update Existing Articles
|
// MARK: - Updating Existing Articles
|
||||||
|
|
||||||
func articlesWithRelatedObjectChanges<T>(_ comparisonKeyPath: KeyPath<Article, Set<T>?>, _ updatedArticles: Set<Article>, _ fetchedArticles: [String: Article]) -> Set<Article> {
|
func articlesWithRelatedObjectChanges<T>(_ comparisonKeyPath: KeyPath<Article, Set<T>?>, _ updatedArticles: Set<Article>, _ fetchedArticles: [String: Article]) -> Set<Article> {
|
||||||
|
|
||||||
return updatedArticles.filter{ (updatedArticle) -> Bool in
|
return updatedArticles.filter{ (updatedArticle) -> Bool in
|
||||||
if let fetchedArticle = fetchedArticles[updatedArticle.articleID] {
|
if let fetchedArticle = fetchedArticles[updatedArticle.articleID] {
|
||||||
return updatedArticle[keyPath: comparisonKeyPath] != fetchedArticle[keyPath: comparisonKeyPath]
|
return updatedArticle[keyPath: comparisonKeyPath] != fetchedArticle[keyPath: comparisonKeyPath]
|
||||||
@ -652,7 +630,6 @@ private extension ArticlesTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateRelatedObjects<T>(_ comparisonKeyPath: KeyPath<Article, Set<T>?>, _ updatedArticles: Set<Article>, _ fetchedArticles: [String: Article], _ lookupTable: DatabaseLookupTable, _ database: FMDatabase) {
|
func updateRelatedObjects<T>(_ comparisonKeyPath: KeyPath<Article, Set<T>?>, _ updatedArticles: Set<Article>, _ fetchedArticles: [String: Article], _ lookupTable: DatabaseLookupTable, _ database: FMDatabase) {
|
||||||
|
|
||||||
let articlesWithChanges = articlesWithRelatedObjectChanges(comparisonKeyPath, updatedArticles, fetchedArticles)
|
let articlesWithChanges = articlesWithRelatedObjectChanges(comparisonKeyPath, updatedArticles, fetchedArticles)
|
||||||
if !articlesWithChanges.isEmpty {
|
if !articlesWithChanges.isEmpty {
|
||||||
lookupTable.saveRelatedObjects(for: articlesWithChanges.databaseObjects(), in: database)
|
lookupTable.saveRelatedObjects(for: articlesWithChanges.databaseObjects(), in: database)
|
||||||
@ -660,13 +637,11 @@ private extension ArticlesTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func saveUpdatedRelatedObjects(_ updatedArticles: Set<Article>, _ fetchedArticles: [String: Article], _ database: FMDatabase) {
|
func saveUpdatedRelatedObjects(_ updatedArticles: Set<Article>, _ fetchedArticles: [String: Article], _ database: FMDatabase) {
|
||||||
|
|
||||||
updateRelatedObjects(\Article.authors, updatedArticles, fetchedArticles, authorsLookupTable, database)
|
updateRelatedObjects(\Article.authors, updatedArticles, fetchedArticles, authorsLookupTable, database)
|
||||||
updateRelatedObjects(\Article.attachments, updatedArticles, fetchedArticles, attachmentsLookupTable, database)
|
updateRelatedObjects(\Article.attachments, updatedArticles, fetchedArticles, attachmentsLookupTable, database)
|
||||||
}
|
}
|
||||||
|
|
||||||
func findUpdatedArticles(_ incomingArticles: Set<Article>, _ fetchedArticlesDictionary: [String: Article]) -> Set<Article>? {
|
func findUpdatedArticles(_ incomingArticles: Set<Article>, _ fetchedArticlesDictionary: [String: Article]) -> Set<Article>? {
|
||||||
|
|
||||||
let updatedArticles = incomingArticles.filter{ (incomingArticle) -> Bool in //6
|
let updatedArticles = incomingArticles.filter{ (incomingArticle) -> Bool in //6
|
||||||
if let existingArticle = fetchedArticlesDictionary[incomingArticle.articleID] {
|
if let existingArticle = fetchedArticlesDictionary[incomingArticle.articleID] {
|
||||||
if existingArticle != incomingArticle {
|
if existingArticle != incomingArticle {
|
||||||
@ -680,7 +655,6 @@ private extension ArticlesTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findAndSaveUpdatedArticles(_ incomingArticles: Set<Article>, _ fetchedArticlesDictionary: [String: Article], _ database: FMDatabase) -> Set<Article>? { //6
|
func findAndSaveUpdatedArticles(_ incomingArticles: Set<Article>, _ fetchedArticlesDictionary: [String: Article], _ database: FMDatabase) -> Set<Article>? { //6
|
||||||
|
|
||||||
guard let updatedArticles = findUpdatedArticles(incomingArticles, fetchedArticlesDictionary) else {
|
guard let updatedArticles = findUpdatedArticles(incomingArticles, fetchedArticlesDictionary) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -690,7 +664,6 @@ private extension ArticlesTable {
|
|||||||
|
|
||||||
|
|
||||||
func saveUpdatedArticles(_ updatedArticles: Set<Article>, _ fetchedArticles: [String: Article], _ database: FMDatabase) {
|
func saveUpdatedArticles(_ updatedArticles: Set<Article>, _ fetchedArticles: [String: Article], _ database: FMDatabase) {
|
||||||
|
|
||||||
saveUpdatedRelatedObjects(updatedArticles, fetchedArticles, database)
|
saveUpdatedRelatedObjects(updatedArticles, fetchedArticles, database)
|
||||||
|
|
||||||
for updatedArticle in updatedArticles {
|
for updatedArticle in updatedArticles {
|
||||||
@ -699,7 +672,6 @@ private extension ArticlesTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func saveUpdatedArticle(_ updatedArticle: Article, _ fetchedArticles: [String: Article], _ database: FMDatabase) {
|
func saveUpdatedArticle(_ updatedArticle: Article, _ fetchedArticles: [String: Article], _ database: FMDatabase) {
|
||||||
|
|
||||||
// Only update exactly what has changed in the Article (if anything).
|
// Only update exactly what has changed in the Article (if anything).
|
||||||
// Untested theory: this gets us better performance and less database fragmentation.
|
// Untested theory: this gets us better performance and less database fragmentation.
|
||||||
|
|
||||||
@ -708,7 +680,6 @@ private extension ArticlesTable {
|
|||||||
saveNewArticles(Set([updatedArticle]), database)
|
saveNewArticles(Set([updatedArticle]), database)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let changesDictionary = updatedArticle.changesFrom(fetchedArticle), changesDictionary.count > 0 else {
|
guard let changesDictionary = updatedArticle.changesFrom(fetchedArticle), changesDictionary.count > 0 else {
|
||||||
// Not unexpected. There may be no changes.
|
// Not unexpected. There may be no changes.
|
||||||
return
|
return
|
||||||
@ -718,9 +689,7 @@ private extension ArticlesTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func statusIndicatesArticleIsIgnorable(_ status: ArticleStatus) -> Bool {
|
func statusIndicatesArticleIsIgnorable(_ status: ArticleStatus) -> Bool {
|
||||||
|
|
||||||
// Ignorable articles: either userDeleted==1 or (not starred and arrival date > 4 months).
|
// Ignorable articles: either userDeleted==1 or (not starred and arrival date > 4 months).
|
||||||
|
|
||||||
if status.userDeleted {
|
if status.userDeleted {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -731,9 +700,7 @@ private extension ArticlesTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func filterIncomingArticles(_ articles: Set<Article>) -> Set<Article> {
|
func filterIncomingArticles(_ articles: Set<Article>) -> Set<Article> {
|
||||||
|
|
||||||
// Drop Articles that we can ignore.
|
// Drop Articles that we can ignore.
|
||||||
|
|
||||||
return Set(articles.filter{ !statusIndicatesArticleIsIgnorable($0.status) })
|
return Set(articles.filter{ !statusIndicatesArticleIsIgnorable($0.status) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//
|
//
|
||||||
// AttachmentsTable.swift
|
// AttachmentsTable.swift
|
||||||
// Database
|
// NetNewsWire
|
||||||
//
|
//
|
||||||
// Created by Brent Simmons on 7/15/17.
|
// Created by Brent Simmons on 7/15/17.
|
||||||
// Copyright © 2017 Ranchero Software. All rights reserved.
|
// Copyright © 2017 Ranchero Software. All rights reserved.
|
||||||
@ -17,14 +17,12 @@ final class AttachmentsTable: DatabaseRelatedObjectsTable {
|
|||||||
var cache = DatabaseObjectCache()
|
var cache = DatabaseObjectCache()
|
||||||
|
|
||||||
init(name: String) {
|
init(name: String) {
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: DatabaseRelatedObjectsTable
|
// MARK: - DatabaseRelatedObjectsTable
|
||||||
|
|
||||||
func objectWithRow(_ row: FMResultSet) -> DatabaseObject? {
|
func objectWithRow(_ row: FMResultSet) -> DatabaseObject? {
|
||||||
|
|
||||||
if let attachment = Attachment(row: row) {
|
if let attachment = Attachment(row: row) {
|
||||||
return attachment as DatabaseObject
|
return attachment as DatabaseObject
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//
|
//
|
||||||
// AuthorsTable.swift
|
// AuthorsTable.swift
|
||||||
// Database
|
// NetNewsWire
|
||||||
//
|
//
|
||||||
// Created by Brent Simmons on 7/13/17.
|
// Created by Brent Simmons on 7/13/17.
|
||||||
// Copyright © 2017 Ranchero Software. All rights reserved.
|
// Copyright © 2017 Ranchero Software. All rights reserved.
|
||||||
@ -24,14 +24,12 @@ final class AuthorsTable: DatabaseRelatedObjectsTable {
|
|||||||
var cache = DatabaseObjectCache()
|
var cache = DatabaseObjectCache()
|
||||||
|
|
||||||
init(name: String) {
|
init(name: String) {
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: DatabaseRelatedObjectsTable
|
// MARK: - DatabaseRelatedObjectsTable
|
||||||
|
|
||||||
func objectWithRow(_ row: FMResultSet) -> DatabaseObject? {
|
func objectWithRow(_ row: FMResultSet) -> DatabaseObject? {
|
||||||
|
|
||||||
if let author = Author(row: row) {
|
if let author = Author(row: row) {
|
||||||
return author as DatabaseObject
|
return author as DatabaseObject
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//
|
//
|
||||||
// Keys.swift
|
// Keys.swift
|
||||||
// Database
|
// NetNewsWire
|
||||||
//
|
//
|
||||||
// Created by Brent Simmons on 7/3/17.
|
// Created by Brent Simmons on 7/3/17.
|
||||||
// Copyright © 2017 Ranchero Software. All rights reserved.
|
// Copyright © 2017 Ranchero Software. All rights reserved.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//
|
//
|
||||||
// DatabaseArticle.swift
|
// DatabaseArticle.swift
|
||||||
// Database
|
// NetNewsWire
|
||||||
//
|
//
|
||||||
// Created by Brent Simmons on 9/21/17.
|
// Created by Brent Simmons on 9/21/17.
|
||||||
// Copyright © 2017 Ranchero Software. All rights reserved.
|
// Copyright © 2017 Ranchero Software. All rights reserved.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//
|
//
|
||||||
// DatabaseObject+Database.swift
|
// DatabaseObject+Database.swift
|
||||||
// Database
|
// NetNewsWire
|
||||||
//
|
//
|
||||||
// Created by Brent Simmons on 9/13/17.
|
// Created by Brent Simmons on 9/13/17.
|
||||||
// Copyright © 2017 Ranchero Software. All rights reserved.
|
// Copyright © 2017 Ranchero Software. All rights reserved.
|
||||||
@ -13,13 +13,11 @@ import Articles
|
|||||||
extension Array where Element == DatabaseObject {
|
extension Array where Element == DatabaseObject {
|
||||||
|
|
||||||
func asAuthors() -> Set<Author>? {
|
func asAuthors() -> Set<Author>? {
|
||||||
|
|
||||||
let authors = Set(self.map { $0 as! Author })
|
let authors = Set(self.map { $0 as! Author })
|
||||||
return authors.isEmpty ? nil : authors
|
return authors.isEmpty ? nil : authors
|
||||||
}
|
}
|
||||||
|
|
||||||
func asAttachments() -> Set<Attachment>? {
|
func asAttachments() -> Set<Attachment>? {
|
||||||
|
|
||||||
let attachments = Set(self.map { $0 as! Attachment })
|
let attachments = Set(self.map { $0 as! Attachment })
|
||||||
return attachments.isEmpty ? nil : attachments
|
return attachments.isEmpty ? nil : attachments
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//
|
//
|
||||||
// Article+Database.swift
|
// Article+Database.swift
|
||||||
// Database
|
// NetNewsWire
|
||||||
//
|
//
|
||||||
// Created by Brent Simmons on 7/3/17.
|
// Created by Brent Simmons on 7/3/17.
|
||||||
// Copyright © 2017 Ranchero Software. All rights reserved.
|
// Copyright © 2017 Ranchero Software. All rights reserved.
|
||||||
@ -14,12 +14,10 @@ import RSParser
|
|||||||
extension Article {
|
extension Article {
|
||||||
|
|
||||||
init(databaseArticle: DatabaseArticle, accountID: String, authors: Set<Author>?, attachments: Set<Attachment>?) {
|
init(databaseArticle: DatabaseArticle, accountID: String, authors: Set<Author>?, attachments: Set<Attachment>?) {
|
||||||
|
|
||||||
self.init(accountID: accountID, articleID: databaseArticle.articleID, feedID: databaseArticle.feedID, uniqueID: databaseArticle.uniqueID, title: databaseArticle.title, contentHTML: databaseArticle.contentHTML, contentText: databaseArticle.contentText, url: databaseArticle.url, externalURL: databaseArticle.externalURL, summary: databaseArticle.summary, imageURL: databaseArticle.imageURL, bannerImageURL: databaseArticle.bannerImageURL, datePublished: databaseArticle.datePublished, dateModified: databaseArticle.dateModified, authors: authors, attachments: attachments, status: databaseArticle.status)
|
self.init(accountID: accountID, articleID: databaseArticle.articleID, feedID: databaseArticle.feedID, uniqueID: databaseArticle.uniqueID, title: databaseArticle.title, contentHTML: databaseArticle.contentHTML, contentText: databaseArticle.contentText, url: databaseArticle.url, externalURL: databaseArticle.externalURL, summary: databaseArticle.summary, imageURL: databaseArticle.imageURL, bannerImageURL: databaseArticle.bannerImageURL, datePublished: databaseArticle.datePublished, dateModified: databaseArticle.dateModified, authors: authors, attachments: attachments, status: databaseArticle.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(parsedItem: ParsedItem, maximumDateAllowed: Date, accountID: String, feedID: String, status: ArticleStatus) {
|
init(parsedItem: ParsedItem, maximumDateAllowed: Date, accountID: String, feedID: String, status: ArticleStatus) {
|
||||||
|
|
||||||
let authors = Author.authorsWithParsedAuthors(parsedItem.authors)
|
let authors = Author.authorsWithParsedAuthors(parsedItem.authors)
|
||||||
let attachments = Attachment.attachmentsWithParsedAttachments(parsedItem.attachments)
|
let attachments = Attachment.attachmentsWithParsedAttachments(parsedItem.attachments)
|
||||||
|
|
||||||
@ -135,7 +133,6 @@ extension Article: DatabaseObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func relatedObjectsWithName(_ name: String) -> [DatabaseObject]? {
|
public func relatedObjectsWithName(_ name: String) -> [DatabaseObject]? {
|
||||||
|
|
||||||
switch name {
|
switch name {
|
||||||
case RelationshipName.authors:
|
case RelationshipName.authors:
|
||||||
return databaseObjectArray(with: authors)
|
return databaseObjectArray(with: authors)
|
||||||
@ -147,7 +144,6 @@ extension Article: DatabaseObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func databaseObjectArray<T: DatabaseObject>(with objects: Set<T>?) -> [DatabaseObject]? {
|
private func databaseObjectArray<T: DatabaseObject>(with objects: Set<T>?) -> [DatabaseObject]? {
|
||||||
|
|
||||||
guard let objects = objects else {
|
guard let objects = objects else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -158,12 +154,10 @@ extension Article: DatabaseObject {
|
|||||||
extension Set where Element == Article {
|
extension Set where Element == Article {
|
||||||
|
|
||||||
func statuses() -> Set<ArticleStatus> {
|
func statuses() -> Set<ArticleStatus> {
|
||||||
|
|
||||||
return Set<ArticleStatus>(map { $0.status })
|
return Set<ArticleStatus>(map { $0.status })
|
||||||
}
|
}
|
||||||
|
|
||||||
func dictionary() -> [String: Article] {
|
func dictionary() -> [String: Article] {
|
||||||
|
|
||||||
var d = [String: Article]()
|
var d = [String: Article]()
|
||||||
for article in self {
|
for article in self {
|
||||||
d[article.articleID] = article
|
d[article.articleID] = article
|
||||||
@ -172,12 +166,10 @@ extension Set where Element == Article {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func databaseObjects() -> [DatabaseObject] {
|
func databaseObjects() -> [DatabaseObject] {
|
||||||
|
|
||||||
return self.map{ $0 as DatabaseObject }
|
return self.map{ $0 as DatabaseObject }
|
||||||
}
|
}
|
||||||
|
|
||||||
func databaseDictionaries() -> [DatabaseDictionary]? {
|
func databaseDictionaries() -> [DatabaseDictionary]? {
|
||||||
|
|
||||||
return self.compactMap { $0.databaseDictionary() }
|
return self.compactMap { $0.databaseDictionary() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//
|
//
|
||||||
// ArticleStatus+Database.swift
|
// ArticleStatus+Database.swift
|
||||||
// Database
|
// NetNewsWire
|
||||||
//
|
//
|
||||||
// Created by Brent Simmons on 7/3/17.
|
// Created by Brent Simmons on 7/3/17.
|
||||||
// Copyright © 2017 Ranchero Software. All rights reserved.
|
// Copyright © 2017 Ranchero Software. All rights reserved.
|
||||||
@ -13,7 +13,6 @@ import Articles
|
|||||||
extension ArticleStatus {
|
extension ArticleStatus {
|
||||||
|
|
||||||
convenience init(articleID: String, dateArrived: Date, row: FMResultSet) {
|
convenience init(articleID: String, dateArrived: Date, row: FMResultSet) {
|
||||||
|
|
||||||
let read = row.bool(forColumn: DatabaseKey.read)
|
let read = row.bool(forColumn: DatabaseKey.read)
|
||||||
let starred = row.bool(forColumn: DatabaseKey.starred)
|
let starred = row.bool(forColumn: DatabaseKey.starred)
|
||||||
let userDeleted = row.bool(forColumn: DatabaseKey.userDeleted)
|
let userDeleted = row.bool(forColumn: DatabaseKey.userDeleted)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//
|
//
|
||||||
// Attachment+Database.swift
|
// Attachment+Database.swift
|
||||||
// Database
|
// NetNewsWire
|
||||||
//
|
//
|
||||||
// Created by Brent Simmons on 7/4/17.
|
// Created by Brent Simmons on 7/4/17.
|
||||||
// Copyright © 2017 Ranchero Software. All rights reserved.
|
// Copyright © 2017 Ranchero Software. All rights reserved.
|
||||||
@ -14,7 +14,6 @@ import RSParser
|
|||||||
extension Attachment {
|
extension Attachment {
|
||||||
|
|
||||||
init?(row: FMResultSet) {
|
init?(row: FMResultSet) {
|
||||||
|
|
||||||
guard let url = row.string(forColumn: DatabaseKey.url) else {
|
guard let url = row.string(forColumn: DatabaseKey.url) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -22,19 +21,17 @@ extension Attachment {
|
|||||||
let attachmentID = row.string(forColumn: DatabaseKey.attachmentID)
|
let attachmentID = row.string(forColumn: DatabaseKey.attachmentID)
|
||||||
let mimeType = row.string(forColumn: DatabaseKey.mimeType)
|
let mimeType = row.string(forColumn: DatabaseKey.mimeType)
|
||||||
let title = row.string(forColumn: DatabaseKey.title)
|
let title = row.string(forColumn: DatabaseKey.title)
|
||||||
let sizeInBytes = optionalIntForColumn(row, DatabaseKey.sizeInBytes)
|
let sizeInBytes = row.optionalIntForColumn(DatabaseKey.sizeInBytes)
|
||||||
let durationInSeconds = optionalIntForColumn(row, DatabaseKey.durationInSeconds)
|
let durationInSeconds = row.optionalIntForColumn(DatabaseKey.durationInSeconds)
|
||||||
|
|
||||||
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) {
|
init?(parsedAttachment: ParsedAttachment) {
|
||||||
|
|
||||||
self.init(attachmentID: nil, url: parsedAttachment.url, mimeType: parsedAttachment.mimeType, title: parsedAttachment.title, sizeInBytes: parsedAttachment.sizeInBytes, durationInSeconds: parsedAttachment.durationInSeconds)
|
self.init(attachmentID: nil, url: parsedAttachment.url, mimeType: parsedAttachment.mimeType, title: parsedAttachment.title, sizeInBytes: parsedAttachment.sizeInBytes, durationInSeconds: parsedAttachment.durationInSeconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func attachmentsWithParsedAttachments(_ parsedAttachments: Set<ParsedAttachment>?) -> Set<Attachment>? {
|
static func attachmentsWithParsedAttachments(_ parsedAttachments: Set<ParsedAttachment>?) -> Set<Attachment>? {
|
||||||
|
|
||||||
guard let parsedAttachments = parsedAttachments else {
|
guard let parsedAttachments = parsedAttachments else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -42,17 +39,8 @@ extension Attachment {
|
|||||||
let attachments = parsedAttachments.compactMap{ Attachment(parsedAttachment: $0) }
|
let attachments = parsedAttachments.compactMap{ Attachment(parsedAttachment: $0) }
|
||||||
return attachments.isEmpty ? nil : Set(attachments)
|
return attachments.isEmpty ? nil : Set(attachments)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func optionalIntForColumn(_ row: FMResultSet, _ columnName: String) -> Int? {
|
|
||||||
|
|
||||||
let intValue = row.long(forColumn: columnName)
|
|
||||||
if intValue < 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return intValue
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Attachment: DatabaseObject {
|
extension Attachment: DatabaseObject {
|
||||||
|
|
||||||
@ -79,15 +67,24 @@ extension Attachment: DatabaseObject {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension FMResultSet {
|
||||||
|
|
||||||
|
func optionalIntForColumn(_ columnName: String) -> Int? {
|
||||||
|
let intValue = long(forColumn: columnName)
|
||||||
|
if intValue < 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension Set where Element == Attachment {
|
extension Set where Element == Attachment {
|
||||||
|
|
||||||
func databaseDictionaries() -> [DatabaseDictionary] {
|
func databaseDictionaries() -> [DatabaseDictionary] {
|
||||||
|
|
||||||
return self.compactMap { $0.databaseDictionary() }
|
return self.compactMap { $0.databaseDictionary() }
|
||||||
}
|
}
|
||||||
|
|
||||||
func databaseObjects() -> [DatabaseObject] {
|
func databaseObjects() -> [DatabaseObject] {
|
||||||
|
|
||||||
return self.compactMap { $0 as DatabaseObject }
|
return self.compactMap { $0 as DatabaseObject }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//
|
//
|
||||||
// Author+Database.swift
|
// Author+Database.swift
|
||||||
// Database
|
// NetNewsWire
|
||||||
//
|
//
|
||||||
// Created by Brent Simmons on 7/8/17.
|
// Created by Brent Simmons on 7/8/17.
|
||||||
// Copyright © 2017 Ranchero Software. All rights reserved.
|
// Copyright © 2017 Ranchero Software. All rights reserved.
|
||||||
@ -16,7 +16,6 @@ import RSParser
|
|||||||
extension Author {
|
extension Author {
|
||||||
|
|
||||||
init?(row: FMResultSet) {
|
init?(row: FMResultSet) {
|
||||||
|
|
||||||
let authorID = row.string(forColumn: DatabaseKey.authorID)
|
let authorID = row.string(forColumn: DatabaseKey.authorID)
|
||||||
let name = row.string(forColumn: DatabaseKey.name)
|
let name = row.string(forColumn: DatabaseKey.name)
|
||||||
let url = row.string(forColumn: DatabaseKey.url)
|
let url = row.string(forColumn: DatabaseKey.url)
|
||||||
@ -27,12 +26,10 @@ extension Author {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init?(parsedAuthor: ParsedAuthor) {
|
init?(parsedAuthor: ParsedAuthor) {
|
||||||
|
|
||||||
self.init(authorID: nil, name: parsedAuthor.name, url: parsedAuthor.url, avatarURL: parsedAuthor.avatarURL, emailAddress: parsedAuthor.emailAddress)
|
self.init(authorID: nil, name: parsedAuthor.name, url: parsedAuthor.url, avatarURL: parsedAuthor.avatarURL, emailAddress: parsedAuthor.emailAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func authorsWithParsedAuthors(_ parsedAuthors: Set<ParsedAuthor>?) -> Set<Author>? {
|
public static func authorsWithParsedAuthors(_ parsedAuthors: Set<ParsedAuthor>?) -> Set<Author>? {
|
||||||
|
|
||||||
guard let parsedAuthors = parsedAuthors else {
|
guard let parsedAuthors = parsedAuthors else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -49,7 +46,6 @@ extension Author: DatabaseObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func databaseDictionary() -> DatabaseDictionary? {
|
public func databaseDictionary() -> DatabaseDictionary? {
|
||||||
|
|
||||||
var d: DatabaseDictionary = [DatabaseKey.authorID: authorID]
|
var d: DatabaseDictionary = [DatabaseKey.authorID: authorID]
|
||||||
if let name = name {
|
if let name = name {
|
||||||
d[DatabaseKey.name] = name
|
d[DatabaseKey.name] = name
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//
|
//
|
||||||
// ParsedArticle+Database.swift
|
// ParsedArticle+Database.swift
|
||||||
// Database
|
// NetNewsWire
|
||||||
//
|
//
|
||||||
// Created by Brent Simmons on 9/18/17.
|
// Created by Brent Simmons on 9/18/17.
|
||||||
// Copyright © 2017 Ranchero Software. All rights reserved.
|
// Copyright © 2017 Ranchero Software. All rights reserved.
|
||||||
|
@ -13,7 +13,6 @@ import Articles
|
|||||||
extension RelatedObjectsMap {
|
extension RelatedObjectsMap {
|
||||||
|
|
||||||
func attachments(for articleID: String) -> Set<Attachment>? {
|
func attachments(for articleID: String) -> Set<Attachment>? {
|
||||||
|
|
||||||
if let objects = self[articleID] {
|
if let objects = self[articleID] {
|
||||||
return objects.asAttachments()
|
return objects.asAttachments()
|
||||||
}
|
}
|
||||||
@ -21,7 +20,6 @@ extension RelatedObjectsMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func authors(for articleID: String) -> Set<Author>? {
|
func authors(for articleID: String) -> Set<Author>? {
|
||||||
|
|
||||||
if let objects = self[articleID] {
|
if let objects = self[articleID] {
|
||||||
return objects.asAuthors()
|
return objects.asAuthors()
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//
|
//
|
||||||
// SearchTable.swift
|
// SearchTable.swift
|
||||||
// ArticlesDatabase
|
// NetNewsWire
|
||||||
//
|
//
|
||||||
// Created by Brent Simmons on 2/23/19.
|
// Created by Brent Simmons on 2/23/19.
|
||||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||||
@ -109,10 +109,6 @@ private extension SearchTable {
|
|||||||
|
|
||||||
func insert(_ article: ArticleSearchInfo, _ database: FMDatabase) -> Int {
|
func insert(_ article: ArticleSearchInfo, _ database: FMDatabase) -> Int {
|
||||||
let rowDictionary: DatabaseDictionary = [DatabaseKey.body: article.bodyForIndex, DatabaseKey.title: article.title ?? ""]
|
let rowDictionary: DatabaseDictionary = [DatabaseKey.body: article.bodyForIndex, DatabaseKey.title: article.title ?? ""]
|
||||||
// rowDictionary[DatabaseKey.title] = article.title ?? ""
|
|
||||||
// rowDictionary[DatabaseKey.body] = article.bodyForIndex
|
|
||||||
// rowDictionary.setObject(article.title ?? "", forKey: DatabaseKey.title as NSString)
|
|
||||||
// rowDictionary.setObject(article.bodyForIndex, forKey: DatabaseKey.body as NSString)
|
|
||||||
insertRow(rowDictionary, insertType: .normal, in: database)
|
insertRow(rowDictionary, insertType: .normal, in: database)
|
||||||
return Int(database.lastInsertRowId())
|
return Int(database.lastInsertRowId())
|
||||||
}
|
}
|
||||||
|
@ -22,14 +22,12 @@ final class StatusesTable: DatabaseTable {
|
|||||||
private let queue: RSDatabaseQueue
|
private let queue: RSDatabaseQueue
|
||||||
|
|
||||||
init(queue: RSDatabaseQueue) {
|
init(queue: RSDatabaseQueue) {
|
||||||
|
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Creating/Updating
|
// MARK: - Creating/Updating
|
||||||
|
|
||||||
func ensureStatusesForArticleIDs(_ articleIDs: Set<String>, _ read: Bool, _ database: FMDatabase) -> [String: ArticleStatus] {
|
func ensureStatusesForArticleIDs(_ articleIDs: Set<String>, _ read: Bool, _ database: FMDatabase) -> [String: ArticleStatus] {
|
||||||
|
|
||||||
// Check cache.
|
// Check cache.
|
||||||
let articleIDsMissingCachedStatus = articleIDsWithNoCachedStatus(articleIDs)
|
let articleIDsMissingCachedStatus = articleIDsWithNoCachedStatus(articleIDs)
|
||||||
if articleIDsMissingCachedStatus.isEmpty {
|
if articleIDsMissingCachedStatus.isEmpty {
|
||||||
@ -48,17 +46,15 @@ final class StatusesTable: DatabaseTable {
|
|||||||
return statusesDictionary(articleIDs)
|
return statusesDictionary(articleIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Marking
|
// MARK: - Marking
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func mark(_ statuses: Set<ArticleStatus>, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ database: FMDatabase) -> Set<ArticleStatus>? {
|
func mark(_ statuses: Set<ArticleStatus>, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ database: FMDatabase) -> Set<ArticleStatus>? {
|
||||||
|
|
||||||
// Sets flag in both memory and in database.
|
// Sets flag in both memory and in database.
|
||||||
|
|
||||||
var updatedStatuses = Set<ArticleStatus>()
|
var updatedStatuses = Set<ArticleStatus>()
|
||||||
|
|
||||||
for status in statuses {
|
for status in statuses {
|
||||||
|
|
||||||
if status.boolStatus(forKey: statusKey) == flag {
|
if status.boolStatus(forKey: statusKey) == flag {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -76,7 +72,7 @@ final class StatusesTable: DatabaseTable {
|
|||||||
return updatedStatuses
|
return updatedStatuses
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Fetching
|
// MARK: - Fetching
|
||||||
|
|
||||||
func fetchUnreadArticleIDs(_ callback: @escaping (Set<String>) -> Void) {
|
func fetchUnreadArticleIDs(_ callback: @escaping (Set<String>) -> Void) {
|
||||||
fetchArticleIDs("select articleID from statuses where read=0 and userDeleted=0;", callback)
|
fetchArticleIDs("select articleID from statuses where read=0 and userDeleted=0;", callback)
|
||||||
@ -106,7 +102,6 @@ final class StatusesTable: DatabaseTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func statusWithRow(_ row: FMResultSet) -> ArticleStatus? {
|
func statusWithRow(_ row: FMResultSet) -> ArticleStatus? {
|
||||||
|
|
||||||
guard let articleID = row.string(forColumn: DatabaseKey.articleID) else {
|
guard let articleID = row.string(forColumn: DatabaseKey.articleID) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -125,7 +120,6 @@ final class StatusesTable: DatabaseTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func statusesDictionary(_ articleIDs: Set<String>) -> [String: ArticleStatus] {
|
func statusesDictionary(_ articleIDs: Set<String>) -> [String: ArticleStatus] {
|
||||||
|
|
||||||
var d = [String: ArticleStatus]()
|
var d = [String: ArticleStatus]()
|
||||||
|
|
||||||
for articleID in articleIDs {
|
for articleID in articleIDs {
|
||||||
@ -142,23 +136,20 @@ final class StatusesTable: DatabaseTable {
|
|||||||
|
|
||||||
private extension StatusesTable {
|
private extension StatusesTable {
|
||||||
|
|
||||||
// MARK: Cache
|
// MARK: - Cache
|
||||||
|
|
||||||
func articleIDsWithNoCachedStatus(_ articleIDs: Set<String>) -> Set<String> {
|
func articleIDsWithNoCachedStatus(_ articleIDs: Set<String>) -> Set<String> {
|
||||||
|
|
||||||
return Set(articleIDs.filter { cache[$0] == nil })
|
return Set(articleIDs.filter { cache[$0] == nil })
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Creating
|
// MARK: - Creating
|
||||||
|
|
||||||
func saveStatuses(_ statuses: Set<ArticleStatus>, _ database: FMDatabase) {
|
func saveStatuses(_ statuses: Set<ArticleStatus>, _ database: FMDatabase) {
|
||||||
|
|
||||||
let statusArray = statuses.map { $0.databaseDictionary()! }
|
let statusArray = statuses.map { $0.databaseDictionary()! }
|
||||||
self.insertRows(statusArray, insertType: .orIgnore, in: database)
|
self.insertRows(statusArray, insertType: .orIgnore, in: database)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createAndSaveStatusesForArticleIDs(_ articleIDs: Set<String>, _ read: Bool, _ database: FMDatabase) {
|
func createAndSaveStatusesForArticleIDs(_ articleIDs: Set<String>, _ read: Bool, _ database: FMDatabase) {
|
||||||
|
|
||||||
let now = Date()
|
let now = Date()
|
||||||
let statuses = Set(articleIDs.map { ArticleStatus(articleID: $0, read: read, dateArrived: now) })
|
let statuses = Set(articleIDs.map { ArticleStatus(articleID: $0, read: read, dateArrived: now) })
|
||||||
cache.addIfNotCached(statuses)
|
cache.addIfNotCached(statuses)
|
||||||
@ -167,7 +158,6 @@ private extension StatusesTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fetchAndCacheStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) {
|
func fetchAndCacheStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) {
|
||||||
|
|
||||||
guard let resultSet = self.selectRowsWhere(key: DatabaseKey.articleID, inValues: Array(articleIDs), in: database) else {
|
guard let resultSet = self.selectRowsWhere(key: DatabaseKey.articleID, inValues: Array(articleIDs), in: database) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -176,10 +166,9 @@ private extension StatusesTable {
|
|||||||
self.cache.addIfNotCached(statuses)
|
self.cache.addIfNotCached(statuses)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Marking
|
// MARK: - Marking
|
||||||
|
|
||||||
func markArticleIDs(_ articleIDs: Set<String>, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ database: FMDatabase) {
|
func markArticleIDs(_ articleIDs: Set<String>, _ statusKey: ArticleStatus.Key, _ flag: Bool, _ database: FMDatabase) {
|
||||||
|
|
||||||
updateRowsWithValue(NSNumber(value: flag), valueKey: statusKey.rawValue, whereKey: DatabaseKey.articleID, matches: Array(articleIDs), database: database)
|
updateRowsWithValue(NSNumber(value: flag), valueKey: statusKey.rawValue, whereKey: DatabaseKey.articleID, matches: Array(articleIDs), database: database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -196,21 +185,17 @@ private final class StatusCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func add(_ statuses: Set<ArticleStatus>) {
|
func add(_ statuses: Set<ArticleStatus>) {
|
||||||
|
|
||||||
// Replaces any cached statuses.
|
// Replaces any cached statuses.
|
||||||
|
|
||||||
for status in statuses {
|
for status in statuses {
|
||||||
self[status.articleID] = status
|
self[status.articleID] = status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addStatusIfNotCached(_ status: ArticleStatus) {
|
func addStatusIfNotCached(_ status: ArticleStatus) {
|
||||||
|
|
||||||
addIfNotCached(Set([status]))
|
addIfNotCached(Set([status]))
|
||||||
}
|
}
|
||||||
|
|
||||||
func addIfNotCached(_ statuses: Set<ArticleStatus>) {
|
func addIfNotCached(_ statuses: Set<ArticleStatus>) {
|
||||||
|
|
||||||
// Does not replace already cached statuses.
|
// Does not replace already cached statuses.
|
||||||
|
|
||||||
for status in statuses {
|
for status in statuses {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user