tv-anarchy/Sources/TVAnarchyCore/ContentID.swift

45 lines
2.4 KiB
Swift

import Foundation
import CryptoKit
/// A stable, library-agnostic identity for a piece of content, so the SAME episode
/// in two different users' libraries normalizes to the SAME id regardless of folder
/// name, release group, or rip. This is the keystone the mesh is built on:
///
/// - **dedup** collapse duplicate entries within and across libraries;
/// - **`peers_for(id)`** "who has this" without naming a person's files;
/// - **k-anonymity** count distinct holders of an id to decide what's safe to
/// surface (raise the threshold for adult content);
/// - **content-addressed serving** a friend requests an id, not "your files", and
/// it's served via relay so the holder stays anonymous.
///
/// Two layers: the byte-exact swarm identity is the torrent **infohash** (handled at
/// the transmission layer); this canonical id matches the *same episode across
/// different rips* (where infohashes differ) via normalized metadata.
public enum ContentID {
/// Canonical id for one episode: the work's canonical key (spacing/punctuation
/// collapsed see `ShowGrouping.canonicalKey`) + season/episode. Quality is
/// deliberately excluded so a 1080p and a 720p rip of the same episode share an
/// id (they're the same *content*); include it via `withQuality` when you need to
/// distinguish resolutions.
public static func canonical(work: String, season: Int, episode: Int) -> String {
"\(ShowGrouping.canonicalKey(work))/s\(season)e\(episode)"
}
public static func canonical(show: CachedShow, episode: CachedEpisode) -> String {
canonical(work: show.name, season: episode.season, episode: episode.episode)
}
/// Resolution-qualified variant (for the collection page's per-resolution cells).
public static func withQuality(_ canonical: String, quality: String?) -> String {
guard let q = quality, !q.isEmpty else { return canonical }
return "\(canonical)@\(q.lowercased())"
}
/// A short, stable hex digest of a canonical id compact for storage / the wire
/// / k-anonymity counters. SHA-256, first 16 hex chars (64 bits ample for a
/// personal mesh, and it leaks no title).
public static func digest(_ id: String) -> String {
let hash = SHA256.hash(data: Data(id.utf8))
return hash.map { String(format: "%02x", $0) }.joined().prefix(16).description
}
}