junkerstock
 mu003 

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ラジオ局リスト管理</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #f0f2f5;
color: #1c1e21;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background-color: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
width: 90%;
max-width: 500px;
text-align: center;
}
h1 {
font-size: 24px;
margin-bottom: 20px;
}
#station-select {
width: 100%;
padding: 12px;
font-size: 16px;
border-radius: 6px;
border: 1px solid #dddfe2;
margin-bottom: 16px;
}
audio {
width: 100%;
margin-top: 16px;
}
.controls {
display: flex;
gap: 12px;
justify-content: center;
margin-top: 20px;
}
button {
padding: 10px 20px;
font-size: 15px;
font-weight: bold;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s;
}
#exclude-button {
background-color: #ffebe5;
color: #db2828;
}
#exclude-button:hover {
background-color: #ffdad3;
}
#reset-button {
background-color: #e7f3ff;
color: #1877f2;
}
#reset-button:hover {
background-color: #d2e7ff;
}
#loading-status {
color: #606770;
font-size: 14px;
margin-top: 10px;
}
</style>
</head>
<body>

<div class="container">
<h1>ラジオプレイヤー</h1>
<select id="station-select" disabled></select>
<div class="controls">
<button id="exclude-button">この局を除外</button>
<button id="reset-button">除外をリセット</button>
</div>
<audio id="player" controls></audio>
<p id="loading-status">再生可能なラジオ局を確認中...</p>
</div>

<script>
document.addEventListener('DOMContentLoaded', () => {
// --- データ定義 ---
const STATIONS = [
{ name: "Tokyo FM", url: "https://tfm.leanstream.co/TFM-ML" },
{ name: "J-WAVE", url: "https://j-wave.leanstream.co/J-WAVE-ML" },
{ name: "NHK-FM 東京", url: "https://nhkradiofm-tokyo.akamaized.net/hls/live/512133/nhkradiofm-tokyo/master.m3u8" },
{ name: "AFN TOKYO", url: "https://playerservices.streamtheworld.com/api/livestream-redirect/AFN_YOK32.aac" },
{ name: "BBC World Service", url: "https://stream.live.vc.bbcmedia.co.uk/bbc_world_service" },
// 再生テストで失敗するであろうダミーURL
{ name: "再生不能な局 (テスト用)", url: "https://example.com/invalid-stream.mp3" },
];
const EXCLUSION_STORAGE_KEY = 'excludedStations';

// --- DOM要素の取得 ---
const stationSelect = document.getElementById('station-select');
const player = document.getElementById('player');
const excludeButton = document.getElementById('exclude-button');
const resetButton = document.getElementById('reset-button');
const loadingStatus = document.getElementById('loading-status');

let playableStations = [];

/**
* ローカルストレージから除外リストを読み込む
* @returns {string[]} 除外された局のURLの配列
*/
const loadExclusions = () => {
const stored = localStorage.getItem(EXCLUSION_STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
};

/**
* 除外リストをローカルストレージに保存する
* @param {string[]} exclusions - 除外する局のURLの配列
*/
const saveExclusions = (exclusions) => {
localStorage.setItem(EXCLUSION_STORAGE_KEY, JSON.stringify(exclusions));
};

/**
* ドロップダウンリストを再描画する
*/
const populateDropdown = () => {
const exclusions = loadExclusions();
stationSelect.innerHTML = ''; // リストを初期化

// 再生可能で、かつ除外リストに含まれていない局をフィルタリング
const visibleStations = playableStations.filter(station => !exclusions.includes(station.url));

if (visibleStations.length === 0) {
const option = new Option("再生できる局がありません", "");
option.disabled = true;
stationSelect.add(option);
stationSelect.disabled = true;
excludeButton.disabled = true;
return;
}

stationSelect.disabled = false;
excludeButton.disabled = false;

visibleStations.forEach((station, index) => {
// "01 局名" の形式でテキストを作成
const number = String(index + 1).padStart(2, '0');
const optionText = `${number} ${station.name}`;
const option = new Option(optionText, station.url);
stationSelect.add(option);
});

// 最初の局をプレイヤーに設定(再生はしない)
player.src = stationSelect.value;
};

/**
* URLが再生可能かチェックする
* @param {string} url - チェックするURL
* @returns {Promise<boolean>} - 再生可能ならtrue
*/
const checkPlayability = (url) => {
return new Promise((resolve) => {
const audio = new Audio();
audio.src = url;
let resolved = false;

// タイムアウト処理
const timer = setTimeout(() => {
if (!resolved) {
resolved = true;
audio.src = ""; // リソース解放
resolve(false); // 5秒経ってもイベントが発生しなければ再生不能とみなす
}
}, 5000);

// 再生可能になった時点で成功
audio.oncanplay = () => {
if (!resolved) {
resolved = true;
clearTimeout(timer);
audio.src = "";
resolve(true);
}
};

// エラーが発生した時点で失敗
audio.onerror = () => {
if (!resolved) {
resolved = true;
clearTimeout(timer);
audio.src = "";
resolve(false);
}
};
});
};

/**
* 初期化処理
*/
const initialize = async () => {
// 全ての局の再生可否を並行してチェック
const results = await Promise.all(STATIONS.map(station => checkPlayability(station.url)));

// 再生可能な局だけをリストアップ
playableStations = STATIONS.filter((_, index) => results[index]);

loadingStatus.textContent = `確認完了 (${playableStations.length} / ${STATIONS.length} 局が再生可能)`;
populateDropdown();
};

// --- イベントリスナーの設定 ---
stationSelect.addEventListener('change', () => {
player.src = stationSelect.value;
player.play();
});

excludeButton.addEventListener('click', () => {
const selectedUrl = stationSelect.value;
if (!selectedUrl) return;

const exclusions = loadExclusions();
if (!exclusions.includes(selectedUrl)) {
exclusions.push(selectedUrl);
saveExclusions(exclusions);
populateDropdown();
alert(`「${stationSelect.options[stationSelect.selectedIndex].text.substring(3)}」を除外しました。`);
}
});

resetButton.addEventListener('click', () => {
// ローカルストレージから除外リストのキー自体を削除
localStorage.removeItem(EXCLUSION_STORAGE_KEY);
populateDropdown();
alert('除外リストをリセットしました。');
});

// アプリケーションの初期化を実行
initialize();
});
</script>

</body>
</html>


使用変数

audio
charset
checkPlayability
class
disabled
excludeButton
exclusions
id
initialize
innerHTML
lang
length
loadExclusions
loadingStatus
LUSION_STORAGE_KEY
number
oncanplay
onerror
option
optionText
playableStations
player
populateDropdown
resetButton
resolved
results
saveExclusions
selectedUrl
src
station
STATIONS
stationSelect
stored
textContent
timer
visibleStations