こんにちは!東京農工大学で情報工学を学んでいる者です ( @nibs_tuat )。
Qiita初投稿の記事となります。
この記事は 農工大アドベントカレンダー Advent Calendar 2024 の4日目のものです。
はじめに
DiscordにはDiscord Rich Presence
と呼ばれる、アクティビティをステータスに表示できる機能があります。この機能によってプレイ中のゲームを表示したり、Spotifyなどのアプリケーションと連携することで、聞いている音楽を表示したりできます。
「ここにアニメの視聴状態も表示したい!」 と思ったのがこの記事で紹介するものを制作した経緯です。
本記事では以下のように、Discordのステータスにdアニメストアで視聴中のアニメの情報を表示できるChrome拡張&ネイティブアプリケーション(Rust製)を紹介します。
表示できる情報は以下の通りです。
- タイトル
- エピソード / 総再生時間
- 現在の再生時間
アニメのサムネイルも表示しようと思えば簡単にできるのですが、著作権的に微妙そうなのと、少なからずdアニメストアのサーバーに負荷をかけることになってしまうので断念…
代わりに表示するものを検討中です。
すでに最低限動作するものはリリース済みで、プログラム等は全て以下のリポジトリにあります。
仕組み
次のような構成で動作します。
-
content.js
dアニメストアの各再生ウィンドウで動作し、後ろで動いているbackground.js
に再生・停止などの状態の変化とタイトルなどの情報を送信 -
background.js
content.js
から送られてくる各ウィンドウの情報を管理し、いずれかのウィンドウが新たに再生状態になるとネイティブアプリケーションに情報を送信 -
ネイティブアプリケーション(Native Messaging Host)
Chrome拡張から情報を受け取り、Rustのdiscord-sdk
でDiscord側にアクティビティを表示するための情報を送信
特殊な事情がない限り、Windows・MacOS・LinuxのいずれのOSでも動作する技術で構成されています(Linuxは動作未確認)。
また、YouTube・YouTube Musicで同様のサービスが存在しており、今回使用した技術の構成はこれを参考にしています。
準備
Discordにアクティビティを表示するため、あらかじめ Discord Developer Portal でアプリケーションを登録し、Application ID
を控えておきます。
詳細
簡単に各プログラムの説明をします。
Chrome拡張
manifest.json
まず、以下のようにmanifest.json
を作成しておきます。
content_scripts
でdアニメストアの再生ページでcontent.js
が動作するように指定し、background
でbackground.js
がバックグラウンドで動作できるように指定します。
また、ネイティブアプリケーションと通信できるようにするため、permissions
にnativeMessaging
を書いておきます。
{
"name": "dAnimeDiscordPresence",
"version": "0.1.0",
"manifest_version": 3,
"content_scripts": [
{
"matches": [
"https://animestore.docomo.ne.jp/animestore/sc_d_pc*"
],
"js": [
"content.js"
]
}
],
"permissions": [
"nativeMessaging",
"background",
],
"background": {
"service_worker": "background.js"
}
}
content.js
各content.js
では始めにランダムなUUIDが生成され、このUUIDは各データと一緒にbackground.js
へ送信されます。background.js
はこのUUIDを基に各ウィンドウから送られてくるデータを識別します。
background.js
へのデータ送信はchrome.runtime.sendMessage
を使用しており、データ送信は主に以下のタイミングで行われます。
- ページロード時
- 動画再生開始時
- 動画再生停止時
- ウィンドウ消去時
background.js
background.js
は各content.js
から送られてきたデータを管理し、ステータスの更新が必要な場合のみネイティブアプリケーションにjson形式でデータを送信します。
ネイティブアプリケーションへのデータ送信にはChrome拡張のNativeMessagingという機能を使用しています。この機能を使用すると、ネイティブアプリケーションの標準入出力を介してChrome拡張との通信が可能になります。
次のようにして簡単に接続することができ、接続後はport.postMessage()
を呼び出すだけでデータを送信できます。
// connect to native host
function assertNative() {
if (!isNativeConnected) {
if (port.disconnect) port.disconnect();
port = chrome.runtime.connectNative("com.dadp.discord.presence");
isNativeConnected = true;
port.onDisconnect.addListener(() => {
if (chrome.runtime.lastError) {
isNativeConnected = false;
console.log("Couldn't connect to native app");
console.log(chrome.runtime.lastError);
}
console.log("Disconnect")
})
}
}
ネイティブアプリケーション
ネイティブアプリケーションでは、Discordにデータを送信するためにRust用のdiscord-sdk
を使用しています。
プログラム実行時に、このSDKとあらかじめ作成したDiscordアプリケーションのIDを使用してクライアントを作成します。この方法はライブラリのリポジトリに例が書かれています。
また、データの受信では先ほど述べたように、ネイティブアプリケーションは標準入出力を介して通信できるため実装時にChrome拡張を意識する必要はありません。ただし、データのフォーマットは決まっているため、それに合わせる必要があります。
以下のようなループを回すことでChrome拡張から受信したデータをDiscordに送信しています。
最初の32bitがデータ長を表しているので先にこれを読み取り、そのうえで受信したデータを読み込みます。受信するデータはjson形式の文字列となっているため、これをパースして指定された処理を行います。
loop {
// read JSON length
let mut length_buffer = [0u8; 4];
if reader.read_exact(&mut length_buffer).is_err() {
break;
}
let data_length = u32::from_le_bytes(length_buffer) as usize;
// read JSON data
let mut json_buffer = vec![0u8; data_length];
let _ = reader.read_exact(&mut json_buffer);
let input = String::from_utf8(json_buffer).expect("Invalid UTF-8 data");
let data: PresenceData = serde_json::from_str(&input).expect("Invalid JSON data");
if data.message_type == CLAER_MESSAGE {
tracing::info!(
"cleared activity: {:?}",
client.discord.clear_activity().await
);
continue;
} else if data.message_type == DISCONNECT_MESSAGE {
break;
} else if data.message_type == UPDATE_MESSAGE {
let rp = ds::activity::ActivityBuilder::default()
.assets(
ds::activity::Assets::default().large("tsumugi", Some("Watching anime")),
)
.kind(ds::activity::ActivityKind::Watching)
.details(&data.title)
.state(format!("{} / {}", &data.episodes, &data.total_duration))
.start_timestamp((Local::now() - parse_time_string(&data.current_time)).timestamp());
tracing::info!(
"updated activity: {:?}",
client.discord.update_activity(rp).await
);
}
}
最近になってDiscord側の仕様変更によりkind
を指定できるようになり、これをWatching
とすることで「アニメを"視聴中"」と表示できるようになりました。
おわりに
「Discordにアニメ視聴のステータスを表示したい」というモチベーションから制作したものでしたが、ちゃんと動いて満足しています。
また、いつか触れたいと思っていたChrome拡張とRustにも触れることができてよかったです。
改善点もまだまだあると思うので今後も改良していけたらと思います。