aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorXe Iaso <me@christine.website>2023-02-02 08:05:34 -0500
committerXe Iaso <me@christine.website>2023-02-02 08:05:34 -0500
commitc117eae7c5af977d0299d34169e4a403f77e2afa (patch)
tree83438cf5abc605e0ec174aa4a3f0e4b91b640008 /src
parent757cee6fdd13502eb62305f77e73698878b01aa2 (diff)
downloadxesite-c117eae7c5af977d0299d34169e4a403f77e2afa.tar.xz
xesite-c117eae7c5af977d0299d34169e4a403f77e2afa.zip
add stream VOD page
Signed-off-by: Xe Iaso <me@christine.website>
Diffstat (limited to 'src')
-rw-r--r--src/app/config.rs60
-rw-r--r--src/app/config/markdown_string.rs64
-rw-r--r--src/frontend/components/ConvSnippet.tsx27
-rw-r--r--src/handlers/mod.rs1
-rw-r--r--src/handlers/streams.rs94
-rw-r--r--src/main.rs4
-rw-r--r--src/tmpl/blog.rs6
-rw-r--r--src/tmpl/mod.rs2
8 files changed, 253 insertions, 5 deletions
diff --git a/src/app/config.rs b/src/app/config.rs
index 7ea46fd..8c7d64d 100644
--- a/src/app/config.rs
+++ b/src/app/config.rs
@@ -1,11 +1,15 @@
use crate::signalboost::Person;
-use maud::{html, Markup, Render};
+use chrono::prelude::*;
+use maud::{html, Markup, PreEscaped, Render};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fmt::{self, Display},
};
+mod markdown_string;
+use markdown_string::MarkdownString;
+
#[derive(Clone, Deserialize, Default)]
pub struct Config {
pub signalboost: Vec<Person>,
@@ -29,6 +33,7 @@ pub struct Config {
pub contact_links: Vec<Link>,
pub pronouns: Vec<PronounSet>,
pub characters: Vec<Character>,
+ pub vods: Vec<VOD>,
}
#[derive(Clone, Deserialize, Serialize, Default)]
@@ -336,3 +341,56 @@ impl Render for Job {
}
}
}
+
+#[derive(Clone, Deserialize, Serialize, Default)]
+pub struct VOD {
+ pub title: String,
+ pub slug: String,
+ pub date: NaiveDate,
+ pub description: MarkdownString,
+ #[serde(rename = "cdnPath")]
+ pub cdn_path: String,
+ pub tags: Vec<String>,
+}
+
+impl VOD {
+ pub fn detri(&self) -> String {
+ self.date.format("M%m %d %Y").to_string()
+ }
+}
+
+impl Render for VOD {
+ fn render(&self) -> Markup {
+ html! {
+ meta name="twitter:card" content="summary";
+ meta name="twitter:site" content="@theprincessxena";
+ meta name="twitter:title" content={(self.title)};
+ meta property="og:type" content="website";
+ meta property="og:title" content={(self.title)};
+ meta property="og:site_name" content="Xe's Blog";
+ meta name="description" content={(self.title) " - Xe's Blog"};
+ meta name="author" content="Xe Iaso";
+
+ h1 {(self.title)}
+ small {"Streamed on " (self.detri())}
+
+ (xesite_templates::advertiser_nag(Some(html!{
+ (xesite_templates::conv("Cadey".into(), "coffee".into(), html!{
+ "Hi. This page embeds a video file that is potentially multiple hours long. Hosting this stuff is not free. Bandwidth in particular is expensive. If you really want to continue to block ads, please consider donating via "
+ a href="https://patreon.com/cadey" {"Patreon"}
+ " because servers and bandwidth do not grow on trees."
+ }))
+ })))
+
+ (xesite_templates::video(self.cdn_path.clone()))
+ (self.description)
+ p {
+ "Tags: "
+ @for tag in &self.tags {
+ code{(tag)}
+ " "
+ }
+ }
+ }
+ }
+}
diff --git a/src/app/config/markdown_string.rs b/src/app/config/markdown_string.rs
new file mode 100644
index 0000000..aad803b
--- /dev/null
+++ b/src/app/config/markdown_string.rs
@@ -0,0 +1,64 @@
+use std::fmt;
+
+use maud::{html, Markup, PreEscaped, Render};
+use serde::{
+ de::{self, Visitor},
+ Deserialize, Deserializer, Serialize,
+};
+
+struct StringVisitor;
+
+impl<'de> Visitor<'de> for StringVisitor {
+ type Value = MarkdownString;
+
+ fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E>
+ where
+ E: de::Error,
+ {
+ Ok(MarkdownString(xesite_markdown::render(value).map_err(
+ |why| de::Error::invalid_value(de::Unexpected::Other(&format!("{why}")), &self),
+ )?))
+ }
+
+ fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
+ where
+ E: de::Error,
+ {
+ Ok(MarkdownString(xesite_markdown::render(value).map_err(
+ |why| de::Error::invalid_value(de::Unexpected::Other(&format!("{why}")), &self),
+ )?))
+ }
+
+ fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
+ where
+ E: de::Error,
+ {
+ Ok(MarkdownString(xesite_markdown::render(&value).map_err(
+ |why| de::Error::invalid_value(de::Unexpected::Other(&format!("{why}")), &self),
+ )?))
+ }
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a string with xesite-flavored markdown")
+ }
+}
+
+#[derive(Serialize, Clone, Default)]
+pub struct MarkdownString(String);
+
+impl<'de> Deserialize<'de> for MarkdownString {
+ fn deserialize<D>(deserializer: D) -> Result<MarkdownString, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer.deserialize_string(StringVisitor)
+ }
+}
+
+impl Render for MarkdownString {
+ fn render(&self) -> Markup {
+ html! {
+ (PreEscaped(&self.0))
+ }
+ }
+}
diff --git a/src/frontend/components/ConvSnippet.tsx b/src/frontend/components/ConvSnippet.tsx
new file mode 100644
index 0000000..02b2121
--- /dev/null
+++ b/src/frontend/components/ConvSnippet.tsx
@@ -0,0 +1,27 @@
+export interface ConvSnippetProps {
+ name: string;
+ mood: string;
+ children: HTMLElement[];
+}
+
+const ConvSnippet = ({name, mood, children}: ConvSnippetProps) => {
+ const nameLower = name.toLowerCase();
+ name = name.replace(" ", "_");
+
+ return (
+ <div className="conversation">
+ <div className="conversation-standalone">
+ <picture>
+ <source type="image/avif" srcset={`https://cdn.xeiaso.net/file/christine-static/stickers/${nameLower}/${mood}.avif`} />
+ <source type="image/webp" srcset={`https://cdn.xeiaso.net/file/christine-static/stickers/${nameLower}/${mood}.webp`} />
+ <img style="max-height:4.5rem" alt={`${name} is ${mood}`} loading="lazy" src={`https://cdn.xeiaso.net/file/christine-static/stickers/${nameLower}/${mood}.png`} />
+ </picture>
+ </div>
+ <div className="conversation-chat">
+ &lt;<a href={`/characters#${nameLower}`}><b>{name}</b></a>&gt; {children}
+ </div>
+ </div>
+ );
+};
+
+export default ConvSnippet;
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
index a638c77..dba9275 100644
--- a/src/handlers/mod.rs
+++ b/src/handlers/mod.rs
@@ -16,6 +16,7 @@ pub mod api;
pub mod blog;
pub mod feeds;
pub mod gallery;
+pub mod streams;
pub mod talks;
fn weekday_to_name(w: Weekday) -> &'static str {
diff --git a/src/handlers/streams.rs b/src/handlers/streams.rs
new file mode 100644
index 0000000..0d582a8
--- /dev/null
+++ b/src/handlers/streams.rs
@@ -0,0 +1,94 @@
+use crate::{
+ app::{State, VOD},
+ tmpl::{base, nag},
+};
+use axum::{extract::Path, Extension};
+use chrono::prelude::*;
+use http::StatusCode;
+use lazy_static::lazy_static;
+use maud::{html, Markup, Render};
+use prometheus::{opts, register_int_counter_vec, IntCounterVec};
+use serde::{Deserialize, Serialize};
+use std::sync::Arc;
+
+lazy_static! {
+ static ref HIT_COUNTER: IntCounterVec = register_int_counter_vec!(
+ opts!("streams_hits", "Number of hits to stream vod pages"),
+ &["name"]
+ )
+ .unwrap();
+}
+
+pub async fn list(Extension(state): Extension<Arc<State>>) -> Markup {
+ let state = state.clone();
+ let cfg = state.cfg.clone();
+
+ crate::tmpl::base(
+ Some("Stream VODs"),
+ None,
+ html! {
+ h1 {"Stream VODs"}
+ p {
+ "I'm a VTuber and I stream every other weekend on "
+ a href="https://twitch.tv/princessxen" {"Twitch"}
+ " about technology, the weird art of programming, and sometimes video games. This page will contain copies of my stream recordings/VODs so that you can watch your favorite stream again. All VOD pages support picture-in-picture mode so that you can have the recordings open in the background while you do something else."
+ }
+ p {
+ "Please note that to save on filesize, all videos are rendered at 720p and optimized for viewing at that resolution or on most mobile phone screens. If you run into video quality issues, please contact me as I am still trying to find the correct balance between video quality and filesize. These videos have been tested and known to work on most of the browser and OS combinations that visit this site."
+ }
+ ul {
+ @for vod in &cfg.vods {
+ li {
+ (vod.detri())
+ " - "
+ a href={
+ "/vods/"
+ (vod.date.year())
+ "/"
+ (vod.date.month())
+ "/"
+ (vod.slug)
+ } {(vod.title)}
+ }
+ }
+ }
+ },
+ )
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct ShowArgs {
+ pub year: i32,
+ pub month: u32,
+ pub slug: String,
+}
+
+pub async fn show(
+ Extension(state): Extension<Arc<State>>,
+ Path(args): Path<ShowArgs>,
+) -> (StatusCode, Markup) {
+ let state = state.clone();
+ let cfg = state.cfg.clone();
+
+ let mut found: Option<&VOD> = None;
+
+ for vod in &cfg.vods {
+ if vod.date.year() == args.year && vod.date.month() == args.month && vod.slug == args.slug {
+ found = Some(vod);
+ }
+ }
+
+ if found.is_none() {
+ return (
+ StatusCode::NOT_FOUND,
+ crate::tmpl::error(html! {
+ "What you requested may not exist. Good luck."
+ }),
+ );
+ }
+
+ let vod = found.unwrap();
+ HIT_COUNTER.with_label_values(&[&vod.slug]).inc();
+
+ (StatusCode::OK, base(Some(&vod.title), None, vod.render()))
+}
diff --git a/src/main.rs b/src/main.rs
index c5b0472..68f3371 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -175,6 +175,10 @@ async fn main() -> Result<()> {
.route("/signalboost", get(handlers::signalboost))
.route("/salary-transparency", get(handlers::salary_transparency))
.route("/pronouns", get(handlers::pronouns))
+ // vods
+ .route("/vods", get(handlers::streams::list))
+ .route("/vods/", get(handlers::streams::list))
+ .route("/vods/:year/:month/:slug", get(handlers::streams::show))
// feeds
.route("/blog.json", get(handlers::feeds::jsonfeed))
.route("/blog.atom", get(handlers::feeds::atom))
diff --git a/src/tmpl/blog.rs b/src/tmpl/blog.rs
index 2d8b603..ebee376 100644
--- a/src/tmpl/blog.rs
+++ b/src/tmpl/blog.rs
@@ -41,11 +41,11 @@ fn twitch_vod(post: &Post) -> Markup {
@if let Some(vod) = &post.front_matter.vod {
p {
"This post was written live on "
- a href="https://twitch.tv/princessxen" {"Twitch"}
+ a href="https://twitch.tv/princessxen" {"Twitch"}
". You can check out the stream recording on "
- a href=(vod.twitch) {"Twitch"}
+ a href=(vod.twitch) {"Twitch"}
" and on "
- a href=(vod.youtube) {"YouTube"}
+ a href=(vod.youtube) {"YouTube"}
". If you are reading this in the first day or so of this post being published, you will need to watch it on Twitch."
}
}
diff --git a/src/tmpl/mod.rs b/src/tmpl/mod.rs
index 5fbd77e..8f63867 100644
--- a/src/tmpl/mod.rs
+++ b/src/tmpl/mod.rs
@@ -80,7 +80,7 @@ pub fn base(title: Option<&str>, styles: Option<&str>, content: Markup) -> Marku
" - "
a href="/signalboost" { "Signal Boost" }
" - "
- a href="/feeds" { "Feeds" }
+ a href="/vods" { "VODs" }
" | "
a target="_blank" rel="noopener noreferrer" href="https://graphviz.christine.website" { "Graphviz" }
" - "