tv-anarchy/Sources/TVAnarchyCore/PornCollectionService.swift
Natalie 92b38b1bae refactor(tv-anarchy): rename PlumTV→TVAnarchy and land session work
Renames Sources/PlumTV→TVAnarchy and PlumTVCore→TVAnarchyCore (the rename
the auto-commit service couldn't stage — it git-add'd the old, now-gone
paths and aborted every cycle), and commits the accumulated work:

- Library: black-built index fast path (LibraryIndex + scanFromIndex) with
  NFS-walk fallback; incremental --add on download-complete; mtime staleness
  gate; loose-file series-collapse fix; determinate scan/index progress.
- Cover art: keyless TVmaze cartoon-vs-live-action disambiguation (type/year).
- Player: sleep timer (timed + end-of-episode); visibility-gated polling.
- Home: Continue Watching cover art + live refresh; Recently Added; adult gate.
- Logs: multi-line selection + copy; truncated giant tx-list errors.
- Hover previews (opt-in) via black ffmpeg + scp.

Also gitignores foreign project trees (governor/mcp/fleet/recommender) that
sit in this directory but belong to their own repos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:04:22 -07:00

52 lines
2.3 KiB
Swift

import Foundation
/// One porn collection as reported by the script, with live fresh/total counts.
public struct PornCollection: Identifiable, Sendable, Equatable, Decodable {
public let name: String
public let desc: String
public let fresh: Int
public let total: Int
public var id: String { name }
}
/// Bridges TVAnarchy to portable-net-tv's `porn-rotation.py`, the source of truth
/// for the porn collections AND their freshness. Porn isn't in the shared
/// watchlog the script keeps its own play-log (`porn-plays.json`) so we shell
/// out rather than reimplement the filters here, which would lose the don't-replay
/// behavior that's the script's whole reason to exist. `paths(...)` marks what it
/// returns played, so freshness advances even though playback happens app-side.
public enum PornCollectionService {
private static var script: String {
ProcessInfo.processInfo.environment["PORN_ROTATION_SCRIPT"]
?? FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Code/@applications/portable-net-tv/tools/porn-rotation.py").path
}
private static func shq(_ s: String) -> String {
"'" + s.replacingOccurrences(of: "'", with: "'\\''") + "'"
}
private static func py(_ args: String) -> ProcessResult {
ProcessRunner.runShell("python3 \(shq(script)) \(args)", timeout: 40)
}
/// The defined collections with fresh/total counts. Empty when the script or
/// the media mount is unavailable the UI just shows nothing then.
public static func list() async -> [PornCollection] {
await Task.detached(priority: .utility) {
let r = py("collections --json")
guard r.ok, let d = r.stdout.data(using: .utf8),
let cols = try? JSONDecoder().decode([PornCollection].self, from: d) else { return [] }
return cols
}.value
}
/// Fresh, shuffled file paths for a collection (plum-side), marking them played.
public static func paths(collection: String, count: Int) async -> [String] {
await Task.detached(priority: .utility) {
let r = py("paths -c \(shq(collection)) --count \(count) --mark")
guard r.ok else { return [] }
return r.stdout.split(separator: "\n").map(String.init).filter { !$0.isEmpty }
}.value
}
}