chore: refactor into a RecordingDataManager, lay the ground work for a RenderManager

This commit is contained in:
2026-05-27 01:28:47 -04:00
parent f86c094dda
commit e72633f26a
22 changed files with 830 additions and 49 deletions

View File

@@ -0,0 +1,44 @@
use futures::{TryStream, TryStreamExt as _};
use snafu::{ResultExt as _, Snafu};
use std::{fmt::Display, str::FromStr};
use time::UtcDateTime;
use twilight_model::id::{
Id,
marker::{ChannelMarker, GuildMarker},
};
use super::{ListError, Recording, RecordingDataManager, recording};
#[derive(Debug, Snafu)]
pub enum RecordingEntryError {
/// failed to get an entry from the storage operator's lister
ReceiveEntryError { source: opendal::Error },
/// failed to parse the entry as a recording
ParseError { source: recording::TakeError },
}
// impl RecordingDataManager {
// pub async fn between(
// &self,
// start: UtcDateTime,
// end: UtcDateTime,
// ) -> Result<impl TryStream<Ok = Recording, Error = RecordingEntryError> + Unpin, ListError>
// {
// todo!();
// }
// }
// impl RecordingDataManager {
// pub async fn between_in_vc(
// &self,
// start: UtcDateTime,
// end: UtcDateTime,
// guild_id: Id<GuildMarker>,
// voice_channel_id: Id<ChannelMarker>,
// ) -> Result<impl TryStream<Ok = Recording, Error = RecordingEntryError> + Unpin, ListError>
// {
// todo!();
// Ok(self.between(start, end)?)
// }
// }

111
src/recording_data/clip.rs Normal file
View File

@@ -0,0 +1,111 @@
use futures::{TryStream, TryStreamExt as _};
use snafu::{ResultExt as _, Snafu};
use std::{fmt::Display, str::FromStr};
use super::{
CreateListerSnafu, Day, Guild, Hour, ListError, Microsecond, Minute, Month,
RecordingDataManager, Second, User, VoiceChannel, Year, guild, microsecond, second, user,
voice_channel,
};
#[derive(Debug, Clone)]
pub struct Clip {
pub second: Second,
pub microsecond: Microsecond,
pub guild: Guild,
pub voice_channel: VoiceChannel,
pub user: User,
}
#[derive(Debug, Snafu)]
pub enum TakeError {
/// could not parse the second out of the clip metadata
TakeSecondError { source: second::TakeError },
/// could not parse the microsecond out of the clip metadata
TakeMicrosecondError { source: microsecond::TakeError },
/// could not parse the guild out of the clip metadata
TakeGuildError { source: guild::TakeError },
/// could not parse the voice channel out of the clip metadata
TakeVoiceChannelError { source: voice_channel::TakeError },
/// could not parse the user out of the clip metadata
TakeUserError { source: user::TakeError },
}
pub fn take(s: &str) -> Result<Clip, TakeError> {
let (second, s) = second::take(s).context(TakeSecondSnafu)?;
let (microsecond, s) = microsecond::take(s).context(TakeMicrosecondSnafu)?;
let (guild, s) = guild::take(s).context(TakeGuildSnafu)?;
let (voice_channel, s) = voice_channel::take(s).context(TakeVoiceChannelSnafu)?;
let user = user::take(s).context(TakeUserSnafu)?;
Ok(Clip {
second,
microsecond,
guild,
voice_channel,
user,
})
}
impl FromStr for Clip {
type Err = TakeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
take(s)
}
}
impl Display for Clip {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
second,
microsecond,
guild,
voice_channel,
user,
} = self;
let user = user
.as_ref()
.map_or_else(|| "UNKNOWN".into(), ToString::to_string);
write!(
f,
"audio-{second}.{microsecond}-{guild}-{voice_channel}-{user}.wav"
)
}
}
#[derive(Debug, Snafu)]
pub enum ClipEntryError {
/// failed to get an entry from the storage operator's lister
ReceiveEntryError { source: opendal::Error },
/// failed to parse the entry as a clip
ParseError { source: TakeError },
}
impl RecordingDataManager {
pub async fn clips(
&self,
year: Year,
month: Month,
day: Day,
hour: Hour,
minute: Minute,
) -> Result<impl TryStream<Ok = Clip, Error = ClipEntryError> + Unpin, ListError> {
let lister = self
.operator
.lister(&format!("{year}/{month}/{day}/{hour}/{minute}/"))
.await
.context(CreateListerSnafu)?;
Ok(lister
.map_err(|error| ClipEntryError::ReceiveEntryError { source: error })
.and_then(|entry| std::future::ready(entry.name().parse().context(ParseSnafu))))
}
}

57
src/recording_data/day.rs Normal file
View File

@@ -0,0 +1,57 @@
use futures::{TryStream, TryStreamExt as _};
use snafu::{OptionExt as _, ResultExt as _, Snafu};
use std::num::ParseIntError;
use super::{CreateListerSnafu, ListError, Month, RecordingDataManager, Year};
pub type Day = u8;
#[derive(Debug, Snafu)]
pub enum TakeError {
/// days are supposed to be directories, but this wasn't (because it didn't end with `/`)
NotADirectory,
/// could not parse the day as an integer
ParseIntegerError { source: ParseIntError },
}
pub fn take(s: &str) -> Result<(Day, &str), TakeError> {
let (day, rest) = s.split_once('/').context(NotADirectorySnafu)?;
let day = day.parse().context(ParseIntegerSnafu)?;
Ok((day, rest))
}
#[derive(Debug, Snafu)]
pub enum DayEntryError {
/// failed to get an entry from the storage operator's lister
ReceiveEntryError { source: opendal::Error },
/// failed to parse the entry as a day
ParseError { source: TakeError },
}
impl RecordingDataManager {
pub async fn days(
&self,
year: Year,
month: Month,
) -> Result<impl TryStream<Ok = Day, Error = DayEntryError> + Unpin, ListError> {
let lister = self
.operator
.lister(&format!("{year}/{month}/"))
.await
.context(CreateListerSnafu)?;
Ok(lister
.map_err(|error| DayEntryError::ReceiveEntryError { source: error })
.and_then(|entry| {
std::future::ready(
take(entry.name())
.map(|(day, rest)| day)
.context(ParseSnafu),
)
}))
}
}

View File

@@ -0,0 +1,22 @@
use snafu::{OptionExt as _, ResultExt as _, Snafu};
use std::str::FromStr;
use twilight_model::id::{Id, marker::GuildMarker};
pub type Guild = Id<GuildMarker>;
#[derive(Debug, Snafu)]
pub enum TakeError {
/// guilds are supposed to be followed by -
Malformed,
/// could not parse the guild ID
ParseIdError { source: <Guild as FromStr>::Err },
}
pub fn take(path: &str) -> Result<(Guild, &str), TakeError> {
let (guild, rest) = path.split_once('-').context(MalformedSnafu)?;
let guild = guild.parse().context(ParseIdSnafu)?;
Ok((guild, rest))
}

View File

@@ -0,0 +1,58 @@
use futures::{TryStream, TryStreamExt as _};
use snafu::{OptionExt as _, ResultExt as _, Snafu};
use std::num::ParseIntError;
use super::{CreateListerSnafu, Day, ListError, Month, RecordingDataManager, Year};
pub type Hour = u8;
#[derive(Debug, Snafu)]
pub enum TakeError {
/// hours are supposed to be directories, but this wasn't (because it didn't end with `/`)
NotADirectory,
/// could not parse the hour as an integer
ParseIntegerError { source: ParseIntError },
}
pub fn take(s: &str) -> Result<(Hour, &str), TakeError> {
let (hour, rest) = s.split_once('/').context(NotADirectorySnafu)?;
let hour = hour.parse().context(ParseIntegerSnafu)?;
Ok((hour, rest))
}
#[derive(Debug, Snafu)]
pub enum HourEntryError {
/// failed to get an entry from the storage operator's lister
ReceiveEntryError { source: opendal::Error },
/// failed to parse the entry as a hour
ParseError { source: TakeError },
}
impl RecordingDataManager {
pub async fn hours(
&self,
year: Year,
month: Month,
day: Day,
) -> Result<impl TryStream<Ok = Hour, Error = HourEntryError> + Unpin, ListError> {
let lister = self
.operator
.lister(&format!("{year}/{month}/{day}/"))
.await
.context(CreateListerSnafu)?;
Ok(lister
.map_err(|error| HourEntryError::ReceiveEntryError { source: error })
.and_then(|entry| {
std::future::ready(
take(entry.name())
.map(|(hour, _rest)| hour)
.context(ParseSnafu),
)
}))
}
}

View File

@@ -0,0 +1,21 @@
use snafu::{OptionExt as _, ResultExt as _, Snafu};
use std::num::ParseIntError;
pub type Microsecond = u32;
#[derive(Debug, Snafu)]
pub enum TakeError {
/// microseconds are supposed to be followed by -
Malformed,
/// could not parse the microsecond as an integer
ParseIntegerError { source: ParseIntError },
}
pub fn take(path: &str) -> Result<(Microsecond, &str), TakeError> {
let (microsecond, rest) = path.split_once('-').context(MalformedSnafu)?;
let microsecond = microsecond.parse().context(ParseIntegerSnafu)?;
Ok((microsecond, rest))
}

View File

@@ -0,0 +1,59 @@
use futures::{TryStream, TryStreamExt as _};
use snafu::{OptionExt as _, ResultExt as _, Snafu};
use std::num::ParseIntError;
use super::{CreateListerSnafu, Day, Hour, ListError, Month, RecordingDataManager, Year};
pub type Minute = u8;
#[derive(Debug, Snafu)]
pub enum TakeError {
/// minutes are supposed to be directories, but this wasn't (because it didn't end with `/`)
NotADirectory,
/// could not parse the minute as an integer
ParseIntegerError { source: ParseIntError },
}
pub fn take(s: &str) -> Result<(Minute, &str), TakeError> {
let (minute, rest) = s.split_once('/').context(NotADirectorySnafu)?;
let minute = minute.parse().context(ParseIntegerSnafu)?;
Ok((minute, rest))
}
#[derive(Debug, Snafu)]
pub enum MinuteEntryError {
/// failed to get an entry from the storage operator's lister
ReceiveEntryError { source: opendal::Error },
/// failed to parse the entry as a minute
ParseError { source: TakeError },
}
impl RecordingDataManager {
pub async fn minutes(
&self,
year: Year,
month: Month,
day: Day,
hour: Hour,
) -> Result<impl TryStream<Ok = Minute, Error = MinuteEntryError> + Unpin, ListError> {
let lister = self
.operator
.lister(&format!("{year}/{month}/{day}/{hour}/"))
.await
.context(CreateListerSnafu)?;
Ok(lister
.map_err(|error| MinuteEntryError::ReceiveEntryError { source: error })
.and_then(|entry| {
std::future::ready(
take(entry.name())
.map(|(minute, _rest)| minute)
.context(ParseSnafu),
)
}))
}
}

47
src/recording_data/mod.rs Normal file
View File

@@ -0,0 +1,47 @@
use futures::TryStream;
use opendal::Operator;
use snafu::Snafu;
mod between;
mod clip;
mod day;
mod guild;
mod hour;
mod microsecond;
mod minute;
mod month;
mod recording;
mod second;
mod user;
mod voice_channel;
mod year;
pub use clip::Clip;
use day::Day;
use guild::Guild;
use hour::Hour;
use microsecond::Microsecond;
use minute::Minute;
use month::Month;
pub use recording::Recording;
use second::Second;
use user::User;
use voice_channel::VoiceChannel;
use year::Year;
#[derive(Debug, Clone)]
pub struct RecordingDataManager {
operator: Operator,
}
impl RecordingDataManager {
pub fn new(operator: Operator) -> Self {
Self { operator }
}
}
#[derive(Debug, Snafu)]
pub enum ListError {
/// error creating a lister through the storage operator
CreateListerError { source: opendal::Error },
}

View File

@@ -0,0 +1,55 @@
use futures::{TryStream, TryStreamExt as _};
use snafu::{OptionExt as _, ResultExt as _, Snafu};
use super::{CreateListerSnafu, ListError, RecordingDataManager, Year};
pub use time::Month;
#[derive(Debug, Snafu)]
pub enum TakeError {
/// months are supposed to be directories, but this wasn't (because it didn't end with `/`)
NotADirectory,
/// could not parse the month as its name
ParseMonthNameError { source: time::error::InvalidVariant },
}
pub fn take(s: &str) -> Result<(Month, &str), TakeError> {
let (month, rest) = s.split_once('/').context(NotADirectorySnafu)?;
let month = month.parse().context(ParseMonthNameSnafu)?;
Ok((month, rest))
}
#[derive(Debug, Snafu)]
pub enum MonthEntryError {
/// failed to get an entry from the storage operator's lister
ReceiveEntryError { source: opendal::Error },
/// failed to parse the entry as a month
ParseError { source: TakeError },
}
impl RecordingDataManager {
pub async fn months(
&self,
year: Year,
) -> Result<impl TryStream<Ok = Month, Error = MonthEntryError> + Unpin, ListError> {
let lister = self
.operator
.lister(&format!("{year}/"))
.await
.context(CreateListerSnafu)?;
Ok(lister
.map_err(|error| MonthEntryError::ReceiveEntryError { source: error })
.and_then(|entry| {
std::future::ready(
take(entry.name())
.map(|(month, rest)| month)
.context(ParseSnafu),
)
}))
}
}

View File

@@ -0,0 +1,143 @@
use futures::{TryStream, TryStreamExt as _};
use hound::{SampleFormat, WavSpec};
use snafu::{ResultExt as _, Snafu};
use std::{convert::Infallible, fmt::Display, io::Cursor, str::FromStr};
use time::UtcDateTime;
use twilight_model::id::{
Id,
marker::{ChannelMarker, GuildMarker},
};
use super::{
Clip, Day, Hour, ListError, Minute, Month, RecordingDataManager, Year, clip, day, hour, minute,
month, year,
};
#[derive(Debug, Clone)]
pub struct Recording {
pub year: Year,
pub month: Month,
pub day: Day,
pub hour: Hour,
pub minute: Minute,
pub clip: Clip,
}
#[derive(Debug, Snafu)]
pub enum TakeError {
/// could not parse the year out of the recording
TakeYearError { source: year::TakeError },
/// could not parse the month out of the recording
TakeMonthError { source: month::TakeError },
/// could not parse the day out of the recording
TakeDayError { source: day::TakeError },
/// could not parse the hour out of the recording
TakeHourError { source: hour::TakeError },
/// could not parse the minute out of the recording
TakeMinuteError { source: minute::TakeError },
/// could not parse the clip out of the recording
TakeClipError { source: clip::TakeError },
}
pub fn take(s: &str) -> Result<Recording, TakeError> {
let (year, s) = year::take(s).context(TakeYearSnafu)?;
let (month, s) = month::take(s).context(TakeMonthSnafu)?;
let (day, s) = day::take(s).context(TakeDaySnafu)?;
let (hour, s) = hour::take(s).context(TakeHourSnafu)?;
let (minute, s) = minute::take(s).context(TakeMinuteSnafu)?;
let clip = clip::take(s).context(TakeClipSnafu)?;
Ok(Recording {
year,
month,
day,
hour,
minute,
clip,
})
}
impl FromStr for Recording {
type Err = TakeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
take(s)
}
}
impl Display for Recording {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
year,
month,
day,
hour,
minute,
clip,
} = self;
write!(f, "{year}/{month}/{day}/{hour}/{minute}/{clip}")
}
}
impl RecordingDataManager {
pub async fn write(
&self,
recording: &Recording,
samples: &[i16],
sample_rate: u32,
channels: u16,
) -> Result<
(),
Infallible, // TODO: a real error type
> {
let wav_spec = WavSpec {
channels,
sample_rate,
bits_per_sample: 16,
sample_format: SampleFormat::Int,
};
let mut buffer = Vec::new();
let writer = Cursor::new(&mut buffer);
let mut wav_writer = hound::WavWriter::new(writer, wav_spec).expect("TODO");
let mut sample_writer = wav_writer.get_i16_writer(samples.len() as u32);
for sample in samples {
sample_writer.write_sample(*sample);
}
sample_writer.flush().expect("TODO");
wav_writer.finalize().expect("TODO");
let path = recording.to_string();
self.operator.write(&path, buffer).await.expect("TODO");
Ok(())
}
}
impl RecordingDataManager {
pub async fn read(&self, recording: &Recording, sample_rate: u32, channels: u16) -> Vec<i16> {
let path = recording.to_string();
let buffer = self.operator.read(&path).await.expect("TODO");
let wav_spec = WavSpec {
channels,
sample_rate,
bits_per_sample: 16,
sample_format: SampleFormat::Int,
};
todo!();
}
}

View File

@@ -0,0 +1,25 @@
use snafu::{OptionExt as _, ResultExt as _, Snafu};
use std::num::ParseIntError;
pub type Second = u8;
#[derive(Debug, Snafu)]
pub enum TakeError {
/// seconds are supposed to be preceded by audio-
MalformedPrefix,
/// seconds are supposed to be followed by .
MalformedSuffix,
/// could not parse the second as an integer
ParseIntegerError { source: ParseIntError },
}
pub fn take(path: &str) -> Result<(Second, &str), TakeError> {
let (_prefix, path) = path.split_once("audio-").context(MalformedPrefixSnafu)?;
let (second, rest) = path.split_once('.').context(MalformedSuffixSnafu)?;
let second = second.parse().context(ParseIntegerSnafu)?;
Ok((second, rest))
}

View File

@@ -0,0 +1,25 @@
use snafu::{OptionExt as _, ResultExt as _, Snafu};
use std::num::ParseIntError;
use twilight_model::id::{Id, marker::UserMarker};
pub type User = Option<Id<UserMarker>>;
#[derive(Debug, Snafu)]
pub enum TakeError {
/// users are supposed to be terminated by .wav
Malformed,
/// could not parse the user ID
ParseIntegerError { source: ParseIntError },
}
pub fn take(path: &str) -> Result<User, TakeError> {
let user = path.strip_suffix(".wav").context(MalformedSnafu)?;
let user = match user {
"UNKNOWN" => None,
user => Some(user.parse().context(ParseIntegerSnafu)?),
};
Ok(user)
}

View File

@@ -0,0 +1,26 @@
use snafu::{OptionExt as _, ResultExt as _, Snafu};
use std::num::ParseIntError;
use std::str::FromStr;
use twilight_model::id::Id;
use twilight_model::id::marker::ChannelMarker;
pub type VoiceChannel = Id<ChannelMarker>;
#[derive(Debug, Snafu)]
pub enum TakeError {
/// voice channels are supposed to be followed by -
Malformed,
/// could not parse the voice channel ID
ParseIdError {
source: <VoiceChannel as FromStr>::Err,
},
}
pub fn take(path: &str) -> Result<(VoiceChannel, &str), TakeError> {
let (voice_channel, rest) = path.split_once('-').context(MalformedSnafu)?;
let voice_channel = voice_channel.parse().context(ParseIdSnafu)?;
Ok((voice_channel, rest))
}

View File

@@ -0,0 +1,51 @@
use futures::{TryStream, TryStreamExt as _};
use snafu::{OptionExt as _, ResultExt as _, Snafu};
use std::num::ParseIntError;
use super::{CreateListerSnafu, ListError, RecordingDataManager};
pub type Year = i32;
#[derive(Debug, Snafu)]
pub enum TakeError {
/// years are supposed to be directories, but this wasn't (because it didn't end with `/`)
NotADirectory,
/// could not parse the year as an integer
ParseIntegerError { source: ParseIntError },
}
pub fn take(path: &str) -> Result<(Year, &str), TakeError> {
let (year, rest) = path.split_once('/').context(NotADirectorySnafu)?;
let year = year.parse().context(ParseIntegerSnafu)?;
Ok((year, rest))
}
#[derive(Debug, Snafu)]
pub enum YearEntryError {
/// failed to get an entry from the storage operator's lister
ReceiveEntryError { source: opendal::Error },
/// failed to parse the entry as a year
ParseError { source: TakeError },
}
impl RecordingDataManager {
pub async fn years(
&self,
) -> Result<impl TryStream<Ok = Year, Error = YearEntryError> + Unpin, ListError> {
let lister = self.operator.lister("").await.context(CreateListerSnafu)?;
Ok(lister
.map_err(|error| YearEntryError::ReceiveEntryError { source: error })
.and_then(|entry| {
std::future::ready(
take(entry.name())
.map(|(year, _rest)| year)
.context(ParseSnafu),
)
}))
}
}