Initial commit
This commit is contained in:
commit
fe4e09ef5a
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
autoytarchivers.session
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "autoytarchivers"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["blank X <theblankx@protonmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
reqwest = "0.11"
|
||||||
|
grammers-client = { git = "https://github.com/Lonami/grammers" }
|
||||||
|
grammers-session = { git = "https://github.com/Lonami/grammers" }
|
||||||
|
grammers-tl-types = { git = "https://github.com/Lonami/grammers" }
|
||||||
|
quick-xml = "0.22"
|
||||||
|
rand = "0.8"
|
||||||
|
regex = "1.5"
|
||||||
|
url = { version = "2.2", features = ["serde"] }
|
||||||
|
tokio = { version = "1.5", features = ["rt-multi-thread", "process", "fs", "sync", "time", "io-util"] }
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 blank X
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,25 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub const API_ID: i32 = 0;
|
||||||
|
pub const API_HASH: &str = "https://my.telegram.org";
|
||||||
|
pub const BOT_TOKEN: &str = "https://telegram.org/BotFather";
|
||||||
|
|
||||||
|
// To obtain the packed chat, set to None and send a message to the chat
|
||||||
|
pub const STORAGE_CHAT: Option<&[u8]> = None;
|
||||||
|
pub const STORAGE_MESSAGE_ID: i32 = 7;
|
||||||
|
pub const WAIT_DURATION: Duration = Duration::from_secs(30 * 60);
|
||||||
|
pub const VIDEO_WORKERS: usize = 2;
|
||||||
|
pub const UPLOAD_WORKERS: usize = 5;
|
||||||
|
|
||||||
|
pub const CHANNEL_IDS: [&str; 2] = [
|
||||||
|
"UCL_qhgtOy0dy1Agp8vkySQg",
|
||||||
|
"UCHsx4Hqa-1ORjQTh9TYDhww",
|
||||||
|
];
|
||||||
|
pub const INVIDIOUS_INSTANCES: Option<[&str; 6]> = Some([
|
||||||
|
"https://tube.connect.cafe",
|
||||||
|
"https://invidious.zapashcanon.fr",
|
||||||
|
"https://invidious.site",
|
||||||
|
"https://invidious.048596.xyz",
|
||||||
|
"https://vid.puffyan.us",
|
||||||
|
"https://invidious.silkky.cloud",
|
||||||
|
]);
|
|
@ -0,0 +1,337 @@
|
||||||
|
mod config;
|
||||||
|
mod structs;
|
||||||
|
mod utils;
|
||||||
|
mod workers;
|
||||||
|
use grammers_client::{types::chat::PackedChat, Client, Config, InputMessage, Update};
|
||||||
|
use grammers_session::Session;
|
||||||
|
use grammers_tl_types::functions::Ping;
|
||||||
|
use rand::{random, thread_rng, Rng};
|
||||||
|
use regex::Regex;
|
||||||
|
use reqwest::ClientBuilder;
|
||||||
|
use std::collections::{HashSet, VecDeque};
|
||||||
|
use std::env::args;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::process::exit;
|
||||||
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::sync::Semaphore;
|
||||||
|
use tokio::time::{sleep, Instant};
|
||||||
|
extern crate tokio;
|
||||||
|
|
||||||
|
async fn async_main() {
|
||||||
|
let nodl = match args().nth(1) {
|
||||||
|
Some(i) => {
|
||||||
|
if i == "nodl" {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
eprintln!("Unknown argument: {}", i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
let rclient = ClientBuilder::new()
|
||||||
|
.timeout(Duration::from_secs(60))
|
||||||
|
.build()
|
||||||
|
.expect("Failed to build reqwest::Client");
|
||||||
|
println!("Connecting to Telegram...");
|
||||||
|
let mut tclient = Client::connect(Config {
|
||||||
|
session: Session::load_file_or_create("autoytarchivers.session")
|
||||||
|
.expect("Failed to Session::load_file_or_create"),
|
||||||
|
api_id: config::API_ID,
|
||||||
|
api_hash: config::API_HASH.to_string(),
|
||||||
|
params: Default::default(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Failed to connect to Telegram");
|
||||||
|
println!("Connected to Telegram");
|
||||||
|
if !tclient
|
||||||
|
.is_authorized()
|
||||||
|
.await
|
||||||
|
.expect("Failed to check if client is authorized")
|
||||||
|
{
|
||||||
|
println!("Signing in...");
|
||||||
|
tclient
|
||||||
|
.bot_sign_in(config::BOT_TOKEN, config::API_ID, config::API_HASH)
|
||||||
|
.await
|
||||||
|
.expect("Failed to sign in");
|
||||||
|
println!("Signed in");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let tclient = tclient.clone();
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let ping_id = random();
|
||||||
|
if let Err(err) = tclient.invoke(&Ping { ping_id }).await {
|
||||||
|
eprintln!("Failed to ping Telegram: {:?}", err);
|
||||||
|
}
|
||||||
|
sleep(Duration::from_secs(60)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if config::STORAGE_CHAT.is_none() {
|
||||||
|
while let Some(update) = tclient
|
||||||
|
.next_update()
|
||||||
|
.await
|
||||||
|
.expect("Failed client.next_updates()")
|
||||||
|
{
|
||||||
|
if let Update::NewMessage(message) = update {
|
||||||
|
println!(
|
||||||
|
"Received a message in {} ({}), packed chat: {:?}",
|
||||||
|
message.chat().id(),
|
||||||
|
message.chat().name(),
|
||||||
|
message.chat().pack().to_bytes()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut seen_videos: Option<Vec<String>> = None;
|
||||||
|
let chat = PackedChat::from_bytes(config::STORAGE_CHAT.unwrap())
|
||||||
|
.expect("Failed to unpack chat")
|
||||||
|
.unpack();
|
||||||
|
match tclient
|
||||||
|
.get_messages_by_id(&chat, &[config::STORAGE_MESSAGE_ID])
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(mut messages) => {
|
||||||
|
if let Some(message) = messages.pop().expect("Telegram returned 0 messages") {
|
||||||
|
if let Some(media) = message.media() {
|
||||||
|
let mut data = vec![];
|
||||||
|
let mut download = tclient.iter_download(&media);
|
||||||
|
loop {
|
||||||
|
match download.next().await {
|
||||||
|
Ok(Some(chunk)) => data.extend(chunk),
|
||||||
|
Ok(None) => break,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to iter_download: {:?}", err);
|
||||||
|
data.clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if !data.is_empty() {
|
||||||
|
match serde_json::from_slice(&data) {
|
||||||
|
Ok(i) => seen_videos = Some(i),
|
||||||
|
Err(err) => eprintln!("Failed to parse seen videos json: {:?}", err),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Seen videos message has no media");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Seen videos message does not exist");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => eprintln!("Failed to get seen videos message: {:?}", err),
|
||||||
|
};
|
||||||
|
let seen_videos = Arc::new(RwLock::new(seen_videos.unwrap_or_default()));
|
||||||
|
let tmp_handled = Arc::new(Mutex::new(HashSet::new()));
|
||||||
|
let video_semaphore = Arc::new(Semaphore::new(0));
|
||||||
|
let video_mutex = Arc::new(Mutex::new(VecDeque::new()));
|
||||||
|
let upload_semaphore = Arc::new(Semaphore::new(0));
|
||||||
|
let upload_mutex = Arc::new(Mutex::new(VecDeque::new()));
|
||||||
|
let query_lock = Arc::new(tokio::sync::Mutex::new(()));
|
||||||
|
if !nodl {
|
||||||
|
let date_regex = Arc::new(Regex::new(r#" *\d{4}-\d{2}-\d{2} \d{2}:\d{2}$"#).unwrap());
|
||||||
|
for _ in 0..config::VIDEO_WORKERS {
|
||||||
|
tokio::task::spawn(workers::video_worker(
|
||||||
|
rclient.clone(),
|
||||||
|
tclient.clone(),
|
||||||
|
chat.clone(),
|
||||||
|
date_regex.clone(),
|
||||||
|
video_semaphore.clone(),
|
||||||
|
video_mutex.clone(),
|
||||||
|
upload_semaphore.clone(),
|
||||||
|
upload_mutex.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for _ in 0..config::UPLOAD_WORKERS {
|
||||||
|
tokio::task::spawn(workers::upload_worker(
|
||||||
|
tclient.clone(),
|
||||||
|
chat.clone(),
|
||||||
|
upload_semaphore.clone(),
|
||||||
|
upload_mutex.clone(),
|
||||||
|
seen_videos.clone(),
|
||||||
|
tmp_handled.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loop {
|
||||||
|
for i in &config::CHANNEL_IDS {
|
||||||
|
println!("Checking channel {}", i);
|
||||||
|
match utils::get_videos(&rclient, i).await {
|
||||||
|
Ok(videos) => {
|
||||||
|
for j in videos {
|
||||||
|
{
|
||||||
|
if tmp_handled.lock().unwrap().contains(&j) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nodl {
|
||||||
|
let mut seen_videos = seen_videos.write().unwrap();
|
||||||
|
if !seen_videos.contains(&j) {
|
||||||
|
seen_videos.push(j);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
let seen_videos = seen_videos.read().unwrap();
|
||||||
|
if seen_videos.contains(&j) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mutex = video_mutex.clone();
|
||||||
|
let semaphore = video_semaphore.clone();
|
||||||
|
let query_lock = query_lock.clone();
|
||||||
|
let tclient = tclient.clone();
|
||||||
|
let chat = chat.clone();
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
let mut waited = false;
|
||||||
|
let mut i = 1;
|
||||||
|
loop {
|
||||||
|
let guard = query_lock.lock().await;
|
||||||
|
match utils::get_video(&j).await {
|
||||||
|
Ok(Some(i)) => {
|
||||||
|
let first_try_live =
|
||||||
|
i.is_live.unwrap_or_default() && !waited;
|
||||||
|
mutex.lock().unwrap().push_back((
|
||||||
|
i,
|
||||||
|
Instant::now(),
|
||||||
|
first_try_live,
|
||||||
|
));
|
||||||
|
semaphore.add_permits(1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(None) => break,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to get video data: {:?}", err);
|
||||||
|
waited = true;
|
||||||
|
if let structs::Error::YoutubeDL(ref err) = err {
|
||||||
|
if err.output.contains("429")
|
||||||
|
|| err
|
||||||
|
.output
|
||||||
|
.to_lowercase()
|
||||||
|
.contains("too many request")
|
||||||
|
{
|
||||||
|
sleep(Duration::from_secs(i * 60 * 60)).await;
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
} else if err.output.starts_with("autoytarchivers:")
|
||||||
|
{
|
||||||
|
drop(guard);
|
||||||
|
let time: u64 = err
|
||||||
|
.output
|
||||||
|
.splitn(3, &[':', ' '][..])
|
||||||
|
.nth(1)
|
||||||
|
.unwrap()
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
sleep(Duration::from_secs(time + 30)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let text = format!("{:#?}", err);
|
||||||
|
let size = text.len();
|
||||||
|
let mut stream = Cursor::new(text.into_bytes());
|
||||||
|
match tclient
|
||||||
|
.upload_stream(
|
||||||
|
&mut stream,
|
||||||
|
size,
|
||||||
|
"failed-get-video-data.log".to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(uploaded) => {
|
||||||
|
let message = InputMessage::text(
|
||||||
|
"Failed to get video data",
|
||||||
|
)
|
||||||
|
.mime_type("text/plain")
|
||||||
|
.file(uploaded);
|
||||||
|
if let Err(err) =
|
||||||
|
tclient.send_message(&chat, message).await
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about failing to get video data: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to send message about failing to get video data, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to send message about failing to get video data: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to upload logs about failing to get video data: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to upload logs about failing to get video data, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to upload logs about failing to get video data: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let tmp = thread_rng().gen_range(30..=10 * 60);
|
||||||
|
sleep(Duration::from_secs(tmp)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to get video list: {:?}", err);
|
||||||
|
let text = format!("{:#?}", err);
|
||||||
|
let size = text.len();
|
||||||
|
let mut stream = Cursor::new(text.into_bytes());
|
||||||
|
match tclient
|
||||||
|
.upload_stream(&mut stream, size, "failed-get-video-list.log".to_string())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(uploaded) => {
|
||||||
|
let message = InputMessage::text("Failed to get video list")
|
||||||
|
.mime_type("text/plain")
|
||||||
|
.file(uploaded);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, message).await {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about failing to get video list: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to send message about failing to get video list, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to send message about failing to get video list: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to upload logs about failing to get video list: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to upload logs about failing to get video list, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to upload logs about failing to get video list: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sleep(Duration::from_secs(thread_rng().gen_range(30..=60))).await;
|
||||||
|
}
|
||||||
|
if nodl {
|
||||||
|
if utils::update_seen_videos(&mut tclient, &chat, seen_videos.read().unwrap().clone())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sleep(config::WAIT_DURATION).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("Failed to build tokio runtime")
|
||||||
|
.block_on(async_main());
|
||||||
|
exit(1);
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::io;
|
||||||
|
use std::process::ExitStatus;
|
||||||
|
use std::string::FromUtf8Error;
|
||||||
|
use tokio::task::JoinError;
|
||||||
|
use url::Url;
|
||||||
|
extern crate quick_xml;
|
||||||
|
extern crate reqwest;
|
||||||
|
extern crate serde_json;
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct InvidiousVideo {
|
||||||
|
pub video_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct VideoURL {
|
||||||
|
pub url: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct VideoData {
|
||||||
|
#[serde(default)]
|
||||||
|
pub requested_formats: Vec<VideoURL>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub url: Option<Url>,
|
||||||
|
pub duration: u64,
|
||||||
|
pub id: String,
|
||||||
|
pub title: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub thumbnail: Option<Url>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_live: Option<bool>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub json: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct YoutubeDLError {
|
||||||
|
pub status: ExitStatus,
|
||||||
|
pub output: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
IO(io::Error),
|
||||||
|
JoinError(JoinError),
|
||||||
|
Reqwest(reqwest::Error),
|
||||||
|
QuickXML(quick_xml::Error),
|
||||||
|
SerdeJSON(serde_json::Error),
|
||||||
|
FromUtf8Error(FromUtf8Error),
|
||||||
|
YoutubeDL(YoutubeDLError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for Error {
|
||||||
|
#[inline]
|
||||||
|
fn from(error: io::Error) -> Error {
|
||||||
|
Error::IO(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<JoinError> for Error {
|
||||||
|
#[inline]
|
||||||
|
fn from(error: JoinError) -> Error {
|
||||||
|
Error::JoinError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for Error {
|
||||||
|
#[inline]
|
||||||
|
fn from(error: reqwest::Error) -> Error {
|
||||||
|
Error::Reqwest(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<quick_xml::Error> for Error {
|
||||||
|
#[inline]
|
||||||
|
fn from(error: quick_xml::Error) -> Error {
|
||||||
|
Error::QuickXML(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for Error {
|
||||||
|
#[inline]
|
||||||
|
fn from(error: serde_json::Error) -> Error {
|
||||||
|
Error::SerdeJSON(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FromUtf8Error> for Error {
|
||||||
|
#[inline]
|
||||||
|
fn from(error: FromUtf8Error) -> Error {
|
||||||
|
Error::FromUtf8Error(error)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,289 @@
|
||||||
|
use crate::config::{INVIDIOUS_INSTANCES, STORAGE_MESSAGE_ID};
|
||||||
|
use crate::structs::{Error, InvidiousVideo, Result, VideoData, YoutubeDLError};
|
||||||
|
use grammers_client::{types::Chat, InputMessage};
|
||||||
|
use quick_xml::{events::Event, Reader};
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
use reqwest::Client;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::process::Stdio;
|
||||||
|
use tokio::io::{copy_buf, AsyncWriteExt, BufReader};
|
||||||
|
use tokio::process::Command;
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
extern crate grammers_client;
|
||||||
|
extern crate serde_json;
|
||||||
|
extern crate tokio;
|
||||||
|
|
||||||
|
const PYTHON_INPUT: &[u8] = br#"import sys
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
import yt_dlp as youtube_dl
|
||||||
|
except ImportError:
|
||||||
|
import youtube_dl
|
||||||
|
_try_get = youtube_dl.extractor.youtube.try_get
|
||||||
|
def traverse_dict(src):
|
||||||
|
for (key, value) in src.items():
|
||||||
|
if key == 'scheduledStartTime':
|
||||||
|
return value
|
||||||
|
if isinstance(value, dict):
|
||||||
|
if value := traverse_dict(value):
|
||||||
|
return value
|
||||||
|
return None
|
||||||
|
|
||||||
|
def try_get(src, getter, expected_type=None):
|
||||||
|
if isinstance(src, dict):
|
||||||
|
if reason := src.get('reason'):
|
||||||
|
if isinstance(reason, str) and (reason.startswith('This live event will begin in ') or reason.startswith('Premieres in ')):
|
||||||
|
if t := _try_get(src, traverse_dict, str):
|
||||||
|
src['reason'] = f'autoytarchivers:{t} {reason}'
|
||||||
|
return _try_get(src, getter, expected_type)
|
||||||
|
youtube_dl.extractor.youtube.try_get = try_get
|
||||||
|
ytdl = youtube_dl.YoutubeDL({"skip_download": True, "no_color": True, "quiet": True})
|
||||||
|
try:
|
||||||
|
print(json.dumps(ytdl.extract_info("https://www.youtube.com/watch?v=" + sys.argv[1]), indent=4), file=sys.stderr)
|
||||||
|
except Exception as e:
|
||||||
|
sys.exit(str(e))"#;
|
||||||
|
|
||||||
|
pub async fn get_videos(client: &Client, channel_id: &str) -> Result<Vec<String>> {
|
||||||
|
let mut video_ids = vec![];
|
||||||
|
if let Some(invidious_instances) = INVIDIOUS_INSTANCES {
|
||||||
|
for i in &invidious_instances {
|
||||||
|
let resp = match client
|
||||||
|
.get(&format!("{}/api/v1/channels/{}/latest", i, channel_id))
|
||||||
|
.query(&[("fields", "videoId")])
|
||||||
|
.header("Cache-Control", "no-store, max-age=0")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(i) => i,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to connect to {}: {:?}", i, err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if resp.status() != 200 {
|
||||||
|
eprintln!("Got {} from {}", resp.status(), i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let resp = match resp.bytes().await {
|
||||||
|
Ok(i) => i,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to get data from {}: {:?}", i, err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let resp: Vec<InvidiousVideo> = match serde_json::from_slice(&resp) {
|
||||||
|
Ok(i) => i,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to parse data from {}: {:?}", i, err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
video_ids.extend(resp.into_iter().take(15).map(|i| i.video_id));
|
||||||
|
if !video_ids.is_empty() {
|
||||||
|
return Ok(video_ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let resp = client
|
||||||
|
.get("https://www.youtube.com/feeds/videos.xml")
|
||||||
|
.query(&[("channel_id", channel_id)])
|
||||||
|
.header("Cache-Control", "no-store, max-age=0")
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.text()
|
||||||
|
.await?;
|
||||||
|
let mut reader = Reader::from_str(&resp);
|
||||||
|
let mut buf = vec![];
|
||||||
|
let mut inside = false;
|
||||||
|
loop {
|
||||||
|
match reader.read_event(&mut buf) {
|
||||||
|
Ok(Event::Start(ref e)) if e.name() == b"yt:videoId" => inside = true,
|
||||||
|
Ok(Event::Text(e)) if inside => {
|
||||||
|
video_ids.push(e.unescape_and_decode(&reader)?);
|
||||||
|
if video_ids.len() >= 15 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
inside = false;
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(err) => Err(err)?,
|
||||||
|
_ => (),
|
||||||
|
};
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
Ok(video_ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_video(video_id: &str) -> Result<Option<VideoData>> {
|
||||||
|
let mut command = Command::new("python3");
|
||||||
|
let mut process = command
|
||||||
|
.args(&["-", video_id])
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
let mut stdin = process.stdin.take().unwrap();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(err) = stdin.write_all(PYTHON_INPUT).await {
|
||||||
|
eprintln!("Failed to write PYTHON_INPUT: {:?}", err);
|
||||||
|
}
|
||||||
|
drop(stdin)
|
||||||
|
});
|
||||||
|
let stderr = process.stderr.take().unwrap();
|
||||||
|
let task: tokio::task::JoinHandle<std::result::Result<_, std::io::Error>> =
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut stderr = BufReader::new(stderr);
|
||||||
|
let mut writer = vec![];
|
||||||
|
copy_buf(&mut stderr, &mut writer).await?;
|
||||||
|
Ok(writer)
|
||||||
|
});
|
||||||
|
let status = process.wait().await?;
|
||||||
|
let stderr = String::from_utf8(task.await??)?;
|
||||||
|
if status.success() {
|
||||||
|
let mut data: VideoData = serde_json::from_str(&stderr)?;
|
||||||
|
data.json = stderr;
|
||||||
|
Ok(Some(data))
|
||||||
|
} else {
|
||||||
|
let stderr_lowercase = stderr.to_lowercase();
|
||||||
|
if stderr_lowercase.contains("private video")
|
||||||
|
|| stderr_lowercase.contains("unavailable")
|
||||||
|
|| stderr_lowercase.contains("not available")
|
||||||
|
{
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
Err(Error::YoutubeDL(YoutubeDLError {
|
||||||
|
status,
|
||||||
|
output: stderr,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_video_retry(
|
||||||
|
tclient: &mut grammers_client::Client,
|
||||||
|
chat: &Chat,
|
||||||
|
video_data: VideoData,
|
||||||
|
) -> Option<VideoData> {
|
||||||
|
for i in 1..=5 {
|
||||||
|
match get_video(&video_data.id).await {
|
||||||
|
Ok(i) => return i,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to get video data: {:?}", err);
|
||||||
|
if let Error::YoutubeDL(ref err) = err {
|
||||||
|
if err.output.contains("429")
|
||||||
|
|| err.output.to_lowercase().contains("too many requests")
|
||||||
|
{
|
||||||
|
sleep(Duration::from_secs(i * 60 * 60)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let text = format!("{:#?}", err);
|
||||||
|
let size = text.len();
|
||||||
|
let mut stream = Cursor::new(text.into_bytes());
|
||||||
|
match tclient
|
||||||
|
.upload_stream(&mut stream, size, "failed-get-video-data.log".to_string())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(uploaded) => {
|
||||||
|
let message = InputMessage::text("Failed to get video data")
|
||||||
|
.mime_type("text/plain")
|
||||||
|
.file(uploaded);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, message).await {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about failing to get video data: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to send message about failing to get video data, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to send message about failing to get video data: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to upload logs about failing to get video data: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to upload logs about failing to get video data, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to upload logs about failing to get video data: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let tmp = thread_rng().gen_range(30..=10 * 60);
|
||||||
|
sleep(Duration::from_secs(tmp)).await;
|
||||||
|
}
|
||||||
|
Some(video_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_manifest(video_data: &VideoData) -> bool {
|
||||||
|
if video_data.requested_formats.is_empty() {
|
||||||
|
video_data.url.as_ref().unwrap().domain() == Some("manifest.googlevideo.com")
|
||||||
|
} else {
|
||||||
|
video_data
|
||||||
|
.requested_formats
|
||||||
|
.iter()
|
||||||
|
.any(|i| i.url.domain() == Some("manifest.googlevideo.com"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extension(text: &str) -> Option<&str> {
|
||||||
|
text.trim_start_matches(".").splitn(2, ".").nth(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_seen_videos(
|
||||||
|
tclient: &mut grammers_client::Client,
|
||||||
|
chat: &Chat,
|
||||||
|
seen_videos: Vec<String>,
|
||||||
|
) -> bool {
|
||||||
|
let bytes = serde_json::to_vec(&serde_json::json!(seen_videos)).unwrap();
|
||||||
|
let size = bytes.len();
|
||||||
|
let mut stream = Cursor::new(bytes);
|
||||||
|
match tclient
|
||||||
|
.upload_stream(&mut stream, size, "autoytarchivers.json".to_string())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(uploaded) => {
|
||||||
|
let message = InputMessage::text("")
|
||||||
|
.mime_type("application/json")
|
||||||
|
.file(uploaded);
|
||||||
|
if let Err(err) = tclient
|
||||||
|
.edit_message(&chat, STORAGE_MESSAGE_ID, message)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("Failed to edit seen videos: {:?}", err);
|
||||||
|
if let Err(err) = tclient
|
||||||
|
.send_message(
|
||||||
|
&chat,
|
||||||
|
InputMessage::text("Failed to edit seen videos, see logs"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about failing to edit seen videos: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to upload seen videos: {:?}", err);
|
||||||
|
if let Err(err) = tclient
|
||||||
|
.send_message(
|
||||||
|
&chat,
|
||||||
|
InputMessage::text("Failed to upload seen videos, see logs"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about failing to upload seen videos: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,725 @@
|
||||||
|
use crate::structs::VideoData;
|
||||||
|
use crate::utils;
|
||||||
|
use grammers_client::types::input_message::InputMessage;
|
||||||
|
use grammers_client::types::Chat;
|
||||||
|
use regex::Regex;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fs::{remove_file, File, OpenOptions};
|
||||||
|
use std::io::{Cursor, Seek, SeekFrom};
|
||||||
|
use std::process::Stdio;
|
||||||
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
|
use tokio::io::{AsyncSeekExt, AsyncWriteExt};
|
||||||
|
use tokio::process::{Child, ChildStdin, Command};
|
||||||
|
use tokio::sync::Semaphore;
|
||||||
|
use tokio::task;
|
||||||
|
use tokio::time::{sleep, Duration, Instant};
|
||||||
|
extern crate grammers_client;
|
||||||
|
extern crate reqwest;
|
||||||
|
|
||||||
|
pub async fn video_worker(
|
||||||
|
rclient: reqwest::Client,
|
||||||
|
mut tclient: grammers_client::Client,
|
||||||
|
chat: Chat,
|
||||||
|
date_regex: Arc<Regex>,
|
||||||
|
semaphore: Arc<Semaphore>,
|
||||||
|
mutex: Arc<Mutex<VecDeque<(VideoData, Instant, bool)>>>,
|
||||||
|
upload_semaphore: Arc<Semaphore>,
|
||||||
|
upload_mutex: Arc<Mutex<VecDeque<String>>>,
|
||||||
|
) {
|
||||||
|
loop {
|
||||||
|
semaphore.acquire().await.unwrap().forget();
|
||||||
|
let (mut video_data, start_time, first_try_live) =
|
||||||
|
mutex.lock().unwrap().pop_front().unwrap();
|
||||||
|
let late_to_queue =
|
||||||
|
first_try_live || Instant::now().duration_since(start_time).as_secs() > 5;
|
||||||
|
if late_to_queue {
|
||||||
|
match utils::get_video_retry(&mut tclient, &chat, video_data).await {
|
||||||
|
Some(i) => video_data = i,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if utils::is_manifest(&video_data) {
|
||||||
|
sleep(Duration::from_secs(video_data.duration + 30)).await;
|
||||||
|
match utils::get_video_retry(&mut tclient, &chat, video_data).await {
|
||||||
|
Some(i) => video_data = i,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let video_filename = format!("{}.mkv", &video_data.id);
|
||||||
|
let file = match OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.append(true)
|
||||||
|
.create(true)
|
||||||
|
.open(format!("{}.log", &video_data.id))
|
||||||
|
{
|
||||||
|
Ok(i) => i,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to open video log file: {:?}", err);
|
||||||
|
let text = format!("{:#?}", err);
|
||||||
|
let size = text.len();
|
||||||
|
let mut stream = Cursor::new(text.into_bytes());
|
||||||
|
match tclient
|
||||||
|
.upload_stream(&mut stream, size, "failed-open-video-log.log".to_string())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(uploaded) => {
|
||||||
|
let message = InputMessage::text("Failed to open video log file")
|
||||||
|
.mime_type("text/plain")
|
||||||
|
.file(uploaded);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, message).await {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about failing to open video log file: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to send message about failing to open video log file, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to send message about failing to open video log file: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to upload logs about failing to open video log filr: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to upload logs about failing to open video log file, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to upload logs about failing to open video log file: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut child = match start_ffmpeg(
|
||||||
|
&video_data,
|
||||||
|
&video_filename,
|
||||||
|
file.try_clone().expect("Failed to clone file"),
|
||||||
|
&mut tclient,
|
||||||
|
&chat,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Some(i) => i,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let mut text = "New video".to_string();
|
||||||
|
if late_to_queue {
|
||||||
|
text.push_str(" (is late)");
|
||||||
|
}
|
||||||
|
text.push_str(": ");
|
||||||
|
let title = date_regex.replace(&video_data.title, "");
|
||||||
|
text.push_str(&format!(
|
||||||
|
"{}\nhttps://www.youtube.com/watch?v={}",
|
||||||
|
title, &video_data.id
|
||||||
|
));
|
||||||
|
let mut stream = Cursor::new(video_data.json.as_bytes());
|
||||||
|
match tclient
|
||||||
|
.upload_stream(
|
||||||
|
&mut stream,
|
||||||
|
video_data.json.len(),
|
||||||
|
format!("{}.json", &video_data.id),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(uploaded) => {
|
||||||
|
let message = InputMessage::text(&text)
|
||||||
|
.mime_type("application/json")
|
||||||
|
.file(uploaded);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, message).await {
|
||||||
|
eprintln!("Failed to send message of video json: {:?}", err);
|
||||||
|
if let Err(err) = tclient
|
||||||
|
.send_message(
|
||||||
|
&chat,
|
||||||
|
InputMessage::text("Failed to send message of video json, see logs"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("Failed to send message about failing to send message of video json: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to upload video json: {:?}", err);
|
||||||
|
if let Err(err) = tclient
|
||||||
|
.send_message(
|
||||||
|
&chat,
|
||||||
|
InputMessage::text("Failed to upload video json, see logs"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about failing to upload video json: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(ref thumbnail) = video_data.thumbnail {
|
||||||
|
match rclient.get(thumbnail.as_str()).send().await {
|
||||||
|
Ok(resp) => {
|
||||||
|
let mut filename = video_data.id.clone();
|
||||||
|
if let Some(path) = resp.url().path_segments() {
|
||||||
|
if let Some(name) = path.last() {
|
||||||
|
if let Some(extension) = utils::extension(name) {
|
||||||
|
filename.push('.');
|
||||||
|
filename.push_str(extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match resp.bytes().await {
|
||||||
|
Ok(bytes) => {
|
||||||
|
let size = bytes.len();
|
||||||
|
let mut stream = Cursor::new(bytes);
|
||||||
|
match tclient.upload_stream(&mut stream, size, filename).await {
|
||||||
|
Ok(uploaded) => {
|
||||||
|
let message = InputMessage::text(&text).file(uploaded);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, message).await {
|
||||||
|
eprintln!("Failed to send thumbnail: {:?}", err);
|
||||||
|
if let Err(err) = tclient
|
||||||
|
.send_message(
|
||||||
|
&chat,
|
||||||
|
InputMessage::text(
|
||||||
|
"Failed to send thumbnail, see logs",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("Failed to send message about failing to send thumbnail: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to upload thumbnail: {:?}", err);
|
||||||
|
if let Err(err) = tclient
|
||||||
|
.send_message(
|
||||||
|
&chat,
|
||||||
|
InputMessage::text(
|
||||||
|
"Failed to upload thumbnail, see logs",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("Failed to send message about failing to upload thumbnail: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to get thumbnail bytes: {:?}", err);
|
||||||
|
let text = format!("{:#?}", err);
|
||||||
|
let size = text.len();
|
||||||
|
let mut stream = Cursor::new(text.into_bytes());
|
||||||
|
match tclient
|
||||||
|
.upload_stream(
|
||||||
|
&mut stream,
|
||||||
|
size,
|
||||||
|
"failed-get-thumbnail-bytes.log".to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(uploaded) => {
|
||||||
|
let message =
|
||||||
|
InputMessage::text("Failed to get thumbnail bytes")
|
||||||
|
.mime_type("text/plain")
|
||||||
|
.file(uploaded);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, message).await {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about failing to get thumbnail bytes: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to send message about failing to get thumbnail bytes, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to send message about failing to get thumbnail bytes: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to upload logs about failing to get thumbnail bytes: {:?}", err);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to upload logs about failing to get thumbnail bytes, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to upload logs about failing to get thumbnail bytes: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to connect to thumbnail server: {:?}", err);
|
||||||
|
let text = format!("{:#?}", err);
|
||||||
|
let size = text.len();
|
||||||
|
let mut stream = Cursor::new(text.into_bytes());
|
||||||
|
match tclient
|
||||||
|
.upload_stream(
|
||||||
|
&mut stream,
|
||||||
|
size,
|
||||||
|
"failed-connect-thumbnail-server.log".to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(uploaded) => {
|
||||||
|
let message =
|
||||||
|
InputMessage::text("Failed to connect to thumbnail server")
|
||||||
|
.mime_type("text/plain")
|
||||||
|
.file(uploaded);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, message).await {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about failing to connect to thumbnail server: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to send message about failing to connect to thumbnail server, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to send message about failing to connect to thumbnail server: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to upload logs about failing to connect to thumbnail server: {:?}", err);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to upload logs about failing to connect to thumbnail server, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to upload logs about failing to connect to thumbnail server: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for _ in 0..50u8 {
|
||||||
|
let task = task::spawn(auto_kill(
|
||||||
|
child.stdin.take().unwrap(),
|
||||||
|
file.try_clone().expect("Failed to clone file"),
|
||||||
|
));
|
||||||
|
let is_ok = match child.wait().await {
|
||||||
|
Ok(i) => i.success(),
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to wait for ffmpeg: {:?}", err);
|
||||||
|
let text = format!("{:#?}", err);
|
||||||
|
let size = text.len();
|
||||||
|
let mut stream = Cursor::new(text.into_bytes());
|
||||||
|
match tclient
|
||||||
|
.upload_stream(&mut stream, size, "failed-wait-ffmpeg.log".to_string())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(uploaded) => {
|
||||||
|
let message = InputMessage::text("Failed to wait for ffmpeg")
|
||||||
|
.mime_type("text/plain")
|
||||||
|
.file(uploaded);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, message).await {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about failing to wait for ffmpeg: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to send message about failing to wait for ffmpeg, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to send message about failing to wait for ffmpeg: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to upload logs about failing to wait for ffmpeg: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to upload logs about failing to wait for ffmpeg, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to upload logs about failing to wait for ffmpeg: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
task.abort();
|
||||||
|
match task.await {
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(err) => {
|
||||||
|
if !err.is_cancelled() {
|
||||||
|
eprintln!("auto_kill panicked: {:?}", err);
|
||||||
|
let text = format!("{:#?}", err);
|
||||||
|
let size = text.len();
|
||||||
|
let mut stream = Cursor::new(text.into_bytes());
|
||||||
|
match tclient
|
||||||
|
.upload_stream(&mut stream, size, "auto-kill-panic.log".to_string())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(uploaded) => {
|
||||||
|
let message = InputMessage::text("auto_kill panicked")
|
||||||
|
.mime_type("text/plain")
|
||||||
|
.file(uploaded);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, message).await {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about auto_kill panicking: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to send message about auto_kill panicking, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to send message about auto_kill panicking: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to upload logs about auto_kill panicking: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to upload logs about auto_kill panicking, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to upload logs about auto_kill panicking: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if is_ok {
|
||||||
|
upload_mutex.lock().unwrap().push_back(video_data.id);
|
||||||
|
upload_semaphore.add_permits(1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Err(err) = tclient
|
||||||
|
.send_message(
|
||||||
|
&chat,
|
||||||
|
InputMessage::text(&format!(
|
||||||
|
"Failed to download video {}, see logs",
|
||||||
|
&video_data.id
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about failing to download video {}: {:?}",
|
||||||
|
&video_data.id, err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Err(err) = remove_file(&video_filename) {
|
||||||
|
eprintln!("Failed to delete {}: {:?}", &video_filename, err);
|
||||||
|
let text = format!("{:#?}", err);
|
||||||
|
let size = text.len();
|
||||||
|
let mut stream = Cursor::new(text.into_bytes());
|
||||||
|
match tclient
|
||||||
|
.upload_stream(
|
||||||
|
&mut stream,
|
||||||
|
size,
|
||||||
|
format!("failed-delete-{}.log", &video_filename),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(uploaded) => {
|
||||||
|
let message =
|
||||||
|
InputMessage::text(format!("Failed to delete {}", &video_filename))
|
||||||
|
.mime_type("text/plain")
|
||||||
|
.file(uploaded);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, message).await {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about failing to delete {}: {:?}",
|
||||||
|
&video_filename, err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text(format!("Failed to send message about failing to delete {}, see logs", &video_filename))).await {
|
||||||
|
eprintln!("Failed to send message about failing to send message about failing to delete {}: {:?}", &video_filename, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to upload logs about failing to delete {}: {:?}",
|
||||||
|
&video_filename, err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient
|
||||||
|
.send_message(
|
||||||
|
&chat,
|
||||||
|
InputMessage::text(format!(
|
||||||
|
"Failed to upload logs about failing to delete {}, see logs",
|
||||||
|
&video_filename
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("Failed to send message about failing to upload logs about failing to delete {}: {:?}", &video_filename, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
match utils::get_video_retry(&mut tclient, &chat, video_data).await {
|
||||||
|
Some(i) => video_data = i,
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
if utils::is_manifest(&video_data) {
|
||||||
|
sleep(Duration::from_secs(video_data.duration + 30)).await;
|
||||||
|
match utils::get_video_retry(&mut tclient, &chat, video_data).await {
|
||||||
|
Some(i) => video_data = i,
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
child = match start_ffmpeg(
|
||||||
|
&video_data,
|
||||||
|
&video_filename,
|
||||||
|
file.try_clone().expect("Failed to clone file"),
|
||||||
|
&mut tclient,
|
||||||
|
&chat,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Some(i) => i,
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_ffmpeg(
|
||||||
|
video_data: &VideoData,
|
||||||
|
filename: &str,
|
||||||
|
file: File,
|
||||||
|
tclient: &mut grammers_client::Client,
|
||||||
|
chat: &Chat,
|
||||||
|
) -> Option<Child> {
|
||||||
|
let mut command = Command::new("ffmpeg");
|
||||||
|
let mut command = command
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(file.try_clone().expect("Failed to clone file"))
|
||||||
|
.stderr(file)
|
||||||
|
.arg("-y");
|
||||||
|
if video_data.requested_formats.is_empty() {
|
||||||
|
command = command
|
||||||
|
.arg("-i")
|
||||||
|
.arg(video_data.url.as_ref().unwrap().as_str());
|
||||||
|
} else {
|
||||||
|
for i in &video_data.requested_formats {
|
||||||
|
command = command.arg("-i").arg(i.url.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match command.args(&["-c", "copy", filename]).spawn() {
|
||||||
|
Ok(i) => Some(i),
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to spawn ffmpeg: {:?}", err);
|
||||||
|
let text = format!("{:#?}", err);
|
||||||
|
let size = text.len();
|
||||||
|
let mut stream = Cursor::new(text.into_bytes());
|
||||||
|
match tclient
|
||||||
|
.upload_stream(&mut stream, size, "failed-spawn-ffmpeg.log".to_string())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(uploaded) => {
|
||||||
|
let message = InputMessage::text("Failed to spawn ffmpeg")
|
||||||
|
.mime_type("text/plain")
|
||||||
|
.file(uploaded);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, message).await {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about failing to spawn ffmpeg: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to send message about failing to spawn ffmpeg, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to send message about failing to spawn ffmpeg: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to upload logs about failing to spawn ffmpeg: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient
|
||||||
|
.send_message(
|
||||||
|
&chat,
|
||||||
|
InputMessage::text(
|
||||||
|
"Failed to upload logs about failing to spawn ffmpeg, see logs",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("Failed to send message about failing to upload logs about failing to spawn ffmpeg: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auto_kill(mut stdin: ChildStdin, mut file: File) {
|
||||||
|
let mut last_tell = file
|
||||||
|
.stream_position()
|
||||||
|
.expect("Failed to get stream position");
|
||||||
|
loop {
|
||||||
|
sleep(Duration::from_secs(5 * 30)).await;
|
||||||
|
let current_tell = file
|
||||||
|
.stream_position()
|
||||||
|
.expect("Failed to get stream position");
|
||||||
|
if current_tell == last_tell {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
last_tell = current_tell;
|
||||||
|
}
|
||||||
|
stdin
|
||||||
|
.write_all(b"q")
|
||||||
|
.await
|
||||||
|
.expect("Failed to write to ffmpeg stdin");
|
||||||
|
drop(stdin);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn upload_worker(
|
||||||
|
mut tclient: grammers_client::Client,
|
||||||
|
chat: Chat,
|
||||||
|
semaphore: Arc<Semaphore>,
|
||||||
|
mutex: Arc<Mutex<VecDeque<String>>>,
|
||||||
|
seen_videos: Arc<RwLock<Vec<String>>>,
|
||||||
|
tmp_handled: Arc<Mutex<HashSet<String>>>,
|
||||||
|
) {
|
||||||
|
loop {
|
||||||
|
semaphore.acquire().await.unwrap().forget();
|
||||||
|
let video_id = mutex.lock().unwrap().pop_front().unwrap();
|
||||||
|
let video_filename = format!("{}.mkv", &video_id);
|
||||||
|
let mut success = true;
|
||||||
|
let mut file = match tokio::fs::File::open(&video_filename).await {
|
||||||
|
Ok(i) => i,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to open video: {:?}", err);
|
||||||
|
let text = format!("{:#?}", err);
|
||||||
|
let size = text.len();
|
||||||
|
let mut stream = Cursor::new(text.into_bytes());
|
||||||
|
match tclient
|
||||||
|
.upload_stream(&mut stream, size, "failed-open-video.log".to_string())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(uploaded) => {
|
||||||
|
let message = InputMessage::text("Failed to open video")
|
||||||
|
.mime_type("text/plain")
|
||||||
|
.file(uploaded);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, message).await {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about failing to open video: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text("Failed to send message about failing to open video, see logs")).await {
|
||||||
|
eprintln!("Failed to send message about failing to send message about failing to open video: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to upload logs about failing to open video: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient
|
||||||
|
.send_message(
|
||||||
|
&chat,
|
||||||
|
InputMessage::text(
|
||||||
|
"Failed to upload logs about failing to open video, see logs",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("Failed to send message about failing to upload logs about failing to open video: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let total_size = file.seek(SeekFrom::End(0)).await.unwrap();
|
||||||
|
file.seek(SeekFrom::Start(0)).await.unwrap();
|
||||||
|
let parts = (total_size as f64 / (2000.0 * 1024.0 * 1024.0)).ceil() as usize;
|
||||||
|
let current_position = 0;
|
||||||
|
for i in 0..parts {
|
||||||
|
let filename = if parts == 1 {
|
||||||
|
video_filename.clone()
|
||||||
|
} else {
|
||||||
|
format!("{}.part{:02}", &video_filename, i)
|
||||||
|
};
|
||||||
|
let mut size = total_size - current_position;
|
||||||
|
if size > 2000 * 1024 * 1024 {
|
||||||
|
size = 2000 * 1024 * 1024;
|
||||||
|
}
|
||||||
|
match tclient
|
||||||
|
.upload_stream(&mut file, usize::try_from(size).unwrap(), filename.clone())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(uploaded) => {
|
||||||
|
let message = InputMessage::text(&filename)
|
||||||
|
.mime_type("video/x-matroska")
|
||||||
|
.file(uploaded);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, message).await {
|
||||||
|
eprintln!("Failed to send video: {:?}", err);
|
||||||
|
if let Err(err) = tclient
|
||||||
|
.send_message(
|
||||||
|
&chat,
|
||||||
|
InputMessage::text("Failed to send video, see logs"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about failing to send video: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
success = false;
|
||||||
|
eprintln!("Failed to upload video: {:?}", err);
|
||||||
|
if let Err(err) = tclient
|
||||||
|
.send_message(
|
||||||
|
&chat,
|
||||||
|
InputMessage::text("Failed to upload video, see logs"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about failing to upload video: {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if success {
|
||||||
|
if let Err(err) = remove_file(&video_filename) {
|
||||||
|
eprintln!("Failed to delete {}: {:?}", &video_filename, err);
|
||||||
|
let text = format!("{:#?}", err);
|
||||||
|
let size = text.len();
|
||||||
|
let mut stream = Cursor::new(text.into_bytes());
|
||||||
|
match tclient
|
||||||
|
.upload_stream(
|
||||||
|
&mut stream,
|
||||||
|
size,
|
||||||
|
format!("failed-delete-{}.log", &video_filename),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(uploaded) => {
|
||||||
|
let message =
|
||||||
|
InputMessage::text(format!("Failed to delete {}", &video_filename))
|
||||||
|
.mime_type("text/plain")
|
||||||
|
.file(uploaded);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, message).await {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to send message about failing to delete {}: {:?}",
|
||||||
|
&video_filename, err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient.send_message(&chat, InputMessage::text(format!("Failed to send message about failing to delete {}, see logs", &video_filename))).await {
|
||||||
|
eprintln!("Failed to send message about failing to send message about failing to delete {}: {:?}", &video_filename, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to upload logs about failing to delete {}: {:?}",
|
||||||
|
&video_filename, err
|
||||||
|
);
|
||||||
|
if let Err(err) = tclient
|
||||||
|
.send_message(
|
||||||
|
&chat,
|
||||||
|
InputMessage::text(format!(
|
||||||
|
"Failed to upload logs about failing to delete {}, see logs",
|
||||||
|
&video_filename
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("Failed to send message about failing to upload logs about failing to delete {}: {:?}", &video_filename, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{
|
||||||
|
tmp_handled.lock().unwrap().remove(&video_id);
|
||||||
|
seen_videos.write().unwrap().push(video_id);
|
||||||
|
}
|
||||||
|
let tmp = seen_videos.read().unwrap().clone();
|
||||||
|
utils::update_seen_videos(&mut tclient, &chat, tmp).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue