use std::{
fmt::{self, Debug, Display, Formatter},
vec::IntoIter,
};
use chrono::{NaiveDate, NaiveDateTime};
use serde::Deserialize;
use crate::{
attr::{Attribute, AttributeId},
client::Client,
error::ApiResult,
film::{Film, FilmFormat, FilmId},
package::{FilmPackage, FilmPackageId},
screen::{Screen, ScreenId},
};
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
#[serde(rename_all = "PascalCase")]
pub enum Seating {
Allocated,
Select,
Open,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
#[serde(rename_all = "PascalCase")]
pub enum ShowType {
Private,
Public,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
#[serde(rename_all = "PascalCase")]
pub enum SessionStatus {
Open,
Closed,
Planned,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[allow(clippy::struct_excessive_bools)] pub struct SalesVia {
pub kiosk: bool,
pub pos: bool,
pub www: bool,
pub mx: bool,
pub rsp: bool,
}
impl<'de> Deserialize<'de> for SalesVia {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let vec: Vec<String> = Deserialize::deserialize(deserializer)?;
let mut sales_via = Self {
kiosk: false,
pos: false,
www: false,
mx: false,
rsp: false,
};
for entry in vec {
match entry.as_str() {
"KIOSK" => sales_via.kiosk = true,
"POS" => sales_via.pos = true,
"WWW" => sales_via.www = true,
"MX" => sales_via.mx = true,
"RSP" => sales_via.rsp = true,
_ => {}
}
}
Ok(sales_via)
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SessionList(Vec<Session>);
impl SessionList {
#[must_use]
pub fn into_vec(self) -> Vec<Session> {
self.0
}
#[must_use]
pub const fn as_vec(&self) -> &Vec<Session> {
&self.0
}
#[must_use]
pub fn filter_by_screen(self, screen_id: ScreenId) -> Self {
let filtered: Vec<Session> = self
.0
.into_iter()
.filter(|session| session.screen_id == screen_id)
.collect();
Self(filtered)
}
#[must_use]
pub fn filter_by_film(self, film_id: &FilmId) -> Self {
let filtered: Vec<Session> = self
.0
.into_iter()
.filter(|session| session.film_id == *film_id)
.collect();
Self(filtered)
}
#[must_use]
pub fn filter_containing_attribute(self, attribute_id: &AttributeId) -> Self {
let filtered: Vec<Session> = self
.0
.into_iter()
.filter(|session| session.attributes.contains(attribute_id))
.collect();
Self(filtered)
}
#[must_use]
pub fn filter_by_time_range(self, start: NaiveDateTime, end: NaiveDateTime) -> Self {
let filtered: Vec<Session> = self
.0
.into_iter()
.filter(|session| {
let date = session.pre_show_start_time;
date >= start && date <= end
})
.collect();
Self(filtered)
}
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn filter_by_date_range(self, start: NaiveDate, end: NaiveDate) -> Self {
self.filter_by_time_range(
start.and_hms_opt(0, 0, 0).expect("midnight should exist"),
end.and_hms_opt(0, 0, 0).expect("midnight should exist"),
)
}
#[must_use]
pub fn group_by_date(&self) -> Vec<(NaiveDate, Vec<&Session>)> {
let mut grouped: Vec<(NaiveDate, Vec<&Session>)> = Vec::new();
for session in &self.0 {
let date = session.pre_show_start_time.date();
if let Some((_, sessions)) = grouped.iter_mut().find(|(d, _)| *d == date) {
sessions.push(session);
} else {
grouped.push((date, vec![session]));
}
}
grouped.sort_by(|(a, _), (b, _)| a.cmp(b));
grouped
}
pub async fn films(&self, client: &Client) -> ApiResult<Vec<Film>> {
let mut films = Vec::new();
let mut seen_ids = Vec::new();
for session in &self.0 {
if !seen_ids.contains(&session.film_id) {
let film = client.get_film(&session.film_id).await?;
films.push(film);
seen_ids.push(session.film_id.clone());
}
}
Ok(films)
}
pub async fn screens(&self, client: &Client) -> ApiResult<Vec<Screen>> {
let mut screens = Vec::new();
let mut seen_ids = Vec::new();
for session in &self.0 {
if !seen_ids.contains(&session.screen_id) {
let screen = client.get_screen(session.screen_id).await?;
screens.push(screen);
seen_ids.push(session.screen_id);
}
}
Ok(screens)
}
pub fn iter(&self) -> impl Iterator<Item = &Session> {
self.0.iter()
}
}
impl From<Vec<Session>> for SessionList {
fn from(sessions: Vec<Session>) -> Self {
Self(sessions)
}
}
impl From<SessionList> for Vec<Session> {
fn from(val: SessionList) -> Self {
val.0
}
}
impl IntoIterator for SessionList {
type Item = Session;
type IntoIter = IntoIter<Session>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash)]
#[serde(transparent)]
pub struct SessionId(u32);
impl SessionId {
#[must_use]
pub const fn into_u32(self) -> u32 {
self.0
}
pub async fn fetch(self, client: &Client) -> ApiResult<Session> {
client.get_session(self).await
}
}
impl Display for SessionId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct Session {
pub id: SessionId,
pub film_id: FilmId,
pub film_package_id: Option<FilmPackageId>,
pub title: String,
pub screen_id: ScreenId,
pub seating: Seating,
pub are_complimentaries_allowed: bool,
pub show_type: ShowType,
pub sales_via: SalesVia,
pub status: SessionStatus,
pub pre_show_start_time: NaiveDateTime,
pub sales_cut_off_time: NaiveDateTime,
pub feature_start_time: NaiveDateTime,
pub feature_end_time: NaiveDateTime,
pub cleanup_end_time: NaiveDateTime,
pub tickets_sold_out: bool,
pub few_tickets_left: bool,
pub seats_available: u32,
pub seats_held: u32,
pub seats_house: u32,
pub seats_sold: u32,
pub film_format: FilmFormat,
pub price_card_name: String,
pub attributes: Vec<AttributeId>,
pub audio_language: Option<String>,
}
impl Session {
pub async fn film(&self, client: &Client) -> ApiResult<Film> {
client.get_film(&self.film_id).await
}
pub async fn film_package(&self, client: &Client) -> ApiResult<Option<FilmPackage>> {
match &self.film_package_id {
Some(id) => {
let package = client.get_film_package(*id).await?;
Ok(Some(package))
}
None => Ok(None),
}
}
pub async fn screen(&self, client: &Client) -> ApiResult<Screen> {
client.get_screen(self.screen_id).await
}
pub async fn attributes(&self, client: &Client) -> ApiResult<Vec<Attribute>> {
let mut attrs = Vec::new();
for attr_id in &self.attributes {
let attr = client.get_attribute(attr_id).await?;
attrs.push(attr);
}
Ok(attrs)
}
#[must_use]
pub fn is_open_for_sales(&self) -> bool {
let now = chrono::Utc::now().naive_utc();
self.status == SessionStatus::Open
&& now < self.sales_cut_off_time
&& self.seats_available > 0
}
}