// yt - A fully featured command line YouTube client // // Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> // 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 <https://www.gnu.org/licenses/gpl-3.0.txt>. use std::str::FromStr; use anyhow::{bail, Context, Result}; use futures::FutureExt; use log::warn; use serde_json::{json, Value}; use tokio::io::{AsyncBufRead, AsyncBufReadExt}; use url::Url; use yt_dlp::wrapper::info_json::InfoType; use crate::{ app::App, storage::subscriptions::{ add_subscription, check_url, get_subscriptions, remove_all_subscriptions, remove_subscription, Subscription, }, }; pub async fn unsubscribe(app: &App, name: String) -> Result<()> { let present_subscriptions = get_subscriptions(&app).await?; if let Some(subscription) = present_subscriptions.0.get(&name) { remove_subscription(&app, subscription).await?; } else { bail!("Couldn't find subscription: '{}'", &name); } Ok(()) } pub async fn import<W: AsyncBufRead + AsyncBufReadExt + Unpin>( app: &App, reader: W, force: bool, ) -> Result<()> { if force { remove_all_subscriptions(&app).await?; } let mut lines = reader.lines(); while let Some(line) = lines.next_line().await? { let url = Url::from_str(&line).with_context(|| format!("Failed to parse '{}' as url", line))?; match subscribe(app, None, url) .await .with_context(|| format!("Failed to subscribe to: '{}'", line)) { Ok(_) => (), Err(err) => eprintln!( "Error while subscribing to '{}': '{}'", line, err.source().expect("Should have a source").to_string() ), } } Ok(()) } pub async fn subscribe(app: &App, name: Option<String>, url: Url) -> Result<()> { if !(url.as_str().ends_with("videos") || url.as_str().ends_with("streams") || url.as_str().ends_with("shorts") || url.as_str().ends_with("videos/") || url.as_str().ends_with("streams/") || url.as_str().ends_with("shorts/")) && url.as_str().contains("youtube.com") { warn!("Your youtbe url does not seem like it actually tracks a channels playlist (videos, streams, shorts). Adding subscriptions for each of them..."); let url = Url::parse(&(url.as_str().to_owned() + "/")) .expect("This was an url, it should stay one"); if let Some(name) = name { let out: Result<()> = async move { actual_subscribe( &app, Some(name.clone() + " {Videos}"), url.join("videos/").expect("Works"), ) .await .with_context(|| { format!("Failed to subscribe to '{}'", name.clone() + " {Videos}") })?; actual_subscribe( &app, Some(name.clone() + " {Streams}"), url.join("streams/").expect("Works"), ) .await .with_context(|| { format!("Failed to subscribe to '{}'", name.clone() + " {Streams}") })?; actual_subscribe( &app, Some(name.clone() + " {Shorts}"), url.join("shorts/").expect("Works"), ) .await .with_context(|| format!("Failed to subscribe to '{}'", name + " {Shorts}"))?; Ok(()) } .boxed() .await; out? } else { actual_subscribe(&app, None, url.join("videos/").expect("Works")) .await .with_context(|| format!("Failed to subscribe to the '{}' variant", "{Videos}"))?; actual_subscribe(&app, None, url.join("streams/").expect("Works")) .await .with_context(|| format!("Failed to subscribe to the '{}' variant", "{Streams}"))?; actual_subscribe(&app, None, url.join("shorts/").expect("Works")) .await .with_context(|| format!("Failed to subscribe to the '{}' variant", "{Shorts}"))?; } } else { actual_subscribe(&app, name, url).await?; } Ok(()) } async fn actual_subscribe(app: &App, name: Option<String>, url: Url) -> Result<()> { if !check_url(&url).await? { bail!("The url ('{}') does not represent a playlist!", &url) }; let name = if let Some(name) = name { name } else { let yt_opts = match json!( { "playliststart": 1, "playlistend": 10, "noplaylist": false, "extract_flat": "in_playlist", }) { Value::Object(map) => map, _ => unreachable!("This is hardcoded"), }; let info = yt_dlp::extract_info(&yt_opts, &url, false, false).await?; if info._type == Some(InfoType::Playlist) { info.title.expect("This should be some for a playlist") } else { bail!("The url ('{}') does not represent a playlist!", &url) } }; let present_subscriptions = get_subscriptions(&app).await?; if let Some(subs) = present_subscriptions.0.get(&name) { bail!( "The subscription '{}' could not be added, \ as another one with the same name ('{}') already exists. It links to the Url: '{}'", name, name, subs.url ); } let sub = Subscription { name, url }; add_subscription(&app, &sub).await?; Ok(()) }