Downloading Podcasts

Today I decided to write a little program to download podcasts for me. Turns out Rust has an excellent rss crate which does most of the heavy lifting. The rest is done by reqwest.

I had something rudimentary working in short order

async fn get_feed(url: &str) -> Result<Channel, Box<dyn std::error::Error>> {
    let content = reqwest::get(url).await?.bytes().await?;
    let channel = Channel::read_from(&content[..])?;
    Ok(channel)
}

and I started happily adding the podcasts I wanted to it. But when I got to My Favorite Theorem, it stopped working. The only feedback from rss was Error: InvalidStartTag. Hrm. I looked at the RSS file, but it seemed okay. What's up?

I had a look at the git repo and there was only one place where that error appears I cloned the repo and added a debug statement there.

    dbg!(element.name());

It produced "104, 101, 97, 100" which spells "head." What? There isn't a <head tag in that file anywhere. Then I realized I was trying to parse the bytes of a response failure as an RSS feed. This example assumes success! So much for testing a bugfix in rss.

So why was reqwest giving me a failure for just that one podcast? What was different about it? I found the answer in the closed issues: there is no user-agent header. None of the other podcast feeds cared, but My Favorite Theorem (really Squarespace, I guess) did. Thanks to Bauke for asking my question last year and thanks to seanmonstar for answering it!

There are a number of ways to add a header to a reqwest. Here's one

async fn get_feed(url: &str) -> Result<Channel, Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();

    let content = client
        .get(url)
        .header(reqwest::header::USER_AGENT, "feedread 0.1")
        .send()
        .await?
        .bytes()
        .await?;

    let channel = Channel::read_from(&content[..])?;

    Ok(channel)
}

But it turns out the ClientBuilder struct has a user_agent method with an example that does exactly what I want: name the user agent after the app.

// Name your user agent after your app?
const APP_USER_AGENT: &str = concat!(
    env!("CARGO_PKG_NAME"),
    "/",
    env!("CARGO_PKG_VERSION"),
);

Then, in main

let client = reqwest::Client::builder()
    .user_agent(APP_USER_AGENT)
    .build()?;

and pass that client in to the get_feed routine

async fn get_feed(
    client: &reqwest::Client,
    url: &str,
) -> Result<Channel, Box<dyn std::error::Error>> {
    let res = client.get(url).send().await?;
    if res.status() != 200 {
        return Err(format!("Response status {}", res.status()).into());
    }
    let content = res.bytes().await?;
    let channel = Channel::read_from(&content[..])?;
    Ok(channel)
}

I'm guessing I will want that almost every time I use reqwest. And using ClientBuilder seems better than using Client, as I had been doing.

And now it looks like the snow has stopped, so it's time to go shovel!