ポキログ〈出会う系〉アプリ&コード配布

ポッドキャスト「桐野美也子の #ポキログ ポッドキャストに恋をする」で利用するために作った、世界中の日本語ポッドキャストから1エピソードをランダムに抽選してピックアップするだけの自作ウェブアプリです。

楽しんでいただけたら、番組宛にメッセージください


歴史

2025年2月18日

そもそもはJavaScriptで記述したコードでした。スクリプトはHTMLに埋め込んであるので、以下のコードをローカル環境で単一の .html ファイルとして保存し、ウェブブラウザで実行できます。動作にあたってはpodcastindex.orgでAPI KeyとAPI Secretを取得して、ソースコードの該当部分に記入して動かしてください。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ポッドキャストに恋をする #ポキログ by Miyako Kirino</title>
</head>
<body>

<h1>ポッドキャストに恋をする #ポキログ by Miyako Kirino</h1>
<p>・<a href="https://unracer.com/f1/" target="_blank">桐野美也子ポッドキャストチャンネル</a></p>
<p>・<a href="https://www.youtube.com/channel/UCjKTuB_dLaHyWB-2XMnebmw" target="_blank">桐野美也子YouTubeチャンネル</a></p>
<p>新しいポッドキャストとの出会い。<a href="https://podcastindex-org.github.io/docs-api/#overview--example-code" target="_blank">Podcastindex.orgのAPI</a>を使って、日本語ポッドキャスト番組の中からランダムに1エピソードを抽出して表示します。API Keyが必要です。</p>
<p>2025/2/18 Ver.4 developed by Miyako Kirino with Gemini 2.0 Flash and Claude 3.5 Sonnet</p>
<button onclick="getRandomJapanesePodcastEpisode()">Get Random Episode</button>
<div id="podcastInfo"></div>

<script>
	const apiKey = '********'; // あなたのAPIキー
	const apiSecret = '*******'; // あなたのAPIシークレット

async function getRandomJapanesePodcastEpisode() {
    const apiUrl = `https://api.podcastindex.org/api/1.0/episodes/random?max=1&lang=ja`;
    const apiHeaderTime = Math.floor(Date.now() / 1000);
    const authorization = await generateAuthorization(apiKey, apiSecret, apiHeaderTime);

    try {
        const response = await fetch(apiUrl, {
            method: 'GET',
            headers: {
                'X-Auth-Date': '' + apiHeaderTime,
                'X-Auth-Key': apiKey,
                'Authorization': authorization,
                'User-Agent': "MyPodcastApp/1.0"
            }
        });

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        const data = await response.json();
        console.log('API Response:', data); // データの確認

        if (data.status === "true" && data.episodes && data.episodes.length > 0) {
            console.log('Episode to display:', data.episodes[0]); // 表示するエピソードの確認
            displayPodcastInfo(data.episodes[0]);
        } else {
            console.log('Data structure:', data); // データ構造の確認
            document.getElementById('podcastInfo').innerHTML = 'ポッドキャストエピソードが見つかりませんでした。';
        }

    } catch (error) {
        console.error('Error:', error);
        document.getElementById('podcastInfo').innerHTML = 'エラーが発生しました: ' + error.message;
    }
}

function displayPodcastInfo(episode) {
    const podcastInfoDiv = document.getElementById('podcastInfo');
    
    // 日付を日本時間に変換
    const publishDate = new Date(episode.datePublished * 1000);
    const jpDate = publishDate.toLocaleString('ja-JP', {
        timeZone: 'Asia/Tokyo',
        year: 'numeric',
        month: 'long',
        day: 'numeric',
        hour: '2-digit',
        minute: '2-digit'
    });

    podcastInfoDiv.innerHTML = `
        <div class="podcast-container">
            <div class="podcast-header">
                <img src="${episode.feedImage}" alt="${episode.feedTitle}" 
                    style="max-width: 200px; height: auto; margin-bottom: 1rem;">
                <h2>${episode.title}</h2>
                <h3>${episode.feedTitle}</h3>
            </div>
            
            <div class="podcast-meta">
                <p>公開日: ${jpDate}</p>
                <p>カテゴリー: ${Object.values(episode.categories).join(', ')}</p>
            </div>

            <div class="podcast-description">
                ${episode.description}
            </div>

            <div class="podcast-player">
                <audio controls style="width: 100%; margin-top: 1rem;">
                    <source src="${episode.enclosureUrl}" type="${episode.enclosureType}">
                    お使いのブラウザは音声の再生に対応していません。
                </audio>
                <p\><a href\="${episode.link}" target="_blank">番組ページへ</a></p>
        </div>
            </div>
        </div>
    `;

    // スタイルを追加
    const style = document.createElement('style');
    style.textContent = `
        .podcast-container {
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background: #f8f9fa;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        .podcast-header {
            text-align: center;
            margin-bottom: 20px;
        }
        .podcast-header h2 {
            margin: 10px 0;
            color: #333;
        }
        .podcast-header h3 {
            color: #666;
            font-weight: normal;
        }
        .podcast-meta {
            color: #666;
            font-size: 0.9em;
            margin: 10px 0;
        }
        .podcast-description {
            margin: 20px 0;
            line-height: 1.6;
        }
        .podcast-player {
            margin-top: 20px;
        }
    `;
    document.head.appendChild(style);
}

async function generateAuthorization(apiKey, apiSecret, apiHeaderTime) {
    const data4Hash = apiKey + apiSecret + apiHeaderTime;
    const msgBuffer = new TextEncoder().encode(data4Hash);
    const hashBuffer = await crypto.subtle.digest('SHA-1', msgBuffer);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    return hashHex;
}

</script>

</body>
</html>

2025年3月4日

上のJavascriptをまずはGoogle Clab上で動くようにPythonに移植しました。

import requests
import hashlib
import time
import IPython.display as display

# APIキーとシークレット(適宜変更すること)
API_KEY = '********'
API_SECRET = '********'

def generate_authorization(api_key, api_secret, api_time):
    """ API認証ヘッダーを生成 """
    data4hash = f"{api_key}{api_secret}{api_time}"
    hash_hex = hashlib.sha1(data4hash.encode()).hexdigest()
    return hash_hex

def get_random_japanese_podcast_episode():
    """ 日本語ポッドキャストのランダムなエピソードを取得 """
    api_url = "https://api.podcastindex.org/api/1.0/episodes/random?max=1&lang=ja"
    api_time = int(time.time())
    authorization = generate_authorization(API_KEY, API_SECRET, api_time)

    headers = {
        "X-Auth-Date": str(api_time),
        "X-Auth-Key": API_KEY,
        "Authorization": authorization,
        "User-Agent": "MyPodcastApp/1.0"
    }

    try:
        response = requests.get(api_url, headers=headers)
        response.raise_for_status()  # エラーハンドリング
        data = response.json()

        if data.get("status") == "true" and "episodes" in data and len(data["episodes"]) > 0:
            return data["episodes"][0]
        else:
            return None
    except requests.exceptions.RequestException as e:
        print(f"APIリクエストエラー: {e}")
        return None

def display_podcast_info(episode):
    """ ポッドキャストエピソードの情報を表示 """
    if not episode:
        print("ポッドキャストエピソードが見つかりませんでした。")
        return

    pub_date = time.strftime('%Y年%m月%d日 %H:%M', time.localtime(episode["datePublished"]))

    html_content = f"""
    <div style='max-width: 800px; margin: auto; padding: 20px; background: #f8f9fa; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'>
        <div style='text-align: center;'>
            <img src='{episode["feedImage"]}' alt='{episode["feedTitle"]}' style='max-width: 200px; height: auto; margin-bottom: 1rem;'>
            <h2>{episode["title"]}</h2>
            <h3>{episode["feedTitle"]}</h3>
        </div>
        <p>公開日: {pub_date}</p>
        <p>カテゴリー: {", ".join(episode["categories"].values())}</p>
        <p>{episode["description"]}</p>
        <audio controls style='width: 100%; margin-top: 1rem;'>
            <source src='{episode["enclosureUrl"]}' type='{episode["enclosureType"]}'>
            お使いのブラウザは音声の再生に対応していません。
        </audio>
        <p><a href='{episode["link"]}' target='_blank'>番組ページへ</a></p>
    </div>
    """
    display.display(display.HTML(html_content))

# 実行
episode = get_random_japanese_podcast_episode()
display_podcast_info(episode)

2025年3月6日

そのPython版を元にウェブアプリにして自分が借りているサーバーに設置したのが、上で動いているものです。桐野のサーバーでは直接Pythonが実行できないので、PHPプロシキの仕組みで動かしています。ひとまずはこれでOKですかね。そもそもは自分で使うためだけのものだからJavascriptでローカルで動けばいいかと思っていたので……。しかし #ポキログ リスナーの皆さんにも使っていただければ楽しいかなと思います。

もちろん、Google Gemini Pro、Claude 3.7 Sonnet、Chat GPT Plus 4o、と生成AIのお助けが必要でした。素晴らしい!


この自作アプリを使ってポッドキャスト番組を配信してますので、ぜひお楽しみくださいませ(2025年3月8日から新シリーズがスタートです)。