use color_eyre::eyre::{Result, eyre};
use serde_json::json;
use tracing::{debug, info, warn};
use crate::ConfigArgs;
use crate::music_api::{DynMusicApi, MusicApiType, Playlist, Song};
use crate::utils::dedup_songs;
const SKIPPED_PLAYLISTS: [&str; 10] = [
"New playlist",
"Your Likes",
"My Supermix",
"Discover Mix",
"Episodes for Later",
"Liked Songs",
"Discover Weekly",
"Big Room House Mix",
"Motivation Electronic Mix",
"High Energy Mix",
];
pub async fn synchronize(
src_api: DynMusicApi,
dst_api: DynMusicApi,
config: ConfigArgs,
) -> Result<()> {
if !config.diff_country
&& src_api.api_type() != MusicApiType::YtMusic
&& dst_api.api_type() != MusicApiType::YtMusic
&& src_api.country_code() != dst_api.country_code()
{
return Err(eyre!(
"source and destination music platforms are in different countries ({} vs {}). \
You can specify --diff-country to allow it, \
but this might result in incorrect sync results.",
src_api.country_code(),
dst_api.country_code()
));
}
if config.debug {
std::fs::create_dir_all("debug")?;
}
info!("retrieving source playlists...");
let src_playlists = src_api.get_playlists_full().await?;
synchronize_playlists(src_playlists, &dst_api, &config).await?;
if config.sync_likes {
info!("synchronizing likes...");
let src_likes = src_api.get_likes().await?;
let dst_likes = dst_api.get_likes().await?;
let mut new_likes = Vec::new();
for src_like in src_likes.into_iter() {
if dst_likes.contains(&src_like) {
continue;
}
let Some(song) = dst_api.search_song(&src_like).await? else {
debug!("no match found for song: {}", src_like);
continue;
};
if dst_likes.contains(&song) {
debug!("discrepancy, song already liked: {}", song);
continue;
}
new_likes.push(song);
}
info!("synchronizing {} new likes", new_likes.len());
dst_api.add_likes(&new_likes).await?;
}
Ok(())
}
pub async fn synchronize_playlists(
src_playlists: Vec<Playlist>,
dst_api: &DynMusicApi,
config: &ConfigArgs,
) -> Result<()> {
let mut all_missing_songs = json!({});
let mut all_new_songs = json!({});
let mut no_albums = json!({});
let mut stats = json!({});
info!("retrieving destination playlists...");
let mut dst_playlists = dst_api.get_playlists_full().await?;
let mut dst_likes = vec![];
if config.like_all {
info!("retrieving destination likes...");
dst_likes = dst_api.get_likes().await?;
}
for mut src_playlist in src_playlists
.into_iter()
.filter(|p| !SKIPPED_PLAYLISTS.contains(&p.name.as_str()) && !p.songs.is_empty())
{
if src_playlist.songs.is_empty() {
continue;
}
let mut dst_playlist = match dst_playlists
.iter()
.position(|p| p.name == src_playlist.name)
{
Some(i) => dst_playlists.remove(i),
None => dst_api.create_playlist(&src_playlist.name, false).await?,
};
let mut missing_songs = json!([]);
let mut new_songs = json!([]);
let mut no_albums_songs = json!([]);
let mut dst_songs = vec![];
let mut success = 0;
let mut attempts = 0;
if dedup_songs(&mut src_playlist.songs) {
warn!(
"duplicates found in source playlist \"{}\", they will be skipped",
src_playlist.name
);
}
info!("synchronizing playlist \"{}\" ...", src_playlist.name);
for src_song in src_playlist.songs.iter() {
if dst_playlist.songs.contains(src_song) {
continue;
}
if src_song.album.is_none() {
warn!(
"No album metadata for source song \"{}\", skipping",
src_song
);
if config.debug {
no_albums_songs
.as_array_mut()
.unwrap()
.push(json!(src_song));
}
continue;
}
attempts += 1;
let dst_song = dst_api.search_song(src_song).await?;
let Some(dst_song) = dst_song else {
debug!("no match found for song: {}", src_song);
if config.debug {
missing_songs.as_array_mut().unwrap().push(json!(src_song));
}
continue;
};
dst_songs.push(dst_song);
success += 1;
}
if !dst_songs.is_empty() {
let mut to_sync = Vec::new();
for dst_song in dst_songs.iter() {
if dst_playlist.songs.contains(dst_song) {
debug!(
"discrepancy, song already in destination playlist: {}",
dst_song
);
continue;
}
if to_sync.contains(dst_song) {
debug!(
"discrepancy, duplicate song in songs to synchronize: {}",
dst_song
);
continue;
}
if config.debug {
new_songs.as_array_mut().unwrap().push(json!(dst_song));
}
to_sync.push(dst_song.clone());
}
dst_api
.add_songs_to_playlist(&mut dst_playlist, &to_sync)
.await?;
if config.like_all {
let new_likes = to_sync
.iter()
.filter(|s| !dst_likes.contains(s))
.cloned()
.collect::<Vec<Song>>();
dst_api.add_likes(&new_likes).await?;
}
}
let mut conversion_rate = 1.0;
if attempts != 0 {
conversion_rate = success as f64 / attempts as f64;
info!(
"synchronizing playlist \"{}\" [ok], {}/{} songs ({}%)",
src_playlist.name,
success,
attempts,
conversion_rate * 100.0
);
} else {
info!(
"synchronizing playlist \"{}\" [ok], no new songs to add",
src_playlist.name
);
}
if config.debug {
stats.as_object_mut().unwrap().insert(
src_playlist.name.clone(),
json!({
"percentage": conversion_rate,
"number": format!("{}/{}", success, attempts),
}),
);
std::fs::write(
"debug/conversion_rate.json",
serde_json::to_string_pretty(&stats)?,
)?;
if !new_songs.as_array().unwrap().is_empty() {
all_new_songs
.as_object_mut()
.unwrap()
.insert(src_playlist.name.clone(), new_songs);
std::fs::write(
"debug/new_songs.json",
serde_json::to_string_pretty(&all_new_songs)?,
)?;
}
if !missing_songs.as_array().unwrap().is_empty() {
all_missing_songs
.as_object_mut()
.unwrap()
.insert(src_playlist.name.clone(), missing_songs);
std::fs::write(
"debug/missing_songs.json",
serde_json::to_string_pretty(&all_missing_songs)?,
)?;
}
if !no_albums_songs.as_array().unwrap().is_empty() {
no_albums
.as_object_mut()
.unwrap()
.insert(src_playlist.name.clone(), no_albums_songs);
std::fs::write(
"debug/song_with_no_albums.json",
serde_json::to_string_pretty(&no_albums)?,
)?;
}
}
}
info!("Synchronization complete!");
Ok(())
}