use crate::data::SPEC_VERSION;
use crate::json::Json;
use crate::parser::{LineProbe, ListParser};
use crate::types::{Position, RawTrack};
use crate::words::{chars, string, tail};
use super::kinds::{
has_marker_word, has_separator, is_blank, is_header, is_link, layered_prefix, marker_table,
};
use super::options::ResolvedOptions;
use super::positions::{bare_candidate, confirm_positions, BareCandidate};
use super::split::{split_lines, Line};
use super::times::{scan_time, TimeScan};
use super::types::{tracklist_json, EntryKind, Stats, Tracklist, TracklistEntry, TracklistWarning};
use super::warnings::{tracklist_warnings, WarningInput};
struct Row<'a> {
line: Line,
scan: TimeScan,
text: String,
layered: bool,
blank: bool,
candidate: bool,
dash: bool,
probe: Option<LineProbe<'a>>,
bare: Option<BareCandidate>,
track: Option<RawTrack>,
separated: bool,
marker: bool,
}
pub(crate) struct RawTracklist {
entries: Vec<TracklistEntry>,
tracks: Vec<Option<RawTrack>>,
warnings: Vec<TracklistWarning>,
stats: Stats,
}
impl RawTracklist {
pub(crate) fn into_tracklist(self) -> Tracklist {
let mut entries = self.entries;
for (entry, track) in entries.iter_mut().zip(self.tracks) {
entry.track = track.map(RawTrack::into_track);
}
Tracklist {
spec_version: SPEC_VERSION.to_string(),
entries,
warnings: self.warnings,
stats: self.stats,
}
}
pub(crate) fn to_json_value(&self) -> Json {
let entries = self
.entries
.iter()
.zip(&self.tracks)
.map(|(e, t)| e.json_with(t.as_ref().map_or(Json::Null, RawTrack::to_json_value)))
.collect();
tracklist_json(SPEC_VERSION, entries, &self.warnings, &self.stats)
}
}
pub(crate) fn parse_tracklist(text: &str, opts: &ResolvedOptions) -> RawTracklist {
let markers = marker_table(&opts.marker_words);
let lp = ListParser::new(opts.parse.clone());
let mut rows: Vec<Row<'_>> = split_lines(text)
.into_iter()
.map(|line| {
let scan = scan_time(&line.raw);
let blank = is_blank(&line.raw);
let scanned = chars(&scan.text);
let layered = if blank { 0 } else { layered_prefix(&scanned) };
let mut row = Row {
line,
scan,
text: string(tail(&scanned, layered)),
layered: layered > 0,
blank,
candidate: false,
dash: false,
probe: None,
bare: None,
track: None,
separated: false,
marker: false,
};
if blank {
return row;
}
let probe = lp.probe(&row.text); row.dash = probe.spaced_dash;
row.candidate = row.scan.time.is_some() || row.dash;
row.bare = if row.layered { None } else { bare_candidate(&row.text, probe.position) };
row.probe = Some(probe);
row
})
.collect();
let candidates = rows.iter().filter(|r| r.candidate).count();
let dashes = rows.iter().filter(|r| r.candidate && r.dash).count();
let list_context = candidates > 0 && 2 * dashes >= candidates;
let confirmed = confirm_positions(
&rows.iter().map(|r| r.bare.as_ref().map(|b| b.number)).collect::<Vec<_>>(),
&rows.iter().map(|r| r.candidate && !r.layered).collect::<Vec<_>>(),
);
for (r, is_position) in rows.iter_mut().zip(confirmed) {
let Some(own_probe) = r.probe.take() else { continue };
let bare = if is_position { r.bare.as_ref() } else { None };
let probe = match bare {
Some(b) => lp.probe(&b.stripped),
None => own_probe,
};
r.marker = has_marker_word(bare.map_or(&r.text, |b| &b.stripped), &markers);
let empty = probe.empty;
let mut track = probe.finish(list_context && r.scan.time.is_some() && !r.marker);
if let Some(b) = bare {
track.position = Some(Position { raw: b.raw.clone(), number: b.number });
}
r.separated = has_separator(&track, empty);
r.track = Some(track);
}
let mut entries = Vec::with_capacity(rows.len());
let mut tracks = Vec::with_capacity(rows.len());
let mut positions = Vec::with_capacity(rows.len());
let mut invalid = Vec::with_capacity(rows.len());
let mut unparsed = Vec::with_capacity(rows.len());
for (i, r) in rows.into_iter().enumerate() {
let kind = kind_of(&r);
let keep = kind == EntryKind::Track || kind == EntryKind::Marker;
let track = if keep { r.track } else { None };
positions.push(track.as_ref().and_then(|t| t.position.as_ref()).map(|p| p.number));
invalid.push(r.scan.invalid);
unparsed.push(kind == EntryKind::Track && !r.separated);
entries.push(TracklistEntry {
line: i + 1,
kind,
eol: r.line.eol,
raw: r.line.raw,
parts: r.scan.parts,
time: r.scan.time,
track: None,
layered: r.layered,
});
tracks.push(track);
}
let warnings = tracklist_warnings(&WarningInput {
entries: &entries,
positions: &positions,
invalid: &invalid,
unparsed: &unparsed,
audio_seconds: opts.audio_seconds,
});
let stats = stats_of(&entries);
RawTracklist { entries, tracks, warnings, stats }
}
fn kind_of(r: &Row<'_>) -> EntryKind {
if r.blank || r.track.is_none() {
return EntryKind::Blank;
}
let marker = !r.separated && r.marker;
if r.scan.time.is_some() {
return if marker { EntryKind::Marker } else { EntryKind::Track };
}
if is_link(&r.text) {
EntryKind::Link
} else if is_header(&r.text) {
EntryKind::Header
} else if marker {
EntryKind::Marker
} else if r.separated {
EntryKind::Track
} else {
EntryKind::Note
}
}
fn stats_of(entries: &[TracklistEntry]) -> Stats {
let mut stats = Stats { lines: entries.len(), ..Stats::default() };
for e in entries {
match e.kind {
EntryKind::Track => stats.tracks += 1,
EntryKind::Marker => stats.markers += 1,
_ => {}
}
if let Some(t) = &e.time {
stats.timed += 1;
stats.first_time.get_or_insert(t.seconds);
stats.last_time = Some(t.seconds);
}
}
stats
}