Add Clip struct to Title — expose clip references for playlist analysis

Title.clips[] contains clip_id, in/out times, duration, source_packets.
Apps can detect fake/scrambled playlists by checking unique clip count
vs total (253 clips referencing 2 unique = fake). Removed clip_count
field (use clips.len()).
This commit is contained in:
MattJackson
2026-04-07 16:03:56 -07:00
parent b01010f2d1
commit 6f0eea48fa
2 changed files with 34 additions and 9 deletions
+33 -8
View File
@@ -66,14 +66,29 @@ pub struct Title {
pub duration_secs: f64, pub duration_secs: f64,
/// Total size in bytes /// Total size in bytes
pub size_bytes: u64, pub size_bytes: u64,
/// Number of clips /// Clip references in playback order
pub clip_count: usize, pub clips: Vec<Clip>,
/// All streams (video, audio, subtitle, etc.) /// All streams (video, audio, subtitle, etc.)
pub streams: Vec<Stream>, pub streams: Vec<Stream>,
/// Sector extents for ripping (clip LBA ranges) /// Sector extents for ripping (clip LBA ranges)
pub extents: Vec<Extent>, pub extents: Vec<Extent>,
} }
/// A clip reference within a title.
#[derive(Debug, Clone)]
pub struct Clip {
/// Clip filename without extension (e.g. "00001")
pub clip_id: String,
/// In-time in 45kHz ticks
pub in_time: u32,
/// Out-time in 45kHz ticks
pub out_time: u32,
/// Duration in seconds
pub duration_secs: f64,
/// Source packet count (from CLPI, 0 if unavailable)
pub source_packets: u32,
}
/// A stream within a title. /// A stream within a title.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Stream { pub struct Stream {
@@ -660,23 +675,33 @@ impl Disc {
return None; return None;
} }
// Parse each clip for size and sector extents // Parse each clip for size, duration, and sector extents
let mut extents = Vec::new(); let mut extents = Vec::new();
let mut total_size: u64 = 0; let mut total_size: u64 = 0;
let clip_count = parsed.play_items.len(); let mut clips = Vec::with_capacity(parsed.play_items.len());
for play_item in &parsed.play_items { for play_item in &parsed.play_items {
let clip_dur = play_item.out_time.saturating_sub(play_item.in_time) as f64 / 45000.0;
let mut pkt_count: u32 = 0;
let clpi_path = format!("/BDMV/CLIPINF/{}.clpi", play_item.clip_id); let clpi_path = format!("/BDMV/CLIPINF/{}.clpi", play_item.clip_id);
if let Ok(clpi_data) = udf_fs.read_file(session, &clpi_path) { if let Ok(clpi_data) = udf_fs.read_file(session, &clpi_path) {
if let Ok(clip_info) = clpi::parse(&clpi_data) { if let Ok(clip_info) = clpi::parse(&clpi_data) {
// Size from source packet count (192 bytes per packet) pkt_count = clip_info.source_packet_count;
total_size += clip_info.source_packet_count as u64 * 192; total_size += pkt_count as u64 * 192;
// EP map extents for ripping (sector ranges on disc)
let clip_extents = clip_info.get_extents(play_item.in_time, play_item.out_time); let clip_extents = clip_info.get_extents(play_item.in_time, play_item.out_time);
extents.extend(clip_extents); extents.extend(clip_extents);
} }
} }
clips.push(Clip {
clip_id: play_item.clip_id.clone(),
in_time: play_item.in_time,
out_time: play_item.out_time,
duration_secs: clip_dur,
source_packets: pkt_count,
});
} }
// Build streams from STN table // Build streams from STN table
@@ -727,7 +752,7 @@ impl Disc {
playlist_id, playlist_id,
duration_secs, duration_secs,
size_bytes: total_size, size_bytes: total_size,
clip_count, clips,
streams, streams,
extents, extents,
}) })
+1 -1
View File
@@ -87,5 +87,5 @@ pub use profile::{DriveProfile, Chipset};
pub use platform::{Platform, DriveStatus}; pub use platform::{Platform, DriveStatus};
pub use scsi::ScsiTransport; pub use scsi::ScsiTransport;
pub use speed::DriveSpeed; pub use speed::DriveSpeed;
pub use disc::{Disc, DiscFormat, Title, Stream, StreamKind, Codec, HdrFormat, ColorSpace, pub use disc::{Disc, DiscFormat, Title, Clip, Stream, StreamKind, Codec, HdrFormat, ColorSpace,
Extent, ContentReader, AacsState, KeySource, ScanOptions}; Extent, ContentReader, AacsState, KeySource, ScanOptions};