Skip to content

Commit

Permalink
refactor: adding ytmusic oauth back as it's possible to create your o…
Browse files Browse the repository at this point in the history
…wn app
  • Loading branch information
SilentVoid13 committed Dec 18, 2024
1 parent 77aae0b commit 35576a6
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 61 deletions.
30 changes: 22 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SyncDisBoi
# SyncDisBoi - Sync this boy!

SyncDisBoi is a simple and efficient tool designed to synchronize playlists across different music streaming platforms. It currently supports:
- [Youtube Music](https://music.youtube.com/)
Expand Down Expand Up @@ -39,18 +39,18 @@ A [Nix flake](https://github.com/SilentVoid13/SyncDisBoi/blob/master/flake.nix)
Here are some command examples:
```bash
# sync from Youtube Music to Spotify
./sync_dis_boi yt-music --headers "./browser.json" spotify --client-id "<CLIENT_ID>" --client-secret "<CLIENT_SECRET>"
./sync_dis_boi yt-music --client-id "<CLIENT_ID>" --client-secret "<CLIENT_SECRET>" spotify --client-id "<CLIENT_ID>" --client-secret "<CLIENT_SECRET>"
# sync from Spotify to Tidal, sync likes as well
./sync_dis_boi --sync-likes spotify --client-id "<CLIENT_ID>" --client-secret "<CLIENT_SECRET>" tidal
# sync from Tidal to Youtube Music, like all synchronized songs
./sync_dis_boi --like-all tidal yt-music --headers "./browser.json"
./sync_dis_boi --like-all tidal yt-music --client-id "<CLIENT_ID>" --client-secret "<CLIENT_SECRET>"
# sync from Spotify to Youtube Music, with debug mode enabled to generate detailed statistics about the synchronization process
./sync_dis_boi --debug spotify --client-id "<CLIENT_ID>" --client-secret "<CLIENT_SECRET>" yt-music --headers "./browser.json"
./sync_dis_boi --debug spotify --client-id "<CLIENT_ID>" --client-secret "<CLIENT_SECRET>" yt-music --client-id "<CLIENT_ID>" --client-secret "<CLIENT_SECRET>"

# export Spotify playlists to JSON
./sync_dis_boi spotify --client-id "<CLIENT_ID>" --client-secret "<CLIENT_SECRET>" export -d ./spotify.json
# export Youtube Music playlists to JSON
./sync_dis_boi yt-music --headers "./browser.json" export -d ./yt_music.json
./sync_dis_boi yt-music --client-id "<CLIENT_ID>" --client-secret "<CLIENT_SECRET>" export -d ./yt_music.json
# export Tidal playlists to JSON
./sync_dis_boi tidal export -d ./tidal.json
```
Expand All @@ -72,20 +72,34 @@ Notes:

### Youtube Music API setup

The OAuth access has been removed by Youtube. You should now log in using your browser cookies:
The convenient OAuth "Android Auto" access has been removed by Youtube. You now have to create your own OAuth application:

- Sign in at [https://console.developers.google.com/](https://console.developers.google.com/)
- Create a new project
- Select the project
- Under "Enabled APIs & services" click "+ Enable APIs and services", select "Youtube Data API v3" and enable
- Under "OAuth consent screen" create an "external" user type (fill in the app name, set the developer email as your own)
- Add your own email for "Test users"
- Under "Credentials" click "+ Create credentials" > OAuth client ID > Set "TVs and Limited Input devices" as the application type
- Copy the Client ID and the Client secret

You will then need to provide the client id and client secret as arguments for SyncDisBoi.
After the first authorization, the OAuth token will be cached in `~/.config/SyncDisBoi/ytmusic_oauth.json` (on Linux) for future use.

Alternatively, you can use request headers to login:

- Follow [ytmusicapi's guide](https://ytmusicapi.readthedocs.io/en/stable/setup/browser.html) to generate a `browser.json` file.
- Pass the `browser.json` file as an argument for SyncDisBoi

Notes:
- You may have to refresh the cookies regularly as they can expire
- You may have to refresh the cookies regularly as they can expire very quickly

### Tidal API setup

- On the first run, SyncDisBoi will open up a browser tab to request OAuth access for your Tidal Account.
- Authorize the application in your browser, then press ENTER in the CLI to continue.

The OAuth token will be cached in `~/.config/SyncDisBoi/tidal_oauth.json` (on Linux) for future use.
After the first authorization, the OAuth token will be cached in `~/.config/SyncDisBoi/tidal_oauth.json` (on Linux) for future use.

Notes:
- By default, SyncDisBoi uses Tidal's "Android Auto" application credentials to request OAuth access.
Expand Down
50 changes: 30 additions & 20 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,31 @@ pub enum MusicPlatformSrc {
YtMusic {
/// The path to the headers JSON file
#[arg(short, long)]
headers: PathBuf,
/*
headers: Option<PathBuf>,
// FIXME: Android Auto Oauth is broken, probably forever
// https://github.com/sigma67/ytmusicapi/discussions/682
// https://github.com/yt-dlp/yt-dlp/issues/11462
/// The client ID for the Youtube API application
#[arg(
long,
env = "YTMUSIC_CLIENT_ID",
default_value = "861556708454-d6dlm3lh05idd8npek18k6be8ba3oc68.apps.googleusercontent.com"
conflicts_with = "headers",
requires = "client_secret"
//default_value = "861556708454-d6dlm3lh05idd8npek18k6be8ba3oc68.apps.googleusercontent.com"
)]
client_id: String,
client_id: Option<String>,
/// The client secret for the Youtube API application
#[arg(
long,
env = "YTMUSIC_CLIENT_SECRET",
default_value = "SboVhoG9s0rNafixCSGGKXAT"
conflicts_with = "headers",
requires = "client_secret"
//default_value = "SboVhoG9s0rNafixCSGGKXAT"
)]
/// The client secret for the Youtube API application
client_secret: String,
client_secret: Option<String>,
/// Clear the cached ytmusic_oauth.json file
#[arg(long)]
#[arg(long, requires = "client_id", requires = "client_secret")]
clear_cache: bool,
*/
/// The destination music platform
#[command(subcommand)]
dst: MusicPlatformDst,
Expand All @@ -64,12 +69,12 @@ pub enum MusicPlatformSrc {
/// The client ID for the Tidal API application
#[arg(long, env = "TIDAL_CLIENT_ID", default_value = "zU4XHVVkc2tDPo4t")]
client_id: String,
/// The client secret for the Tidal API application
#[arg(
long,
env = "TIDAL_CLIENT_SECRET",
default_value = "VJKhDFqJPqvsPVNBV6ukXTJmwlvbttP7wlMlrc72se4="
)]
/// The client secret for the Tidal API application
client_secret: String,
/// Clear the cached tidal_oauth.json file
#[arg(long)]
Expand All @@ -88,26 +93,31 @@ pub enum MusicPlatformDst {
YtMusic {
/// The path to the headers JSON file
#[arg(short, long)]
headers: PathBuf,
/*
headers: Option<PathBuf>,
// FIXME: Android Auto Oauth is broken, probably forever
// https://github.com/sigma67/ytmusicapi/discussions/682
// https://github.com/yt-dlp/yt-dlp/issues/11462
/// The client ID for the Youtube API application
#[arg(
long,
env = "YTMUSIC_CLIENT_ID",
default_value = "861556708454-d6dlm3lh05idd8npek18k6be8ba3oc68.apps.googleusercontent.com"
conflicts_with = "headers",
requires = "client_secret"
//default_value = "861556708454-d6dlm3lh05idd8npek18k6be8ba3oc68.apps.googleusercontent.com"
)]
client_id: String,
client_id: Option<String>,
/// The client secret for the Youtube API application
#[arg(
long,
env = "YTMUSIC_CLIENT_SECRET",
default_value = "SboVhoG9s0rNafixCSGGKXAT"
conflicts_with = "headers",
requires = "client_secret"
//default_value = "SboVhoG9s0rNafixCSGGKXAT"
)]
/// The client secret for the Youtube API application
client_secret: String,
client_secret: Option<String>,
/// Clear the cached ytmusic_oauth.json file
#[arg(long)]
#[arg(long, requires = "client_id", requires = "client_secret")]
clear_cache: bool,
*/
},
Spotify {
/// The client ID for the Spotify API application
Expand Down Expand Up @@ -139,7 +149,7 @@ pub enum MusicPlatformDst {
/// Minify the exported JSON file
#[arg(long, default_value = "false")]
minify: bool,
}
},
}

#[derive(ValueEnum, Clone, Debug)]
Expand Down
38 changes: 20 additions & 18 deletions src/build_api.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::path::{Path, PathBuf};
use std::path::Path;

use async_trait::async_trait;
use color_eyre::eyre::{eyre, Result};
Expand All @@ -22,31 +22,33 @@ macro_rules! impl_build_api {
async fn parse(&self, args: &RootArgs, config_dir: &Path) -> Result<DynMusicApi> {
let api: DynMusicApi = match &self {
Self::YtMusic {
/*
client_id,
client_secret,
clear_cache,
*/
headers,
..
} => {
Box::new(
YtMusicApi::new(headers, args.config.clone())
if let Some(headers) = headers {
Box::new(YtMusicApi::new_headers(headers, args.config.clone()).await?)
} else {
let Some(client_id) = client_id else {
return Err(eyre!("Missing Youtube Music client_id"));
};
let Some(client_secret) = client_secret else {
return Err(eyre!("Missing Youtube Music client_secret"));
};
let oauth_token_path = config_dir.join("ytmusic_oauth.json");
Box::new(
YtMusicApi::new_oauth(
client_id,
client_secret,
oauth_token_path,
*clear_cache,
args.config.clone(),
)
.await?,
)
/*
let oauth_token_path = config_dir.join("ytmusic_oauth.json");
Box::new(
YtMusicApi::new(
client_id,
client_secret,
oauth_token_path,
*clear_cache,
args.config.clone(),
)
.await?,
)
*/
}
}
Self::Tidal {
client_id,
Expand Down
5 changes: 2 additions & 3 deletions src/spotify/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,12 +442,11 @@ mod tests {
let yt_client_secret = env::var("YTMUSIC_CLIENT_SECRET").unwrap();
let config_dir = dirs::config_dir().unwrap();
let oauth_token_path = config_dir.join("SyncDisBoi").join("ytmusic_oauth.json");
let ytmusic = YtMusicApi::new(
let ytmusic = YtMusicApi::new_oauth(
&yt_client_id,
&yt_client_secret,
oauth_token_path,
false,
false,
None,
)
.await
Expand All @@ -459,7 +458,7 @@ mod tests {

let spotify_client_id = env::var("SPOTIFY_CLIENT_ID").unwrap();
let spotify_secret = env::var("SPOTIFY_CLIENT_SECRET").unwrap();
let spotify = SpotifyApi::new(&spotify_client_id, &spotify_secret, false, None)
let spotify = SpotifyApi::new(&spotify_client_id, &spotify_secret, None)
.await
.unwrap();

Expand Down
20 changes: 9 additions & 11 deletions src/yt_music/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,13 @@ impl YtMusicApi {
const BASE_API: &'static str = "https://music.youtube.com/youtubei/v1/";
const BASE_PARAMS: &'static str = "?alt=json&key=AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30";

// FIXME: Oauth is broken, probably forever
// https://github.com/sigma67/ytmusicapi/discussions/682
// https://github.com/yt-dlp/yt-dlp/issues/11462
/*
const OAUTH_SCOPE: &'static str = "https://www.googleapis.com/auth/youtube";
const OAUTH_CODE_URL: &'static str = "https://www.youtube.com/o/oauth2/device/code";
const OAUTH_TOKEN_URL: &'static str = "https://oauth2.googleapis.com/token";
const OAUTH_GRANT_TYPE: &'static str = "http://oauth.net/grant_type/device/1.0";
const OAUTH_USER_AGENT: &'static str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0 Cobalt/Version";

pub async fn new(
pub async fn new_oauth(
client_id: &str,
client_secret: &str,
oauth_token_path: PathBuf,
Expand Down Expand Up @@ -166,16 +162,18 @@ impl YtMusicApi {
let token: OAuthToken = res.json().await?;
Ok(token)
}
*/

pub async fn new(headers: &PathBuf, config: ConfigArgs) -> Result<Self> {
let header_json = std::fs::read_to_string(headers)?;
pub async fn new_headers(headers: &PathBuf, config: ConfigArgs) -> Result<Self> {
let header_data = std::fs::read_to_string(headers)?;
let header_json: serde_json::Map<String, serde_json::Value> =
serde_json::from_str(&header_json)?;
serde_json::from_str(&header_data)?;
let mut headers = HeaderMap::new();
for (key, val) in header_json.into_iter() {
if let serde_json::Value::String(s) = val {
headers.insert(HeaderName::from_bytes(key.as_bytes())?, s.parse()?);
headers.insert(
HeaderName::from_bytes(key.to_lowercase().as_bytes())?,
s.parse()?,
);
}
}
headers.remove("accept-encoding");
Expand Down Expand Up @@ -241,7 +239,7 @@ impl YtMusicApi {
{
let body = self.add_context(body);
let endpoint = self.build_endpoint(path, ctoken);
let res = self.client.post(endpoint).json(&body).send().await?;
let res = self.client.post(&endpoint).json(&body).send().await?;
let obj = if self.config.debug {
let status = res.status();
let text = res.text().await?;
Expand Down
3 changes: 2 additions & 1 deletion src/yt_music/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ impl TryInto<SearchSongs> for YtMusicResponse {
None => return Ok(SearchSongs(songs_vec)),
};

let re_duration = Regex::new(r"^(\d+:)*\d+:\d+$")?;

for mrlir in mrlirs
.iter()
.filter(|item| item.playlist_item_data.is_some())
Expand All @@ -146,7 +148,6 @@ impl TryInto<SearchSongs> for YtMusicResponse {
let mut album = None;
let mut artists: Vec<Artist> = vec![];
let mut duration = 0;
let re_duration = Regex::new(r"^(\d+:)*\d+:\d+$")?;

for run in mrlir
.get_col_runs(1, true)
Expand Down

0 comments on commit 35576a6

Please sign in to comment.