45 lines
2.4 KiB
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
|
|
}
|
|
}
|