youtube download added
This commit is contained in:
parent
d6db006412
commit
8a398ceb2c
25
Cargo.lock
generated
25
Cargo.lock
generated
@ -577,7 +577,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1320,6 +1320,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "json"
|
||||||
|
version = "0.12.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@ -1442,6 +1448,7 @@ dependencies = [
|
|||||||
"toml",
|
"toml",
|
||||||
"tui-big-text",
|
"tui-big-text",
|
||||||
"twox-hash",
|
"twox-hash",
|
||||||
|
"youtube-api",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2105,7 +2112,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2494,7 +2501,7 @@ dependencies = [
|
|||||||
"getrandom 0.3.1",
|
"getrandom 0.3.1",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3025,7 +3032,7 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3215,6 +3222,16 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "youtube-api"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "git+https://gitea.awain.net/alterwain/youtube_api.git#bfac55ac2e9459b4eb51ed0bfe15fe61e29da4d7"
|
||||||
|
dependencies = [
|
||||||
|
"json",
|
||||||
|
"reqwest",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.7.35"
|
version = "0.7.35"
|
||||||
|
@ -21,6 +21,7 @@ futures = "0.3"
|
|||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-util = { version = "0.7.12", features = ["codec"] }
|
tokio-util = { version = "0.7.12", features = ["codec"] }
|
||||||
soundcloud = { version = "0.1.9", git = "https://gitea.awain.net/alterwain/soundcloud_api.git" }
|
soundcloud = { version = "0.1.9", git = "https://gitea.awain.net/alterwain/soundcloud_api.git" }
|
||||||
|
youtube-api = { version = "0.1.1", git = "https://gitea.awain.net/alterwain/youtube_api.git" }
|
||||||
itunesdb = { version = "0.1.99", git = "https://gitea.awain.net/alterwain/ITunesDB.git" }
|
itunesdb = { version = "0.1.99", git = "https://gitea.awain.net/alterwain/ITunesDB.git" }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
tui-big-text = "0.7.1"
|
tui-big-text = "0.7.1"
|
||||||
|
@ -32,31 +32,22 @@ pub fn get_temp_itunesdb() -> PathBuf {
|
|||||||
p
|
p
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize, Default)]
|
||||||
pub struct YouTubeConfiguration {
|
pub struct YouTubeConfiguration {
|
||||||
pub user_id: u64,
|
pub user_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize, Default)]
|
||||||
pub struct SoundCloudConfiguration {
|
pub struct SoundCloudConfiguration {
|
||||||
pub user_id: u64,
|
pub user_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize, Default)]
|
||||||
pub struct LyricaConfiguration {
|
pub struct LyricaConfiguration {
|
||||||
soundcloud: SoundCloudConfiguration,
|
soundcloud: SoundCloudConfiguration,
|
||||||
youtube: YouTubeConfiguration,
|
youtube: YouTubeConfiguration,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LyricaConfiguration {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
soundcloud: SoundCloudConfiguration { user_id: 0 },
|
|
||||||
youtube: YouTubeConfiguration { user_id: 0 },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LyricaConfiguration {
|
impl LyricaConfiguration {
|
||||||
pub fn get_soundcloud(&self) -> &SoundCloudConfiguration {
|
pub fn get_soundcloud(&self) -> &SoundCloudConfiguration {
|
||||||
&self.soundcloud
|
&self.soundcloud
|
||||||
|
126
src/dlp.rs
126
src/dlp.rs
@ -18,6 +18,64 @@ pub struct DownloadProgress {
|
|||||||
pub eta: String,
|
pub eta: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn download_track_from_youtube(
|
||||||
|
track_url: &str,
|
||||||
|
download_dir: &PathBuf,
|
||||||
|
sender: Sender<AppEvent>,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let _ = sender
|
||||||
|
.send(AppEvent::SwitchScreen(crate::AppState::LoadingScreen))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if download_dir.exists() {
|
||||||
|
let _ = std::fs::remove_dir_all(download_dir);
|
||||||
|
}
|
||||||
|
let _ = std::fs::create_dir_all(download_dir);
|
||||||
|
|
||||||
|
let args = &[
|
||||||
|
"-f",
|
||||||
|
"bestaudio",
|
||||||
|
"-x",
|
||||||
|
"--audio-format",
|
||||||
|
"mp3",
|
||||||
|
"--audio-quality",
|
||||||
|
"0",
|
||||||
|
"-o",
|
||||||
|
"%(id)s.%(ext)s",
|
||||||
|
"--ignore-errors",
|
||||||
|
"--newline",
|
||||||
|
"--progress-template",
|
||||||
|
"{\"progress_percentage\":\"%(progress._percent_str)s\",\"progress_total\":\"%(progress._total_bytes_str)s\",\"speed\":\"%(progress._speed_str)s\",\"eta\":\"%(progress._eta_str)s\"}",
|
||||||
|
"--write-thumbnail",
|
||||||
|
&*["https://youtube.com/watch?v=", track_url].concat()
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut command = Command::new("yt-dlp");
|
||||||
|
command.args(args);
|
||||||
|
command.stdout(Stdio::piped());
|
||||||
|
command.stderr(Stdio::null());
|
||||||
|
command.current_dir(download_dir);
|
||||||
|
|
||||||
|
let mut child = command.spawn()?;
|
||||||
|
|
||||||
|
let stdout = child.stdout.take().unwrap();
|
||||||
|
let mut reader = BufReader::new(stdout).lines();
|
||||||
|
|
||||||
|
while let Ok(Some(line)) = reader.next_line().await {
|
||||||
|
if line.starts_with("{") {
|
||||||
|
let progress: DownloadProgress = serde_json::from_str(&line).unwrap();
|
||||||
|
let _ = sender
|
||||||
|
.send(AppEvent::OverallProgress((0, 1, Color::Green)))
|
||||||
|
.await;
|
||||||
|
let _ = sender.send(AppEvent::CurrentProgress(progress)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = sender
|
||||||
|
.send(AppEvent::OverallProgress((1, 1, Color::Green)))
|
||||||
|
.await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn download_track_from_soundcloud(
|
pub async fn download_track_from_soundcloud(
|
||||||
track_url: &str,
|
track_url: &str,
|
||||||
download_dir: &PathBuf,
|
download_dir: &PathBuf,
|
||||||
@ -71,6 +129,74 @@ pub async fn download_track_from_soundcloud(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn download_from_youtube(
|
||||||
|
playlist_url: &str,
|
||||||
|
download_dir: &PathBuf,
|
||||||
|
sender: Sender<AppEvent>,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let _ = sender
|
||||||
|
.send(AppEvent::SwitchScreen(crate::AppState::LoadingScreen))
|
||||||
|
.await;
|
||||||
|
let dl_rx: Regex = Regex::new(r"\[download\] Downloading item \d+ of \d+").unwrap();
|
||||||
|
|
||||||
|
if download_dir.exists() {
|
||||||
|
let _ = std::fs::remove_dir_all(download_dir);
|
||||||
|
}
|
||||||
|
let _ = std::fs::create_dir_all(download_dir);
|
||||||
|
|
||||||
|
let args = &[
|
||||||
|
"-f",
|
||||||
|
"bestaudio",
|
||||||
|
"-x",
|
||||||
|
"--audio-format",
|
||||||
|
"mp3",
|
||||||
|
"--audio-quality",
|
||||||
|
"0",
|
||||||
|
"-o",
|
||||||
|
"%(id)s.%(ext)s",
|
||||||
|
"--ignore-errors",
|
||||||
|
"--newline",
|
||||||
|
"--progress-template",
|
||||||
|
"{\"progress_percentage\":\"%(progress._percent_str)s\",\"progress_total\":\"%(progress._total_bytes_str)s\",\"speed\":\"%(progress._speed_str)s\",\"eta\":\"%(progress._eta_str)s\"}",
|
||||||
|
"--write-thumbnail",
|
||||||
|
&*["https://youtube.com", playlist_url].concat()
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut command = Command::new("yt-dlp");
|
||||||
|
command.args(args);
|
||||||
|
command.stdout(Stdio::piped());
|
||||||
|
command.stderr(Stdio::null());
|
||||||
|
command.current_dir(download_dir);
|
||||||
|
|
||||||
|
let mut child = command.spawn()?;
|
||||||
|
|
||||||
|
let stdout = child.stdout.take().unwrap();
|
||||||
|
let mut reader = BufReader::new(stdout).lines();
|
||||||
|
|
||||||
|
while let Ok(Some(line)) = reader.next_line().await {
|
||||||
|
match dl_rx.find(&line) {
|
||||||
|
Some(m) => {
|
||||||
|
let mut s = m.as_str();
|
||||||
|
s = s.split("Downloading item ").last().unwrap();
|
||||||
|
let s: Vec<&str> = s.split(' ').collect();
|
||||||
|
let cur = s.first().unwrap().trim().parse().unwrap();
|
||||||
|
let max = s.last().unwrap().trim().parse().unwrap();
|
||||||
|
let _ = sender
|
||||||
|
.send(AppEvent::OverallProgress((cur, max, Color::Green)))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if line.starts_with("{") {
|
||||||
|
let progress: DownloadProgress = serde_json::from_str(&line).unwrap();
|
||||||
|
let _ = sender.send(AppEvent::CurrentProgress(progress)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn download_from_soundcloud(
|
pub async fn download_from_soundcloud(
|
||||||
playlist_url: &str,
|
playlist_url: &str,
|
||||||
download_dir: &PathBuf,
|
download_dir: &PathBuf,
|
||||||
|
@ -121,6 +121,10 @@ impl App {
|
|||||||
let screen: &mut MainScreen = self.get_screen(&AppState::MainScreen);
|
let screen: &mut MainScreen = self.get_screen(&AppState::MainScreen);
|
||||||
screen.set_soundcloud_playlists(playlists);
|
screen.set_soundcloud_playlists(playlists);
|
||||||
},
|
},
|
||||||
|
AppEvent::YoutubeGot(playlists) => {
|
||||||
|
let screen: &mut MainScreen = self.get_screen(&AppState::MainScreen);
|
||||||
|
screen.set_youtube_playlists(playlists);
|
||||||
|
},
|
||||||
AppEvent::OverallProgress((c, max, color)) => {
|
AppEvent::OverallProgress((c, max, color)) => {
|
||||||
self.state = AppState::LoadingScreen;
|
self.state = AppState::LoadingScreen;
|
||||||
let screen: &mut LoadingScreen = self.get_screen(&AppState::LoadingScreen);
|
let screen: &mut LoadingScreen = self.get_screen(&AppState::LoadingScreen);
|
||||||
|
@ -11,7 +11,7 @@ use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists};
|
|||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
use crate::component::table::SmartTable;
|
use crate::component::table::SmartTable;
|
||||||
use crate::sync::DBPlaylist;
|
use crate::sync::{DBPlaylist, YTPlaylist};
|
||||||
use crate::{screen::AppScreen, sync::AppEvent, theme::Theme, AppState};
|
use crate::{screen::AppScreen, sync::AppEvent, theme::Theme, AppState};
|
||||||
|
|
||||||
pub struct MainScreen {
|
pub struct MainScreen {
|
||||||
@ -20,6 +20,7 @@ pub struct MainScreen {
|
|||||||
pl_table: SmartTable,
|
pl_table: SmartTable,
|
||||||
song_table: SmartTable,
|
song_table: SmartTable,
|
||||||
tab_titles: Vec<String>,
|
tab_titles: Vec<String>,
|
||||||
|
youtube: Option<Vec<YTPlaylist>>,
|
||||||
soundcloud: Option<Vec<CloudPlaylist>>,
|
soundcloud: Option<Vec<CloudPlaylist>>,
|
||||||
playlists: Option<Vec<DBPlaylist>>,
|
playlists: Option<Vec<DBPlaylist>>,
|
||||||
sender: UnboundedSender<AppEvent>,
|
sender: UnboundedSender<AppEvent>,
|
||||||
@ -104,6 +105,7 @@ impl MainScreen {
|
|||||||
pl_table: SmartTable::default(),
|
pl_table: SmartTable::default(),
|
||||||
song_table: SmartTable::default(),
|
song_table: SmartTable::default(),
|
||||||
soundcloud: None,
|
soundcloud: None,
|
||||||
|
youtube: None,
|
||||||
playlists: None,
|
playlists: None,
|
||||||
selected_tab: 0,
|
selected_tab: 0,
|
||||||
tab_titles: vec![
|
tab_titles: vec![
|
||||||
@ -229,33 +231,73 @@ impl MainScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn download_row(&mut self) {
|
fn download_row(&mut self) {
|
||||||
if self.selected_tab == 1 {
|
match self.selected_tab {
|
||||||
// SC
|
0 => {
|
||||||
match self.mode {
|
// YT
|
||||||
false => {
|
match self.mode {
|
||||||
let playlist = self
|
false => {
|
||||||
.soundcloud
|
let playlist = self
|
||||||
.as_ref()
|
.youtube
|
||||||
.unwrap()
|
.as_ref()
|
||||||
.get(self.pl_table.selected_row())
|
.unwrap()
|
||||||
.unwrap()
|
.get(self.pl_table.selected_row())
|
||||||
.clone();
|
.unwrap()
|
||||||
let _ = self.sender.send(AppEvent::DownloadPlaylist(playlist));
|
.clone();
|
||||||
}
|
|
||||||
true => {
|
let _ = self.sender.send(AppEvent::DownloadYTPlaylist(playlist));
|
||||||
let track = self
|
}
|
||||||
.soundcloud
|
true => {
|
||||||
.as_ref()
|
let track = self
|
||||||
.unwrap()
|
.youtube
|
||||||
.get(self.pl_table.selected_row())
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.tracks
|
.get(self.pl_table.selected_row())
|
||||||
.get(self.song_table.selected_row())
|
.unwrap()
|
||||||
.unwrap()
|
.videos
|
||||||
.clone();
|
.get(self.song_table.selected_row())
|
||||||
let _ = self.sender.send(AppEvent::DownloadTrack(track));
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let _ = self.sender.send(AppEvent::DownloadYTTrack(track));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1 => {
|
||||||
|
// SC
|
||||||
|
match self.mode {
|
||||||
|
false => {
|
||||||
|
let playlist = self
|
||||||
|
.soundcloud
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.get(self.pl_table.selected_row())
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
let _ = self.sender.send(AppEvent::DownloadPlaylist(playlist));
|
||||||
|
}
|
||||||
|
true => {
|
||||||
|
let track = self
|
||||||
|
.soundcloud
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.get(self.pl_table.selected_row())
|
||||||
|
.unwrap()
|
||||||
|
.tracks
|
||||||
|
.get(self.song_table.selected_row())
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
let _ = self.sender.send(AppEvent::DownloadTrack(track));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_youtube_playlists(&mut self, pls: Vec<YTPlaylist>) {
|
||||||
|
self.youtube = Some(pls);
|
||||||
|
if self.selected_tab == 0 {
|
||||||
|
self.update_tables();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,6 +334,23 @@ impl MainScreen {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let data = match self.selected_tab {
|
let data = match self.selected_tab {
|
||||||
|
0 => {
|
||||||
|
if let Some(yt) = &self.youtube {
|
||||||
|
yt.iter()
|
||||||
|
.map(|playlist| {
|
||||||
|
vec![
|
||||||
|
0.to_string(),
|
||||||
|
playlist.title.clone(),
|
||||||
|
[playlist.videos.len().to_string(), " songs".to_string()].concat(),
|
||||||
|
String::new(),
|
||||||
|
"NO".to_string(),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect::<Vec<Vec<String>>>()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
1 => {
|
1 => {
|
||||||
if let Some(sc) = &self.soundcloud {
|
if let Some(sc) = &self.soundcloud {
|
||||||
sc.iter()
|
sc.iter()
|
||||||
@ -349,6 +408,35 @@ impl MainScreen {
|
|||||||
.to_vec();
|
.to_vec();
|
||||||
|
|
||||||
match self.selected_tab {
|
match self.selected_tab {
|
||||||
|
0 => {
|
||||||
|
self.song_table = SmartTable::new(
|
||||||
|
["Id", "Title", "Artist", "Duration", ""]
|
||||||
|
.iter_mut()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect(),
|
||||||
|
constraints,
|
||||||
|
);
|
||||||
|
self.set_mode(self.mode);
|
||||||
|
|
||||||
|
if let Some(pls) = &self.youtube {
|
||||||
|
let y = &pls.get(self.pl_table.selected_row()).unwrap().videos;
|
||||||
|
let data = y
|
||||||
|
.iter()
|
||||||
|
.map(|video| {
|
||||||
|
vec![
|
||||||
|
video.videoId.clone(),
|
||||||
|
video.title.clone(),
|
||||||
|
video.publisher.clone(),
|
||||||
|
video.lengthSeconds.to_string(),
|
||||||
|
String::new(),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect::<Vec<Vec<String>>>();
|
||||||
|
|
||||||
|
self.song_table.set_data(data);
|
||||||
|
}
|
||||||
|
self.song_table.set_title(" Songs ".to_string());
|
||||||
|
}
|
||||||
1 => {
|
1 => {
|
||||||
self.song_table = SmartTable::new(
|
self.song_table = SmartTable::new(
|
||||||
["Id", "Title", "Artist", "Duration", "Genre"]
|
["Id", "Title", "Artist", "Duration", "Genre"]
|
||||||
|
220
src/sync.rs
220
src/sync.rs
@ -1,3 +1,11 @@
|
|||||||
|
use crate::util::IPodImage;
|
||||||
|
use crate::{
|
||||||
|
config::{
|
||||||
|
get_config_path, get_configs_dir, get_temp_dl_dir, get_temp_itunesdb, LyricaConfiguration,
|
||||||
|
},
|
||||||
|
dlp::{self, DownloadProgress},
|
||||||
|
util, AppState,
|
||||||
|
};
|
||||||
use audiotags::Tag;
|
use audiotags::Tag;
|
||||||
use color_eyre::owo_colors::OwoColorize;
|
use color_eyre::owo_colors::OwoColorize;
|
||||||
use image::imageops::FilterType;
|
use image::imageops::FilterType;
|
||||||
@ -6,6 +14,7 @@ use itunesdb::artworkdb::aobjects::ADatabase;
|
|||||||
use itunesdb::objects::{ListSortOrder, PlaylistItem};
|
use itunesdb::objects::{ListSortOrder, PlaylistItem};
|
||||||
use itunesdb::serializer;
|
use itunesdb::serializer;
|
||||||
use itunesdb::xobjects::{XDatabase, XPlArgument, XPlaylist, XTrackItem};
|
use itunesdb::xobjects::{XDatabase, XPlArgument, XPlaylist, XTrackItem};
|
||||||
|
use rand::random;
|
||||||
use ratatui::style::Color;
|
use ratatui::style::Color;
|
||||||
use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists, CloudTrack};
|
use soundcloud::sobjects::{CloudPlaylist, CloudPlaylists, CloudTrack};
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
@ -18,23 +27,18 @@ use tokio::{
|
|||||||
sync::mpsc::{Sender, UnboundedReceiver},
|
sync::mpsc::{Sender, UnboundedReceiver},
|
||||||
};
|
};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
use youtube_api::objects::YoutubeVideo;
|
||||||
use crate::util::IPodImage;
|
|
||||||
use crate::{
|
|
||||||
config::{
|
|
||||||
get_config_path, get_configs_dir, get_temp_dl_dir, get_temp_itunesdb, LyricaConfiguration,
|
|
||||||
},
|
|
||||||
dlp::{self, DownloadProgress},
|
|
||||||
util, AppState,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub enum AppEvent {
|
pub enum AppEvent {
|
||||||
SearchIPod,
|
SearchIPod,
|
||||||
IPodNotFound,
|
IPodNotFound,
|
||||||
ITunesParsed(Vec<DBPlaylist>),
|
ITunesParsed(Vec<DBPlaylist>),
|
||||||
|
YoutubeGot(Vec<YTPlaylist>),
|
||||||
SoundcloudGot(CloudPlaylists),
|
SoundcloudGot(CloudPlaylists),
|
||||||
DownloadPlaylist(CloudPlaylist),
|
DownloadPlaylist(CloudPlaylist),
|
||||||
DownloadTrack(CloudTrack),
|
DownloadTrack(CloudTrack),
|
||||||
|
DownloadYTPlaylist(YTPlaylist),
|
||||||
|
DownloadYTTrack(YoutubeVideo),
|
||||||
CurrentProgress(DownloadProgress),
|
CurrentProgress(DownloadProgress),
|
||||||
OverallProgress((u32, u32, ratatui::style::Color)),
|
OverallProgress((u32, u32, ratatui::style::Color)),
|
||||||
ArtworkProgress((u32, u32)),
|
ArtworkProgress((u32, u32)),
|
||||||
@ -54,6 +58,84 @@ pub struct DBPlaylist {
|
|||||||
pub tracks: Vec<XTrackItem>,
|
pub tracks: Vec<XTrackItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct YTPlaylist {
|
||||||
|
pub title: String,
|
||||||
|
pub url: String,
|
||||||
|
pub videos: Vec<YoutubeVideo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn track_from_video(
|
||||||
|
value: &YoutubeVideo,
|
||||||
|
ipod_path: String,
|
||||||
|
sender: &Sender<AppEvent>,
|
||||||
|
) -> Option<XTrackItem> {
|
||||||
|
let mut track_path = get_temp_dl_dir();
|
||||||
|
track_path.push(&value.videoId);
|
||||||
|
track_path.set_extension("mp3");
|
||||||
|
let mut image_path = get_temp_dl_dir();
|
||||||
|
image_path.push(&value.videoId);
|
||||||
|
image_path.set_extension("webp");
|
||||||
|
|
||||||
|
let audio_file = audio_file_info::from_path(track_path.to_str().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let audio_info = &audio_file.audio_file.tracks.track;
|
||||||
|
let song_dbid = util::hash_from_path(track_path.clone());
|
||||||
|
|
||||||
|
let mut track = XTrackItem::new(
|
||||||
|
random(),
|
||||||
|
File::open(track_path)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.metadata()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.size() as u32,
|
||||||
|
(audio_info.duration * 1000.0) as u32,
|
||||||
|
0,
|
||||||
|
(audio_info.bit_rate / 1000) as u32,
|
||||||
|
audio_info.sample_rate as u32,
|
||||||
|
song_dbid,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if image_path.exists() {
|
||||||
|
let _ = sender.send(AppEvent::ArtworkProgress((0, 2))).await;
|
||||||
|
let mut adb = get_artwork_db(&ipod_path);
|
||||||
|
|
||||||
|
let image_data = std::fs::read(image_path).unwrap();
|
||||||
|
|
||||||
|
let cover_hash = util::hash(&image_data);
|
||||||
|
|
||||||
|
let if_cover_present = adb.if_cover_present(cover_hash);
|
||||||
|
|
||||||
|
let (small_img_name, large_img_name) = adb.add_images(song_dbid, cover_hash);
|
||||||
|
|
||||||
|
let size = image_data.len();
|
||||||
|
|
||||||
|
if !if_cover_present {
|
||||||
|
make_cover_image(&image_data, &ipod_path, &small_img_name, (100, 100));
|
||||||
|
let _ = sender.send(AppEvent::ArtworkProgress((1, 2))).await;
|
||||||
|
make_cover_image(&image_data, &ipod_path, &large_img_name, (200, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
write_artwork_db(adb, &ipod_path);
|
||||||
|
|
||||||
|
track.data.artwork_size = size as u32;
|
||||||
|
track.data.mhii_link = 0;
|
||||||
|
track.data.has_artwork = 1;
|
||||||
|
track.data.artwork_count = 1;
|
||||||
|
let _ = sender.send(AppEvent::ArtworkProgress((2, 2))).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio_file.modify_xtrack(&mut track);
|
||||||
|
|
||||||
|
track.set_title(value.title.clone());
|
||||||
|
track.set_artist(value.publisher.clone());
|
||||||
|
Some(track)
|
||||||
|
}
|
||||||
|
|
||||||
async fn track_from_soundcloud(
|
async fn track_from_soundcloud(
|
||||||
value: &CloudTrack,
|
value: &CloudTrack,
|
||||||
ipod_path: String,
|
ipod_path: String,
|
||||||
@ -203,6 +285,8 @@ pub fn initialize_async_service(
|
|||||||
},
|
},
|
||||||
AppEvent::DownloadPlaylist(playlist) => { download_playlist(playlist, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await; },
|
AppEvent::DownloadPlaylist(playlist) => { download_playlist(playlist, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await; },
|
||||||
AppEvent::DownloadTrack(track) => { download_track(track, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await; },
|
AppEvent::DownloadTrack(track) => { download_track(track, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await; },
|
||||||
|
AppEvent::DownloadYTTrack(video) => { download_video(video, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await; },
|
||||||
|
AppEvent::DownloadYTPlaylist(ytplaylist) => { download_youtube_playlist(ytplaylist, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await; },
|
||||||
AppEvent::SwitchScreen(state) => { let _ = sender.send(AppEvent::SwitchScreen(state)).await;},
|
AppEvent::SwitchScreen(state) => { let _ = sender.send(AppEvent::SwitchScreen(state)).await;},
|
||||||
AppEvent::LoadFromFS(path) => {
|
AppEvent::LoadFromFS(path) => {
|
||||||
load_from_fs(path, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await;
|
load_from_fs(path, database.as_mut().unwrap(), &sender, ipod_db.clone().unwrap()).await;
|
||||||
@ -584,6 +668,48 @@ fn make_cover_image(cover: &[u8], ipod_path: &str, file_name: &str, dim: (u32, u
|
|||||||
img.write(dst);
|
img.write(dst);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn download_video(
|
||||||
|
video: YoutubeVideo,
|
||||||
|
database: &mut XDatabase,
|
||||||
|
sender: &Sender<AppEvent>,
|
||||||
|
ipod_path: String,
|
||||||
|
) {
|
||||||
|
if let Ok(()) =
|
||||||
|
dlp::download_track_from_youtube(&video.videoId.clone(), &get_temp_dl_dir(), sender.clone())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
let p: PathBuf = Path::new(&ipod_path).into();
|
||||||
|
|
||||||
|
if let Some(mut t) = track_from_video(&video, ipod_path.clone(), sender).await {
|
||||||
|
if !database.if_track_in_library(t.data.dbid) {
|
||||||
|
t.data.unique_id = database.get_unique_id();
|
||||||
|
t.set_location(get_track_location(t.data.unique_id, "mp3"));
|
||||||
|
let dest = get_full_track_location(p.clone(), t.data.unique_id, "mp3");
|
||||||
|
|
||||||
|
let mut track_path = get_temp_dl_dir();
|
||||||
|
track_path.push(&video.videoId);
|
||||||
|
track_path.set_extension("mp3");
|
||||||
|
|
||||||
|
let _ = std::fs::copy(track_path.to_str().unwrap(), dest.to_str().unwrap());
|
||||||
|
|
||||||
|
database.add_track(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = sender
|
||||||
|
.send(AppEvent::SwitchScreen(AppState::MainScreen))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let _ = sender
|
||||||
|
.send(AppEvent::ITunesParsed(get_playlists(database)))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
overwrite_database(database, &ipod_path);
|
||||||
|
|
||||||
|
crate::config::clear_temp_dl_dir();
|
||||||
|
}
|
||||||
|
|
||||||
async fn download_track(
|
async fn download_track(
|
||||||
track: CloudTrack,
|
track: CloudTrack,
|
||||||
database: &mut XDatabase,
|
database: &mut XDatabase,
|
||||||
@ -629,6 +755,60 @@ async fn download_track(
|
|||||||
crate::config::clear_temp_dl_dir();
|
crate::config::clear_temp_dl_dir();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn download_youtube_playlist(
|
||||||
|
playlist: YTPlaylist,
|
||||||
|
database: &mut XDatabase,
|
||||||
|
sender: &Sender<AppEvent>,
|
||||||
|
ipod_path: String,
|
||||||
|
) {
|
||||||
|
if let Ok(()) =
|
||||||
|
dlp::download_from_youtube(&playlist.url, &get_temp_dl_dir(), sender.clone()).await
|
||||||
|
{
|
||||||
|
let videos = playlist.videos;
|
||||||
|
|
||||||
|
let p: PathBuf = Path::new(&ipod_path).into();
|
||||||
|
|
||||||
|
let mut new_playlist = XPlaylist::new(rand::random(), ListSortOrder::SongTitle);
|
||||||
|
|
||||||
|
new_playlist.set_title(playlist.title);
|
||||||
|
|
||||||
|
for video in videos {
|
||||||
|
if let Some(mut t) = track_from_video(&video, ipod_path.clone(), sender).await {
|
||||||
|
if !database.if_track_in_library(t.data.dbid) {
|
||||||
|
t.data.unique_id = database.get_unique_id();
|
||||||
|
new_playlist.add_elem(t.data.unique_id);
|
||||||
|
t.set_location(get_track_location(t.data.unique_id, "mp3"));
|
||||||
|
let dest = get_full_track_location(p.clone(), t.data.unique_id, "mp3");
|
||||||
|
|
||||||
|
let mut track_path = get_temp_dl_dir();
|
||||||
|
track_path.push(&video.videoId);
|
||||||
|
track_path.set_extension("mp3");
|
||||||
|
|
||||||
|
let _ = std::fs::copy(track_path.to_str().unwrap(), dest.to_str().unwrap());
|
||||||
|
|
||||||
|
database.add_track(t);
|
||||||
|
} else if let Some(unique_id) = database.get_unique_id_by_dbid(t.data.dbid) {
|
||||||
|
new_playlist.add_elem(unique_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
database.add_playlist(new_playlist);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = sender
|
||||||
|
.send(AppEvent::SwitchScreen(AppState::MainScreen))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let _ = sender
|
||||||
|
.send(AppEvent::ITunesParsed(get_playlists(database)))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
overwrite_database(database, &ipod_path);
|
||||||
|
|
||||||
|
crate::config::clear_temp_dl_dir();
|
||||||
|
}
|
||||||
|
|
||||||
async fn download_playlist(
|
async fn download_playlist(
|
||||||
playlist: CloudPlaylist,
|
playlist: CloudPlaylist,
|
||||||
database: &mut XDatabase,
|
database: &mut XDatabase,
|
||||||
@ -732,6 +912,28 @@ async fn parse_itunes(sender: &Sender<AppEvent>, path: String) -> XDatabase {
|
|||||||
file.read_to_string(&mut content).await.unwrap();
|
file.read_to_string(&mut content).await.unwrap();
|
||||||
let config: LyricaConfiguration = toml::from_str(&content).unwrap();
|
let config: LyricaConfiguration = toml::from_str(&content).unwrap();
|
||||||
|
|
||||||
|
let yt_channel_id = config.get_youtube().user_id.clone();
|
||||||
|
|
||||||
|
let rid = youtube_api::get_channel(yt_channel_id.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let pls = youtube_api::get_playlists(yt_channel_id, rid)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut yt_v = Vec::new();
|
||||||
|
|
||||||
|
for pl in pls {
|
||||||
|
let videos = youtube_api::get_playlist(pl.browse_id).await.unwrap();
|
||||||
|
yt_v.push(YTPlaylist {
|
||||||
|
title: pl.title,
|
||||||
|
url: pl.pl_url,
|
||||||
|
videos,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = sender.send(AppEvent::YoutubeGot(yt_v)).await;
|
||||||
|
|
||||||
let app_version = soundcloud::get_app().await.unwrap().unwrap();
|
let app_version = soundcloud::get_app().await.unwrap().unwrap();
|
||||||
let client_id = soundcloud::get_client_id().await.unwrap().unwrap();
|
let client_id = soundcloud::get_client_id().await.unwrap().unwrap();
|
||||||
let playlists = soundcloud::get_playlists(
|
let playlists = soundcloud::get_playlists(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user