~nickbp/force-rss

972a4db5a5cc0ba270a97750e311aa69e28e6ed8 — Nick Parker 8 months ago 63687ed
Have the requestor pass the embed hostname explicitly - it can vary by client etc
5 files changed, 32 insertions(+), 40 deletions(-)

M README.md
M src/config.rs
M src/main.rs
M src/rss.rs
M src/server.rs
M README.md => README.md +7 -1
@@ 71,7 71,13 @@ Once `force-rss` is running, you can then query it to get RSS feeds:
For a Twitch feed, try `/twitch?account=ACCOUNT_NAME_HERE`:

```
$ curl -v 'http://127.0.0.1:8080/rss?account=ACCOUNT_NAME_HERE'
$ curl -v 'http://127.0.0.1:8080/twitch?account=ACCOUNT_NAME_HERE'
```

You can include the hostname where the RSS reader is running to get an embedded video iframe:

```
$ curl -v 'http://127.0.0.1:8080/twitch?account=ACCOUNT_NAME_HERE&embed_host=rssreader.example.com'
```

For a website that's been configured with CSS selectors in the TOML file:

M src/config.rs => src/config.rs +0 -9
@@ 45,8 45,6 @@ struct TomlTwitchConfig {
    thumbnail_width: String,
    #[serde(default = "default_twitch_thumbnail_height")]
    thumbnail_height: String,
    #[serde(default = "default_twitch_embed_video")]
    embed_video: bool,
    #[serde(default = "default_twitch_user_cache_secs")]
    user_cache_secs: u64,
    #[serde(default = "default_twitch_video_cache_secs")]


@@ 57,7 55,6 @@ fn default_twitch_config() -> TomlTwitchConfig {
    TomlTwitchConfig {
        thumbnail_width: default_twitch_thumbnail_width(),
        thumbnail_height: default_twitch_thumbnail_height(),
        embed_video: default_twitch_embed_video(),
        user_cache_secs: default_twitch_user_cache_secs(),
        video_cache_secs: default_twitch_video_cache_secs(),
    }


@@ 71,10 68,6 @@ fn default_twitch_thumbnail_height() -> String {
    "360".to_string()
}

fn default_twitch_embed_video() -> bool {
    true
}

fn default_twitch_user_cache_secs() -> u64 {
    86400 // 1 day
}


@@ 130,7 123,6 @@ impl Config {
                true => Duration::ZERO,
                false => Duration::from_secs(toml_config.twitch.video_cache_secs),
            },
            embed_video: toml_config.twitch.embed_video,
        };

        let mut site_configs_by_domain = HashMap::new();


@@ 182,7 174,6 @@ pub struct TwitchConfig {
    pub thumbnail_height: String,
    pub user_cache_ttl: Duration,
    pub video_cache_ttl: Duration,
    pub embed_video: bool,
}

#[derive(Clone)]

M src/main.rs => src/main.rs +1 -1
@@ 112,7 112,7 @@ async fn main_twitch(
    let videos = &twitch::get_videos(auth, &user.id).await?;
    println!(
        "{}",
        rss::twitch_rss(&user, videos, config, &Utc::now(), &None)?
        rss::twitch_rss(&user, videos, config, &None, &Utc::now())?
    );
    Ok(())
}

M src/rss.rs => src/rss.rs +12 -17
@@ 2,7 2,6 @@ use ::rss::{ChannelBuilder, ImageBuilder, ItemBuilder};
use anyhow::Result;
use chrono::{DateTime, Local, TimeZone, Utc};
use url::Url;
use warp::host::Authority;

use crate::{config, site, twitch};



@@ 12,8 11,8 @@ pub fn twitch_rss(
    user: &twitch::GetUsersEntry,
    videos: &Vec<twitch::GetVideosEntry>,
    config: &config::TwitchConfig,
    embed_host: &Option<String>,
    last_updated: &DateTime<Utc>,
    authority: &Option<Authority>,
) -> Result<String> {
    let mut items = vec![];
    for video in videos {


@@ 27,7 26,7 @@ pub fn twitch_rss(
        let mut item_builder = ItemBuilder::default();
        item_builder
            .title(Some(format!("{} ({})", video.title, video.duration)))
            .content(Some(twitch_content_html(video, config, authority)))
            .content(Some(twitch_content_html(video, config, embed_host)))
            .link(Some(video.url.to_string()))
            .pub_date(Some(pub_date));
        if !video.description.is_empty() {


@@ 59,21 58,17 @@ pub fn twitch_rss(
fn twitch_content_html(
    video: &twitch::GetVideosEntry,
    config: &config::TwitchConfig,
    authority: &Option<Authority>,
    embed_host: &Option<String>,
) -> String {
    if let Some(authority) = authority {
        if config.embed_video {
            // We use the iframe method, since RSS readers dislike the javascript method.
            // The downside is that the iframe method requires that we pass the authority/hostname.
            return format!(
                "<iframe width=\"{width}\" height=\"{height}\" frameborder=\"0\" src=\"https://player.twitch.tv/?video=v{id}&parent={authority}\" allowfullscreen=\"\" loading=\"lazy\"></iframe>",
                id=video.id, width=config.thumbnail_width, height=config.thumbnail_height, authority=authority.host()
            );
        }
    }

    // Ignore invalid thumbnails like: https://vod-secure.twitch.tv/_404/404_processing_640x360.png
    if video.thumbnail_url.contains("/_404") {
    if let Some(embed_host) = embed_host {
        // We use the iframe method, since RSS readers dislike the javascript method.
        // The downside is that the iframe method requires that we pass the authority/hostname.
        format!(
            "<iframe width=\"{width}\" height=\"{height}\" frameborder=\"0\" src=\"https://player.twitch.tv/?video=v{id}&parent={embed_host}\" allowfullscreen=\"\" loading=\"lazy\"></iframe>",
            id=video.id, width=config.thumbnail_width, height=config.thumbnail_height, embed_host=embed_host
        )
    } else if video.thumbnail_url.contains("/_404") {
        // Ignore invalid thumbnails like: https://vod-secure.twitch.tv/_404/404_processing_640x360.png
        format!(
            "<p>[Stream still processing...]</p><p>{}</p>",
            video.duration

M src/server.rs => src/server.rs +12 -12
@@ 43,6 43,7 @@ struct SiteQuery {
#[derive(Deserialize)]
struct TwitchQuery {
    account: String,
    embed_host: Option<String>,
}

pub async fn run(config: config::Config, auth: Option<twitch_auth::TwitchAuth>) -> Result<()> {


@@ 125,15 126,12 @@ pub async fn run(config: config::Config, auth: Option<twitch_auth::TwitchAuth>) 
        let twitch = warp::path("twitch")
            .and(warp::path::end())
            .and(warp::get())
            .and(warp::host::optional())
            .and(warp::query::<TwitchQuery>())
            .and(warp::any().map(move || twitch_state.clone()))
            .then(
                |authority: Option<Authority>,
                 query: TwitchQuery,
                 state: Arc<Mutex<TwitchState>>| async move {
                |query: TwitchQuery, state: Arc<Mutex<TwitchState>>| async move {
                    let mut state = state.lock().await;
                    match handle_twitch(query, &mut state, authority).await {
                    match handle_twitch(query, &mut state).await {
                        Ok(resp) => warp::reply::with_header(
                            resp,
                            warp::http::header::CONTENT_TYPE,


@@ 248,11 246,7 @@ async fn handle_site(query: SiteQuery, state: &mut SiteState) -> Result<String> 
    Ok(response_body)
}

async fn handle_twitch(
    query: TwitchQuery,
    state: &mut TwitchState,
    authority: Option<Authority>,
) -> Result<String> {
async fn handle_twitch(query: TwitchQuery, state: &mut TwitchState) -> Result<String> {
    let user: twitch::GetUsersEntry = match state.username_to_user_cache.get(&query.account).await {
        Some(u) => {
            // Use cached user info


@@ 281,14 275,20 @@ async fn handle_twitch(
            &user,
            &read_guard.videos,
            &state.config,
            &query.embed_host,
            &read_guard.last_updated,
            &authority,
        );
    }

    let videos = twitch::get_videos(&mut state.auth, &user.id).await?;
    let last_updated = Utc::now();
    let response_body = rss::twitch_rss(&user, &videos, &state.config, &last_updated, &authority)?;
    let response_body = rss::twitch_rss(
        &user,
        &videos,
        &state.config,
        &query.embed_host,
        &last_updated,
    )?;
    // Only add content to cache AFTER we've successfully converted it to RSS
    if !state.config.video_cache_ttl.is_zero() {
        let cache_val = VideosCacheVal {