メモ：Twitch API および Steam web API はじめ

ダッシュボードみたいなものを作りたいと思ったのでsteamとtwitchのAPIを使って作ってみた

コードのすべてはChatGPTが書いてくれました

現状できること
・twitchの日本語ストリーマーの視聴者数50人を取得する
・上位50人のプレイしているタイトルについてSteamとIGDBから概要を取得する
・steamの場合、アクティブユーザーも取得する
・jsonファイルを読み込んでブラウザで表示（トップストリーマーのみ）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11300/8ae683b3-2bc4-6400-08db-9fe3c1196698.png)


Steam web API と twitch API を取得する

https://qiita.com/takuya-38/items/92ed22f6877b6f8dce49

https://qiita.com/Chronona/items/2e464ee96799dd0ed5c2

IGDB向けのトークンの準備

https://qiita.com/oka_yu_f/items/05d8c6ce990b14ff4e2b

IGDBトークン取得コード

```python:getIGDBtoken.py 
import requests

CLIENT_ID = "YOUR_TWITCH_CLIENT_ID"
CLIENT_SECRET = "YOUR_TWITCH_CLIENT_SECRET"

def get_access_token():

    url = "https://id.twitch.tv/oauth2/token"
    params = {
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "grant_type": "client_credentials"
    }

    response = requests.post(url, params=params)

    if response.status_code == 200:
        return response.json()["access_token"]

    else:
        print(f"Failed to get access token: {response.status_code}")
        return None

access_token = get_access_token()
print(f"Access Token: {access_token}")
```


上記を実行するとポロンとトークンが発行されるので使用する

APIの取得時にTwitchが2段階認証が必須なので手元にスマホを用意する

<details><summary>Twitch APIでトップストリーマーの取得</summary>

```python:getPopularStreamer.py
import requests
import json
from datetime import datetime
  
# Twitch APIのクライアントIDとシークレット
CLIENT_ID = "YOUR_TWITCH_CLIENT_ID"  # あなたのClient ID
CLIENT_SECRET = "YOUR_TWITCH_CLIENT_SECRET"  # あなたのClient Secret
  
# Twitch APIのエンドポイント
TOKEN_URL = "https://id.twitch.tv/oauth2/token"
STREAMS_URL = "https://api.twitch.tv/helix/streams"
USERS_URL = "https://api.twitch.tv/helix/users"
  
# アクセストークンを取得
def get_access_token():

    params = {
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "grant_type": "client_credentials"
    }

    response = requests.post(TOKEN_URL, params=params)
    
    if response.status_code == 200:
        return response.json()["access_token"]

    else:
        print("Failed to get access token:", response.json())

        return None
  
# 視聴者数が多い日本語配信のストリーマーを取得
def get_top_japanese_streamers_with_details(access_token):

    headers = {
        "Client-ID": CLIENT_ID,
        "Authorization": f"Bearer {access_token}"
    }

    params = {
        "first": 50,       # 最大50件取得
        "language": "ja"   # 日本語配信のみを対象
    }
  
    response = requests.get(STREAMS_URL, headers=headers, params=params)

    if response.status_code != 200:
        print("Failed to get streams:", response.json())
        return []

    streams = response.json().get("data", [])

    # ユーザー情報を追加取得
    user_ids = [stream["user_id"] for stream in streams]
    users_info = get_users_info(user_ids, headers)

    # データを整形
    detailed_streams = []

    for stream in streams:
        user_info = users_info.get(stream["user_id"], {})

        detailed_streams.append({
            "StreamerName": stream["user_name"],
            "GameName": stream.get("game_name", "Unknown"),
            ...#好きなだけAPIでほしいものを返してもらう！
        })

    return detailed_streams

# ユーザー情報を取得
def get_users_info(user_ids, headers):

    user_ids_str = "&id=".join(user_ids)
    response = requests.get(f"{USERS_URL}?id={user_ids_str}", headers=headers)

    if response.status_code != 200:
        print("Failed to get user info:", response.json())
        return {}

    users = response.json().get("data", [])

    return {user["id"]: user for user in users}

# JSONファイルに保存
def save_to_json_file(data, filename="top_50_japanese_streamers.json"):

    with open(filename, "w", encoding="utf-8") as file:
        json.dump(data, file, ensure_ascii=False, indent=4)
 

# 実行

if __name__ == "__main__":

    # アクセストークンを取得
    token = get_access_token()

    if not token:
        exit()
  
    # 日本語配信ストリーマーを取得
    japanese_streamers = get_top_japanese_streamers_with_details(token) 

    if japanese_streamers:
        save_to_json_file(japanese_streamers)

    else:
        print("No data fetched.")
```
</details>

JSONは以下のような感じのものが出る
```JSON:streamer.json
{
        "StreamerName": "k4sen",
        "GameName": "Wuthering Waves",
        "ViewerCount": 13528,
        "StreamTitle": "鳴潮2.0きたあああイベント参加するお＾ω＾ #PR",
        "StreamURL": "https://www.twitch.tv/k4sen",
        "ProfileImageURL": "https://static-cdn.jtvnw.net/jtv_user_pictures/794e906d-64d7-41ab-bb89-8fb159e4124b-profile_image-300x300.png",
        "ThumbnailURL": "https://static-cdn.jtvnw.net/previews-ttv/live_user_k4sen-1920x1080.jpg",
        "StartedAt": "2025-01-06T09:02:58Z",
        "StreamDuration": "2:33:32.486319"
    },
```

<details><summary>保存したjsonファイルから、SteamとIGDBからゲーム情報を引き当てる</summary>

```python:getGameInfo.py
import requests
import json

# APIキーとエンドポイント
STEAM_API_KEY = "STEAM_API_KEY"
IGDB_CLIENT_ID = "YOUR_TWITCH_CLIENT_ID" 
IGDB_ACCESS_TOKEN = "YOUR_IGDB_TOKEN" 

STEAM_API_URL = "https://api.steampowered.com/ISteamApps/GetAppList/v2/"
STEAM_GAME_DETAILS_URL = "https://store.steampowered.com/api/appdetails"
STEAM_PLAYER_COUNT_URL = "https://api.steampowered.com/ISteamUserStats/GetNumberOfCurrentPlayers/v1/"
IGDB_API_URL = "https://api.igdb.com/v4/games"

# Steamから全ゲームのAppIDリストを取得
def fetch_steam_apps():

    response = requests.get(STEAM_API_URL)

    if response.status_code == 200
        apps = response.json().get("applist", {}).get("apps", [])

        return {app["name"].lower(): app["appid"] for app in apps}

    else:
        print(f"Failed to fetch Steam app list: {response.status_code}")

        return {}


# Steamからゲーム詳細を取得
def fetch_game_details_from_steam(app_id):

    params = {"appids": app_id, "l": "japanese"}

    response = requests.get(STEAM_GAME_DETAILS_URL, params=params)

    if response.status_code == 200:
        data = response.json().get(str(app_id), {}).get("data", {})

        if data:
            # アクティブユーザー数を取得
            active_users = fetch_current_players_from_steam(app_id)

            # タイトル比較

            english_name = data.get("name")

            japanese_name = data.get("name", "") if params["l"] == "japanese" else None

            return {
                "EnglishName": english_name if english_name != japanese_name else None,
                "JapaneseName": japanese_name,
                "Genres": [genre["description"] for genre in data.get("genres", [])],
                "ReleaseDate": data.get("release_date", {}).get("date"),
                "Description": data.get("short_description"),
                "CurrentActiveUsers": active_users,
                "Source": "Steam",
            }

    return None

# Steamから現在のアクティブユーザー数を取得

def fetch_current_players_from_steam(app_id):

    params = {"key": STEAM_API_KEY, "appid": app_id}

    response = requests.get(STEAM_PLAYER_COUNT_URL, params=params)

    if response.status_code == 200:
        data = response.json().get("response", {})

        return data.get("player_count", 0)

    return 0  

# IGDBからゲーム情報を取得

def fetch_game_info_from_igdb(game_name):

    headers = {

        "Client-ID": IGDB_CLIENT_ID,
        "Authorization": f"Bearer {IGDB_ACCESS_TOKEN}",

    }

    data = f'search "{game_name}"; fields name,genres.name,first_release_date,summary;'

    response = requests.post(IGDB_API_URL, headers=headers, data=data)

    if response.status_code == 200:

        results = response.json()

        if results:

            game = results[0]

            return {

                "EnglishName": game.get("name"),

                "JapaneseName": None,

                "Genres": [genre["name"] for genre in game.get("genres", [])],

                "ReleaseDate": game.get("first_release_date"),

                "Description": game.get("summary", "No summary available."),

                "Source": "IGDB",

            }

    return None

  

# ゲーム情報を取得

def get_game_info(game_name, steam_apps):

    app_id = steam_apps.get(game_name.lower())

    if app_id:

        steam_info = fetch_game_details_from_steam(app_id)

        if steam_info:

            return steam_info

  

    # Steamにない場合はIGDBを使用

    igdb_info = fetch_game_info_from_igdb(game_name)

    if igdb_info:

        return igdb_info

  

    return {"EnglishName": game_name, "Error": "No data found"}

  

# メイン処理

def main():

    # Steam AppIDリストを取得
    steam_apps = fetch_steam_apps()

  

    # 入力JSONを読み込み
    with open("top_50_japanese_streamers.json", "r", encoding="utf-8") as file:

        streamers = json.load(file)  

    # ゲーム情報を取得
    games_info = {}

    for streamer in streamers:

        game_name = streamer["GameName"]
        if game_name not in games_info:  # 同じゲーム名は再取得しない

            print(f"Fetching info for game: {game_name}")

            games_info[game_name] = get_game_info(game_name, steam_apps)

  

    # 出力JSONに保存

    with open("streamers.json", "w", encoding="utf-8") as streamers_file:

        json.dump(streamers, streamers_file, ensure_ascii=False, indent=4)


    with open("games_info.json", "w", encoding="utf-8") as games_file:

        json.dump(games_info, games_file, ensure_ascii=False, indent=4)  

    print("Data saved to streamers.json and games_info.json")

if __name__ == "__main__":

    main()
```
</details>

メモだし動いてないところも多くあるので解説はなし
これで引き当てたJSONは以下のようにな感じ
```JSON:GameInfo.json
"Marvel Rivals": {

        "EnglishName": null,
        "JapaneseName": "マーベル・ライバルズ",
        "Genres": [
            "アクション",
            "無料プレイ"
        ],
        "ReleaseDate": "2024年12月5日",
        "Description": "『Marvel Rivals』はスーパーヒーローPvPチームシューティングゲームです。プレイヤーは追加され続けるスーパーヒーローやスーパーヴィランたちの中からキャラクターを選び、オールスターチームを編成できます。マーベルマルチバースの破壊可能でダイナミックなマップを舞台に、ユニークなスーパーパワーでバトルを繰り広げましょう！",
        "CurrentActiveUsers": 158223,
        "Source": "Steam"
    },
    "Splatoon 3": {
        "EnglishName": "Splatoon 3",
        "JapaneseName": null,
        "Genres": [
            "Shooter",
            "Platform"
        ],
        "ReleaseDate": 1662681600,
        "Description": "Enter the Splatlands, a sun-scorched desert inhabited by battle-hardened Inklings and Octolings. Splatsville, the city of chaos, is the adrenaline-fueled heart of this dusty wasteland. Even in this desolate environment, Turf War reigns supreme and battles rage in new stages located in the surrounding wilds. Dynamic new moves help these fighters dodge attacks and cover more ground, along with a new bow-shaped weapon to sling ink.",
        "Source": "IGDB"
    },
```
上記のような感じ
英語名と併記しようとしたがなんかうまくいかなかったのであとで修正する

JSONとかを何にも気にせず作れるし成形してくれるの便利！

<details><summary>jsonだと見づらいのでviewなどを用意する</summary>

```html:index.html
<!DOCTYPE html>

<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>streamers</title>
  <link rel="stylesheet" href="styles.css">
</head>

<body>
  <header>
    <h1>Streamers</h1>
  </header>
  <main>
    <div id="ranking-container">
      <p>ランキングを読み込んでいます...</p>
    </div>
  </main>

  <script src="streamers.js"></script>

</body>
</html>
```
```js:streamers.js
// streamers.json を読み込んでランキングを表示する

fetch("streamers.json")

  .then(response => {

    if (!response.ok) {

      throw new Error("Failed to load streamers.json");

    }

    return response.json();

  })

  .then(data => {

    // ランキング表示のためのHTML要素を生成

    const container = document.getElementById("ranking-container");

    container.innerHTML = "";

  

    data.forEach((streamer, index) => {

      // ランキング項目のHTMLを作成

      const streamerElement = document.createElement("div");

      streamerElement.className = "streamer-item";

  

      streamerElement.innerHTML = `

        <div class="rank">${index + 1}</div>

        <div class="profile">

          <img src="${streamer.ProfileImageURL}" alt="${streamer.StreamerName}" />

        </div>

        <div class="details">

          <h3>${streamer.StreamerName}</h3>

          <p><strong>配信タイトル:</strong> ${streamer.StreamTitle}</p>

          <p><strong>ゲーム:</strong> ${streamer.GameName}</p>

          <p><strong>視聴者数:</strong> ${streamer.ViewerCount.toLocaleString()} 人</p>

          <a href="${streamer.StreamURL}" target="_blank">視聴する</a>

        </div>

      `;

  

      container.appendChild(streamerElement);

    });

  })

  .catch(error => {

    console.error("Error:", error);

    const container = document.getElementById("ranking-container");

    container.innerHTML = `<p class="error">ストリーマー情報の読み込みに失敗しました。</p>`;

  });
```
</details>
あとCSSなど好きなものを準備する

localhostはpythonで`python -m http.server 8000`とコマンドを打って、`http://127.0.0.1:8000/` にアクセスすれば確認できる
最初はVSCodeの拡張機能でやろうと思っていたが、もはや何でもlocalhostを立ててくれるのでpython使ってるしよかろうと思った

データが取れることを確認した、次はビジュアルを整えていこうと考えている
AIがすごいというよりもやりたいことのモックアップができちゃうのがいい
以前はここまでのモックアップを作ろうとしたら慣れないAPIのデモを動かしてうにゃうにゃする以外方法がなかったが、便利さを感じる
（多分以前だったらこのモックに3日ぐらい格闘したと思う）