Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: playlist exporter #563

Merged
merged 2 commits into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ tuillez = { path = "./tuillez" }
musicbrainz-db-lite = { path = "./musicbrainz_db_lite" }

# Musicbrainz dependencies
listenbrainz = "0.8.1"
#listenbrainz = "0.8.1"
listenbrainz = { branch = "alistral_version", git = "https://github.com/RustyNova016/listenbrainz-rs.git" }

derive_builder = "0.20.2"
inquire = "0.7.5"
Expand Down
4 changes: 3 additions & 1 deletion alistral_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ musicbrainz-db-lite = { path = "../musicbrainz_db_lite" }
interzic = { path = "../interzic" }
tuillez = { path = "../tuillez" }

#listenbrainz = "0.8.1"
listenbrainz = { branch = "alistral_version", git = "https://github.com/RustyNova016/listenbrainz-rs.git" }

itertools = "0.14.0"
serde = "1.0.218"
sqlx = { version = "0.8.3", features = ["runtime-tokio", "macros"] }
Expand All @@ -18,6 +21,5 @@ chrono = "0.4.39"
rust_decimal = "1.36.0"
rust_decimal_macros = "1.36.0"
futures = "0.3.31"
listenbrainz = "0.8.1"
tracing-indicatif = "0.3.9"
tracing = "0.1.41"
35 changes: 35 additions & 0 deletions docs/book/src/CommandLineHelp.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ This document contains the help content for the `alistral` command-line program.
* [`alistral mapping list-unmapped`↴](#alistral-mapping-list-unmapped)
* [`alistral musicbrainz`↴](#alistral-musicbrainz)
* [`alistral musicbrainz clippy`↴](#alistral-musicbrainz-clippy)
* [`alistral playlist`↴](#alistral-playlist)
* [`alistral playlist convert`↴](#alistral-playlist-convert)
* [`alistral radio`↴](#alistral-radio)
* [`alistral radio circles`↴](#alistral-radio-circles)
* [`alistral radio underrated`↴](#alistral-radio-underrated)
Expand Down Expand Up @@ -65,6 +67,7 @@ A CLI app containing a set of useful tools for Listenbrainz
* `lookup` — Get detailled information about an entity
* `mapping` — Commands for interacting with listen mappings
* `musicbrainz` — Commands for musicbrainz stuff
* `playlist` — Interact with playlists
* `radio` — Generate radio playlists for you
* `stats` — Shows top statistics for a specific target
* `unstable` — A CLI app containing a set of useful tools for Listenbrainz
Expand Down Expand Up @@ -584,6 +587,38 @@ Search for potential mistakes, missing data and style issues. This allows to qui



## `alistral playlist`

Interact with playlists

**Usage:** `alistral playlist <COMMAND>`

###### **Subcommands:**

* `convert` — Convert a playlist from one service to another



## `alistral playlist convert`

Convert a playlist from one service to another

**Usage:** `alistral playlist convert <SOURCE> <ID> <TARGET>`

###### **Arguments:**

* `<SOURCE>` — Get the playlist from which service?

Possible values: `listenbrainz`

* `<ID>` — The id of the playlist on the external service
* `<TARGET>` — Convert to this service

Possible values: `youtube`




## `alistral radio`

Generate radio playlists for you
Expand Down
9 changes: 9 additions & 0 deletions docs/book/src/playlist/playlist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Playlist

Those commands interact with playlists

## `convert`

Convert a playlist from one service to another

[Usage](../CommandLineHelp.md#alistral-playlist-convert)
15 changes: 12 additions & 3 deletions interzic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,29 @@ edition = "2021"
description = "Bunch of utilities to link musicbrainz data to different services"

[dependencies]

# --- MB ecosystem crates ---
#musicbrainz_rs = "0.9.0"
musicbrainz_rs = { branch = "develop", git = "https://github.com/RustyNova016/musicbrainz_rs.git", features = ["extras"]}

#TODO: Make musicbrainz-db-lite optional
musicbrainz-db-lite = { path = "../musicbrainz_db_lite" }
tuillez = { path = "../tuillez" }
#listenbrainz = "0.8.1"
listenbrainz = { branch = "alistral_version", git = "https://github.com/RustyNova016/listenbrainz-rs.git" }


# --- Service crates ---
google-youtube3 = "6.0.0"
musicbrainz_rs = "0.9.0"

# --- Other crates ---
regex = "1.11.1"
sqlx = "0.8.3"
thiserror = "2.0.0"
listenbrainz = "0.8.1"
governor = "0.8.0"
serde = "1.0.218"
serde_json = "1.0.139"
tracing-indicatif = "0.3.9"
tracing = "0.1.41"
futures = "0.3.31"
async-fn-stream = "0.2.2"
async-fn-stream = "0.2.2"
17 changes: 17 additions & 0 deletions interzic/src/models/playlist_stub.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::models::messy_recording::MessyRecording;
use crate::InterzicClient;

#[derive(Debug, Clone)]
pub struct PlaylistStub {
Expand All @@ -7,3 +8,19 @@ pub struct PlaylistStub {
pub recordings: Vec<MessyRecording>,
//TODO: #521 Allow setting playlist visibility
}

impl PlaylistStub {
pub async fn save_recordings(self, client: &InterzicClient) -> Result<Self, crate::Error> {
let mut saved_recordings = Vec::new();

for rec in self.recordings {
saved_recordings.push(rec.upsert(&client.database_client).await?);
}

Ok(Self {
title: self.title,
description: self.description,
recordings: saved_recordings,
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use crate::InterzicClient;

pub struct Listenbrainz;

pub mod playlists;

impl Listenbrainz {
pub async fn create_playlist(
client: &InterzicClient,
Expand Down Expand Up @@ -51,4 +53,14 @@ impl Listenbrainz {
},
}
}

pub fn import_playlist(
client: &InterzicClient,
playlist_id: &str,
) -> Result<PlaylistStub, crate::Error> {
Ok(client
.listenbrainz_client()?
.get_playlist(playlist_id)
.map(|res| PlaylistStub::from(res.playlist))?)
}
}
32 changes: 32 additions & 0 deletions interzic/src/models/services/listenbrainz/playlists.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use listenbrainz::raw::jspf;
use listenbrainz::raw::jspf::Track;
use musicbrainz_rs::utils::parse_mbid;

use crate::models::messy_recording::MessyRecording;
use crate::models::playlist_stub::PlaylistStub;

impl From<jspf::PlaylistInfo> for PlaylistStub {
fn from(value: jspf::PlaylistInfo) -> Self {
PlaylistStub {
title: value.title,
description: value.annotation.unwrap_or_default(),
recordings: value.track.into_iter().map(MessyRecording::from).collect(),
}
}
}

impl From<Track> for MessyRecording {
fn from(value: Track) -> Self {
Self {
id: 0,
artist_credits: value.creator,
title: value.title,
release: Some(value.album),
mbid: value
.identifier
.into_iter()
.filter_map(|s| parse_mbid(&s))
.next(),
}
}
}
8 changes: 4 additions & 4 deletions musicbrainz_db_lite/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ rust-version = "1.82.0"
musicbrainz_db_lite_macros = { path = "./musicbrainz_db_lite_macros" }
musicbrainz_db_lite_schema = { path = "./musicbrainz_db_lite_schema" }

musicbrainz_rs_nova = { version = "0.9.0", package = "musicbrainz_rs" }
#musicbrainz_rs_nova = { branch = "develop", git = "https://github.com/RustyNova016/musicbrainz_rs.git", package = "musicbrainz_rs" }
#musicbrainz_rs_nova = { version = "0.9.0", package = "musicbrainz_rs" }
musicbrainz_rs_nova = { branch = "develop", git = "https://github.com/RustyNova016/musicbrainz_rs.git", package = "musicbrainz_rs" }
#musicbrainz_rs_nova = { path = "../musicbrainz_rs_nova" }

listenbrainz = "0.8.1"
#listenbrainz = { branch = "alistral_version", git = "https://github.com/RustyNova016/listenbrainz-rs.git" }
#listenbrainz = "0.8.1"
listenbrainz = { branch = "alistral_version", git = "https://github.com/RustyNova016/listenbrainz-rs.git" }

async-trait = "0.1.82"
chrono = "0.4.38"
Expand Down
6 changes: 6 additions & 0 deletions src/models/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use crate::tools::bumps::bump_command;
use crate::tools::bumps::bump_down_command;
use crate::tools::compatibility::compatibility_command;
use crate::tools::daily::daily_report;
use crate::tools::playlist::PlaylistCommand;
use crate::tools::stats::stats_command;

use super::config::Config;
Expand Down Expand Up @@ -156,6 +157,9 @@ pub enum Commands {
/// Commands for musicbrainz stuff
Musicbrainz(MusicbrainzCommand),

/// Interact with playlists
Playlist(PlaylistCommand),

/// Generate radio playlists for you
Radio(RadioCommand),

Expand Down Expand Up @@ -228,6 +232,8 @@ impl Commands {

Self::Musicbrainz(val) => val.run(conn).await,

Self::Playlist(val) => val.run(conn).await?,

Self::Bump(val) => bump_command(conn, val.clone()).await,

Self::BumpDown(val) => bump_down_command(conn, val.clone()).await,
Expand Down
1 change: 1 addition & 0 deletions src/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod interzic;
pub mod listens;
pub mod lookup;
pub mod musicbrainz;
pub mod playlist;
pub mod radio;
pub mod stats;
pub mod unstable;
46 changes: 46 additions & 0 deletions src/tools/playlist/convert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use clap::Parser;
use interzic::models::services::listenbrainz::Listenbrainz;
use interzic::models::services::youtube::Youtube;
use tuillez::fatal_error::IntoFatal;

use crate::api::clients::ALISTRAL_CLIENT;
use crate::tools::playlist::PlaylistOrigin;
use crate::tools::playlist::PlaylistTarget;

#[derive(Parser, Debug, Clone)]
pub struct PlaylistConvertCommand {
/// Get the playlist from which service?
pub source: PlaylistOrigin,

/// The id of the playlist on the external service
pub id: String,

/// Convert to this service
pub target: PlaylistTarget,
}

impl PlaylistConvertCommand {
pub async fn run(&self, _conn: &mut sqlx::SqliteConnection) -> Result<(), crate::Error> {
let playlist = match self.source {
PlaylistOrigin::Listenbrainz => {
Listenbrainz::import_playlist(&ALISTRAL_CLIENT.interzic, &self.id)
.expect_fatal("Couldn't retrieve the playlist. Check for typos.")?
}
};

let playlist = playlist
.save_recordings(&ALISTRAL_CLIENT.interzic)
.await
.expect_fatal("Couldn't save the playlist's recording")?;

match self.target {
PlaylistTarget::Youtube => {
Youtube::create_playlist(&ALISTRAL_CLIENT.interzic, playlist)
.await
.expect_fatal("Couldn't send the playlist to youtube")?;
}
}

Ok(())
}
}
Loading