pikadick/commands/
insta_dl.rs1use 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
78async 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
120fn get_extension_from_url(url: &Url) -> Option<&str> {
122 Some(url.path_segments()?.next_back()?.rsplit_once('.')?.1)
123}