<!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">再生可能なラジオ局を確認中 (0/100)...</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: "TBSラジオ", url: "https://tbsradio.leanstream.co/TBS-ML" },
{ name: "文化放送", url: "https://nqr.leanstream.co/NQR-ML" },
{ name: "ニッポン放送", url: "https://lfr.leanstream.co/LFR-ML" },
{ name: "ラジオNIKKEI第1", url: "https://radionikkei.leanstream.co/RN1-ML" },
// テレビ音声
{ name: "NHK総合 (音声)", url: "https://nhk-tv.stream.co.jp/hls/live/202 NHK-G-JP/master.m3u8" },
{ name: "NHK Eテレ (音声)", url: "https://nhk-tv.stream.co.jp/hls/live/202 NHK-E-JP/master.m3u8" },
// JCBAコミュニティFM (一部)
{ name: "FMいるか (北海道 函館市)", url: "https://fm-iruka.jp/addons/player/hls/live.m3u8" },
{ name: "FMくしろ (北海道 釧路市)", url: "https://playerservices.streamtheworld.com/api/livestream-redirect/FMKU32.aac" },
{ name: "FM NORTH WAVE (北海道 札幌市)", url: "https://northwave.leanstream.co/NORTHWAVE-ML" },
{ name: "エフエムとよひら (北海道 札幌市)", url: "https://musicbird.leanstream.co/JCB070-MP3" },
{ name: "BeFM (青森県 八戸市)", url: "https://musicbird.leanstream.co/JCB002-MP3" },
{ name: "ラヂオもりおか (岩手県 盛岡市)", url: "https://musicbird.leanstream.co/JCB007-MP3" },
{ name: "FMあすも (岩手県 一関市)", url: "https://musicbird.leanstream.co/JCB009-MP3" },
{ name: "ラジオ石巻 (宮城県 石巻市)", url: "https://musicbird.leanstream.co/JCB013-MP3" },
{ name: "横手かまくらFM (秋田県 横手市)", url: "https://musicbird.leanstream.co/JCB019-MP3" },
{ name: "FM-POCO (福島県 福島市)", url: "https://musicbird.leanstream.co/JCB024-MP3" },
{ name: "FMひたち (茨城県 日立市)", url: "https://musicbird.leanstream.co/JCB026-MP3" },
{ name: "FM KENTO (新潟県 新潟市)", url: "https://musicbird.leanstream.co/JCB043-MP3" },
{ name: "FM-Hi! (静岡県 静岡市)", url: "https://musicbird.leanstream.co/JCB061-MP3" },
{ name: "FM Haro! (静岡県 浜松市)", url: "https://musicbird.leanstream.co/JCB062-MP3" },
{ name: "RADIO LOVEAT (愛知県 豊田市)", url: "https://musicbird.leanstream.co/JCB067-MP3" },
{ name: "京都三条ラジオカフェ (京都府 京都市)", url: "https://radiocafe.jp/var/hls/live.m3u8" },
{ name: "FMちゅーピー (広島県 広島市)", url: "https://musicbird.leanstream.co/JCB089-MP3" },
{ name: "FMはつかいち (広島県 廿日市市)", url: "https://musicbird.leanstream.co/JCB091-MP3" },
{ name: "FMびざん (徳島県 徳島市)", url: "https://bfm.main.jp/bfm.m3u8" },
{ name: "FM高松 (香川県 高松市)", url: "https://musicbird.leanstream.co/JCB095-MP3" },
{ name: "シティエフエム都城 (宮崎県 都城市)", url: "https://musicbird.leanstream.co/JCB114-MP3" },
{ name: "FMぎんが (鹿児島県 鹿児島市)", url: "https://musicbird.leanstream.co/JCB118-MP3" },
{ name: "FMいしがき (沖縄県 石垣市)", url: "https://musicbird.leanstream.co/JCB121-MP3" },
// ... (さらに多数のJCBA局)
{ name: "REDS WAVE (埼玉県 さいたま市)", url: "https://musicbird.leanstream.co/JCB034-MP3" },
{ name: "FM Kawaguchi (埼玉県 川口市)", url: "https://musicbird.leanstream.co/JCB035-MP3" },
{ name: "市川うららFM (千葉県 市川市)", url: "https://musicbird.leanstream.co/JCB037-MP3" },
{ name: "むさしのFM (東京都 武蔵野市)", url: "https://musicbird.leanstream.co/JCB039-MP3" },
{ name: "かわさきFM (神奈川県 川崎市)", url: "https://musicbird.leanstream.co/JCB041-MP3" },
{ name: "FMカオン (神奈川県 海老名市)", url: "https://musicbird.leanstream.co/JCB042-MP3" },
{ name: "FM K-City (神奈川県 川崎市)", url: "https://musicbird.leanstream.co/JCB084-MP3" },
{ name: "MID-FM (愛知県 名古屋市)", url: "https://musicbird.leanstream.co/JCB066-MP3" },
{ name: "YES-fm (大阪府 大阪市)", url: "https://musicbird.leanstream.co/JCB080-MP3" },
{ name: "バナナFM (和歌山県 和歌山市)", url: "https://musicbird.leanstream.co/JCB083-MP3" },
{ name: "FM岡山", url: "https://fm-okayama.leanstream.co/OKAYAMA-ML" },
{ name: "V-air (FM山陰)", url: "https://v-air.leanstream.co/V-AIR-ML" },
{ name: "FM FUKUOKA", url: "https://fmfukuoka.leanstream.co/FUKUOKA-ML" },
{ name: "LOVE FM (福岡)", url: "https://lovefm.leanstream.co/LOVEFM-ML" },
{ name: "FMK (エフエム熊本)", url: "https://fmk.leanstream.co/FMK-ML" },
{ name: "μFM (エフエム鹿児島)", url: "https://myufm.leanstream.co/MYUFM-ML" },
{ name: "エフエム沖縄", url: "https://fmokinawa.leanstream.co/OKINAWA-ML" },
// 海外局
{ name: "BBC World Service", url: "https://stream.live.vc.bbcmedia.co.uk/bbc_world_service" },
{ name: "BBC Radio 1", url: "https://stream.live.vc.bbcmedia.co.uk/bbc_radio_one" },
{ name: "KISS FM (UK)", url: "https://stream-kiss.planetradio.co.uk/kiss.aac" },
{ name: "Jazz24 (USA)", url: "https://d.live.wostreaming.net/direct/kplu_jazz24_128-flv" },
{ name: "NPR News (USA)", url: "https://npr-ice.streamguys1.com/live.mp3" },
{ name: "Radio France Info", url: "https://stream.radiofrance.fr/franceinfo/franceinfo.mp3" },
{ name: "WNYC FM (USA, New York)", url: "https://fm939.wnyc.org/wnycfm" },
{ name: "Smooth FM 95.3 (Australia)", url: "https://playerservices.streamtheworld.com/api/livestream-redirect/SMOOTHFM_953_SC.aac" },
// J-Pop/Anime
{ name: "J-Pop Powerplay", url: "https://kathy.torontocast.com:2410/stream" },
{ name: "AnimeNfo Radio", url: "https://pool.animeradio.net/AnimeNFO.Radio.m3u" },
{ name: "Japan Hits", url: "https://www.ophanim.net/radio/8020/stream" },
{ name: "Asia Dream Radio", url: "https://play.asiadreamradio.com/radio/8020/radio.mp3" },
// ... 他にも多くの局を追加
{ name: "FMえどがわ (東京都 江戸川区)", url: "https://fm843.com/live.m3u8" },
{ name: "すかがわFM (福島県 須賀川市)", url: "http://153.122.95.143:8000/ultrafm" },
{ name: "FM軽井沢 (長野県 軽井沢町)", url: "http://live.fm-karuizawa.co.jp:8080/karuizawa" },
{ name: "FMぜんこうじ (長野県 長野市)", url: "http://fmzenkoji.net:8000/live" },
{ name: "たんなんFM (福井県 鯖江市)", url: "https://stream.tannan.fm:8000/tannanfm" },
{ name: "FMたんご (京都府 京丹後市)", url: "https://fm-tango.jp/radio/8000/radio.mp3" },
{ name: "FM GENKI (兵庫県 姫路市)", url: "https://asx.fmgenki.jp/genki" },
{ name: "ならどっとFM (奈良県 奈良市)", url: "https://narafm-live.storagerise.jp/narafm/playlist.m3u8" },
{ name: "FMはしもと (和歌山県 橋本市)", url: "https://live.fm-hashimoto.jp:8443/fm816" },
{ name: "FM鳥取 (鳥取県 鳥取市)", url: "https://www.radiobird.net/radiobird.m3u" },
{ name: "FM-Yamaguchi", url: "https://fmy.leanstream.co/FMY-ML" },
{ name: "エフエム愛媛", url: "https://joeufm.leanstream.co/JOEU-FM-ML" },
{ name: "Hi-Six (FM高知)", url: "https://fmkochi.leanstream.co/KOCHI-ML" },
{ name: "Cross FM (福岡)", url: "https://crossfm.leanstream.co/CROSSFM-ML" },
{ name: "AIR-G' (FM北海道)", url: "https://air-g.leanstream.co/AIR-G-ML" },
{ name: "Date fm (FM仙台)", url: "https://datefm.leanstream.co/DATEFM-ML" },
{ name: "FM-NIIGATA", url: "https://fmniigata.leanstream.co/FMNIIGATA-ML" },
{ name: "FM長野", url: "https://fmnagano.leanstream.co/NAGANO-ML" },
{ name: "K-mix (静岡)", url: "https://k-mix.leanstream.co/K-MIX-ML" },
{ name: "ZIP-FM (愛知)", url: "https://zip-fm.leanstream.co/ZIP-FM-ML" },
{ name: "FM802 (大阪)", url: "https://c1.radiko.jp/v2/live/hls/clear/L/27/master.m3u8" },
{ name: "Kiss FM KOBE", url: "https://kiss-fm.leanstream.co/KISSFM-ML" },
{ name: "広島FM", url: "https://hfm.leanstream.co/HFM-ML" },
{ name: "FM Nagasaki", url: "https://fmn.leanstream.co/FMN-ML" },
{ name: "エフエム大分", url: "https://fmoita.leanstream.co/OITA-ML" },
{ name: "JOY FM (FM宮崎)", url: "https://joyfm.leanstream.co/JOYFM-ML" },
{ name: "湘南ビーチFM", url: "https://jpradio.torontocast.stream/6016/stream" },
{ name: "OTTAVA (クラシック)", url: "https://ottava.streamguys1.com/ottava" },
{ name: "Backstage Radio (クラシック)", url: "https://backstageradio.leanstream.co/BSIDE-AAC" },
{ name: "Venice Classic Radio", url: "https://naxos.cdnstream.com/1255_128" },
{ name: "Radio Swiss Classic", url: "https://stream.srg-ssr.ch/m/rsc_de/mp3_128" },
{ name: "Linn Classical", url: "https://radio.linn.co.uk/classical" },
{ name: "Linn Jazz", url: "https://radio.linn.co.uk/jazz" },
];
const EXCLUSION_STORAGE_KEY = 'excludedStations';
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 = [];
const loadExclusions = () => {
const stored = localStorage.getItem(EXCLUSION_STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
};
const saveExclusions = (exclusions) => {
localStorage.setItem(EXCLUSION_STORAGE_KEY, JSON.stringify(exclusions));
};
const populateDropdown = () => {
const exclusions = loadExclusions();
const currentSelectedUrl = stationSelect.value;
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;
player.src = "";
return;
}
stationSelect.disabled = false;
excludeButton.disabled = false;
visibleStations.forEach((station, index) => {
const number = String(index + 1).padStart(2, '0');
const optionText = `${number} ${station.name}`;
const option = new Option(optionText, station.url);
stationSelect.add(option);
});
if (visibleStations.some(s => s.url === currentSelectedUrl)) {
stationSelect.value = currentSelectedUrl;
}
player.src = stationSelect.value;
};
const checkPlayability = (station, onProgress) => {
return new Promise((resolve) => {
const audio = new Audio();
audio.src = station.url;
let resolved = false;
const timer = setTimeout(() => {
if (!resolved) {
resolved = true;
audio.src = "";
resolve(null);
}
}, 7000); // 少し長めの7秒に設定
const conclude = (result) => {
if (!resolved) {
resolved = true;
clearTimeout(timer);
audio.src = "";
onProgress(); // 進捗を通知
resolve(result ? station : null);
}
};
audio.oncanplaythrough = () => conclude(true);
audio.onerror = () => conclude(false);
});
};
const initialize = async () => {
let checkedCount = 0;
const totalCount = STATIONS.length;
const updateProgress = () => {
checkedCount++;
loadingStatus.textContent = `再生可能なラジオ局を確認中 (${checkedCount}/${totalCount})...`;
};
const checkPromises = STATIONS.map(station => checkPlayability(station, updateProgress));
const results = await Promise.all(checkPromises);
playableStations = results.filter(result => result !== null);
loadingStatus.textContent = `確認完了 (${playableStations.length} / ${totalCount} 局が再生可能)`;
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 | |
checkedCount | |
checkPlayability | |
checkPromises | |
class | |
conclude | |
currentSelectedUrl | |
disabled | |
excludeButton | |
exclusions | |
id | |
initialize | |
innerHTML | |
lang | |
length | |
loadExclusions | |
loadingStatus | |
LUSION_STORAGE_KEY | |
number | |
oncanplaythrough | |
onerror | |
option | |
optionText | |
playableStations | |
player | |
populateDropdown | |
resetButton | |
resolved | |
result | |
results | |
s | |
saveExclusions | |
selectedUrl | |
src | |
station | |
STATIONS | |
stationSelect | |
stored | |
textContent | |
timer | |
totalCount | |
updateProgress | |
url | |
value | |
visibleStations |