Skip to content

Commit

Permalink
streaming media api
Browse files Browse the repository at this point in the history
  • Loading branch information
drewvolz authored Jan 25, 2025
1 parent cd3ee26 commit ed61e39
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 1 deletion.
72 changes: 72 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ccc-handlers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ thiserror = { version = "2.0.0" }
tracing = "0.1.40"
serde_urlencoded = "0.7.1"
phf = { version = "0.11.2", features = ["macros"] }
chrono = { version = "0.4", features = ["serde"] }
1 change: 1 addition & 0 deletions ccc-handlers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@

pub mod bonapp;
pub mod github;
pub mod streams;
154 changes: 154 additions & 0 deletions ccc-handlers/src/streams.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use axum::{
extract::Query,
response::{IntoResponse, Json, Response},
};
use chrono::{Duration, Utc};
use http::StatusCode;
use serde::{Deserialize, Serialize};
use tracing::instrument;

#[derive(Debug, Deserialize)]
pub struct StreamParams {
#[serde(default)]
date_from: String,
#[serde(default)]
date_to: String,
#[serde(default = "default_sort")]
sort: String,
}

fn default_sort() -> String {
"ascending".to_string()
}

#[derive(Debug, Serialize)]
enum QueryClass {
Archived,
Upcoming,
}

impl std::fmt::Display for QueryClass {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
QueryClass::Archived => write!(f, "archived"),
QueryClass::Upcoming => write!(f, "current"),
}
}
}

#[inline]
const fn get_query_base_url_and_entity(query_class: &QueryClass) -> (&str, &str) {
match query_class {
QueryClass::Archived => (
"https://www.stolaf.edu/multimedia/api/collection",
"archived",
),
QueryClass::Upcoming => (
"https://www.stolaf.edu/multimedia/api/collection",
"current",
),
}
}

#[instrument]
async fn send_proxied_query<T>(
query_class: QueryClass,
date_from: &str,
date_to: &str,
sort: &str,
) -> Result<Json<T>, StreamProxyError>
where
T: serde::de::DeserializeOwned,
{
tracing::debug!(
sort,
date_to,
date_from,
?query_class,
"handling proxied Stream request"
);

let (base_url, _class) = get_query_base_url_and_entity(&query_class);
let query_type = query_class.to_string();

let request = ccc_proxy::global_proxy()
.client()
.get(base_url)
.query(&[
("class", query_type.as_str()),
("date_from", date_from),
("date_to", date_to),
("sort", sort),
])
.build()
.map_err(ccc_proxy::ProxyError::ProxiedRequest)
.map_err(StreamProxyError::GenericProxy)?;

ccc_proxy::global_proxy()
.send_request_parse_json::<T>(request)
.await
.map(Json)
.map_err(StreamProxyError::GenericProxy)
}

#[instrument(skip(date_from_fn, date_to_fn))]
async fn handle_stream_request(
params: StreamParams,
date_from_fn: impl Fn(chrono::DateTime<Utc>) -> chrono::DateTime<Utc>,
date_to_fn: impl Fn(chrono::DateTime<Utc>) -> chrono::DateTime<Utc>,
query_class: QueryClass,
) -> Result<Json<ccc_types::streams::StreamResponse>, StreamProxyError> {
let now = Utc::now();

let date_from = (!params.date_from.is_empty())
.then(|| params.date_from.clone())
.unwrap_or_else(|| date_from_fn(now).format("%Y-%m-%d").to_string());

let date_to = (!params.date_to.is_empty())
.then(|| params.date_to.clone())
.unwrap_or_else(|| date_to_fn(now).format("%Y-%m-%d").to_string());

send_proxied_query(query_class, &date_from, &date_to, &params.sort).await
}

#[instrument]
pub async fn upcoming_handler(
Query(params): Query<StreamParams>,
) -> Result<Json<ccc_types::streams::StreamResponse>, StreamProxyError> {
handle_stream_request(
params,
|now| now,
|now| now + Duration::days(60),
QueryClass::Upcoming,
)
.await
}

#[instrument]
pub async fn archived_handler(
Query(params): Query<StreamParams>,
) -> Result<Json<ccc_types::streams::StreamResponse>, StreamProxyError> {
handle_stream_request(
params,
|now| now - Duration::days(60),
|now| now,
QueryClass::Archived,
)
.await
}

#[derive(thiserror::Error, Debug)]
pub enum StreamProxyError {
#[error("error from generic proxy: {0}")]
GenericProxy(ccc_proxy::ProxyError),
}

impl IntoResponse for StreamProxyError {
fn into_response(self) -> Response {
let text = self.to_string();
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(text.into())
.unwrap()
}
}
1 change: 1 addition & 0 deletions ccc-routes/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod faqs;
pub mod food;
pub mod printing;
pub mod spaces;
pub mod streams;
pub mod tools;
pub mod transit;
pub mod webcams;
8 changes: 8 additions & 0 deletions ccc-routes/src/streams.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use axum::{routing::get, Router};
use ccc_handlers::streams::{archived_handler, upcoming_handler};

pub fn router() -> Router {
Router::new()
.route("/archived", get(archived_handler))
.route("/upcoming", get(upcoming_handler))
}
1 change: 1 addition & 0 deletions ccc-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ fn init_router() -> Router {
.nest("/food", ccc_routes::food::router())
.nest("/printing", ccc_routes::printing::router())
.nest("/spaces", ccc_routes::spaces::router())
.nest("/streams", ccc_routes::streams::router())
.nest("/tools", ccc_routes::tools::router())
.nest("/transit", ccc_routes::transit::router())
.nest("/webcams", ccc_routes::webcams::router());
Expand Down
2 changes: 1 addition & 1 deletion ccc-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ publish = false
[dependencies]
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
ts-rs = {version = "10.0.0" }
ts-rs = { version = "10.0.0" }
20 changes: 20 additions & 0 deletions ccc-types/bindings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,26 @@ export interface Schedule {
isPhysicallyOpen: boolean | null;
}

export type Stream = {
starttime: string;
location: string;
eid: string;
performer: string;
subtitle: string;
poster: string;
player: string;
status: string;
category: string;
hptitle: string;
category_textcolor: string | null;
category_color: string | null;
thumb: string;
title: string;
iframesrc: string;
};

export type StreamResponse = { results: Array<Stream> };

export interface TransitColors {
bar: string;
dot: string;
Expand Down
1 change: 1 addition & 0 deletions ccc-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod faqs;
pub mod food;
pub mod printing;
pub mod spaces;
pub mod streams;
pub mod tools;
pub mod transit;
pub mod webcams;
34 changes: 34 additions & 0 deletions ccc-types/src/streams.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;

#[derive(Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, rename = "Stream")]
pub struct StreamEntry {
pub starttime: String,
pub location: String,
pub eid: String,
pub performer: String,
pub subtitle: String,
pub poster: String,
pub player: String,
pub status: String,
pub category: String,
pub hptitle: String,
// issues with (de)serde/renaming this field
#[serde(rename = "category_textcolor")]
pub category_textcolor: Option<String>,
// issues with (de)serde/renaming this field
#[serde(rename = "category_color")]
pub category_color: Option<String>,
pub thumb: String,
pub title: String,
pub iframesrc: String,
}

#[derive(Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct StreamResponse {
pub results: Vec<StreamEntry>,
}

0 comments on commit ed61e39

Please sign in to comment.