Last.fmで再生中の曲情報をDiscordに表示しよう！

# はじめに
この記事では、Last.fmで再生中の曲をDiscord Rich Presenceを利用して表示する方法を紹介します。

## 完成品
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3581223/5cee9b9e-3511-0051-b24d-89f3a806b1a3.png)


## 環境
* Rust 1.83.0
* MacOS Sequoia

## リポジトリ
https://github.com/akazdayo/lastfm-rpc-rs

# やってみよう
## Last.fmのAPIを呼び出す
``user.getrecenttracks``を呼び出し、指定したユーザーが現在再生している曲を取得します。
楽曲が現在再生しているかどうかは、``recent_tracks[0]["@attr"]["nowplaying"] == "true"``で確認できます。
https://www.last.fm/api/show/user.getRecentTracks
```rust
use reqwest::Client;

type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
fn main() {
    const API_KEY: &str = "api secret";
    const USERNAME: &str = "username";
    
    let client = Client::new();
    let response = get_api(&client, USERNAME, API_KEY).unwrap();
    let recent_tracks = &response["recenttracks"]["track"];

    if recent_tracks[0]["@attr"].get("nowplaying").is_some()
    {
        let artist = recent_tracks[0]["artist"]["#text"].as_str().unwrap();
        let track = recent_tracks[0]["name"].as_str().unwrap();
        println!("Now playing: {} - {}", artist, track);
    }
}

#[tokio::main]
async fn get_api(client: &Client, username: &str, apikey: &str) -> Result<serde_json::Value> {
    let url = format!("http://ws.audioscrobbler.com/2.0");
    let response = client
        .get(url)
        .query(&[
            ("method", "user.getrecenttracks"),
            ("user", username),
            ("api_key", apikey),
            ("format", "json"),
            ("limit", "1"),
        ])
        .send()
        .await?;
    Ok(response.json().await?)
}
```
```bash:Output
Now playing: ReoNa - Canaria
```

## Discordに反映しよう！
### デザインを考える
:::note warn
今後変更されるかもしれませんが、
stateを入力せずにdetailsを入力すると、App Nameの枠にdetailsが入力されます。
また、stateを入力せずにlarge_imageのaltを設定すると、detailsの枠にlarge_imageのaltが入力されます。
:::

![RPCの説明](https://discord.com/assets/e948e6820b26ea216a6f62f0e2ec1c2c.png)
引用: https://discord.com/developers/docs/rich-presence/using-with-the-embedded-app-sdk

今回は、以下のようにしてみました。
* large_image: アルバムのジャケット
* large_text(ALT): アーティスト名
* details: 曲名

small_imageに、アーティストの写真を入れたかったのですが、Last.fm APIの仕様変更で取得できなくなってしまったようです。
少し調べただけですが、DeezerのAPIを利用するなど他の方法もあるようなので興味のある方は試してみるといいかもしれません！

https://stackoverflow.com/questions/55978243/last-fm-api-returns-same-white-star-image-for-all-artists

### コード
``discord-rich-presence``ライブラリを利用して、Discordに情報を送信します。
```rust
fn update_presence(discord_client: &mut DiscordIpcClient, data: &serde_json::Value) {
    discord_client
        .set_activity(
            activity::Activity::new()
                .details(data[0]["name"].as_str().unwrap())
                .activity_type(ActivityType::Listening)
                .assets(
                    activity::Assets::new()
                        .large_image(data[0]["image"][3]["#text"].as_str().unwrap()) // サムネイル
                        .large_text(data[0]["artist"]["#text"].as_str().unwrap()), // ALTテキスト
                )
                .timestamps(
                    activity::Timestamps::new().start(
                        time::SystemTime::now()
                            .duration_since(time::UNIX_EPOCH)
                            .unwrap()
                            .as_secs() as i64,
                    ),
                ),
        )
        .unwrap();
}
```

# ソースコード
```rust
use discord_rich_presence::{activity, activity::ActivityType, DiscordIpc, DiscordIpcClient};
use reqwest::Client;
use std::thread::sleep;
use std::time;

type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
fn main() {
    const API_KEY: &str = "";
    const USERNAME: &str = "";
    const CLIENT_ID: &str = "";

    // Discordに接続
    let mut discord_client = DiscordIpcClient::new(CLIENT_ID).unwrap();
    discord_client.connect().unwrap();

    let client = Client::new();
    let mut old_track = serde_json::Value::Null;
    loop {
        let response = get_api(&client, USERNAME, API_KEY).unwrap();
        let recent_tracks = &response["recenttracks"]["track"];

        if recent_tracks[0]["@attr"].get("nowplaying").is_some() && &old_track != recent_tracks {
            let artist = recent_tracks[0]["artist"]["#text"].as_str().unwrap();
            let track = recent_tracks[0]["name"].as_str().unwrap();
            println!("Now playing: {} - {}", artist, track);
            update_presence(&mut discord_client, recent_tracks);
            old_track = recent_tracks.clone();
        }
        sleep(time::Duration::from_secs(2));
    }
}

fn update_presence(discord_client: &mut DiscordIpcClient, data: &serde_json::Value) {
    discord_client
        .set_activity(
            activity::Activity::new()
                .details(data[0]["name"].as_str().unwrap())
                .activity_type(ActivityType::Listening)
                .assets(
                    activity::Assets::new()
                        .large_image(data[0]["image"][3]["#text"].as_str().unwrap())
                        .large_text(data[0]["artist"]["#text"].as_str().unwrap()),
                )
                .timestamps(
                    activity::Timestamps::new().start(
                        time::SystemTime::now()
                            .duration_since(time::UNIX_EPOCH)
                            .unwrap()
                            .as_secs() as i64,
                    ),
                ),
        )
        .unwrap();
}

#[tokio::main]
async fn get_api(client: &Client, username: &str, apikey: &str) -> Result<serde_json::Value> {
    let url = format!("http://ws.audioscrobbler.com/2.0");
    let response = client
        .get(url)
        .query(&[
            ("method", "user.getrecenttracks"),
            ("user", username),
            ("api_key", apikey),
            ("format", "json"),
            ("limit", "1"),
        ])
        .send()
        .await?;
    Ok(response.json().await?)
}

```

# 参考
https://www.last.fm/api/show/user.getRecentTracks

https://discord.com/developers/docs/rich-presence/using-with-the-embedded-app-sdk

https://dev.classmethod.jp/articles/tried-to-find-out-what-i-could-do-with-the-last-fm-api/

https://stackoverflow.com/questions/55978243/last-fm-api-returns-same-white-star-image-for-all-artists

https://github.com/VinylMusicPlayer/VinylMusicPlayer/issues/149