mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 21:22:53 +00:00
Reading a server-side playlist was all the app could do: the write half of both APIs was missing, and the only thing that knew which credential to use was the subscription layer. So playlists arrive here as an account concern — hand AccountService an account and it finds the instance, reads the secret, refuses what the backend cannot do, and records what the answer implies about the login. Capabilities decide, never the instance type: a privacy setting sent to a backend that has none is dropped rather than refused, because the playlist is still perfectly creatable there. Every call captures the credential it used and re-reads it before recording an outcome, so a late 401 cannot reject the login the user just fixed, nor a late success clear a rejection it never earned. Removal is where the backends stop being alike. Invidious names the row itself with an indexId that survives refetches, so it removes directly. Piped removes by position, and a position is only true of the fetch that minted it — so the playlist is refetched and the position checked to still hold the same video before anything is deleted. That narrows the window; it cannot close it, and only the refetch afterwards shows what really happened. The cache exists so that refetch cannot be forgotten. Every mutation goes through it and republishes what it touched — on failure as much as on success, because a write that reported failure may still have landed — and nothing is ever edited optimistically. It is in memory alone: the data belongs to a login rather than to this device, and a stored Piped entry id would be aimed at whatever occupies that slot next launch. A per-account generation fences results that come back after the account was cleared, since clearing has never cancelled the request that was filling it. Along the way, clients minted per instance sent no User-Agent at all — instances that whitelist the Yattee prefix answered them with a 401 the shared client never earned. The factory now bakes the app's User-Agent in at construction, where no request can outrun it.
89 lines
3.5 KiB
Swift
89 lines
3.5 KiB
Swift
//
|
|
// HTTPClientFactory.swift
|
|
// Yattee
|
|
//
|
|
// Factory for creating HTTPClient instances with appropriate SSL settings.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// The User-Agent every client a factory mints should identify itself with.
|
|
///
|
|
/// Held apart from the factories so there is one answer for the whole process.
|
|
/// A per-instance client is built deep inside `ContentService`, from a factory
|
|
/// nobody outside it can reach, and the app's User-Agent has to reach it too:
|
|
/// some instances only answer requests whose User-Agent starts with
|
|
/// `Yattee/<version>`, and a client that sends none gets a 401 the shared
|
|
/// client never sees.
|
|
final class UserAgentConfiguration: @unchecked Sendable {
|
|
static let shared = UserAgentConfiguration()
|
|
|
|
private let lock = NSLock()
|
|
private var userAgent: String?
|
|
private var randomizePerRequest = false
|
|
|
|
/// Records the User-Agent settings to apply from now on. Clients already
|
|
/// built keep what they were given — they have their own setters.
|
|
func update(userAgent: String?, randomizePerRequest: Bool) {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
self.userAgent = userAgent
|
|
self.randomizePerRequest = randomizePerRequest
|
|
}
|
|
|
|
/// The settings as they stand.
|
|
func current() -> (userAgent: String?, randomizePerRequest: Bool) {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
return (userAgent, randomizePerRequest)
|
|
}
|
|
}
|
|
|
|
/// Factory for creating HTTPClient instances based on instance SSL settings.
|
|
final class HTTPClientFactory: Sendable {
|
|
private let sessionFactory: URLSessionFactory
|
|
private let userAgentConfiguration: UserAgentConfiguration
|
|
|
|
init(
|
|
sessionFactory: URLSessionFactory = .shared,
|
|
userAgentConfiguration: UserAgentConfiguration = .shared
|
|
) {
|
|
self.sessionFactory = sessionFactory
|
|
self.userAgentConfiguration = userAgentConfiguration
|
|
}
|
|
|
|
/// Creates an HTTPClient configured for the given instance's SSL requirements.
|
|
/// - Parameter instance: The instance to create a client for.
|
|
/// - Returns: An HTTPClient with appropriate SSL settings.
|
|
func createClient(for instance: Instance) -> HTTPClient {
|
|
makeClient(session: sessionFactory.session(allowInvalidCertificates: instance.allowInvalidCertificates))
|
|
}
|
|
|
|
/// Creates an HTTPClient with explicit SSL settings.
|
|
/// - Parameter allowInvalidCertificates: Whether to bypass SSL certificate validation.
|
|
/// - Returns: An HTTPClient with the specified SSL settings.
|
|
func createClient(allowInvalidCertificates: Bool) -> HTTPClient {
|
|
makeClient(session: sessionFactory.session(allowInvalidCertificates: allowInvalidCertificates))
|
|
}
|
|
|
|
/// Creates an HTTPClient with low network priority for background/prefetch work.
|
|
/// - Returns: An HTTPClient configured with `.background` network service type.
|
|
func createLowPriorityClient() -> HTTPClient {
|
|
makeClient(session: sessionFactory.lowPrioritySession())
|
|
}
|
|
|
|
/// Builds the client with the current User-Agent baked in.
|
|
///
|
|
/// Passed through the initializer rather than set afterwards: these clients
|
|
/// are handed straight to an API that sends its first request immediately,
|
|
/// and a `Task` to configure them would sometimes lose that race.
|
|
private func makeClient(session: URLSession) -> HTTPClient {
|
|
let configuration = userAgentConfiguration.current()
|
|
return HTTPClient(
|
|
session: session,
|
|
userAgent: configuration.userAgent,
|
|
randomizeUserAgentPerRequest: configuration.randomizePerRequest
|
|
)
|
|
}
|
|
}
|