// 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(())
}