1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🎧 Spotify NowPlaying を Slack ステータスに自動表示する仕組みを GAS で構築してみた

Posted at

想定読者

  • GASやSlack APIをちょっと触ったことがある人
  • できれば無料で済ませたい人(Vercelや有料DBは使いたくない)
    • 今回は 自分のみ のステータス更新をしています
  • 放置で自動化したい人

やりたいこと

スクリーンショット 2025-06-18 19.02.19.png

Spotifyで今再生している曲名・アーティスト名をSlackのステータスメッセージに自動表示したい。

Slackを開いているだけで「この人、今これ聴いてるんだ」がわかる感じにしたかった。

  • 更新頻度は5分でおk(そんな叩いてもな...感)
  • GASを使って自動更新
    • 貧乏なので無料でなんとかしたかった

技術構成

項目 使用技術
曲情報取得 Spotify Web API
ステータス更新 Slack Web API
スクリプト実行 Google Apps Script(GAS)
トークン取得 Flask + spotipy + ngrok(※refresh_token を1回だけ取得するため)

やること

この記事で紹介する .envスプレッドシート に保存する情報は個人のアクセストークンを含みます。
GitHubなどの公開リポジトリには絶対にアップロードしないよう注意してください。

ステップ 1:Spotify Developer の準備

  1. Spotify Developer Dashboard にログイン
  2. 新しいアプリを作成する
  3. Client IDClient Secret を控える

スクリーンショット 2025-06-18 18.40.59.png

ステップ 2:Slack API アプリの準備

  1. Slack API Console にアクセスし、新しいアプリを作成
  2. 「OAuth & Permissions」→ users.profile:write を User Token Scopes に追加
  3. 「Install to Workspace」でワークスペースに追加
  4. 発行された xoxp-... のトークンを控える

Botトークン(xoxb-...)では変更できない。Userトークンが必須

ステップ 3:Spotifyの refresh_token を取得(初回1回だけ)

Spotifyのアクセストークンは1時間で失効するため、定期更新に使う refresh_token を事前に取得しておく必要がある。これが自動化のキモ

  1. ngrok http 8888 を実行
    1. Forwarding https://xxxx-xxx-xxx-xxx-xxx.ngrok-free.appの形で出てくるので、これをメモる
    2. Spotifyで作成したアプリに https://xxxx-xxx-xxx-xxx-xxx.ngrok-free.app/callbackのredirect uriを設定する
  2. spotipy + Flask のコードをローカルに作成(コードは以下)
  3. .env を設定(client_id など)
    1. SPOTIPY_REDIRECT_URIhttps://xxxx-xxx-xxx-xxx-xxx.ngrok-free.app/callbackを設定する
  4. コードを実行
  5. ブラウザでngrokの https://xxxx-xxx-xxx-xxx-xxx.ngrok-free.appへアクセス
  6. 認可後、ターミナルに表示された refresh_token を控える

Spotify側の仕様として、HTTPSリダイレクトが必須なため、今回は ngrok を使って一時的にHTTPSで公開している

import os
from flask import Flask, redirect, request
from dotenv import load_dotenv
from spotipy.oauth2 import SpotifyOAuth

load_dotenv()
app = Flask(__name__)

sp_oauth = SpotifyOAuth(
    client_id=os.getenv("SPOTIPY_CLIENT_ID"),
    client_secret=os.getenv("SPOTIPY_CLIENT_SECRET"),
    redirect_uri=os.getenv("SPOTIPY_REDIRECT_URI"),
    scope="user-read-currently-playing user-read-playback-state",
    cache_path=None
)

@app.route("/")
def login():
    return redirect(sp_oauth.get_authorize_url())

@app.route("/callback")
def callback():
    code = request.args.get("code")
    token_info = sp_oauth.get_access_token(code)
    return f"Refresh Token: {token_info.get('refresh_token')}"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8888)

ステップ 4:スプレッドシートの作成

Secretな情報を直接スプシに書き込むので、他人への共有設定はしないように

  1. Google スプレッドシートを新規作成
  2. 以下のキーを左列(A列)、値を右列(B列)に入力
spotify_client_id
spotify_client_secret
spotify_refresh_token
spotify_access_token(後で自動で入ってくるので空でOK)
slack_token

ステップ 5:GASスクリプトの設定

  1. スプレッドシート上で「拡張機能」→「Apps Script」を開く
  2. 記事の updateSlackStatusWithSpotify() 以下のスクリプトを貼り付け
function updateSlackStatusWithSpotify() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const rows = sheet.getDataRange().getValues();
  const config = Object.fromEntries(rows);

  // 必要な情報を取得
  const clientId = config["spotify_client_id"];
  const clientSecret = config["spotify_client_secret"];
  const refreshToken = config["spotify_refresh_token"];
  let accessToken = config["spotify_access_token"];
  const slackToken = config["slack_token"];

  // アクセストークンをリフレッシュ(毎回)
  accessToken = refreshSpotifyToken(clientId, clientSecret, refreshToken);
  updateCell(sheet, "spotify_access_token", accessToken); // 更新

  const nowPlaying = getNowPlayingTrack(accessToken);
  if (nowPlaying) {
    setSlackStatus(slackToken, nowPlaying);
  } else {
    const currentStatus = getCurrentSlackStatus(slackToken);
    if (currentStatus?.status_emoji === ":musical_note:") {
      clearSlackStatus(slackToken);
    }
  }
}

function refreshSpotifyToken(clientId, clientSecret, refreshToken) {
  const token = Utilities.base64Encode(`${clientId}:${clientSecret}`);
  const payload = {
    grant_type: "refresh_token",
    refresh_token: refreshToken
  };

  const res = UrlFetchApp.fetch("https://accounts.spotify.com/api/token", {
    method: "post",
    headers: {
      Authorization: `Basic ${token}`,
      "Content-Type": "application/x-www-form-urlencoded"
    },
    payload: Object.entries(payload).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&"),
    muteHttpExceptions: true
  });

  const json = JSON.parse(res.getContentText());
  return json.access_token;
}

function getNowPlayingTrack(token) {
  const res = UrlFetchApp.fetch("https://api.spotify.com/v1/me/player/currently-playing", {
    method: "get",
    headers: {
      Authorization: `Bearer ${token}`
    },
    muteHttpExceptions: true
  });

  if (res.getResponseCode() !== 200) return null;

  const data = JSON.parse(res.getContentText());
  if (!data?.item) return null;

  const track = data.item;
  const name = track.name;
  const artists = track.artists.map(a => a.name).join(", ");
  return `${name} - ${artists}`;
}

function setSlackStatus(token, text) {
  const payload = {
    profile: {
      status_text: text,
      status_emoji: ":musical_note:",
      status_expiration: 0
    }
  };

  UrlFetchApp.fetch("https://slack.com/api/users.profile.set", {
    method: "post",
    contentType: "application/json",
    headers: {
      Authorization: `Bearer ${token}`
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  });
}

function updateCell(sheet, key, value) {
  const range = sheet.getDataRange();
  const values = range.getValues();
  for (let i = 0; i < values.length; i++) {
    if (values[i][0] === key) {
      sheet.getRange(i + 1, 2).setValue(value);
      return;
    }
  }
}

function getCurrentSlackStatus(token) {
  const res = UrlFetchApp.fetch("https://slack.com/api/users.profile.get", {
    method: "get",
    headers: {
      Authorization: `Bearer ${token}`
    },
    muteHttpExceptions: true
  });

  if (res.getResponseCode() !== 200) return null;

  const data = JSON.parse(res.getContentText());
  return {
    status_text: data.profile?.status_text || "",
    status_emoji: data.profile?.status_emoji || ""
  };
}

function clearSlackStatus(token) {
  const payload = {
    profile: {
      status_text: "",
      status_emoji: "",
      status_expiration: 0
    }
  };

  UrlFetchApp.fetch("https://slack.com/api/users.profile.set", {
    method: "post",
    contentType: "application/json",
    headers: {
      Authorization: `Bearer ${token}`
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  });
}

ステップ 6:トリガー設定(自動更新)

  1. GASエディタの「トリガー」アイコンをクリック
  2. 関数:updateSlackStatusWithSpotify を選択
  3. イベントの種類:「時間主導型」→「5分おき」を設定

スクリーンショット 2025-06-18 19.02.19.png

いい感じ!

これを他人にもスケールさせるなら?

今回の構成はあくまで「自分用の自動化」

アクセスキーとかもベタ書きですし、例えば非エンジニア含めてステータス表示を行いたい!と思ったときにスケールが効かない形になっている

なので、気軽に広めるのであれば、Web UI+バックエンド化して、OAuthトークンを一元管理とかするといいのかもなあと思っていたりしておる

最後に

らぷりえーるはいいぞ

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?