// yt - A fully featured command line YouTube client // // Copyright (C) 2025 Benedikt Peetz // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of Yt. // // You should have received a copy of the License along with this program. // If not, see . use curl::easy::Easy; use log::{error, info, warn}; use rustpython::vm::{ PyRef, VirtualMachine, builtins::{PyDict, PyStr}, }; use serde::{Deserialize, Serialize}; use crate::{pydict_cast, pydict_get, wrap_post_processor}; wrap_post_processor!("DeArrow", unwrapped_process, process); /// # Errors /// If the API access fails. pub fn unwrapped_process(info: PyRef, vm: &VirtualMachine) -> Result, Error> { if pydict_get!(@vm, info, "extractor_key", PyStr).as_str() != "Youtube" { warn!("DeArrow: Extractor did not match, exiting."); return Ok(info); } let mut output: DeArrowApi = { let output_bytes = { let mut dst = Vec::new(); let mut easy = Easy::new(); easy.url( format!( "https://sponsor.ajay.app/api/branding?videoID={}", pydict_get!(@vm, info, "id", PyStr).as_str() ) .as_str(), )?; let mut transfer = easy.transfer(); transfer.write_function(|data| { dst.extend_from_slice(data); Ok(data.len()) })?; transfer.perform()?; drop(transfer); dst }; serde_json::from_slice(&output_bytes)? }; // We pop the titles, so we need this vector reversed. output.titles.reverse(); let title_len = output.titles.len(); let selected = loop { let Some(title) = output.titles.pop() else { break false; }; if (title.locked || title.votes < 1) && title_len > 1 { info!( "DeArrow: Skipping title {:#?}, as it is not good enough", title.value ); // Skip titles that are not “good” enough. continue; } update_title(&info, &title.value, vm); break true; }; if !selected && title_len != 0 { // No title was selected, even though we had some titles. // Just pick the first one in this case. update_title(&info, &output.titles[0].value, vm); } Ok(info) } #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Failed to access the DeArrow api: {0}")] Get(#[from] curl::Error), #[error("Failed to deserialize a api json return object: {0}")] Deserialize(#[from] serde_json::Error), } fn update_title(info: &PyRef, new_title: &str, vm: &VirtualMachine) { assert!(!info.contains_key("original_title", vm)); if let Ok(old_title) = info.get_item("title", vm) { warn!( "DeArrow: Updating title from {:#?} to {:#?}", pydict_cast!(@ref old_title, PyStr).as_str(), new_title ); info.set_item("original_title", old_title, vm) .expect("We checked, it is a new key"); } else { warn!("DeArrow: Setting title to {new_title:#?}"); } let cleaned_title = { // NOTE(@bpeetz): DeArrow uses `>` as a “Don't format the next word” mark. // They should be removed, if one does not use a auto-formatter. <2025-06-16> new_title.replace('>', "") }; info.set_item("title", vm.new_pyobj(cleaned_title), vm) .expect("This should work?"); } #[derive(Serialize, Deserialize)] /// See: struct DeArrowApi { titles: Vec, thumbnails: Vec<Thumbnail>, #[serde(alias = "randomTime")] random_time: Option<f64>, #[serde(alias = "videoDuration")] video_duration: Option<f64>, #[serde(alias = "casualVotes")] casual_votes: Vec<CasualVote>, } #[derive(Serialize, Deserialize)] struct CasualVote { id: String, count: u32, title: String, } #[derive(Serialize, Deserialize)] struct Title { /// Note: Titles will sometimes contain > before a word. /// This tells the auto-formatter to not format a word. /// If you have no auto-formatter, you can ignore this and replace it with an empty string #[serde(alias = "title")] value: String, original: bool, votes: u64, locked: bool, #[serde(alias = "UUID")] uuid: String, /// only present if requested #[serde(alias = "userID")] user_id: Option<String>, } #[derive(Serialize, Deserialize)] struct Thumbnail { // null if original is true timestamp: Option<f64>, original: bool, votes: u64, locked: bool, #[serde(alias = "UUID")] uuid: String, /// only present if requested #[serde(alias = "userID")] user_id: Option<String>, }