Create FeedFinder module.

This commit is contained in:
Brent Simmons 2024-04-07 14:57:05 -07:00
parent 826ec7d413
commit c35187900a
26 changed files with 169 additions and 88 deletions

View File

@ -24,6 +24,7 @@ let package = Package(
.package(path: "../CloudKitSync"),
.package(path: "../NewsBlur"),
.package(path: "../Feedbin"),
.package(path: "../FeedFinder"),
.package(path: "../CommonErrors")
],
targets: [
@ -43,6 +44,7 @@ let package = Package(
"NewsBlur",
"CloudKitSync",
"Feedbin",
"FeedFinder",
"CommonErrors"
],
swiftSettings: [

View File

@ -19,6 +19,7 @@ import Web
import os.log
import Secrets
import Core
import CommonErrors
// Main thread only.

View File

@ -14,6 +14,8 @@ import ArticlesDatabase
import Web
import Secrets
import Core
import CommonErrors
import FeedFinder
public enum LocalAccountDelegateError: String, Error {
case invalidParameter = "An invalid parameter was used."

View File

@ -15,6 +15,7 @@ import SyncDatabase
import os.log
import Core
import NewsBlur
import CommonErrors
extension NewsBlurAccountDelegate {

View File

@ -14,6 +14,7 @@ import SyncDatabase
import os.log
import Secrets
import NewsBlur
import CommonErrors
final class NewsBlurAccountDelegate: AccountDelegate {

View File

@ -15,6 +15,8 @@ import Secrets
import Database
import Core
import ReaderAPI
import CommonErrors
import FeedFinder
final class ReaderAPIAccountDelegate: AccountDelegate {

View File

@ -10,9 +10,7 @@ import Foundation
import Web
import CommonErrors
typealias AccountError = CommonError // Temporary, for compatibility with existing code
public extension CommonError {
public extension AccountError {
@MainActor var account: Account? {
if case .wrappedError(_, let accountID, _) = self {
@ -22,7 +20,7 @@ public extension CommonError {
}
}
@MainActor static func wrappedError(error: Error, account: Account) -> CommonError {
@MainActor static func wrappedError(error: Error, account: Account) -> AccountError {
wrappedError(error: error, accountID: account.accountID, accountName: account.nameForDisplay)
}
}

View File

@ -18,6 +18,8 @@ import Web
import Secrets
import Core
import CloudKitExtras
import CommonErrors
import FeedFinder
enum CloudKitAccountDelegateError: LocalizedError {
case invalidParameter
@ -820,80 +822,82 @@ private extension CloudKitAccountDelegate {
refreshProgress.addToNumberOfTasksAndRemaining(5)
FeedFinder.find(url: url) { result in
self.refreshProgress.completeTask()
switch result {
case .success(let feedSpecifiers):
guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers), let url = URL(string: bestFeedSpecifier.urlString) else {
MainActor.assumeIsolated {
self.refreshProgress.completeTask()
switch result {
case .success(let feedSpecifiers):
guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers), let url = URL(string: bestFeedSpecifier.urlString) else {
self.refreshProgress.completeTasks(3)
if validateFeed {
self.refreshProgress.completeTask()
completion(.failure(AccountError.createErrorNotFound))
} else {
addDeadFeed()
}
return
}
if account.hasFeed(withURL: bestFeedSpecifier.urlString) {
self.refreshProgress.completeTasks(4)
completion(.failure(AccountError.createErrorAlreadySubscribed))
return
}
let feed = account.createFeed(with: nil, url: url.absoluteString, feedID: url.absoluteString, homePageURL: nil)
feed.editedName = editedName
container.addFeed(feed)
InitialFeedDownloader.download(url) { parsedFeed in
self.refreshProgress.completeTask()
if let parsedFeed {
Task { @MainActor in
do {
try await account.update(feed: feed, with: parsedFeed)
self.accountZone.createFeed(url: bestFeedSpecifier.urlString,
name: parsedFeed.title,
editedName: editedName,
homePageURL: parsedFeed.homePageURL,
container: container) { result in
self.refreshProgress.completeTask()
switch result {
case .success(let externalID):
feed.externalID = externalID
self.sendNewArticlesToTheCloud(account, feed)
completion(.success(feed))
case .failure(let error):
container.removeFeed(feed)
self.refreshProgress.completeTasks(2)
completion(.failure(error))
}
}
} catch {
container.removeFeed(feed)
self.refreshProgress.completeTasks(3)
completion(.failure(error))
}
}
} else {
self.refreshProgress.completeTasks(3)
container.removeFeed(feed)
completion(.failure(AccountError.createErrorNotFound))
}
}
case .failure:
self.refreshProgress.completeTasks(3)
if validateFeed {
self.refreshProgress.completeTask()
completion(.failure(AccountError.createErrorNotFound))
return
} else {
addDeadFeed()
}
return
}
if account.hasFeed(withURL: bestFeedSpecifier.urlString) {
self.refreshProgress.completeTasks(4)
completion(.failure(AccountError.createErrorAlreadySubscribed))
return
}
let feed = account.createFeed(with: nil, url: url.absoluteString, feedID: url.absoluteString, homePageURL: nil)
feed.editedName = editedName
container.addFeed(feed)
InitialFeedDownloader.download(url) { parsedFeed in
self.refreshProgress.completeTask()
if let parsedFeed {
Task { @MainActor in
do {
try await account.update(feed: feed, with: parsedFeed)
self.accountZone.createFeed(url: bestFeedSpecifier.urlString,
name: parsedFeed.title,
editedName: editedName,
homePageURL: parsedFeed.homePageURL,
container: container) { result in
self.refreshProgress.completeTask()
switch result {
case .success(let externalID):
feed.externalID = externalID
self.sendNewArticlesToTheCloud(account, feed)
completion(.success(feed))
case .failure(let error):
container.removeFeed(feed)
self.refreshProgress.completeTasks(2)
completion(.failure(error))
}
}
} catch {
container.removeFeed(feed)
self.refreshProgress.completeTasks(3)
completion(.failure(error))
}
}
} else {
self.refreshProgress.completeTasks(3)
container.removeFeed(feed)
completion(.failure(AccountError.createErrorNotFound))
}
}
case .failure:
self.refreshProgress.completeTasks(3)
if validateFeed {
self.refreshProgress.completeTask()
completion(.failure(AccountError.createErrorNotFound))
return
} else {
addDeadFeed()
}
}
}

View File

@ -15,6 +15,8 @@ import os.log
import Secrets
import Core
import Feedbin
import CommonErrors
import FeedFinder
public enum FeedbinAccountDelegateError: String, Error {
case invalidParameter = "There was an invalid parameter passed."

View File

@ -13,6 +13,7 @@ import SyncDatabase
import os.log
import Secrets
import Core
import CommonErrors
final class FeedlyAccountDelegate: AccountDelegate {

View File

@ -7,6 +7,7 @@
//
import Foundation
import CommonErrors
protocol FeedlyAddFeedToCollectionService {
func addFeed(with feedId: FeedlyFeedResourceId, title: String?, toCollectionWith collectionId: String, completion: @escaping (Result<[FeedlyFeed], Error>) -> ())

View File

@ -12,6 +12,7 @@ import SyncDatabase
import Web
import Secrets
import Core
import CommonErrors
class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlySearchOperationDelegate, FeedlyCheckpointOperationDelegate {

View File

@ -8,7 +8,7 @@
import Foundation
import Web
public enum CommonError: LocalizedError {
public enum AccountError: LocalizedError {
case createErrorNotFound
case createErrorAlreadySubscribed
@ -73,7 +73,7 @@ public enum CommonError: LocalizedError {
// MARK: Private
private extension CommonError {
private extension AccountError {
func unknownError(_ error: Error, _ accountName: String) -> String {
let localizedText = NSLocalizedString("An error occurred while processing the “%@” account: %@", comment: "Unknown error")

8
FeedFinder/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

37
FeedFinder/Package.swift Normal file
View File

@ -0,0 +1,37 @@
// swift-tools-version: 5.10
import PackageDescription
let package = Package(
name: "FeedFinder",
platforms: [.macOS(.v14), .iOS(.v17)],
products: [
.library(
name: "FeedFinder",
targets: ["FeedFinder"]),
],
dependencies: [
.package(path: "../Web"),
.package(path: "../Parser"),
.package(path: "../FoundationExtras"),
.package(path: "../CommonErrors"),
],
targets: [
.target(
name: "FeedFinder",
dependencies: [
"Parser",
"Web",
"FoundationExtras",
"CommonErrors"
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
),
.testTarget(
name: "FeedFinderTests",
dependencies: ["FeedFinder"]),
]
)

View File

@ -9,10 +9,11 @@
import Foundation
import Parser
import Web
import CommonErrors
class FeedFinder {
@MainActor public final class FeedFinder {
static func find(url: URL) async throws -> Set<FeedSpecifier> {
@MainActor public static func find(url: URL) async throws -> Set<FeedSpecifier> {
try await withCheckedThrowingContinuation { continuation in
Task { @MainActor in
@ -28,7 +29,7 @@ class FeedFinder {
}
}
@MainActor static func find(url: URL, completion: @escaping (Result<Set<FeedSpecifier>, Error>) -> Void) {
@MainActor public static func find(url: URL, completion: @escaping @Sendable (Result<Set<FeedSpecifier>, Error>) -> Void) {
downloadAddingToCache(url) { (data, response, error) in
MainActor.assumeIsolated {

View File

@ -8,9 +8,9 @@
import Foundation
struct FeedSpecifier: Hashable, Sendable {
public struct FeedSpecifier: Hashable, Sendable {
enum Source: Int {
public enum Source: Int, Sendable {
case UserEntered = 0, HTMLHead, HTMLLink
func equalToOrBetterThan(_ otherSource: Source) -> Bool {
@ -26,6 +26,13 @@ struct FeedSpecifier: Hashable, Sendable {
return calculatedScore()
}
public init(title: String?, urlString: String, source: Source, orderFound: Int) {
self.title = title
self.urlString = urlString
self.source = source
self.orderFound = orderFound
}
func feedSpecifierByMerging(_ feedSpecifier: FeedSpecifier) -> FeedSpecifier {
// Take the best data (non-nil title, better source) to create a new feed specifier;

View File

@ -7,6 +7,7 @@
//
import Foundation
import FoundationExtras
import Parser
private let feedURLWordsToMatch = ["feed", "xml", "rss", "atom", "json"]

View File

@ -0,0 +1,12 @@
import XCTest
@testable import FeedFinder
final class FeedFinderTests: XCTestCase {
func testExample() throws {
// XCTest Documentation
// https://developer.apple.com/documentation/xctest
// Defining Test Cases and Test Methods
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
}
}

View File

@ -70,10 +70,10 @@ import CommonErrors
let feed = try await account.createFeed(url: url.absoluteString, name: title, container: container, validateFeed: true)
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
} catch CommonError.createErrorAlreadySubscribed {
} catch AccountError.createErrorAlreadySubscribed {
self.showAlreadySubscribedError(url.absoluteString)
} catch CommonError.createErrorNotFound {
} catch AccountError.createErrorNotFound {
self.showNoFeedsErrorMessage()
} catch {

View File

@ -1467,6 +1467,7 @@
84F9EAE4213660A100CF2DE4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
84FB9FAC2BC33AFE00B7AFC3 /* NewsBlur */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = NewsBlur; sourceTree = "<group>"; };
84FB9FAD2BC344F800B7AFC3 /* Feedbin */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Feedbin; sourceTree = "<group>"; };
84FB9FAE2BC3494B00B7AFC3 /* FeedFinder */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = FeedFinder; sourceTree = "<group>"; };
84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconURLFinder.swift; sourceTree = "<group>"; };
B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+NetNewsWire.swift"; sourceTree = "<group>"; };
B24EFD482330FF99006C6242 /* NetNewsWire-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NetNewsWire-Bridging-Header.h"; sourceTree = "<group>"; };
@ -2359,6 +2360,7 @@
849C64611ED37A5D003D8FC0 /* Products */,
51C452B22265141B00C03939 /* Frameworks */,
51CD32C624D2DEF9009ABAEF /* Account */,
84FB9FAE2BC3494B00B7AFC3 /* FeedFinder */,
84FB9FAD2BC344F800B7AFC3 /* Feedbin */,
84FB9FAC2BC33AFE00B7AFC3 /* NewsBlur */,
84CC98D92BC1DD25006A05C9 /* ReaderAPI */,

View File

@ -297,10 +297,10 @@ public enum CreateReaderAPISubscriptionResult: Sendable {
// There is no call to get a single subscription entry, so we get them all,
// look up the one we just subscribed to and return that
guard let subscriptions = try await retrieveSubscriptions() else {
throw CommonError.createErrorNotFound
throw AccountError.createErrorNotFound
}
guard let subscription = subscriptions.first(where: { $0.feedID == subResult.streamID }) else {
throw CommonError.createErrorNotFound
throw AccountError.createErrorNotFound
}
return .created(subscription)

View File

@ -11,7 +11,6 @@ import Foundation
struct DatabaseTableName {
static let syncStatus = "syncStatus"
}
struct DatabaseKey {
@ -21,5 +20,4 @@ struct DatabaseKey {
static let key = "key"
static let flag = "flag"
static let selected = "selected"
}

View File

@ -44,5 +44,4 @@ public struct SyncStatus: Hashable, Equatable, Sendable {
public func databaseDictionary() -> DatabaseDictionary {
return [DatabaseKey.articleID: articleID, DatabaseKey.key: key.rawValue, DatabaseKey.flag: flag, DatabaseKey.selected: selected]
}
}

View File

@ -106,7 +106,7 @@ class AddFeedViewController: UITableViewController {
}
if account!.hasFeed(withURL: url.absoluteString) {
presentError(CommonError.createErrorAlreadySubscribed)
presentError(AccountError.createErrorAlreadySubscribed)
return
}

View File

@ -13,7 +13,7 @@ import CommonErrors
extension UIViewController {
func presentError(_ error: Error, dismiss: (() -> Void)? = nil) {
if let accountError = error as? CommonError, accountError.isCredentialsError {
if let accountError = error as? AccountError, accountError.isCredentialsError {
presentAccountError(accountError, dismiss: dismiss)
} else if let decodingError = error as? DecodingError {
let errorTitle = NSLocalizedString("Error", comment: "Error")
@ -56,7 +56,7 @@ extension UIViewController {
private extension UIViewController {
func presentAccountError(_ error: CommonError, dismiss: (() -> Void)? = nil) {
func presentAccountError(_ error: AccountError, dismiss: (() -> Void)? = nil) {
let title = NSLocalizedString("Account Error", comment: "Account Error")
let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert)