Create FeedDownloader local package.
This commit is contained in:
parent
fb5a1b28d0
commit
ea3a78b841
|
@ -0,0 +1,8 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/configuration/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
|
@ -0,0 +1,67 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1530"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "FeedDownloader"
|
||||
BuildableName = "FeedDownloader"
|
||||
BlueprintName = "FeedDownloader"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "FeedDownloader"
|
||||
BuildableName = "FeedDownloader"
|
||||
BlueprintName = "FeedDownloader"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
|
@ -0,0 +1,32 @@
|
|||
// swift-tools-version: 5.10
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "FeedDownloader",
|
||||
platforms: [.macOS(.v14), .iOS(.v17)],
|
||||
products: [
|
||||
.library(
|
||||
name: "FeedDownloader",
|
||||
targets: ["FeedDownloader"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../Web"),
|
||||
.package(path: "../FoundationExtras"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "FeedDownloader",
|
||||
dependencies: [
|
||||
"Web",
|
||||
"FoundationExtras"
|
||||
],
|
||||
swiftSettings: [
|
||||
.enableExperimentalFeature("StrictConcurrency")
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "FeedDownloaderTests",
|
||||
dependencies: ["FeedDownloader"]),
|
||||
]
|
||||
)
|
|
@ -0,0 +1,155 @@
|
|||
//
|
||||
// FeedDownloader.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 5/22/24.
|
||||
// Copyright © 2024 Brent Simmons. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import FoundationExtras
|
||||
import os
|
||||
import Web
|
||||
|
||||
public protocol FeedDownloaderDelegate: AnyObject {
|
||||
|
||||
@MainActor func feedDownloader(_: FeedDownloader, requestCompletedForFeedURL: URL, response: URLResponse?, data: Data?, error: Error?)
|
||||
|
||||
@MainActor func feedDownloader(_: FeedDownloader, requestCanceledForFeedURL: URL, response: URLResponse?, data: Data?, error: Error?, reason: FeedDownloader.CancellationReason)
|
||||
|
||||
@MainActor func feedDownloaderSessionDidComplete(_: FeedDownloader)
|
||||
|
||||
@MainActor func feedDownloader(_: FeedDownloader, conditionalGetInfoFor: URL) -> HTTPConditionalGetInfo?
|
||||
}
|
||||
|
||||
/// Use this to download feeds directly (local and iCloud accounts).
|
||||
@MainActor public final class FeedDownloader {
|
||||
|
||||
public enum CancellationReason {
|
||||
case suspended
|
||||
case notFeedData
|
||||
case unexpectedResponse
|
||||
case notModified
|
||||
}
|
||||
|
||||
public weak var delegate: FeedDownloaderDelegate?
|
||||
public var downloadProgress: DownloadProgress {
|
||||
downloadSession.downloadProgress
|
||||
}
|
||||
|
||||
private let downloadSession: DownloadSession
|
||||
private var isSuspended = false
|
||||
|
||||
public init() {
|
||||
|
||||
self.downloadSession = DownloadSession()
|
||||
downloadSession.delegate = self
|
||||
}
|
||||
|
||||
public func downloadFeeds(_ feedURLs: Set<URL>) {
|
||||
|
||||
let feedIdentifiers = Set(feedURLs.map { $0.absoluteString })
|
||||
downloadSession.download(feedIdentifiers)
|
||||
}
|
||||
|
||||
public func suspend() async {
|
||||
|
||||
isSuspended = true
|
||||
await downloadSession.cancelAll()
|
||||
}
|
||||
|
||||
public func resume() {
|
||||
|
||||
isSuspended = false
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedDownloader: DownloadSessionDelegate {
|
||||
|
||||
public func downloadSession(_ downloadSession: DownloadSession, requestForIdentifier identifier: String) -> URLRequest? {
|
||||
|
||||
guard let url = URL(string: identifier) else {
|
||||
assertionFailure("There should be no case where identifier is not convertible to URL.")
|
||||
return nil
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
if let conditionalGetInfo = delegate?.feedDownloader(self, conditionalGetInfoFor: url) {
|
||||
conditionalGetInfo.addRequestHeadersToURLRequest(&request)
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
public func downloadSession(_ downloadSession: DownloadSession, downloadDidCompleteForIdentifier identifier: String, response: URLResponse?, data: Data?, error: Error?) {
|
||||
|
||||
guard let url = URL(string: identifier) else {
|
||||
assertionFailure("There should be no case where identifier is not convertible to URL.")
|
||||
return
|
||||
}
|
||||
|
||||
delegate?.feedDownloader(self, requestCompletedForFeedURL: url, response: response, data: data, error: error)
|
||||
}
|
||||
|
||||
public func downloadSession(_ downloadSession: DownloadSession, shouldContinueAfterReceivingData data: Data, identifier: String) -> Bool {
|
||||
|
||||
guard let url = URL(string: identifier) else {
|
||||
assertionFailure("There should be no case where identifier is not convertible to URL.")
|
||||
return false
|
||||
}
|
||||
|
||||
if isSuspended {
|
||||
delegate?.feedDownloader(self, requestCanceledForFeedURL: url, response: nil, data: nil, error: nil, reason: .suspended)
|
||||
return false
|
||||
}
|
||||
|
||||
if data.isEmpty {
|
||||
return true
|
||||
}
|
||||
|
||||
if data.isNotAFeed() {
|
||||
delegate?.feedDownloader(self, requestCanceledForFeedURL: url, response: nil, data: data, error: nil, reason: .notFeedData)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
public func downloadSession(_ downloadSession: DownloadSession, didReceiveUnexpectedResponse response: URLResponse, identifier: String) {
|
||||
|
||||
guard let url = URL(string: identifier) else {
|
||||
assertionFailure("There should be no case where identifier is not convertible to URL.")
|
||||
return
|
||||
}
|
||||
|
||||
delegate?.feedDownloader(self, requestCanceledForFeedURL: url, response: response, data: nil, error: nil, reason: .unexpectedResponse)
|
||||
}
|
||||
|
||||
public func downloadSession(_ downloadSession: DownloadSession, didReceiveNotModifiedResponse response: URLResponse, identifier: String) {
|
||||
|
||||
guard let url = URL(string: identifier) else {
|
||||
assertionFailure("There should be no case where identifier is not convertible to URL.")
|
||||
return
|
||||
}
|
||||
|
||||
delegate?.feedDownloader(self, requestCanceledForFeedURL: url, response: response, data: nil, error: nil, reason: .notModified)
|
||||
}
|
||||
|
||||
public func downloadSession(_ downloadSession: DownloadSession, didDiscardDuplicateIdentifier: String) {
|
||||
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
public func downloadSessionDidComplete(_ downloadSession: DownloadSession) {
|
||||
|
||||
delegate?.feedDownloaderSessionDidComplete(self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
|
||||
func isNotAFeed() -> Bool {
|
||||
|
||||
isImage // TODO: expand this
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import XCTest
|
||||
@testable import FeedDownloader
|
||||
|
||||
final class FeedDownloaderTests: 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
|
||||
}
|
||||
}
|
|
@ -1020,6 +1020,7 @@
|
|||
8426119D1FCB6ED40086A189 /* HTMLMetadataDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadataDownloader.swift; sourceTree = "<group>"; };
|
||||
842E45CD1ED8C308000A8B52 /* AppNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppNotifications.swift; sourceTree = "<group>"; };
|
||||
842E45DC1ED8C54B000A8B52 /* Browser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Browser.swift; sourceTree = "<group>"; };
|
||||
843429CC2BFEE3B6003D6C70 /* FeedDownloader */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = FeedDownloader; sourceTree = "<group>"; };
|
||||
843EA3EA2BFC293B003F2E97 /* Mac.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Mac.xctestplan; sourceTree = "<group>"; };
|
||||
84411E701FE5FBFA004B527F /* SmallIconProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmallIconProvider.swift; sourceTree = "<group>"; };
|
||||
8444C8F11FED81840051386C /* OPMLExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLExporter.swift; sourceTree = "<group>"; };
|
||||
|
@ -2013,6 +2014,7 @@
|
|||
84A699132BC34E8500605AB8 /* ArticleExtractor */,
|
||||
84FB9FAE2BC3494B00B7AFC3 /* FeedFinder */,
|
||||
51CD32C624D2DEF9009ABAEF /* Account */,
|
||||
843429CC2BFEE3B6003D6C70 /* FeedDownloader */,
|
||||
84A699182BC3524C00605AB8 /* LocalAccount */,
|
||||
84FB9FAD2BC344F800B7AFC3 /* Feedbin */,
|
||||
84A699192BC36EDB00605AB8 /* Feedly */,
|
||||
|
|
Loading…
Reference in New Issue