mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2025-02-01 19:46:56 +01:00
Create FeedFinder module.
This commit is contained in:
parent
826ec7d413
commit
c35187900a
@ -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: [
|
||||
|
@ -19,6 +19,7 @@ import Web
|
||||
import os.log
|
||||
import Secrets
|
||||
import Core
|
||||
import CommonErrors
|
||||
|
||||
// Main thread only.
|
||||
|
||||
|
@ -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."
|
||||
|
@ -15,6 +15,7 @@ import SyncDatabase
|
||||
import os.log
|
||||
import Core
|
||||
import NewsBlur
|
||||
import CommonErrors
|
||||
|
||||
extension NewsBlurAccountDelegate {
|
||||
|
||||
|
@ -14,6 +14,7 @@ import SyncDatabase
|
||||
import os.log
|
||||
import Secrets
|
||||
import NewsBlur
|
||||
import CommonErrors
|
||||
|
||||
final class NewsBlurAccountDelegate: AccountDelegate {
|
||||
|
||||
|
@ -15,6 +15,8 @@ import Secrets
|
||||
import Database
|
||||
import Core
|
||||
import ReaderAPI
|
||||
import CommonErrors
|
||||
import FeedFinder
|
||||
|
||||
final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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."
|
||||
|
@ -13,6 +13,7 @@ import SyncDatabase
|
||||
import os.log
|
||||
import Secrets
|
||||
import Core
|
||||
import CommonErrors
|
||||
|
||||
final class FeedlyAccountDelegate: AccountDelegate {
|
||||
|
||||
|
@ -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>) -> ())
|
||||
|
@ -12,6 +12,7 @@ import SyncDatabase
|
||||
import Web
|
||||
import Secrets
|
||||
import Core
|
||||
import CommonErrors
|
||||
|
||||
class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlySearchOperationDelegate, FeedlyCheckpointOperationDelegate {
|
||||
|
||||
|
@ -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
8
FeedFinder/.gitignore
vendored
Normal 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
37
FeedFinder/Package.swift
Normal 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"]),
|
||||
]
|
||||
)
|
@ -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 {
|
@ -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;
|
||||
|
@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import FoundationExtras
|
||||
import Parser
|
||||
|
||||
private let feedURLWordsToMatch = ["feed", "xml", "rss", "atom", "json"]
|
12
FeedFinder/Tests/FeedFinderTests/FeedFinderTests.swift
Normal file
12
FeedFinder/Tests/FeedFinderTests/FeedFinderTests.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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 */,
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
||||
}
|
||||
|
@ -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]
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ class AddFeedViewController: UITableViewController {
|
||||
}
|
||||
|
||||
if account!.hasFeed(withURL: url.absoluteString) {
|
||||
presentError(CommonError.createErrorAlreadySubscribed)
|
||||
presentError(AccountError.createErrorAlreadySubscribed)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user