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::{
|
use crate::{
|
||||||
OneToManyUniqueBTreeMap, UserDataManager, option_ext::OptionExt as _,
|
OneToManyUniqueBTreeMap, UserDataManager,
|
||||||
user_capnp::user::Consent, user_data::RECORD_IF_CONSENT_UNSPECIFIED,
|
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 async_trait::async_trait;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
@@ -26,7 +29,7 @@ struct Handler {
|
|||||||
start_instant: Instant,
|
start_instant: Instant,
|
||||||
start_utc: UtcDateTime,
|
start_utc: UtcDateTime,
|
||||||
|
|
||||||
recording_data: Operator,
|
recording_data_manager: RecordingDataManager,
|
||||||
|
|
||||||
guild_id: Id<GuildMarker>,
|
guild_id: Id<GuildMarker>,
|
||||||
channel_id: Id<ChannelMarker>,
|
channel_id: Id<ChannelMarker>,
|
||||||
@@ -104,49 +107,44 @@ impl EventHandler for Handler {
|
|||||||
let minute = now_utc.minute();
|
let minute = now_utc.minute();
|
||||||
let second = now_utc.second();
|
let second = now_utc.second();
|
||||||
|
|
||||||
let microseconds = now_utc.microsecond();
|
let microsecond = now_utc.microsecond();
|
||||||
|
|
||||||
let guild_id = self.guild_id;
|
let guild = self.guild_id;
|
||||||
let channel_id = self.channel_id;
|
let voice_channel = self.channel_id;
|
||||||
|
|
||||||
let user = user_id
|
let user = user_id;
|
||||||
.as_ref()
|
|
||||||
.map_or_else(|| "UNKNOWN".into(), ToString::to_string);
|
|
||||||
|
|
||||||
let path = format!(
|
let clip = Clip {
|
||||||
"{year}/{month}/{day}/{hour}/{minute}/audio-{second}.{microseconds}-{guild_id}-{channel_id}-{user}.wav"
|
second,
|
||||||
);
|
microsecond,
|
||||||
|
guild,
|
||||||
|
voice_channel,
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
|
||||||
|
let recording = Recording {
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
hour,
|
||||||
|
minute,
|
||||||
|
clip,
|
||||||
|
};
|
||||||
|
|
||||||
let channels = self.audio_channels;
|
let channels = self.audio_channels;
|
||||||
let sample_rate = self.audio_sample_rate;
|
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");
|
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 {
|
tokio::spawn(async move {
|
||||||
recording_data.write(&path, buffer).await.expect("TODO");
|
recording_data_manager
|
||||||
tracing::info!(?path, "successfully wrote the audio!");
|
.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_channels: Channels,
|
||||||
audio_sample_rate: SampleRate,
|
audio_sample_rate: SampleRate,
|
||||||
guild_id: Id<GuildMarker>,
|
guild_id: Id<GuildMarker>,
|
||||||
recording_data: Operator,
|
recording_data_manager: RecordingDataManager,
|
||||||
songbird: &Songbird,
|
songbird: &Songbird,
|
||||||
user_data_manager: UserDataManager,
|
user_data_manager: UserDataManager,
|
||||||
voice_channel_id: Id<ChannelMarker>,
|
voice_channel_id: Id<ChannelMarker>,
|
||||||
@@ -189,7 +187,7 @@ pub async fn join_and_record(
|
|||||||
let handler = Handler {
|
let handler = Handler {
|
||||||
start_instant,
|
start_instant,
|
||||||
start_utc,
|
start_utc,
|
||||||
recording_data,
|
recording_data_manager,
|
||||||
guild_id,
|
guild_id,
|
||||||
channel_id: voice_channel_id,
|
channel_id: voice_channel_id,
|
||||||
known_ssrcs: Default::default(),
|
known_ssrcs: Default::default(),
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ pub async fn handle(state: State, interaction: Interaction) {
|
|||||||
.audio_channels(state.audio_channels)
|
.audio_channels(state.audio_channels)
|
||||||
.audio_sample_rate(state.audio_sample_rate)
|
.audio_sample_rate(state.audio_sample_rate)
|
||||||
.guild_id(guild_id)
|
.guild_id(guild_id)
|
||||||
.recording_data(state.recording_data)
|
.recording_data_manager(state.recording_data_manager)
|
||||||
.songbird(&state.songbird)
|
.songbird(&state.songbird)
|
||||||
.user_data_manager(state.user_data_manager)
|
.user_data_manager(state.user_data_manager)
|
||||||
.voice_channel_id(voice_channel_id)
|
.voice_channel_id(voice_channel_id)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::{fmt::Debug, sync::Arc};
|
use std::{fmt::Debug, sync::Arc};
|
||||||
|
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use opendal::Operator;
|
|
||||||
use patricia_tree::StringPatriciaMap;
|
use patricia_tree::StringPatriciaMap;
|
||||||
use songbird::{
|
use songbird::{
|
||||||
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 info;
|
||||||
pub mod join;
|
pub mod join;
|
||||||
@@ -42,7 +44,8 @@ pub struct State {
|
|||||||
pub discord_opt_out_command_name: Arc<str>,
|
pub discord_opt_out_command_name: Arc<str>,
|
||||||
pub discord_user_id: Id<UserMarker>,
|
pub discord_user_id: Id<UserMarker>,
|
||||||
pub discord_voice_channel_corresponding_text_channel: Arc<GuildVoiceChannelToTextChannel>,
|
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 songbird: Arc<Songbird>,
|
||||||
pub user_data_manager: UserDataManager,
|
pub user_data_manager: UserDataManager,
|
||||||
pub vcs_sender: VCsSender,
|
pub vcs_sender: VCsSender,
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ use twilight_model::{
|
|||||||
command::{Command, CommandOption, CommandOptionType, CommandType},
|
command::{Command, CommandOption, CommandOptionType, CommandType},
|
||||||
interaction::{Interaction, InteractionData, application_command::CommandOptionValue},
|
interaction::{Interaction, InteractionData, application_command::CommandOptionValue},
|
||||||
},
|
},
|
||||||
channel::{ChannelType, message::{Embed, MessageFlags}},
|
channel::{
|
||||||
|
ChannelType,
|
||||||
|
message::{Embed, MessageFlags},
|
||||||
|
},
|
||||||
http::interaction::{InteractionResponse, InteractionResponseType},
|
http::interaction::{InteractionResponse, InteractionResponseType},
|
||||||
id::{Id, marker::ChannelMarker},
|
id::{Id, marker::ChannelMarker},
|
||||||
};
|
};
|
||||||
@@ -122,7 +125,12 @@ enum ParseOptionsError {
|
|||||||
|
|
||||||
impl From<ParseOptionsError> for Embed {
|
impl From<ParseOptionsError> for Embed {
|
||||||
fn from(error: ParseOptionsError) -> Self {
|
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);
|
tracing::info!(?voice_channel, ?start, ?end);
|
||||||
|
|
||||||
|
let duration = end - start;
|
||||||
|
|
||||||
todo!();
|
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 {
|
if bot_owner_might_be_listening {
|
||||||
heat = heat.min(999);
|
heat = heat.min(999);
|
||||||
@@ -325,7 +326,7 @@ async fn follow_hottest_vc(
|
|||||||
.audio_channels(state.audio_channels)
|
.audio_channels(state.audio_channels)
|
||||||
.audio_sample_rate(state.audio_sample_rate)
|
.audio_sample_rate(state.audio_sample_rate)
|
||||||
.guild_id(guild_id)
|
.guild_id(guild_id)
|
||||||
.recording_data(state.recording_data.clone())
|
.recording_data_manager(state.recording_data_manager.clone())
|
||||||
.songbird(&state.songbird)
|
.songbird(&state.songbird)
|
||||||
.user_data_manager(state.user_data_manager.clone())
|
.user_data_manager(state.user_data_manager.clone())
|
||||||
.voice_channel_id(hottest_vc)
|
.voice_channel_id(hottest_vc)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ mod one_to_many_with_data;
|
|||||||
mod one_to_one;
|
mod one_to_one;
|
||||||
mod operator_ext;
|
mod operator_ext;
|
||||||
mod option_ext;
|
mod option_ext;
|
||||||
|
mod recording_data;
|
||||||
|
mod render_data;
|
||||||
mod storage;
|
mod storage;
|
||||||
mod track_vcs;
|
mod track_vcs;
|
||||||
mod user_data;
|
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_many_with_data::OneToManyUniqueBTreeMapWithData;
|
||||||
pub use one_to_one::OneToOneBTreeMap;
|
pub use one_to_one::OneToOneBTreeMap;
|
||||||
pub use operator_ext::OperatorExt;
|
pub use operator_ext::OperatorExt;
|
||||||
|
pub use recording_data::RecordingDataManager;
|
||||||
|
pub use render_data::RenderDataManager;
|
||||||
pub use storage::Storage;
|
pub use storage::Storage;
|
||||||
pub use track_vcs::{GuildVoiceChannelToTextChannel, VCs, VCsSender, initialize_vcs, update_vcs};
|
pub use track_vcs::{GuildVoiceChannelToTextChannel, VCs, VCsSender, initialize_vcs, update_vcs};
|
||||||
pub use user_data::UserDataManager;
|
pub use user_data::UserDataManager;
|
||||||
|
|||||||
15
src/main.rs
15
src/main.rs
@@ -1,7 +1,8 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use fomo_reducer::{
|
use fomo_reducer::{
|
||||||
BotDataManager, CommandRouter, GuildVoiceChannelToTextChannel, State, Storage, UserDataManager,
|
BotDataManager, CommandRouter, GuildVoiceChannelToTextChannel, RecordingDataManager,
|
||||||
VCsSender, all_commands, command, heat_seek, initialize_vcs, update_vcs,
|
RenderDataManager, State, Storage, UserDataManager, VCsSender, all_commands, command,
|
||||||
|
heat_seek, initialize_vcs, update_vcs,
|
||||||
};
|
};
|
||||||
use secrecy::{ExposeSecret, SecretString};
|
use secrecy::{ExposeSecret, SecretString};
|
||||||
use snafu::{OptionExt, ResultExt, Snafu};
|
use snafu::{OptionExt, ResultExt, Snafu};
|
||||||
@@ -145,6 +146,9 @@ struct AppArgs {
|
|||||||
|
|
||||||
#[arg(long, env)]
|
#[arg(long, env)]
|
||||||
recording_data: Storage,
|
recording_data: Storage,
|
||||||
|
|
||||||
|
#[arg(long, env)]
|
||||||
|
render_data: Storage,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
@@ -207,6 +211,7 @@ async fn main() -> Result<(), MainError> {
|
|||||||
bot_data,
|
bot_data,
|
||||||
user_data,
|
user_data,
|
||||||
recording_data,
|
recording_data,
|
||||||
|
render_data,
|
||||||
} = app_args;
|
} = app_args;
|
||||||
|
|
||||||
let cancellation_token = CancellationToken::new();
|
let cancellation_token = CancellationToken::new();
|
||||||
@@ -340,9 +345,12 @@ async fn main() -> Result<(), MainError> {
|
|||||||
|
|
||||||
let bot_data = bot_data.into_inner();
|
let bot_data = bot_data.into_inner();
|
||||||
let recording_data = recording_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 user_data = user_data.into_inner();
|
||||||
|
|
||||||
let bot_data_manager = BotDataManager::new(bot_data);
|
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 user_data_manager = UserDataManager::new(user_data);
|
||||||
|
|
||||||
let discord_voice_channel_corresponding_text_channel = {
|
let discord_voice_channel_corresponding_text_channel = {
|
||||||
@@ -377,7 +385,8 @@ async fn main() -> Result<(), MainError> {
|
|||||||
discord_opt_out_command_name,
|
discord_opt_out_command_name,
|
||||||
discord_user_id,
|
discord_user_id,
|
||||||
discord_voice_channel_corresponding_text_channel,
|
discord_voice_channel_corresponding_text_channel,
|
||||||
recording_data,
|
recording_data_manager,
|
||||||
|
render_data_manager,
|
||||||
songbird,
|
songbird,
|
||||||
user_data_manager,
|
user_data_manager,
|
||||||
vcs_sender,
|
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