2026-06-09 22:22:56 -07:00
|
|
|
// Wire models for the tv-anarchy-bridge HTTP API. These mirror the JSON the
|
|
|
|
|
// bridge emits (src/http.ts in tv-anarchy-mcp) one-for-one. The iOS app is a
|
2026-06-09 05:34:39 -07:00
|
|
|
// pure bridge client — it never touches the filesystem or SSH — so these are the
|
|
|
|
|
// only "library" types it knows.
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
|
|
|
|
|
struct BridgeShow: Codable, Identifiable, Hashable {
|
|
|
|
|
let id: String
|
|
|
|
|
let name: String
|
|
|
|
|
let episodeCount: Int
|
|
|
|
|
let seasons: [Int]
|
|
|
|
|
let episodes: [BridgeEpisode]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct BridgeEpisode: Codable, Identifiable, Hashable {
|
|
|
|
|
/// Opaque, server-issued stream id (base64url of the file path on black/plum).
|
|
|
|
|
let id: String
|
|
|
|
|
let season: Int
|
|
|
|
|
let episode: Int
|
|
|
|
|
let label: String
|
|
|
|
|
let ext: String
|
2026-06-30 00:12:41 -04:00
|
|
|
/// On-disk size; optional so the app tolerates an older bridge.
|
|
|
|
|
let bytes: Int64?
|
2026-06-09 05:34:39 -07:00
|
|
|
|
2026-06-30 00:12:41 -04:00
|
|
|
/// e.g. "S01E02" — compact badge for the list row. Movies travel as S00E00
|
|
|
|
|
/// and show no badge.
|
2026-06-09 05:34:39 -07:00
|
|
|
var code: String {
|
2026-06-30 00:12:41 -04:00
|
|
|
season == 0 && episode == 0 ? "" : String(format: "S%02dE%02d", season, episode)
|
2026-06-09 05:34:39 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct ShowsResponse: Codable {
|
|
|
|
|
let shows: [BridgeShow]
|
|
|
|
|
}
|
2026-06-09 06:38:45 -07:00
|
|
|
|
2026-06-30 00:12:41 -04:00
|
|
|
/// A film (possibly one of a boxed collection). Same id space as episodes —
|
|
|
|
|
/// streaming, artwork, downloads and progress all reuse the episode plumbing.
|
|
|
|
|
struct BridgeMovie: Codable, Identifiable, Hashable {
|
|
|
|
|
let id: String
|
|
|
|
|
let title: String
|
|
|
|
|
let collection: String?
|
|
|
|
|
let year: Int?
|
|
|
|
|
let ext: String
|
|
|
|
|
let bytes: Int64
|
|
|
|
|
|
|
|
|
|
/// Adapter into the (show:nil, episode:) playback/download path.
|
|
|
|
|
var asEpisode: BridgeEpisode {
|
|
|
|
|
BridgeEpisode(id: id, season: 0, episode: 0, label: title, ext: ext, bytes: bytes)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct MoviesResponse: Codable { let movies: [BridgeMovie] }
|
|
|
|
|
|
2026-06-09 06:38:45 -07:00
|
|
|
// MARK: - Continue watching / prefetch
|
|
|
|
|
|
|
|
|
|
struct ResumePoint: Codable, Hashable {
|
|
|
|
|
let episodeId: String
|
|
|
|
|
let season: Int
|
|
|
|
|
let episode: Int
|
|
|
|
|
let label: String
|
|
|
|
|
let positionSeconds: Double
|
|
|
|
|
let durationSeconds: Double?
|
|
|
|
|
|
|
|
|
|
var code: String { String(format: "S%02dE%02d", season, episode) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct NextEpisode: Codable, Hashable {
|
|
|
|
|
let episodeId: String
|
|
|
|
|
let season: Int
|
|
|
|
|
let episode: Int
|
|
|
|
|
let label: String
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct ContinueItem: Codable, Identifiable, Hashable {
|
|
|
|
|
let show: String
|
|
|
|
|
let showId: String
|
|
|
|
|
let resume: ResumePoint?
|
|
|
|
|
let next: NextEpisode?
|
|
|
|
|
let lastWatched: String
|
|
|
|
|
|
|
|
|
|
var id: String { showId }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct ContinueResponse: Codable { let items: [ContinueItem] }
|
|
|
|
|
|
|
|
|
|
struct ResumeResponse: Codable { let positionSeconds: Double }
|
|
|
|
|
|
2026-06-30 00:12:41 -04:00
|
|
|
// MARK: - Remote transport
|
|
|
|
|
|
|
|
|
|
/// A controllable device from the bridge's registry (`/remote/targets`).
|
|
|
|
|
struct RemoteTarget: Codable, Identifiable, Hashable {
|
|
|
|
|
let id: String
|
|
|
|
|
let name: String
|
|
|
|
|
let kind: String
|
|
|
|
|
let capabilities: [String]
|
|
|
|
|
let reachable: Bool
|
|
|
|
|
|
|
|
|
|
func can(_ capability: String) -> Bool { capabilities.contains(capability) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct TargetsResponse: Codable { let targets: [RemoteTarget] }
|
2026-06-09 06:38:45 -07:00
|
|
|
|
|
|
|
|
struct RemoteStatus: Codable, Hashable {
|
|
|
|
|
let playing: Bool
|
|
|
|
|
let paused: Bool?
|
|
|
|
|
let title: String?
|
|
|
|
|
let volume: Double?
|
|
|
|
|
let position: Double?
|
|
|
|
|
let duration: Double?
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Downloads
|
|
|
|
|
|
|
|
|
|
struct Torrent: Codable, Identifiable, Hashable {
|
|
|
|
|
let id: Int
|
|
|
|
|
let name: String
|
|
|
|
|
let percentDone: Double
|
|
|
|
|
let status: Int
|
|
|
|
|
let rateDownload: Double
|
|
|
|
|
let rateUpload: Double
|
|
|
|
|
let eta: Double
|
|
|
|
|
let sizeWhenDone: Double
|
|
|
|
|
let haveValid: Double
|
|
|
|
|
|
|
|
|
|
var statusLabel: String {
|
|
|
|
|
switch status {
|
|
|
|
|
case 0: return "Stopped"
|
|
|
|
|
case 1, 2: return "Checking"
|
|
|
|
|
case 3, 5: return "Queued"
|
|
|
|
|
case 4: return "Downloading"
|
|
|
|
|
case 6: return "Seeding"
|
|
|
|
|
default: return "—"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct TorrentsResponse: Codable { let torrents: [Torrent] }
|
|
|
|
|
|
|
|
|
|
struct SearchResult: Codable, Identifiable, Hashable {
|
|
|
|
|
let filename: String
|
|
|
|
|
let source: String
|
|
|
|
|
let size: String
|
|
|
|
|
let seeders: Int
|
|
|
|
|
let leechers: Int
|
|
|
|
|
let magnet: String?
|
|
|
|
|
|
|
|
|
|
var id: String { (magnet ?? filename) + source }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct SearchResponse: Codable { let results: [SearchResult] }
|