mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 01:39:46 +00:00
Yattee v2 rewrite
This commit is contained in:
254
Yattee/Models/Instance.swift
Normal file
254
Yattee/Models/Instance.swift
Normal file
@@ -0,0 +1,254 @@
|
||||
//
|
||||
// Instance.swift
|
||||
// Yattee
|
||||
//
|
||||
// Represents a backend instance (Invidious, Piped, PeerTube, or Yattee Server).
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// The type of backend instance.
|
||||
enum InstanceType: String, Codable, CaseIterable, Sendable {
|
||||
case invidious
|
||||
case piped
|
||||
case peertube
|
||||
case yatteeServer
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .invidious: return String(localized: "instances.type.invidious")
|
||||
case .piped: return String(localized: "instances.type.piped")
|
||||
case .peertube: return String(localized: "instances.type.peertube")
|
||||
case .yatteeServer: return String(localized: "instances.type.yatteeServer")
|
||||
}
|
||||
}
|
||||
|
||||
var systemImage: String {
|
||||
"globe"
|
||||
}
|
||||
|
||||
var contentSource: ContentSource {
|
||||
switch self {
|
||||
case .invidious, .piped, .yatteeServer:
|
||||
return .global(provider: ContentSource.youtubeProvider)
|
||||
case .peertube:
|
||||
// For PeerTube, this should be called with the specific instance URL
|
||||
fatalError("Use contentSource(for:) for PeerTube instances")
|
||||
}
|
||||
}
|
||||
|
||||
func contentSource(for url: URL) -> ContentSource {
|
||||
switch self {
|
||||
case .invidious, .piped, .yatteeServer:
|
||||
return .global(provider: ContentSource.youtubeProvider)
|
||||
case .peertube:
|
||||
return .federated(provider: ContentSource.peertubeProvider, instance: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a backend instance configuration.
|
||||
struct Instance: Identifiable, Codable, Hashable, Sendable {
|
||||
/// Unique identifier for this instance configuration.
|
||||
let id: UUID
|
||||
|
||||
/// The type of this instance.
|
||||
let type: InstanceType
|
||||
|
||||
/// The base URL of the instance.
|
||||
let url: URL
|
||||
|
||||
/// Optional user-defined name for this instance.
|
||||
var name: String?
|
||||
|
||||
/// Whether this instance is currently enabled.
|
||||
var isEnabled: Bool
|
||||
|
||||
/// The date this instance was added.
|
||||
let dateAdded: Date
|
||||
|
||||
/// Optional API key if required by the instance.
|
||||
var apiKey: String?
|
||||
|
||||
/// Whether to allow invalid/self-signed SSL certificates.
|
||||
var allowInvalidCertificates: Bool
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
type: InstanceType,
|
||||
url: URL,
|
||||
name: String? = nil,
|
||||
isEnabled: Bool = true,
|
||||
dateAdded: Date = Date(),
|
||||
apiKey: String? = nil,
|
||||
allowInvalidCertificates: Bool = false
|
||||
) {
|
||||
self.id = id
|
||||
self.type = type
|
||||
self.url = url
|
||||
self.name = name
|
||||
self.isEnabled = isEnabled
|
||||
self.dateAdded = dateAdded
|
||||
self.apiKey = apiKey
|
||||
self.allowInvalidCertificates = allowInvalidCertificates
|
||||
}
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
var displayName: String {
|
||||
name ?? url.host ?? url.absoluteString
|
||||
}
|
||||
|
||||
var contentSource: ContentSource {
|
||||
type.contentSource(for: url)
|
||||
}
|
||||
|
||||
/// Whether this instance provides YouTube content.
|
||||
var isYouTubeInstance: Bool {
|
||||
type == .invidious || type == .piped || type == .yatteeServer
|
||||
}
|
||||
|
||||
/// Whether this instance is a PeerTube instance.
|
||||
var isPeerTubeInstance: Bool {
|
||||
type == .peertube
|
||||
}
|
||||
|
||||
/// Whether this instance is a Yattee Server instance.
|
||||
var isYatteeServerInstance: Bool {
|
||||
type == .yatteeServer
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Instance Capabilities
|
||||
|
||||
extension Instance {
|
||||
/// Whether this instance supports advanced search filters (sort, date, duration, features).
|
||||
var supportsSearchFilters: Bool {
|
||||
type == .invidious || type == .yatteeServer
|
||||
}
|
||||
|
||||
/// Whether this instance supports user authentication/login.
|
||||
var supportsAuthentication: Bool {
|
||||
type == .invidious || type == .piped
|
||||
}
|
||||
|
||||
/// Whether this instance supports subscription feed.
|
||||
var supportsFeed: Bool {
|
||||
type == .invidious || type == .piped
|
||||
}
|
||||
|
||||
/// Whether this instance supports search suggestions/autocomplete.
|
||||
var supportsSuggestions: Bool {
|
||||
type == .invidious || type == .piped || type == .yatteeServer
|
||||
}
|
||||
|
||||
/// Whether this instance supports the popular videos endpoint.
|
||||
var supportsPopular: Bool {
|
||||
type == .invidious || type == .yatteeServer
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Instance Validation
|
||||
|
||||
extension Instance {
|
||||
/// Validates the instance URL format.
|
||||
static func validateURL(_ urlString: String) -> URL? {
|
||||
guard var components = URLComponents(string: urlString) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Default to HTTPS if no scheme provided, but preserve explicit HTTP
|
||||
// (needed for local/private network servers like yt-dlp server)
|
||||
if components.scheme == nil {
|
||||
components.scheme = "https"
|
||||
}
|
||||
|
||||
// Remove trailing slash from path
|
||||
if components.path.hasSuffix("/") {
|
||||
components.path = String(components.path.dropLast())
|
||||
}
|
||||
|
||||
return components.url
|
||||
}
|
||||
|
||||
/// Checks if a string is an IPv4 or IPv6 address.
|
||||
static func isIPAddress(_ string: String) -> Bool {
|
||||
// IPv4: four groups of 1-3 digits separated by dots
|
||||
let ipv4Pattern = "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$"
|
||||
if string.range(of: ipv4Pattern, options: .regularExpression) != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// IPv6: contains colons (simplified check)
|
||||
if string.contains(":") && !string.contains("://") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/// Infers the appropriate scheme for a URL string based on the host.
|
||||
/// - IP addresses default to http:// (common for local servers)
|
||||
/// - Domain names default to https://
|
||||
/// - Explicit schemes are preserved
|
||||
static func inferScheme(for urlString: String) -> String {
|
||||
// Already has a scheme
|
||||
if urlString.hasPrefix("http://") || urlString.hasPrefix("https://") || urlString.hasPrefix("smb://") {
|
||||
if urlString.hasPrefix("http://") { return "http" }
|
||||
if urlString.hasPrefix("https://") { return "https" }
|
||||
if urlString.hasPrefix("smb://") { return "smb" }
|
||||
}
|
||||
|
||||
// Extract host part (before any path or port)
|
||||
let hostPart = urlString
|
||||
.replacingOccurrences(of: "http://", with: "")
|
||||
.replacingOccurrences(of: "https://", with: "")
|
||||
.components(separatedBy: "/").first ?? urlString
|
||||
|
||||
// Remove port if present
|
||||
let hostWithoutPort = hostPart.components(separatedBy: ":").first ?? hostPart
|
||||
|
||||
// IP addresses use http (common for local servers)
|
||||
if isIPAddress(hostWithoutPort) {
|
||||
return "http"
|
||||
}
|
||||
|
||||
// Domain names use https
|
||||
return "https"
|
||||
}
|
||||
|
||||
/// Normalizes a source URL string, applying appropriate scheme and cleaning up the URL.
|
||||
/// - Parameter urlString: The raw URL input from user
|
||||
/// - Returns: A normalized URL or nil if invalid
|
||||
static func normalizeSourceURL(_ urlString: String) -> URL? {
|
||||
var input = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// Handle SMB URLs specially
|
||||
if input.lowercased().hasPrefix("smb://") {
|
||||
return URL(string: input)
|
||||
}
|
||||
|
||||
// Add scheme if missing
|
||||
if !input.lowercased().hasPrefix("http://") && !input.lowercased().hasPrefix("https://") {
|
||||
let scheme = inferScheme(for: input)
|
||||
input = "\(scheme)://\(input)"
|
||||
}
|
||||
|
||||
guard var components = URLComponents(string: input) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove trailing slash from path
|
||||
if components.path.hasSuffix("/") {
|
||||
components.path = String(components.path.dropLast())
|
||||
}
|
||||
|
||||
// Strip embedded credentials (security best practice)
|
||||
components.user = nil
|
||||
components.password = nil
|
||||
|
||||
return components.url
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user