mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-08-09 00:17:35 +00:00
303 lines
10 KiB
Swift
303 lines
10 KiB
Swift
import Foundation
|
|
|
|
struct FluxaAppleAddonCatalogResolver {
|
|
private let session: URLSession
|
|
|
|
init(session: URLSession = .shared) {
|
|
self.session = session
|
|
}
|
|
|
|
func resolveRequests(localAddonUrls: [String]) async throws -> [FluxaAppleCatalogRequest] {
|
|
var requests = [FluxaAppleCatalogRequest]()
|
|
var lastError: Error?
|
|
for rawUrl in localAddonUrls {
|
|
do {
|
|
let transportUrl = normalizeManifestUrl(rawUrl)
|
|
guard let manifestUrl = URL(string: transportUrl) else {
|
|
continue
|
|
}
|
|
let (data, response) = try await session.data(from: manifestUrl)
|
|
guard let httpResponse = response as? HTTPURLResponse,
|
|
(200..<300).contains(httpResponse.statusCode) else {
|
|
throw URLError(.badServerResponse)
|
|
}
|
|
guard let manifest = FluxaCoreStremio.parseManifest(
|
|
body: String(decoding: data, as: UTF8.self),
|
|
transportUrl: transportUrl
|
|
) else {
|
|
throw URLError(.cannotParseResponse)
|
|
}
|
|
guard manifest.supportsCatalog else {
|
|
continue
|
|
}
|
|
requests.append(contentsOf: manifest.catalogs.compactMap {
|
|
makeRequest(catalog: $0, manifest: manifest, transportUrl: transportUrl)
|
|
})
|
|
} catch {
|
|
lastError = error
|
|
}
|
|
}
|
|
if requests.isEmpty, let lastError {
|
|
throw lastError
|
|
}
|
|
return requests
|
|
}
|
|
|
|
func resolveSearchRequests(
|
|
localAddonUrls: [String],
|
|
query: String
|
|
) async throws -> [FluxaAppleSearchRequest] {
|
|
let normalizedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !normalizedQuery.isEmpty else {
|
|
return []
|
|
}
|
|
var requests = [FluxaAppleSearchRequest]()
|
|
var lastError: Error?
|
|
for rawUrl in localAddonUrls {
|
|
do {
|
|
let transportUrl = normalizeManifestUrl(rawUrl)
|
|
guard let manifestUrl = URL(string: transportUrl) else {
|
|
continue
|
|
}
|
|
let (data, response) = try await session.data(from: manifestUrl)
|
|
guard let httpResponse = response as? HTTPURLResponse,
|
|
(200..<300).contains(httpResponse.statusCode) else {
|
|
throw URLError(.badServerResponse)
|
|
}
|
|
guard let manifest = FluxaCoreStremio.parseManifest(
|
|
body: String(decoding: data, as: UTF8.self),
|
|
transportUrl: transportUrl
|
|
) else {
|
|
throw URLError(.cannotParseResponse)
|
|
}
|
|
guard manifest.supportsCatalog else {
|
|
continue
|
|
}
|
|
requests.append(contentsOf: manifest.catalogs.compactMap {
|
|
makeSearchRequest(catalog: $0, transportUrl: transportUrl, query: normalizedQuery)
|
|
})
|
|
} catch {
|
|
lastError = error
|
|
}
|
|
}
|
|
if requests.isEmpty, let lastError {
|
|
throw lastError
|
|
}
|
|
return requests
|
|
}
|
|
|
|
func resolveDiscoverCatalogs(
|
|
localAddonUrls: [String],
|
|
contentType: String
|
|
) async throws -> [FluxaAppleDiscoverCatalog] {
|
|
let normalizedType = contentType.lowercased()
|
|
var catalogs = [FluxaAppleDiscoverCatalog]()
|
|
var lastError: Error?
|
|
for rawUrl in localAddonUrls {
|
|
do {
|
|
let transportUrl = normalizeManifestUrl(rawUrl)
|
|
guard let manifestUrl = URL(string: transportUrl) else {
|
|
continue
|
|
}
|
|
let (data, response) = try await session.data(from: manifestUrl)
|
|
guard let httpResponse = response as? HTTPURLResponse,
|
|
(200..<300).contains(httpResponse.statusCode) else {
|
|
throw URLError(.badServerResponse)
|
|
}
|
|
guard let manifest = FluxaCoreStremio.parseManifest(
|
|
body: String(decoding: data, as: UTF8.self),
|
|
transportUrl: transportUrl
|
|
) else {
|
|
throw URLError(.cannotParseResponse)
|
|
}
|
|
guard manifest.supportsCatalog else {
|
|
continue
|
|
}
|
|
catalogs.append(contentsOf: manifest.catalogs.compactMap { catalog in
|
|
makeDiscoverCatalog(
|
|
catalog: catalog,
|
|
manifest: manifest,
|
|
transportUrl: transportUrl,
|
|
contentType: normalizedType
|
|
)
|
|
})
|
|
} catch {
|
|
lastError = error
|
|
}
|
|
}
|
|
if catalogs.isEmpty, let lastError {
|
|
throw lastError
|
|
}
|
|
return catalogs
|
|
}
|
|
|
|
func discoverUrl(
|
|
transportUrl: String,
|
|
contentType: String,
|
|
catalogId: String,
|
|
genre: String?
|
|
) -> URL? {
|
|
URL(string: FluxaCoreStremio.resourceUrl(
|
|
transportUrl: transportUrl,
|
|
resource: "catalog",
|
|
contentType: contentType,
|
|
id: catalogId,
|
|
extra: genre.map { ["genre": $0] } ?? [:]
|
|
) ?? "")
|
|
}
|
|
|
|
func resourceUrl(
|
|
transportUrl: String,
|
|
resource: String,
|
|
contentType: String,
|
|
id: String
|
|
) -> URL? {
|
|
URL(string: resourceUrlString(
|
|
transportUrl: transportUrl,
|
|
resource: resource,
|
|
contentType: contentType,
|
|
id: id
|
|
))
|
|
}
|
|
|
|
private func makeRequest(
|
|
catalog: FluxaCoreAddonCatalog,
|
|
manifest: FluxaCoreAddonManifest,
|
|
transportUrl: String
|
|
) -> FluxaAppleCatalogRequest? {
|
|
guard let catalogId = catalog.id?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
!catalogId.isEmpty,
|
|
let contentType = catalog.type?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
!contentType.isEmpty,
|
|
catalog.supportsInitialLoad,
|
|
let url = resourceUrl(
|
|
transportUrl: transportUrl,
|
|
resource: "catalog",
|
|
contentType: contentType,
|
|
id: catalogId
|
|
) else {
|
|
return nil
|
|
}
|
|
let title = catalog.name?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let resolvedTitle = title.flatMap { $0.isEmpty ? nil : $0 } ?? catalogId
|
|
return FluxaAppleCatalogRequest(
|
|
id: "\(manifest.id):\(contentType):\(catalogId)",
|
|
title: resolvedTitle,
|
|
url: url,
|
|
contentType: contentType,
|
|
addonTransportUrl: transportUrl,
|
|
catalogType: contentType
|
|
)
|
|
}
|
|
|
|
private func makeSearchRequest(
|
|
catalog: FluxaCoreAddonCatalog,
|
|
transportUrl: String,
|
|
query: String
|
|
) -> FluxaAppleSearchRequest? {
|
|
guard let catalogId = catalog.id?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
!catalogId.isEmpty,
|
|
let contentType = catalog.type?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
!contentType.isEmpty,
|
|
catalog.supportsSearch,
|
|
let url = searchUrl(
|
|
transportUrl: transportUrl,
|
|
contentType: contentType,
|
|
catalogId: catalogId,
|
|
query: query
|
|
) else {
|
|
return nil
|
|
}
|
|
return FluxaAppleSearchRequest(
|
|
url: url,
|
|
contentType: contentType,
|
|
addonTransportUrl: transportUrl,
|
|
catalogType: contentType
|
|
)
|
|
}
|
|
|
|
private func makeDiscoverCatalog(
|
|
catalog: FluxaCoreAddonCatalog,
|
|
manifest: FluxaCoreAddonManifest,
|
|
transportUrl: String,
|
|
contentType: String
|
|
) -> FluxaAppleDiscoverCatalog? {
|
|
guard let catalogId = catalog.id?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
!catalogId.isEmpty,
|
|
let catalogType = catalog.type?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(),
|
|
catalogType == contentType,
|
|
!catalog.hasRequiredExtraExceptGenre else {
|
|
return nil
|
|
}
|
|
let trimmedName = catalog.name?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let label: String
|
|
if let trimmedName, !trimmedName.isEmpty {
|
|
label = trimmedName
|
|
} else {
|
|
label = catalogId
|
|
}
|
|
let genres = (catalog.genres ?? []) + (catalog.extra ?? [])
|
|
.filter { $0.name?.lowercased() == "genre" }
|
|
.flatMap { $0.options ?? [] }
|
|
let uniqueGenres = Array(Set(genres.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty })).sorted()
|
|
return FluxaAppleDiscoverCatalog(
|
|
key: "discover:\(manifest.id):\(catalogType):\(catalogId)",
|
|
label: label,
|
|
transportUrl: transportUrl,
|
|
contentType: catalogType,
|
|
catalogId: catalogId,
|
|
genres: uniqueGenres,
|
|
requiresGenre: catalog.extra?.contains { $0.name?.lowercased() == "genre" && $0.isRequired == true } == true
|
|
)
|
|
}
|
|
|
|
private func searchUrl(
|
|
transportUrl: String,
|
|
contentType: String,
|
|
catalogId: String,
|
|
query: String
|
|
) -> URL? {
|
|
URL(string: FluxaCoreStremio.resourceUrl(
|
|
transportUrl: transportUrl,
|
|
resource: "catalog",
|
|
contentType: contentType,
|
|
id: catalogId,
|
|
extra: ["search": query]
|
|
) ?? "")
|
|
}
|
|
|
|
private func normalizeManifestUrl(_ rawUrl: String) -> String {
|
|
FluxaCoreStremio.normalizeManifestUrl(rawUrl)
|
|
}
|
|
|
|
private func resourceUrlString(
|
|
transportUrl: String,
|
|
resource: String,
|
|
contentType: String,
|
|
id: String
|
|
) -> String {
|
|
FluxaCoreStremio.resourceUrl(
|
|
transportUrl: transportUrl,
|
|
resource: resource,
|
|
contentType: contentType,
|
|
id: id
|
|
) ?? ""
|
|
}
|
|
}
|
|
|
|
struct FluxaAppleDiscoverCatalog: Sendable {
|
|
let key: String
|
|
let label: String
|
|
let transportUrl: String
|
|
let contentType: String
|
|
let catalogId: String
|
|
let genres: [String]
|
|
let requiresGenre: Bool
|
|
}
|
|
|
|
struct FluxaAppleSearchRequest: Sendable {
|
|
let url: URL
|
|
let contentType: String
|
|
let addonTransportUrl: String
|
|
let catalogType: String
|
|
}
|