use crate::error::{Error, Result};
use crate::tag::{Tag, TagGroup, TagId};
use crate::value::Value;
const WTV_MAGIC: [u8; 16] = [
0xb7, 0xd8, 0x00, 0x20, 0x37, 0x49, 0xda, 0x11, 0xa6, 0x4e, 0x00, 0x07, 0xe9, 0x5e, 0xad, 0x8d,
];
const DIR_ENTRY_GUID: [u8; 16] = [
0x92, 0xb7, 0x74, 0x91, 0x59, 0x70, 0x70, 0x44, 0x88, 0xdf, 0x06, 0x3b, 0x82, 0xcc, 0x21, 0x3d,
];
const METADATA_ENTRY_GUID: [u8; 16] = [
0x5a, 0xfe, 0xd7, 0x6d, 0xc8, 0x1d, 0x8f, 0x4a, 0x99, 0x22, 0xfa, 0xb1, 0x1c, 0x38, 0x14, 0x53,
];
fn mk_wtv(name: &str, value: Value, print: String) -> Tag {
Tag {
id: TagId::Text(name.to_string()),
name: name.to_string(),
description: name.to_string(),
group: TagGroup {
family0: "WTV".into(),
family1: "WTV".into(),
family2: "Video".into(),
},
raw_value: value,
print_value: print,
priority: 0,
}
}
fn read_u32_le(data: &[u8], off: usize) -> Option<u32> {
if off + 4 > data.len() {
return None;
}
Some(u32::from_le_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
]))
}
fn read_i32_le(data: &[u8], off: usize) -> Option<i32> {
read_u32_le(data, off).map(|v| v as i32)
}
fn read_u64_le(data: &[u8], off: usize) -> Option<u64> {
if off + 8 > data.len() {
return None;
}
Some(u64::from_le_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
data[off + 4],
data[off + 5],
data[off + 6],
data[off + 7],
]))
}
fn decode_utf16le(bytes: &[u8]) -> String {
let words: Vec<u16> = bytes
.chunks_exact(2)
.map(|b| u16::from_le_bytes([b[0], b[1]]))
.collect();
String::from_utf16_lossy(&words)
.trim_end_matches('\0')
.to_string()
}
fn read_sectors(file: &[u8], sec_table: &[u8], mut pos: usize, sec_size: usize) -> Option<Vec<u8>> {
let mut result: Vec<u8> = Vec::new();
while pos + 4 <= sec_table.len() {
let sec = u32::from_le_bytes([
sec_table[pos],
sec_table[pos + 1],
sec_table[pos + 2],
sec_table[pos + 3],
]) as usize;
if sec == 0xffff {
return None;
}
if sec == 0 {
break;
}
let offset = sec * sec_size;
if offset + sec_size > file.len() {
return None;
}
result.extend_from_slice(&file[offset..offset + sec_size]);
pos += 4;
}
if result.is_empty() {
None
} else {
Some(result)
}
}
fn is_unknown_tag(raw: &str) -> bool {
matches!(
raw,
"WM/WMRVBitrate"
| "WM/WMRVExpirationDate"
| "WM/WMRVExpirationSpan"
| "WM/MediaThumbTimeStamp"
)
}
fn map_tag_name(raw: &str) -> String {
match raw {
"WM/Genre" => return "Genre".into(),
"WM/Language" => return "Language".into(),
"WM/MediaClassPrimaryID" => return "MediaClassPrimaryID".into(),
"WM/MediaClassSecondaryID" => return "MediaClassSecondaryID".into(),
"WM/MediaCredits" => return "MediaCredits".into(),
"WM/MediaIsDelay" => return "MediaIsDelay".into(),
"WM/MediaIsFinale" => return "MediaIsFinale".into(),
"WM/MediaIsLive" => return "MediaIsLive".into(),
"WM/MediaIsMovie" => return "MediaIsMovie".into(),
"WM/MediaIsPremiere" => return "MediaIsPremiere".into(),
"WM/MediaIsRepeat" => return "MediaIsRepeat".into(),
"WM/MediaIsSAP" => return "MediaIsSAP".into(),
"WM/MediaIsSport" => return "MediaIsSport".into(),
"WM/MediaIsStereo" => return "MediaIsStereo".into(),
"WM/MediaIsSubtitled" => return "MediaIsSubtitled".into(),
"WM/MediaIsTape" => return "MediaIsTape".into(),
"WM/MediaNetworkAffiliation" => return "MediaNetworkAffiliation".into(),
"WM/MediaOriginalBroadcastDateTime" => return "MediaOriginalBroadcastDateTime".into(),
"WM/MediaOriginalChannel" => return "MediaOriginalChannel".into(),
"WM/MediaOriginalChannelSubNumber" => return "MediaOriginalChannelSubNumber".into(),
"WM/MediaOriginalRunTime" => return "MediaOriginalRunTime".into(),
"WM/MediaStationCallSign" => return "MediaStationCallSign".into(),
"WM/MediaStationName" => return "MediaStationName".into(),
"WM/MediaThumbAspectRatioX" => return "MediaThumbAspectRatioX".into(),
"WM/MediaThumbAspectRatioY" => return "MediaThumbAspectRatioY".into(),
"WM/MediaThumbHeight" => return "MediaThumbHeight".into(),
"WM/MediaThumbRatingAttributes" => return "MediaThumbRatingAttributes".into(),
"WM/MediaThumbRatingLevel" => return "MediaThumbRatingLevel".into(),
"WM/MediaThumbRatingSystem" => return "MediaThumbRatingSystem".into(),
"WM/MediaThumbRet" => return "MediaThumbRet".into(),
"WM/MediaThumbStride" => return "MediaThumbStride".into(),
"WM/MediaThumbWidth" => return "MediaThumbWidth".into(),
"WM/OriginalReleaseTime" => return "OriginalReleaseTime".into(),
"WM/ParentalRating" => return "ParentalRating".into(),
"WM/ParentalRatingReason" => return "ParentalRatingReason".into(),
"WM/Provider" => return "Provider".into(),
"WM/ProviderCopyright" => return "ProviderCopyright".into(),
"WM/ProviderRating" => return "ProviderRating".into(),
"WM/SubTitle" => return "Subtitle".into(),
"WM/SubTitleDescription" => return "SubtitleDescription".into(),
"WM/VideoClosedCaptioning" => return "VideoClosedCaptioning".into(),
"WM/WMRVATSCContent" => return "ATSCContent".into(),
"WM/WMRVActualSoftPostPadding" => return "ActualSoftPostPadding".into(),
"WM/WMRVActualSoftPrePadding" => return "ActualSoftPrePadding".into(),
"WM/WMRVBrandingImageID" => return "BrandingImageID".into(),
"WM/WMRVBrandingName" => return "BrandingName".into(),
"WM/WMRVContentProtected" => return "ContentProtected".into(),
"WM/WMRVContentProtectedPercent" => return "ContentProtectedPercent".into(),
"WM/WMRVDTVContent" => return "DTVContent".into(),
"WM/WMRVEncodeTime" => return "EncodeTime".into(),
"WM/WMRVEndTime" => return "EndTime".into(),
"WM/WMRVHDContent" => return "HDContent".into(),
"WM/WMRVHardPostPadding" => return "HardPostPadding".into(),
"WM/WMRVHardPrePadding" => return "HardPrePadding".into(),
"WM/WMRVInBandRatingAttributes" => return "InBandRatingAttributes".into(),
"WM/WMRVInBandRatingLevel" => return "InBandRatingLevel".into(),
"WM/WMRVInBandRatingSystem" => return "InBandRatingSystem".into(),
"WM/WMRVKeepUntil" => return "KeepUntil".into(),
"WM/WMRVOriginalSoftPostPadding" => return "OriginalSoftPostPadding".into(),
"WM/WMRVOriginalSoftPrePadding" => return "OriginalSoftPrePadding".into(),
"WM/WMRVProgramID" => return "ProgramID".into(),
"WM/WMRVQuality" => return "Quality".into(),
"WM/WMRVRequestID" => return "RequestID".into(),
"WM/WMRVScheduleItemID" => return "ScheduleItemID".into(),
"WM/WMRVSeriesUID" => return "SeriesUID".into(),
"WM/WMRVServiceID" => return "ServiceID".into(),
"WM/WMRVWatched" => return "Watched".into(),
_ => {}
}
let mut name = raw;
if let Some(rest) = name.strip_prefix("WTV_Metadata_") {
name = rest;
}
if let Some(rest) = name.strip_prefix("WM/WMRV") {
return rest.to_string();
}
if let Some(rest) = name.strip_prefix("WM/") {
return rest.to_string();
}
name.to_string()
}
fn bool_print(val: i32) -> String {
if val == 0 {
"No".to_string()
} else {
"Yes".to_string()
}
}
fn convert_time_100ns(val: u64) -> String {
const EPOCH_OFFSET_SECS: i64 = 719162 * 24 * 3600;
let float_secs = val as f64 / 1e7 - EPOCH_OFFSET_SECS as f64;
let int_secs = float_secs.floor() as i64;
let frac = float_secs - float_secs.floor();
let rounded_secs = if frac >= 0.5 { int_secs + 1 } else { int_secs };
unix_secs_to_datetime(rounded_secs)
}
fn unix_secs_to_datetime(secs: i64) -> String {
if secs < 0 {
let pos = (-secs) as u64;
let (y, mo, d, h, mi, s) = secs_to_ymd_hms(-(pos as i64));
return format!("{:04}:{:02}:{:02} {:02}:{:02}:{:02}Z", y, mo, d, h, mi, s);
}
let (y, mo, d, h, mi, s) = secs_to_ymd_hms(secs);
format!("{:04}:{:02}:{:02} {:02}:{:02}:{:02}Z", y, mo, d, h, mi, s)
}
fn secs_to_ymd_hms(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
let s = secs.rem_euclid(60) as u32;
let mins = secs.div_euclid(60);
let mi = mins.rem_euclid(60) as u32;
let hours = mins.div_euclid(60);
let h = hours.rem_euclid(24) as u32;
let days = hours.div_euclid(24);
let z = days + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
let mo = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
let year = if mo <= 2 { y + 1 } else { y } as i32;
(year, mo, d, h, mi, s)
}
fn convert_duration_100ns(val: u64) -> String {
let total_secs = val as f64 / 1e7;
convert_duration(total_secs)
}
fn convert_duration(secs: f64) -> String {
if secs == 0.0 {
return "0 s".to_string();
}
let (sign, secs) = if secs < 0.0 { ("-", -secs) } else { ("", secs) };
if secs < 30.0 {
return format!("{}{:.2} s", sign, secs);
}
let secs = (secs + 0.5) as u64;
let s = secs % 60;
let m = (secs / 60) % 60;
let h = secs / 3600;
format!("{}{}:{:02}:{:02}", sign, h, m, s)
}
fn process_metadata(data: &[u8], tags: &mut Vec<Tag>) {
let end = data.len();
let mut pos = 0;
while pos + 0x18 < end {
if data[pos..pos + 16] != METADATA_ENTRY_GUID {
break;
}
let fmt = match read_u32_le(data, pos + 0x10) {
Some(v) => v,
None => break,
};
let len = match read_u32_le(data, pos + 0x14) {
Some(v) => v as usize,
None => break,
};
pos += 0x18;
let mut name_bytes: Vec<u8> = Vec::new();
loop {
if pos + 2 > end {
return; }
let ch = &data[pos..pos + 2];
pos += 2;
if ch == [0, 0] {
break;
}
name_bytes.extend_from_slice(ch);
}
if pos + len > end {
break;
}
let raw_name = decode_utf16le(&name_bytes);
if is_unknown_tag(&raw_name) {
pos += len;
continue;
}
let tag_name = map_tag_name(&raw_name);
let value_bytes = &data[pos..pos + len];
let tag = match fmt {
0 | 3 => {
if len < 4 {
pos += len;
continue;
}
let int_val = match read_i32_le(value_bytes, 0) {
Some(v) => v,
None => {
pos += len;
continue;
}
};
let print = if fmt == 3 {
bool_print(int_val)
} else {
int_val.to_string()
};
mk_wtv(&tag_name, Value::I32(int_val), print)
}
1 => {
let raw_s = decode_utf16le(value_bytes);
let s = match tag_name.as_str() {
"MediaOriginalBroadcastDateTime" | "OriginalReleaseTime" => {
raw_s.replace('-', ":").replace('T', " ")
}
_ => raw_s,
};
mk_wtv(&tag_name, Value::String(s.clone()), s)
}
4 => {
if len < 8 {
pos += len;
continue;
}
let u64_val = match read_u64_le(value_bytes, 0) {
Some(v) => v,
None => {
pos += len;
continue;
}
};
let print = match tag_name.as_str() {
"Duration" => {
let secs = u64_val as f64 / 1e7;
convert_duration(secs)
}
"EncodeTime" | "EndTime" => convert_time_100ns(u64_val),
"MediaOriginalRunTime" => convert_duration_100ns(u64_val),
_ => u64_val.to_string(),
};
let raw_val = match tag_name.as_str() {
"Duration" => Value::F64(u64_val as f64 / 1e7),
_ => Value::String(u64_val.to_string()),
};
mk_wtv(&tag_name, raw_val, print)
}
6 => {
let hex = value_bytes
.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>();
mk_wtv(&tag_name, Value::String(hex.clone()), hex)
}
_ => {
pos += len;
continue;
}
};
tags.push(tag);
pos += len;
}
}
pub fn read_wtv(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 0x60 {
return Err(Error::InvalidData("WTV file too small".into()));
}
if data[..16] != WTV_MAGIC {
return Err(Error::InvalidData("not a WTV file".into()));
}
let raw_sec_size =
u32::from_le_bytes([data[0x28], data[0x29], data[0x2a], data[0x2b]]) as usize;
let sec_size = if raw_sec_size == 0x1000 || raw_sec_size == 0x100 {
raw_sec_size
} else {
0x1000
};
let header = &data[..0x60.min(data.len())];
let directory = match read_sectors(data, header, 0x38, sec_size) {
Some(d) => d,
None => return Err(Error::InvalidData("failed to read WTV directory".into())),
};
let mut tags = Vec::new();
let target = "table.0.entries.legacy_attrib";
let mut pos = 0;
while pos + 0x28 < directory.len() {
if directory[pos..pos + 16] != DIR_ENTRY_GUID {
if pos > 0 {
break; }
break;
}
let entry_len = match read_u32_le(&directory, pos + 0x10) {
Some(v) => v as usize,
None => break,
};
if entry_len < 0x28 || pos + entry_len > directory.len() {
break;
}
let n = match read_u32_le(&directory, pos + 0x20) {
Some(v) => v as usize,
None => break,
};
if 0x28 + n * 2 + 8 > entry_len {
break;
}
let name_end = pos + 0x28 + n * 2;
if name_end > directory.len() {
break;
}
let tag_name = decode_utf16le(&directory[pos + 0x28..name_end]);
let ptr = name_end;
let sec_num = match read_u32_le(&directory, ptr) {
Some(v) => v as usize,
None => break,
};
let flag = match read_u32_le(&directory, ptr + 4) {
Some(v) => v,
None => break,
};
if tag_name == target && (flag == 0 || flag == 1) {
let sec_bytes = (sec_num as u32).to_le_bytes();
if let Some(level1) = read_sectors(data, &sec_bytes, 0, sec_size) {
let metadata_data = if flag == 1 {
read_sectors(data, &level1, 0, sec_size)
} else {
Some(level1)
};
if let Some(md) = metadata_data {
process_metadata(&md, &mut tags);
}
}
}
pos += entry_len;
}
Ok(tags)
}