Initial commit

master
Mageas 3 months ago
commit 408e018a59
Signed by: Mageas
GPG Key ID: B45836562531E7AD

1
.gitignore vendored

@ -0,0 +1 @@
/target

1220
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,18 @@
[package]
name = "piped_playlist_importer"
version = "0.1.0"
edition = "2021"
authors = ["Mageas <dev@mageas.net>"]
description = "Import playlist to Piped from text files"
keywords = ["piped"]
license = "GPL-3.0"
repository = "https://gitea.heartnerds.org/Mageas/piped_playlist_importer"
[dependencies]
thiserror = "1.0.37"
anyhow = "1.0.66"
reqwest = { version = "0.11.12", features = ["json"] }
tokio = { version = "1.21.2", features = ["full"] }
clap = { version = "4.0.18", features = ["derive"] }
serde = { version = "1.0.147", features = ["derive"] }
serde_json = "1.0.87"

@ -0,0 +1,38 @@
# Piped Playlist Importer
Import your playlists to Piped from text files.
## **How to use**
``` text
Usage: piped_playlist_importer [OPTIONS] --authorization <AUTHORIZATION>
Options:
-p, --playlists-directory <PLAYLISTS_DIRECTORY>
Path of the directory containing playlists
-a, --authorization <AUTHORIZATION>
Authorization code found in the local storage of your browser (authToken...)
-i, --instance <INSTANCE>
Authentication instance [default: https://pipedapi.kavin.rocks]
-h, --help
Print help information
-V, --version
Print version information
```
## **Install instructions**
Clone the repository:
```
git clone https://gitea.heartnerds.org/Mageas/piped_playlist_importer
```
Move into the project directory:
```
cd piped_playlist_importer
```
Install the project with cargo:
```
cargo install --path=.
```

@ -0,0 +1 @@
edition = "2021"

@ -0,0 +1,15 @@
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about)]
pub struct Cli {
/// Path of the directory containing playlists
#[arg(short, long)]
pub playlists_directory: Option<String>,
/// Authorization code found in the local storage of your browser (authToken...)
#[arg(short, long)]
pub authorization: String,
/// Authentication instance
#[arg(short, long, default_value = "https://pipedapi.kavin.rocks")]
pub instance: String,
}

@ -0,0 +1,30 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PipedPlaylistImporterError {
#[error("Unable to parse the filename for {0}")]
FileName(std::path::PathBuf),
#[error("Unable to list files for {1}: {0}")]
ListFiles(#[source] std::io::Error, String),
#[error("Unable to read lines from {1}: {0}")]
ReadLines(#[source] std::io::Error, String),
#[error("Unable to read : {0}")]
ReadFile(#[from] std::io::Error),
#[error("Unable to deserialize : {0}")]
Deserialize(#[from] serde_json::Error),
#[error("Unable contact '{1}' : {0}")]
ContactApi(#[source] reqwest::Error, String),
#[error("{0}")]
GeneralRequest(#[from] reqwest::Error),
#[error("[{0}] Unable to contact '{1}' : {2}")]
Request(reqwest::StatusCode, String, String),
}
pub type PipedPlaylistImporterResult<T = ()> = Result<T, PipedPlaylistImporterError>;

@ -0,0 +1,83 @@
use anyhow::{Context, Result};
use clap::Parser;
mod error;
mod piped;
use piped::*;
mod playlist;
use playlist::*;
mod args;
use args::*;
#[tokio::main]
async fn main() -> Result<()> {
let args = Cli::parse();
let current_directory =
std::env::current_dir().context("Unable to get the current directory from the env")?;
let directory = match &args.playlists_directory {
Some(input) => std::path::PathBuf::from(input),
None => current_directory,
};
let directory_files = directory.read_dir().context(format!(
"Unable to read the directory {}",
&directory.display()
))?;
let playlists = Playlists::new(directory_files, &directory)?;
let client = PipedClient::new(&args.instance, &args.authorization);
let piped_playlists = client.get_playlists().await?;
let mut global_count = 0;
playlists
.iter()
.for_each(|playlist| global_count += playlist.urls.len());
let mut errors: Vec<(String, String)> = vec![];
let mut count = 0;
for playlist in playlists {
if piped_playlists.iter().any(|c| c.name == playlist.name) {
println!("Skip {}", playlist.name);
continue;
}
let response = client.create_playlist(&playlist.name).await?;
let playlist_id = &response.playlist_id;
println!("Created {} ({})", playlist.name, playlist_id);
for video in playlist.urls {
let video_id = video.split('=').nth(1).unwrap();
let response = client.add_video_to_playlist(video_id, playlist_id).await?;
count += 1;
if response.message == "ok" {
println!(
"({}/{}) Added {} to {} ({})",
count, global_count, video_id, playlist.name, playlist_id
);
} else {
println!(
"({}/{}) Error while adding {} to {} ({})",
count, global_count, video_id, playlist.name, playlist_id
);
errors.push((playlist_id.to_owned(), video_id.to_owned()));
}
}
}
for error in errors {
println!("Unable to add {} in {}", error.0, error.1);
}
Ok(())
}

@ -0,0 +1,146 @@
use serde::{Deserialize, Serialize};
use reqwest::header::{AUTHORIZATION, USER_AGENT};
use reqwest::Client;
use crate::error::{PipedPlaylistImporterError, PipedPlaylistImporterResult};
#[derive(Debug)]
pub struct PipedClient {
pub httpclient: Client,
pub instance: String,
pub authorization: String,
}
const USER_AGENT_: &str = "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0";
impl PipedClient {
/// New piped client
pub fn new<S: AsRef<str>>(instance: S, authorization: S) -> PipedClient {
let mut url = instance.as_ref().to_owned();
if !url.starts_with("http") {
url = format!("https://{}", url);
}
if url.ends_with('/') {
url.pop();
}
PipedClient {
httpclient: Client::new(),
instance: url,
authorization: authorization.as_ref().to_owned(),
}
}
/// Retrieve the playlists from piped
pub async fn get_playlists(
&self,
) -> PipedPlaylistImporterResult<Vec<PipedGetPlaylistResponse>> {
let url = format!("{}/user/playlists", self.instance);
let response = self.get(&url).await?;
Ok(serde_json::from_str::<Vec<PipedGetPlaylistResponse>>(
&response,
)?)
}
/// Create a playlist
pub async fn create_playlist(
&self,
name: &str,
) -> PipedPlaylistImporterResult<PipedCreatePlaylistResponse> {
let url = format!("{}/user/playlists/create", self.instance);
let body = std::collections::HashMap::from([("name", name)]);
let response = self.post(&url, &body).await?;
Ok(serde_json::from_str::<PipedCreatePlaylistResponse>(
&response,
)?)
}
/// Add a video to a playlist
pub async fn add_video_to_playlist(
&self,
video_id: &str,
playlist_id: &str,
) -> PipedPlaylistImporterResult<PipedAddVideoToPlaylistResponse> {
let url = format!("{}/user/playlists/add", self.instance);
let body =
std::collections::HashMap::from([("videoId", video_id), ("playlistId", playlist_id)]);
let response = self.post(&url, &body).await?;
Ok(serde_json::from_str::<PipedAddVideoToPlaylistResponse>(
&response,
)?)
}
}
impl PipedClient {
/// Post request
async fn post<S>(&self, url: &str, body: &S) -> PipedPlaylistImporterResult<String>
where
S: Serialize,
{
let response = self
.httpclient
.post(url)
.json(&body)
.header(USER_AGENT, USER_AGENT_)
.header(AUTHORIZATION, &self.authorization)
.send()
.await
.map_err(|e| PipedPlaylistImporterError::ContactApi(e, url.to_owned()))?;
if response.status() != 200 {
return Err(PipedPlaylistImporterError::Request(
response.status(),
url.to_owned(),
response.text().await?,
));
}
Ok(response
.text()
.await
.map_err(|e| PipedPlaylistImporterError::ContactApi(e, url.to_owned()))?)
}
/// Get request
async fn get(&self, url: &str) -> PipedPlaylistImporterResult<String> {
let response = self
.httpclient
.get(url)
.header(USER_AGENT, USER_AGENT_)
.header(AUTHORIZATION, &self.authorization)
.send()
.await
.map_err(|e| PipedPlaylistImporterError::ContactApi(e, url.to_owned()))?;
if response.status() != 200 {
return Err(PipedPlaylistImporterError::Request(
response.status(),
url.to_owned(),
response.text().await?,
));
}
Ok(response
.text()
.await
.map_err(|e| PipedPlaylistImporterError::ContactApi(e, url.to_owned()))?)
}
}
#[derive(Debug, Deserialize)]
pub struct PipedGetPlaylistResponse {
pub id: String,
pub name: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PipedCreatePlaylistResponse {
pub playlist_id: String,
}
#[derive(Debug, Deserialize)]
pub struct PipedAddVideoToPlaylistResponse {
pub message: String,
}

@ -0,0 +1,89 @@
use std::fs::ReadDir;
use std::io::{prelude::*, BufReader};
use std::path::{Path, PathBuf};
use crate::error::{PipedPlaylistImporterError, PipedPlaylistImporterResult};
pub struct Playlists {}
impl Playlists {
pub fn new(files: ReadDir, directory: &Path) -> PipedPlaylistImporterResult<Vec<Playlist>> {
let playlists = Self::list_directory_files(files, directory)?;
let playlists = Self::convert_to_playlist(playlists)?;
Ok(playlists)
}
/// Convert to vec of playlists
fn convert_to_playlist(playlists: Vec<PathBuf>) -> PipedPlaylistImporterResult<Vec<Playlist>> {
let mut return_playlists: Vec<Playlist> = vec![];
let mut return_actual_playlist: Playlist = Playlist::new("".to_string());
for playlist in playlists {
let path = playlist
.to_str()
.ok_or_else(|| PipedPlaylistImporterError::FileName(playlist.clone()))?;
let urls = Self::read_file(path)?;
let name = playlist
.file_name()
.ok_or_else(|| PipedPlaylistImporterError::FileName(playlist.clone()))?
.to_str()
.ok_or_else(|| PipedPlaylistImporterError::FileName(playlist.clone()))?;
for url in urls {
if name != return_actual_playlist.name {
return_playlists.push(return_actual_playlist);
return_actual_playlist = Playlist::new(name.to_owned())
}
return_actual_playlist.push(url);
}
}
return_playlists.remove(0);
Ok(return_playlists)
}
/// List the files from a directory
fn list_directory_files(
files: ReadDir,
directory: &Path,
) -> PipedPlaylistImporterResult<Vec<PathBuf>> {
let mut output = vec![];
for file in files {
let file = file
.map_err(|e| {
PipedPlaylistImporterError::ListFiles(e, directory.to_str().unwrap().to_owned())
})?
.path();
output.push(file.to_owned());
}
Ok(output)
}
/// Read the file to a vec of strings
fn read_file(path: &str) -> PipedPlaylistImporterResult<Vec<String>> {
let mut lines = vec![];
for line in BufReader::new(std::fs::File::open(path)?).lines() {
let line =
line.map_err(|e| PipedPlaylistImporterError::ReadLines(e, path.to_owned()))?;
lines.push(line);
}
Ok(lines)
}
}
#[derive(Debug)]
pub struct Playlist {
pub name: String,
pub urls: Vec<String>,
}
impl Playlist {
fn new(name: String) -> Self {
Self { name, urls: vec![] }
}
fn push(&mut self, url: String) {
self.urls.push(url);
}
}
Loading…
Cancel
Save