chore: refactor into a RecordingDataManager, lay the ground work for a RenderManager
This commit is contained in:
74
src/call.rs
74
src/call.rs
@@ -1,6 +1,9 @@
|
||||
use crate::{
|
||||
OneToManyUniqueBTreeMap, UserDataManager, option_ext::OptionExt as _,
|
||||
user_capnp::user::Consent, user_data::RECORD_IF_CONSENT_UNSPECIFIED,
|
||||
OneToManyUniqueBTreeMap, UserDataManager,
|
||||
option_ext::OptionExt as _,
|
||||
recording_data::{Clip, Recording, RecordingDataManager},
|
||||
user_capnp::user::Consent,
|
||||
user_data::RECORD_IF_CONSENT_UNSPECIFIED,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use futures::FutureExt;
|
||||
@@ -26,7 +29,7 @@ struct Handler {
|
||||
start_instant: Instant,
|
||||
start_utc: UtcDateTime,
|
||||
|
||||
recording_data: Operator,
|
||||
recording_data_manager: RecordingDataManager,
|
||||
|
||||
guild_id: Id<GuildMarker>,
|
||||
channel_id: Id<ChannelMarker>,
|
||||
@@ -104,49 +107,44 @@ impl EventHandler for Handler {
|
||||
let minute = now_utc.minute();
|
||||
let second = now_utc.second();
|
||||
|
||||
let microseconds = now_utc.microsecond();
|
||||
let microsecond = now_utc.microsecond();
|
||||
|
||||
let guild_id = self.guild_id;
|
||||
let channel_id = self.channel_id;
|
||||
let guild = self.guild_id;
|
||||
let voice_channel = self.channel_id;
|
||||
|
||||
let user = user_id
|
||||
.as_ref()
|
||||
.map_or_else(|| "UNKNOWN".into(), ToString::to_string);
|
||||
let user = user_id;
|
||||
|
||||
let path = format!(
|
||||
"{year}/{month}/{day}/{hour}/{minute}/audio-{second}.{microseconds}-{guild_id}-{channel_id}-{user}.wav"
|
||||
);
|
||||
let clip = Clip {
|
||||
second,
|
||||
microsecond,
|
||||
guild,
|
||||
voice_channel,
|
||||
user,
|
||||
};
|
||||
|
||||
let recording = Recording {
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
clip,
|
||||
};
|
||||
|
||||
let channels = self.audio_channels;
|
||||
let sample_rate = self.audio_sample_rate;
|
||||
|
||||
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(pcm.len() as u32);
|
||||
|
||||
for sample in pcm {
|
||||
sample_writer.write_sample(*sample);
|
||||
}
|
||||
sample_writer.flush().expect("TODO");
|
||||
|
||||
wav_writer.finalize().expect("TODO");
|
||||
|
||||
tracing::info!("going to write the audio shortly");
|
||||
|
||||
let recording_data = self.recording_data.clone();
|
||||
let recording_data_manager = self.recording_data_manager.clone();
|
||||
let pcm = pcm.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
recording_data.write(&path, buffer).await.expect("TODO");
|
||||
tracing::info!(?path, "successfully wrote the audio!");
|
||||
recording_data_manager
|
||||
.write(&recording, &pcm, sample_rate, channels)
|
||||
.await
|
||||
.expect("TODO");
|
||||
tracing::info!(?recording, "successfully wrote the audio!");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -174,7 +172,7 @@ pub async fn join_and_record(
|
||||
audio_channels: Channels,
|
||||
audio_sample_rate: SampleRate,
|
||||
guild_id: Id<GuildMarker>,
|
||||
recording_data: Operator,
|
||||
recording_data_manager: RecordingDataManager,
|
||||
songbird: &Songbird,
|
||||
user_data_manager: UserDataManager,
|
||||
voice_channel_id: Id<ChannelMarker>,
|
||||
@@ -189,7 +187,7 @@ pub async fn join_and_record(
|
||||
let handler = Handler {
|
||||
start_instant,
|
||||
start_utc,
|
||||
recording_data,
|
||||
recording_data_manager,
|
||||
guild_id,
|
||||
channel_id: voice_channel_id,
|
||||
known_ssrcs: Default::default(),
|
||||
|
||||
@@ -123,7 +123,7 @@ pub async fn handle(state: State, interaction: Interaction) {
|
||||
.audio_channels(state.audio_channels)
|
||||
.audio_sample_rate(state.audio_sample_rate)
|
||||
.guild_id(guild_id)
|
||||
.recording_data(state.recording_data)
|
||||
.recording_data_manager(state.recording_data_manager)
|
||||
.songbird(&state.songbird)
|
||||
.user_data_manager(state.user_data_manager)
|
||||
.voice_channel_id(voice_channel_id)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use futures::future::BoxFuture;
|
||||
use opendal::Operator;
|
||||
use patricia_tree::StringPatriciaMap;
|
||||
use songbird::{
|
||||
Songbird,
|
||||
@@ -16,7 +15,10 @@ use twilight_model::{
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{BotDataManager, GuildVoiceChannelToTextChannel, UserDataManager, VCsSender};
|
||||
use crate::{
|
||||
BotDataManager, GuildVoiceChannelToTextChannel, UserDataManager, VCsSender,
|
||||
recording_data::RecordingDataManager, render_data::RenderDataManager,
|
||||
};
|
||||
|
||||
pub mod info;
|
||||
pub mod join;
|
||||
@@ -42,7 +44,8 @@ pub struct State {
|
||||
pub discord_opt_out_command_name: Arc<str>,
|
||||
pub discord_user_id: Id<UserMarker>,
|
||||
pub discord_voice_channel_corresponding_text_channel: Arc<GuildVoiceChannelToTextChannel>,
|
||||
pub recording_data: Operator,
|
||||
pub recording_data_manager: RecordingDataManager,
|
||||
pub render_data_manager: RenderDataManager,
|
||||
pub songbird: Arc<Songbird>,
|
||||
pub user_data_manager: UserDataManager,
|
||||
pub vcs_sender: VCsSender,
|
||||
|
||||
@@ -6,7 +6,10 @@ use twilight_model::{
|
||||
command::{Command, CommandOption, CommandOptionType, CommandType},
|
||||
interaction::{Interaction, InteractionData, application_command::CommandOptionValue},
|
||||
},
|
||||
channel::{ChannelType, message::{Embed, MessageFlags}},
|
||||
channel::{
|
||||
ChannelType,
|
||||
message::{Embed, MessageFlags},
|
||||
},
|
||||
http::interaction::{InteractionResponse, InteractionResponseType},
|
||||
id::{Id, marker::ChannelMarker},
|
||||
};
|
||||
@@ -122,7 +125,12 @@ enum ParseOptionsError {
|
||||
|
||||
impl From<ParseOptionsError> for Embed {
|
||||
fn from(error: ParseOptionsError) -> Self {
|
||||
EmbedBuilder::new().title("Error parsing options").description(Report::from_error(error).to_string()).validate().unwrap().build()
|
||||
EmbedBuilder::new()
|
||||
.title("Error parsing options")
|
||||
.description(Report::from_error(error).to_string())
|
||||
.validate()
|
||||
.unwrap()
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,5 +255,7 @@ pub async fn handle(state: State, interaction: Interaction) {
|
||||
|
||||
tracing::info!(?voice_channel, ?start, ?end);
|
||||
|
||||
let duration = end - start;
|
||||
|
||||
todo!();
|
||||
}
|
||||
|
||||
@@ -169,7 +169,8 @@ async fn get_heat(
|
||||
}
|
||||
}
|
||||
|
||||
let bot_owner_might_be_listening = bot_owner.is_some_and(|user_data| matches!(user_data.headphone, Headphone::Undeafened));
|
||||
let bot_owner_might_be_listening =
|
||||
bot_owner.is_some_and(|user_data| matches!(user_data.headphone, Headphone::Undeafened));
|
||||
|
||||
if bot_owner_might_be_listening {
|
||||
heat = heat.min(999);
|
||||
@@ -325,7 +326,7 @@ async fn follow_hottest_vc(
|
||||
.audio_channels(state.audio_channels)
|
||||
.audio_sample_rate(state.audio_sample_rate)
|
||||
.guild_id(guild_id)
|
||||
.recording_data(state.recording_data.clone())
|
||||
.recording_data_manager(state.recording_data_manager.clone())
|
||||
.songbird(&state.songbird)
|
||||
.user_data_manager(state.user_data_manager.clone())
|
||||
.voice_channel_id(hottest_vc)
|
||||
|
||||
@@ -7,6 +7,8 @@ mod one_to_many_with_data;
|
||||
mod one_to_one;
|
||||
mod operator_ext;
|
||||
mod option_ext;
|
||||
mod recording_data;
|
||||
mod render_data;
|
||||
mod storage;
|
||||
mod track_vcs;
|
||||
mod user_data;
|
||||
@@ -22,6 +24,8 @@ pub use one_to_many::OneToManyUniqueBTreeMap;
|
||||
pub use one_to_many_with_data::OneToManyUniqueBTreeMapWithData;
|
||||
pub use one_to_one::OneToOneBTreeMap;
|
||||
pub use operator_ext::OperatorExt;
|
||||
pub use recording_data::RecordingDataManager;
|
||||
pub use render_data::RenderDataManager;
|
||||
pub use storage::Storage;
|
||||
pub use track_vcs::{GuildVoiceChannelToTextChannel, VCs, VCsSender, initialize_vcs, update_vcs};
|
||||
pub use user_data::UserDataManager;
|
||||
|
||||
15
src/main.rs
15
src/main.rs
@@ -1,7 +1,8 @@
|
||||
use clap::Parser;
|
||||
use fomo_reducer::{
|
||||
BotDataManager, CommandRouter, GuildVoiceChannelToTextChannel, State, Storage, UserDataManager,
|
||||
VCsSender, all_commands, command, heat_seek, initialize_vcs, update_vcs,
|
||||
BotDataManager, CommandRouter, GuildVoiceChannelToTextChannel, RecordingDataManager,
|
||||
RenderDataManager, State, Storage, UserDataManager, VCsSender, all_commands, command,
|
||||
heat_seek, initialize_vcs, update_vcs,
|
||||
};
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use snafu::{OptionExt, ResultExt, Snafu};
|
||||
@@ -145,6 +146,9 @@ struct AppArgs {
|
||||
|
||||
#[arg(long, env)]
|
||||
recording_data: Storage,
|
||||
|
||||
#[arg(long, env)]
|
||||
render_data: Storage,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
@@ -207,6 +211,7 @@ async fn main() -> Result<(), MainError> {
|
||||
bot_data,
|
||||
user_data,
|
||||
recording_data,
|
||||
render_data,
|
||||
} = app_args;
|
||||
|
||||
let cancellation_token = CancellationToken::new();
|
||||
@@ -340,9 +345,12 @@ async fn main() -> Result<(), MainError> {
|
||||
|
||||
let bot_data = bot_data.into_inner();
|
||||
let recording_data = recording_data.into_inner();
|
||||
let render_data = render_data.into_inner();
|
||||
let user_data = user_data.into_inner();
|
||||
|
||||
let bot_data_manager = BotDataManager::new(bot_data);
|
||||
let recording_data_manager = RecordingDataManager::new(recording_data);
|
||||
let render_data_manager = RenderDataManager::new(render_data);
|
||||
let user_data_manager = UserDataManager::new(user_data);
|
||||
|
||||
let discord_voice_channel_corresponding_text_channel = {
|
||||
@@ -377,7 +385,8 @@ async fn main() -> Result<(), MainError> {
|
||||
discord_opt_out_command_name,
|
||||
discord_user_id,
|
||||
discord_voice_channel_corresponding_text_channel,
|
||||
recording_data,
|
||||
recording_data_manager,
|
||||
render_data_manager,
|
||||
songbird,
|
||||
user_data_manager,
|
||||
vcs_sender,
|
||||
|
||||
44
src/recording_data/between.rs
Normal file
44
src/recording_data/between.rs
Normal 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
111
src/recording_data/clip.rs
Normal 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
57
src/recording_data/day.rs
Normal 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),
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
22
src/recording_data/guild.rs
Normal file
22
src/recording_data/guild.rs
Normal 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))
|
||||
}
|
||||
58
src/recording_data/hour.rs
Normal file
58
src/recording_data/hour.rs
Normal 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),
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
21
src/recording_data/microsecond.rs
Normal file
21
src/recording_data/microsecond.rs
Normal 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))
|
||||
}
|
||||
59
src/recording_data/minute.rs
Normal file
59
src/recording_data/minute.rs
Normal 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
47
src/recording_data/mod.rs
Normal 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 },
|
||||
}
|
||||
55
src/recording_data/month.rs
Normal file
55
src/recording_data/month.rs
Normal 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),
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
143
src/recording_data/recording.rs
Normal file
143
src/recording_data/recording.rs
Normal 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!();
|
||||
}
|
||||
}
|
||||
25
src/recording_data/second.rs
Normal file
25
src/recording_data/second.rs
Normal 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))
|
||||
}
|
||||
25
src/recording_data/user.rs
Normal file
25
src/recording_data/user.rs
Normal 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)
|
||||
}
|
||||
26
src/recording_data/voice_channel.rs
Normal file
26
src/recording_data/voice_channel.rs
Normal 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))
|
||||
}
|
||||
51
src/recording_data/year.rs
Normal file
51
src/recording_data/year.rs
Normal 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),
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
12
src/render_data.rs
Normal file
12
src/render_data.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use opendal::Operator;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderDataManager {
|
||||
operator: Operator,
|
||||
}
|
||||
|
||||
impl RenderDataManager {
|
||||
pub fn new(operator: Operator) -> Self {
|
||||
Self { operator }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user