use std::collections::BTreeMap;
use crate::{IdMetadataExt, Item, Seconds, Stack, Track, TrackKind};
pub const TRACK_CREATED_BY_KEY: &str = "created_by";
pub const TRACK_CREATED_BY_AUTO: &str = "auto";
impl Track {
pub fn is_auto_created(&self) -> bool {
self.metadata
.get("tellers.ai")
.and_then(|ai| ai.get(TRACK_CREATED_BY_KEY))
.and_then(|value| value.as_str())
== Some(TRACK_CREATED_BY_AUTO)
}
pub fn set_auto_created(&mut self, auto_created: bool) {
if self.metadata.as_object().is_none() {
self.metadata = serde_json::Value::Object(serde_json::Map::new());
}
let map = self.metadata.as_object_mut().unwrap();
if auto_created {
let ai = map
.entry("tellers.ai".to_string())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if ai.as_object().is_none() {
*ai = serde_json::Value::Object(serde_json::Map::new());
}
ai.as_object_mut().unwrap().insert(
TRACK_CREATED_BY_KEY.to_string(),
serde_json::Value::String(TRACK_CREATED_BY_AUTO.to_string()),
);
} else if let Some(ai) = map.get_mut("tellers.ai").and_then(|ai| ai.as_object_mut()) {
ai.remove(TRACK_CREATED_BY_KEY);
}
}
pub fn set_name(&mut self, name: Option<String>) {
self.name = name;
self.set_auto_created(false);
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TrackGroup {
pub owner_track_index: usize,
pub owner_track_id: Option<String>,
pub kind: TrackKind,
pub partner_track_indices: Vec<usize>,
pub partner_track_ids: Vec<Option<String>>,
}
impl Stack {
fn sync_group_members(&self) -> BTreeMap<i64, Vec<usize>> {
let mut members: BTreeMap<i64, Vec<usize>> = BTreeMap::new();
for (track_index, track) in self.children.iter().enumerate() {
for item in &track.items {
let Item::Clip(clip) = item else {
continue;
};
let Some(sync_clips_id) = clip.sync_clips_id() else {
continue;
};
let tracks = members.entry(sync_clips_id).or_default();
if !tracks.contains(&track_index) {
tracks.push(track_index);
}
}
}
members
}
fn track_clip_count(&self, track_index: usize) -> usize {
self.children
.get(track_index)
.map(|track| {
track
.items
.iter()
.filter(|item| matches!(item, Item::Clip(_)))
.count()
})
.unwrap_or(0)
}
pub fn audio_group_main_track(&self, member_track_indices: &[usize]) -> Option<usize> {
member_track_indices
.iter()
.copied()
.max_by_key(|&index| (self.track_clip_count(index), index))
}
fn sync_group_owner(&self, member_track_indices: &[usize]) -> Option<usize> {
member_track_indices
.iter()
.copied()
.filter(|&index| self.children[index].kind == TrackKind::Video)
.max()
.or_else(|| self.audio_group_main_track(member_track_indices))
}
pub fn association_map(&self) -> Vec<Vec<usize>> {
let len = self.children.len();
let mut shared: Vec<Vec<(usize, usize)>> = vec![Vec::new(); len];
for members in self.sync_group_members().values() {
if members.len() < 2 {
continue;
}
let Some(owner) = self.sync_group_owner(members) else {
continue;
};
for &partner in members.iter().filter(|&&index| index != owner) {
match shared[owner]
.iter_mut()
.find(|(index, _)| *index == partner)
{
Some((_, count)) => *count += 1,
None => shared[owner].push((partner, 1)),
}
}
}
let mut best_owner: Vec<Option<(usize, usize)>> = vec![None; len];
for (owner, list) in shared.iter().enumerate() {
for &(partner, count) in list {
if best_owner[partner].is_none_or(|(_, best)| count > best) {
best_owner[partner] = Some((owner, count));
}
}
}
shared
.into_iter()
.enumerate()
.map(|(owner, list)| {
let mut partners: Vec<usize> = list
.into_iter()
.filter(|&(partner, _)| best_owner[partner].map(|(o, _)| o) == Some(owner))
.map(|(partner, _)| partner)
.collect();
partners.sort_by_key(|&index| (index.abs_diff(owner), index));
partners
})
.collect()
}
pub fn associated_track_indices(&self, track_index: usize) -> Vec<usize> {
self.association_map()
.into_iter()
.nth(track_index)
.unwrap_or_default()
}
pub fn associated_track_ids(&self, track_id: &str) -> Option<Vec<String>> {
let (track_index, _) = self.get_track_by_id(track_id)?;
Some(
self.associated_track_indices(track_index)
.into_iter()
.filter_map(|index| self.children[index].get_id())
.collect(),
)
}
pub fn tracks_associating(&self, track_index: usize) -> Vec<usize> {
self.association_map()
.iter()
.enumerate()
.filter(|(owner, partners)| *owner != track_index && partners.contains(&track_index))
.map(|(owner, _)| owner)
.collect()
}
pub fn is_associated_partner(&self, track_index: usize) -> bool {
!self.tracks_associating(track_index).is_empty()
}
pub fn group_owner_of(&self, track_index: usize) -> usize {
self.tracks_associating(track_index)
.into_iter()
.next()
.unwrap_or(track_index)
}
pub fn track_is_free(&self, track_index: usize) -> bool {
!self.is_associated_partner(track_index)
&& self.associated_track_indices(track_index).is_empty()
}
pub fn track_in_video_group(&self, track_index: usize) -> bool {
self.children
.get(self.group_owner_of(track_index))
.is_some_and(|track| track.kind == TrackKind::Video)
}
pub(crate) fn column_may_land_on(
&self,
dest_track_index: usize,
primary_is_gap: bool,
has_video_partner: bool,
) -> bool {
let Some(dest) = self.children.get(dest_track_index) else {
return false;
};
match dest.kind {
TrackKind::Video => !has_video_partner,
TrackKind::Audio => {
if primary_is_gap {
return true;
}
let in_video_group = self.track_in_video_group(dest_track_index);
if has_video_partner {
in_video_group || self.track_is_free(dest_track_index)
} else {
!in_video_group
}
}
TrackKind::Other => !has_video_partner,
}
}
pub fn track_groups(&self) -> Vec<TrackGroup> {
let map = self.association_map();
let len = self.children.len();
let mut owner_of: Vec<usize> = (0..len).collect();
for (owner, partners) in map.iter().enumerate() {
for &partner in partners {
owner_of[partner] = owner;
}
}
(0..len)
.filter(|&owner| owner_of[owner] == owner)
.map(|owner| {
let partner_track_indices: Vec<usize> = map[owner]
.iter()
.copied()
.filter(|&partner| owner_of[partner] == owner)
.collect();
TrackGroup {
owner_track_index: owner,
owner_track_id: self.children[owner].get_id(),
kind: self.children[owner].kind.clone(),
partner_track_ids: partner_track_indices
.iter()
.map(|&partner| self.children[partner].get_id())
.collect(),
partner_track_indices,
}
})
.collect()
}
pub(crate) fn column_owner_track_index(&self, member_track_indices: &[usize]) -> Option<usize> {
let map = self.association_map();
member_track_indices
.iter()
.copied()
.filter(|&index| index < self.children.len())
.max_by_key(|&index| {
let listed = map[index]
.iter()
.filter(|partner| member_track_indices.contains(partner))
.count();
(listed, self.children[index].kind == TrackKind::Video, index)
})
}
pub fn prune_empty_partner_tracks(&mut self) -> Vec<String> {
let candidates: Vec<String> = self
.children
.iter()
.filter(|track| {
track.kind == TrackKind::Audio
&& track.items.iter().all(|item| matches!(item, Item::Gap(_)))
&& track.is_auto_created()
})
.filter_map(|track| track.get_id())
.collect();
let mut removed = Vec::new();
for id in candidates {
if let Some((index, _)) = self.get_track_by_id(&id) {
self.children.remove(index);
removed.push(id);
}
}
removed
}
pub fn normalize_track_order(&mut self) -> bool {
let len = self.children.len();
let mut order: Vec<usize> = Vec::with_capacity(len);
for group in self.track_groups() {
order.extend(group.partner_track_indices.iter().rev());
order.push(group.owner_track_index);
}
for index in 0..len {
if !order.contains(&index) {
order.push(index);
}
}
if order
.iter()
.enumerate()
.all(|(position, &index)| position == index)
{
return false;
}
let mut tracks: Vec<Option<Track>> = self.children.drain(..).map(Some).collect();
self.children = order
.into_iter()
.map(|index| tracks[index].take().expect("each track placed once"))
.collect();
true
}
pub fn free_audio_track_for(&self, start: Seconds, duration: Seconds) -> Option<String> {
let end = start + duration.max(0.0);
let mut candidates: Vec<usize> = self
.children
.iter()
.enumerate()
.filter(|(index, track)| {
track.kind == TrackKind::Audio
&& Stack::track_range_is_free(track, start, end)
&& self.children[*index].get_id().is_some()
&& !self.track_in_video_group(*index)
})
.map(|(index, _)| index)
.collect();
candidates.sort_by_key(|&index| (!self.track_is_free(index), index));
candidates
.into_iter()
.next()
.and_then(|index| self.children[index].get_id())
}
}