diff --git a/.gitignore b/.gitignore index 561a17d..9e40388 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ Cargo.lock **/*.rs.bk pic/ -config.toml \ No newline at end of file +config.toml +tweet-like-listener \ No newline at end of file diff --git a/README-ZH.md b/README-ZH.md deleted file mode 100644 index 20dffae..0000000 --- a/README-ZH.md +++ /dev/null @@ -1,8 +0,0 @@ -# 推特点赞下载器 - -轮询你的点赞过的色图,存到本地 - -## 你需要的 - -* 创建一个[推特开发者账号](https://developer.twitter.com/en/portal/petition/essential/basic-info),创建一个应用,得到一个 access_key -* 有一台能连到推特的电脑 / 服务器 diff --git a/README.md b/README.md index aee1811..5b39507 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,17 @@ -# tweet-like-listener +# tweet-like-listener (推特点赞图片下载) -Download your 'like' images on twitter automatically. +号养好了,每天上推特都是色图,又懒得手动右键下载图片,那就把这项工作自动化吧! + +## 功能 +* 指定账号轮询,下载其近期点赞的推特图片到本地 +* 多账号支持,群 友 严 选 + +## 使用 +1. **important** 需要[推特开发者账号](https://developer.twitter.com/en/portal/petition/essential/basic-info),请在里面申请并创建一个应用,得到 access_key(这是你访问推特 API 的凭证) +2. **important** 自行解决科学上网问题 +3. release 下载对应平台应用(也可以本地 cargo 编译) +4. 执行应用(第一次执行会生成 config.toml 文件,如果没有请在相同目录下新建) +5. 填写 config.toml 文件 +6. 再次执行应用,并使用任意方式让其能一直跑下去 -## What you need -* a twitter developer account -* a server / computer that can reach twitter diff --git a/src/downloader.rs b/src/downloader.rs index 521dcf2..b89d004 100644 --- a/src/downloader.rs +++ b/src/downloader.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::process::exit; +use std::time::Duration; use hyper::http::HeaderValue; use hyper::{HeaderMap, StatusCode}; @@ -7,7 +8,10 @@ use hyper::{HeaderMap, StatusCode}; use crate::model::{Attachments, Media, Tweet, TweetResp, User, UserResp}; use crate::url::UrlBuilder; use crate::Result; -use log::{error, debug}; +use log::{debug, error}; + +pub const TIMEOUT: Duration = Duration::from_secs(6); + pub struct Downloader { pub user_cache: HashMap, pub user_ids: Vec, @@ -48,7 +52,12 @@ impl Downloader { .get_url(); let headers = self.tweet_auth_header(); - let resp = client.get(url).headers(headers).send().await?; + let resp = client + .get(url) + .headers(headers) + .timeout(TIMEOUT) + .send() + .await?; if resp.status() == StatusCode::UNAUTHORIZED { error!("401: Maybe you have a wrong access_key"); exit(1); @@ -127,8 +136,21 @@ impl Downloader { error!("401: Maybe you have a wrong access_key"); exit(1); } + let resp = resp.json::().await?; - Ok(resp.data) + if let Some(errs) = resp.errors { + error!( + "{}", + errs.iter() + .map(|err| err.detail.clone()) + .collect::>() + .join(";") + ); + } + if let Some(users) = resp.data { + return Ok(users); + } + Ok(vec![]) } pub async fn get_users_by_usernames(&self, usernames: Vec<&str>) -> Result> { @@ -138,14 +160,27 @@ impl Downloader { let resp = client .get(url) .headers(self.tweet_auth_header()) + .timeout(TIMEOUT) .send() .await?; if resp.status() == StatusCode::UNAUTHORIZED { - error!("401: Maybe you have a wrong access_key"); + error!("401: Maybe you have a wrong access_key, please check your config.toml"); exit(1); } let resp = resp.json::().await?; - Ok(resp.data) + if let Some(errs) = resp.errors { + error!( + "{}", + errs.iter() + .map(|err| err.detail.clone()) + .collect::>() + .join(";") + ); + } + if let Some(users) = resp.data { + return Ok(users); + } + Ok(vec![]) } } diff --git a/src/main.rs b/src/main.rs index c8115d2..271d7f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use downloader::Downloader; +use downloader::{Downloader, TIMEOUT}; pub type Result = std::result::Result>; @@ -11,7 +11,7 @@ use std::{ process::exit, time::Duration, }; -use tokio::time::sleep; // 1.3.1 +use tokio::{task, time::sleep}; // 1.3.1 mod downloader; mod model; @@ -40,20 +40,35 @@ const CONFIG_CONTENT: &str = r#" # access_key of your application access_key = 'your twitter access_key' -# usernames you would like to listen +# usernames you would like to listen, with prefix '@' usernames = ['@werifu_'] # request frequency. unit: 1 second freq = 5 [storage] +# the dir that stores images dir = './pic' "#; #[tokio::main] async fn main() -> Result<()> { env_logger::init(); - + // check the network + tokio::spawn(async { + loop { + let res = reqwest::Client::new().get("https://twitter.com").timeout(TIMEOUT).send().await; + match res { + Ok(_) => { + info!("ping Twitter ok"); + }, + Err(_) => { + error!("ping Twitter failed. You may not reach twitter. (tips tor China Mainland users: check your proxy)"); + } + }; + sleep(Duration::from_secs(10)).await; + } + }); match fs::read_to_string("./config.toml") { Ok(config_str) => { let config: Config = toml::from_str(&config_str).unwrap(); @@ -72,11 +87,12 @@ async fn main() -> Result<()> { let mut downloader = Downloader::new(config.twitter.access_key); let users = match downloader.get_users_by_usernames(usernames).await { Ok(users) => users, - Err(err) => { - error!("usernames maybe wrong or you may not reach twitter.\nPlease check your config and net.\nerr: {:#?}", err); + Err(_) => { + error!("You may not reach twitter. Please check your config and net. (tips tor China Mainland users: check your proxy)"); exit(1); } }; + info!("ok users: {:?}", users); loop { for user in users.iter() { @@ -87,21 +103,31 @@ async fn main() -> Result<()> { chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"), likes.len() ); + let mut handles = vec![]; for (filename, url) in likes.iter() { - let full_path = dir.join(filename); + let filename = filename.clone(); + let url = url.clone(); + let full_path = dir.join(&filename); if full_path.exists() { continue; } - match reqwest::Client::new().get(url).send().await { - Ok(img_bytes) => { - let img_bytes = img_bytes.bytes().await.unwrap(); - let mut f = File::create(full_path).unwrap(); - f.write(&img_bytes).unwrap(); + // download pictures concurrently + handles.push(task::spawn(async move { + match reqwest::Client::new().get(url).send().await { + Ok(img_bytes) => { + let img_bytes = img_bytes.bytes().await.unwrap(); + let mut f = File::create(full_path).unwrap(); + f.write(&img_bytes).unwrap(); + } + Err(err) => { + error!("download file {} error: {:?}", filename, err); + } } - Err(err) => { - error!("download file error: {:?}", err); - } - } + })); + } + // await for all download tasks + for handle in handles { + let _ = handle.await; } } Err(err) => { diff --git a/src/model.rs b/src/model.rs index d905125..6933169 100644 --- a/src/model.rs +++ b/src/model.rs @@ -44,5 +44,17 @@ pub struct TweetResp { #[derive(Serialize, Deserialize, Debug)] pub struct UserResp { - pub data: Vec, -} \ No newline at end of file + pub data: Option>, + pub errors: Option>, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct UserErr { + pub value: String, + pub detail: String, + pub title: String, + pub resource_type: String, + pub parameter: String, + pub resource_id: String, + pub r#type: String, +}