pikadick/commands/
insta_dl.rs

1use crate::{
2    checks::ENABLED_CHECK,
3    util::LoadingReaction,
4    ClientDataKey,
5};
6use anyhow::{
7    bail,
8    Context as _,
9};
10use bytes::Bytes;
11use insta::MediaType;
12use serenity::{
13    builder::{
14        CreateAttachment,
15        CreateMessage,
16    },
17    framework::standard::{
18        macros::command,
19        Args,
20        CommandResult,
21    },
22    model::prelude::*,
23    prelude::*,
24};
25use tracing::info;
26use url::Url;
27
28#[command("insta-dl")]
29#[description("Download an instagram video or photo")]
30#[usage("<url>")]
31#[example("https://www.instagram.com/p/CIlZpXKFfNt/")]
32#[checks(Enabled)]
33#[min_args(1)]
34#[max_args(1)]
35#[bucket("insta-dl")]
36async fn insta_dl(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
37    let data_lock = ctx.data.read().await;
38    let client_data = data_lock
39        .get::<ClientDataKey>()
40        .expect("missing client data");
41    let client = client_data.insta_client.clone();
42    drop(data_lock);
43
44    let url = args.trimmed().current().expect("missing url");
45
46    info!("downloading instagram post \"{url}\"");
47    let mut loading = LoadingReaction::new(ctx.http.clone(), msg);
48
49    let result = async {
50        let post = client
51            .get_post(url)
52            .await
53            .context("failed to get instagram post")?;
54        download_post(&client.client, &post)
55            .await
56            .context("failed to download post")
57    }
58    .await;
59
60    match result {
61        Ok((post_data, file_name)) => {
62            let file_builder = CreateAttachment::bytes(post_data, file_name);
63            let message_builder = CreateMessage::new().add_file(file_builder);
64
65            msg.channel_id
66                .send_message(&ctx.http, message_builder)
67                .await?;
68            loading.send_ok();
69        }
70        Err(error) => {
71            msg.channel_id.say(&ctx.http, format!("{error:?}")).await?;
72        }
73    }
74
75    Ok(())
76}
77
78// TODO: Cache results
79/// Download an instagram post
80async fn download_post(
81    client: &reqwest::Client,
82    post_page: &'_ insta::AdditionalDataLoaded,
83) -> anyhow::Result<(Bytes, String)> {
84    let post_page_item = post_page.items.first().context("missing post item")?;
85
86    let url = match post_page_item.media_type {
87        MediaType::Photo => {
88            let image_versions2_candidate = post_page_item
89                .get_best_image_versions2_candidate()
90                .context("failed to select an image_versions2_candidate")?;
91            &image_versions2_candidate.url
92        }
93        MediaType::Video => {
94            let video_version = post_page_item
95                .get_best_video_version()
96                .context("failed to get the best video version")?;
97
98            &video_version.url
99        }
100        media_type => {
101            bail!("Unsupported media type `{media_type:?}`",);
102        }
103    };
104
105    let code = &post_page_item.code;
106    let extension = get_extension_from_url(url).context("missing image extension")?;
107    let file_name = format!("{code}.{extension}");
108
109    let data = client
110        .get(url.as_str())
111        .send()
112        .await?
113        .error_for_status()?
114        .bytes()
115        .await?;
116
117    Ok((data, file_name))
118}
119
120/// Get the file extension from a url
121fn get_extension_from_url(url: &Url) -> Option<&str> {
122    Some(url.path_segments()?.next_back()?.rsplit_once('.')?.1)
123}