junkerstock
 mu005 

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SHOUTcast Player Ver0.05</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #f0f2f5;
color: #333;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.player-container {
background-color: #fff;
padding: 2em;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
width: 90%;
max-width: 500px;
}
h1 {
text-align: center;
color: #1a73e8;
margin-top: 0;
}
.controls {
display: flex;
flex-direction: column;
gap: 1em;
margin-bottom: 1.5em;
}
label {
font-weight: bold;
}
select, button {
padding: 0.8em;
border-radius: 6px;
border: 1px solid #ccc;
font-size: 1em;
width: 100%;
box-sizing: border-box;
}
.button-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.8em;
}
button {
background-color: #1a73e8;
color: white;
font-weight: bold;
cursor: pointer;
border: none;
transition: background-color 0.3s;
}
button:hover {
background-color: #155ab6;
}
#play-pause-button.playing {
background-color: #dc3545;
}
#play-pause-button.playing:hover {
background-color: #b02a37;
}
#exclude-button {
background-color: #6c757d;
}
#exclude-button:hover {
background-color: #5a6268;
}
#reset-button {
background-color: #28a745;
}
#reset-button:hover {
background-color: #218838;
}
.info {
background-color: #f9f9f9;
padding: 1em;
border-radius: 6px;
border: 1px solid #eee;
min-height: 4em;
}
#station-name { font-weight: bold; }
#song-title { font-size: 1.1em; color: #000; }
audio { display: none; }
#loading-status { text-align: center; color: #666; padding: 1em 0;}
</style>
</head>
<body>

<div class="player-container">
<h1>SHOUTcast Player <small>Ver0.05</small></h1>

<audio id="audio-player" crossOrigin="anonymous"></audio>

<div id="loading-status">放送局をチェック中...</div>

<div class="controls" style="display: none;">
<div>
<label for="station-select">ラジオ局を選択:</label>
<select id="station-select">
<option value="">-- ここから選択 --</option>
</select>
</div>
<button id="play-pause-button">再生</button>
<div class="button-group">
<button id="exclude-button">この局を除外</button>
<button id="reset-button">リストをリセット</button>
</div>
</div>

<div class="info">
<h3>Now Playing:</h3>
<p id="station-name">放送局が選択されていません</p>
<p id="song-title">曲情報なし</p>
</div>
</div>

<script>
document.addEventListener('DOMContentLoaded', async () => {
// UI要素の取得
const audioPlayer = document.getElementById('audio-player');
const stationSelect = document.getElementById('station-select');
const playPauseButton = document.getElementById('play-pause-button');
const stationNameDisplay = document.getElementById('station-name');
const songTitleDisplay = document.getElementById('song-title');
const excludeButton = document.getElementById('exclude-button');
const resetButton = document.getElementById('reset-button');
const loadingStatus = document.getElementById('loading-status');
const controlsDiv = document.querySelector('.controls');

// ローカルストレージのキー
const EXCLUDED_KEY = 'shoutcast_excluded_urls';

// 全放送局のマスターリスト
const allStations = [
{ name: "R/a/dio", url: "http://relay0.r-a-d.io/main.mp3", metaUrl: "http://relay1.r-a-d.io/status-json.xsl", metaType: "JSON" },
{ name: "Vocaloid Radio", url: "https://vocaloid.radioca.st/stream", metaUrl: "https://vocaloid.radioca.st/status-json.xsl", metaType: "JSON" },
{ name: "J-Pop Powerplay Kawaii", url: "http://149.56.23.7:20002/stream", metaUrl: "http://149.56.23.7:20002/stats?json=1", metaType: "JSON" },
{ name: "JPopsuki Radio", url: "http://jpopsuki.fm:8000/stream", metaUrl: "", metaType: "ICY" },
{ name: "J-Pop Powerplay", url: "http://cabhs30.sonixcast.com:9628/;", metaUrl: "http://cabhs30.sonixcast.com:9628/stats?json=1", metaType: "JSON" },
{ name: "J-Rock Powerplay", url: "http://cabhs30.sonixcast.com:9508/;", metaUrl: "http://cabhs30.sonixcast.com:9508/stats?json=1", metaType: "JSON" },
{ name: "AnimeNfo Radio", url: "http://momori.animenfo.com:8000/", metaUrl: "http://momori.animenfo.com:8000/status-json.xsl", metaType: "JSON" },
{ name: "Ghost Anime Radio", url: "http://animeradio.su:8000/;", metaUrl: "", metaType: "ICY" },
{ name: "Japanimradio - Officiel", url: "http://listen.radioking.com/japanimradio-fm3", metaUrl: "http://listen.radioking.com/japanimradio-fm3/stats?json=1", metaType: "JSON" },
{ name: "Asia DREAM Radio - Japan Hits", url: "http://cabhs30.sonixcast.com:9764/", metaUrl: "http://cabhs30.sonixcast.com:9764/stats?json=1", metaType: "JSON" },
{ name: "J-Pop Sakura", url: "http://cabhs31.sonixcast.com:20278/", metaUrl: "http://cabhs31.sonixcast.com:20278/stats?json=1", metaType: "JSON" },
{ name: "Hotmix Radio Japan", url: "http://hotmixradio-japan.ice.infomaniak.ch/hotmixradio-japan-128.mp3", metaUrl: "", metaType: "ICY" },
{ name: "Shonan Beach FM", url: "http://47.89.252.38:8000/by_the_sea", metaUrl: "http://47.89.252.38:8000/7.html", metaType: "7.html" },
{ name: "Be Happy!789", url: "http://musicbird.leanstream.co/JCB068-MP3", metaUrl: "http://musicbird.leanstream.co/status-json.xsl?mount=/JCB068-MP3", metaType: "JSON" },
{ name: "Ottava (オッターヴァ)", url: "http://rakuten.streamguys1.com/ottava1_b", metaUrl: "", metaType: "ICY" },
{ name: "Tokyo FM World", url: "http://tokyofmworld.leanstream.co/JOAUFM-MP3?args=tunein", metaUrl: "", metaType: "ICY" },
{ name: "Japan-a-Radio", url: "http://audio.misproductions.com/japan48k", metaUrl: "http://audio.misproductions.com/7.html", metaType: "7.html" },
{ name: "J-Club Powerplay HipHop", url: "http://agnes.torontocast.com:8051/;", metaUrl: "http://agnes.torontocast.com:8051/stats?json=1", metaType: "JSON" },
{ name: "WREP", url: "http://139.99.4.27/stream", metaUrl: "", metaType: "ICY" },
{ name: "Kittikun Minimal Techno", url: "http://shoutcast.kittikun.jp:11168/", metaUrl: "http://shoutcast.kittikun.jp:11168/stats?json=1", metaType: "JSON" },
{ name: "Nightwave Plaza", url: "http://plaza.one/ogg", metaUrl: "http://plaza.one/status-json.xsl", metaType: "JSON" },
{ name: "Retro PC GAME", url: "http://gyusyabu.ddo.jp:8000/", metaUrl: "http://gyusyabu.ddo.jp:8000/7.html", metaType: "7.html" },
{ name: "No Life Radio", url: "http://radio.nolife-radio.com:9000/stream", metaUrl: "", metaType: "ICY" },
{ name: "J1 Hits", url: "http://jenny.torontocast.com:20000/stream/J1HITS", metaUrl: "http://jenny.torontocast.com:20000/stats?sid=J1HITS&json=1", metaType: "JSON" },
{ name: "J1 Xtra", url: "http://jenny.torontocast.com:8058/xtra", metaUrl: "http://jenny.torontocast.com:8058/stats?json=1", metaType: "JSON" },
{ name: "Nonstop Casiopea", url: "http://hyades.shoutca.st:8551/", metaUrl: "http://hyades.shoutca.st:8551/stats?json=1", metaType: "JSON" },
{ name: "J-Idols Project Radio", url: "http://stream.radio.co/s953f51e42/listen", metaUrl: "", metaType: "ICY" },
{ name: "Anime Plus Radio", url: "http://192.240.102.133:11216/stream", metaUrl: "http://192.240.102.133:11216/stats?json=1", metaType: "JSON" },
{ name: "The Kyoto Connection", url: "https://radio.thekyotoconnection.com/stream.mp3", metaUrl: "", metaType: "ICY" },
{ name: "JPHiP Radio", url: "http://radio.jphip.com:8800/", metaUrl: "http://radio.jphip.com:8800/7.html", metaType: "7.html" },
{ name: "Isekai-Online", url: "https://www.isekai-online.com/radioking/icecast.php", metaUrl: "", metaType: "ICY" },
{ name: "Stereo Anime", url: "http://192.99.150.42:2345/stream", metaUrl: "http://192.99.150.42:2345/stats?json=1", metaType: "JSON" },
{ name: "Anison FM", url: "http://anison.fm:8000/radio", metaUrl: "", metaType: "ICY" },
{ name: "Extreme Anime Radio", url: "http://radio.keiichi.net:8000/ear.mp3", metaUrl: "", metaType: "ICY" },
{ name: "J-Pop Project Radio", url: "http://stream.radio.co/s953f51e42/listen", metaUrl: "", metaType: "ICY" },
{ name: "Radio Anime Nexus", url: "http://nexus.radio:8000/stream", metaUrl: "", metaType: "ICY" },
{ name: "Yggdrasil Radio", url: "http://radio.yggdrasil.me/radio/8000/radio.mp3", metaUrl: "", metaType: "ICY" },
{ name: "JMusicAnime Radio", url: "http://192.99.8.192:3560/stream", metaUrl: "http://192.99.8.192:3560/stats?json=1", metaType: "JSON" },
{ name: "Radio Animati", url: "http://stream.radioanimati.it:8000/radioanimati", metaUrl: "", metaType: "ICY" },
{ name: "Anime Web Radio", url: "http://animewebradio.it:8000/stream.mp3", metaUrl: "", metaType: "ICY" },
{ name: "Kibo.FM", url: "http://stream.kibo.fm/live", metaUrl: "", metaType: "ICY" },
{ name: "Radio-AniNeko", url: "http://radio-anineko.de:8000/live.mp3", metaUrl: "", metaType: "ICY" },
{ name: "鎌倉エフエム", url: "http://musicbird.leanstream.co/JCB016-MP3", metaUrl: "http://musicbird.leanstream.co/status-json.xsl?mount=/JCB016-MP3", metaType: "JSON" },
{ name: "FMちゅーピー", url: "http://musicbird.leanstream.co/JCB082-MP3", metaUrl: "http://musicbird.leanstream.co/status-json.xsl?mount=/JCB082-MP3", metaType: "JSON" },
{ name: "FM Edogawa", url: "http://musicbird.leanstream.co/JCB033-MP3", metaUrl: "http://musicbird.leanstream.co/status-json.xsl?mount=/JCB033-MP3", metaType: "JSON" },
{ name: "FM千里", url: "http://simul.freebit.net:8310/fmsenri", metaUrl: "", metaType: "ICY" },
{ name: "Radio MOMO", url: "http://musicbird.leanstream.co/JCB079-MP3", metaUrl: "http://musicbird.leanstream.co/status-json.xsl?mount=/JCB079-MP3", metaType: "JSON" },
{ name: "Radio Hayama", url: "http://199.195.194.140:8073/", metaUrl: "http://199.195.194.140:8073/7.html", metaType: "7.html" },
{ name: "FM-POCO", url: "http://musicbird.leanstream.co/JCB010-MP3", metaUrl: "http://musicbird.leanstream.co/status-json.xsl?mount=/JCB010-MP3", metaType: "JSON" },
{ name: "富士山GOGOエフエム", url: "http://musicbird.leanstream.co/JCB037-MP3", metaUrl: "http://musicbird.leanstream.co/status-json.xsl?mount=/JCB037-MP3", metaType: "JSON" },
{ name: "FM Apple Wave", url: "http://musicbird.leanstream.co/JCB004-MP3", metaUrl: "http://musicbird.leanstream.co/status-json.xsl?mount=/JCB004-MP3", metaType: "JSON" }
];
let playableStations = []; // 再生可能な局だけを保持する
let metadataInterval;

// 再生可能かチェックする関数
const checkPlayability = (url) => {
return new Promise(resolve => {
const testAudio = new Audio();
testAudio.crossOrigin = "anonymous";
let resolved = false;
const timeout = setTimeout(() => {
if (!resolved) {
resolved = true;
cleanup();
resolve({ url, playable: false });
}
}, 7000); // 7秒でタイムアウト

const cleanup = () => {
testAudio.removeEventListener('canplay', onSuccess);
testAudio.removeEventListener('error', onError);
testAudio.src = "";
clearTimeout(timeout);
};
const onSuccess = () => {
if (!resolved) {
resolved = true;
cleanup();
resolve({ url, playable: true });
}
};
const onError = () => {
if (!resolved) {
resolved = true;
cleanup();
resolve({ url, playable: false });
}
};
testAudio.addEventListener('canplay', onSuccess);
testAudio.addEventListener('error', onError);
testAudio.src = url;
});
};

// ドロップダウンリストを更新する関数
const updateDropdown = () => {
const excludedUrls = JSON.parse(localStorage.getItem(EXCLUDED_KEY)) || [];
const visibleStations = playableStations.filter(s => !excludedUrls.includes(s.url));

stationSelect.innerHTML = '<option value="">-- ここから選択 --</option>'; // 初期オプション

visibleStations.forEach((station, index) => {
const option = document.createElement('option');
const number = String(index + 1).padStart(2, '0');
option.value = station.url;
option.textContent = `${number}. ${station.name}`;
stationSelect.appendChild(option);
});
};

// ---- 初期化処理 ----
const checkResults = await Promise.all(allStations.map(s => checkPlayability(s.url)));
const playableUrls = new Set(checkResults.filter(r => r.playable).map(r => r.url));
playableStations = allStations.filter(s => playableUrls.has(s.url));
playableStations.sort((a, b) => a.name.localeCompare(b.name, 'ja'));

loadingStatus.style.display = 'none';
controlsDiv.style.display = 'flex';
updateDropdown();

// ---- イベントリスナー ----
playPauseButton.addEventListener('click', () => {
if (audioPlayer.paused) {
if (audioPlayer.src) audioPlayer.play().catch(e => console.error("再生エラー:", e));
else alert("先にラジオ局を選択してください。");
} else {
audioPlayer.pause();
}
});

audioPlayer.addEventListener('play', () => {
playPauseButton.textContent = '停止';
playPauseButton.classList.add('playing');
});

audioPlayer.addEventListener('pause', () => {
playPauseButton.textContent = '再生';
playPauseButton.classList.remove('playing');
});

stationSelect.addEventListener('change', () => {
const url = stationSelect.value;
if (!url) {
audioPlayer.src = '';
stationNameDisplay.textContent = '放送局が選択されていません';
songTitleDisplay.textContent = '曲情報なし';
if (metadataInterval) clearInterval(metadataInterval);
return;
}

const station = playableStations.find(s => s.url === url);
if (!station) return;

audioPlayer.src = station.url;
audioPlayer.play().catch(e => console.error("再生エラー:", e));

stationNameDisplay.textContent = station.name;
songTitleDisplay.textContent = '...';

if (metadataInterval) clearInterval(metadataInterval);
updateMetadata(station);
metadataInterval = setInterval(() => updateMetadata(station), 10000);
});

excludeButton.addEventListener('click', () => {
const selectedUrl = stationSelect.value;
if (!selectedUrl) {
alert('除外する局を選択してください。');
return;
}

let excludedUrls = JSON.parse(localStorage.getItem(EXCLUDED_KEY)) || [];
if (!excludedUrls.includes(selectedUrl)) {
excludedUrls.push(selectedUrl);
localStorage.setItem(EXCLUDED_KEY, JSON.stringify(excludedUrls));
}

// プレイヤーをリセット
audioPlayer.pause();
audioPlayer.src = '';
stationNameDisplay.textContent = '放送局が選択されていません';
songTitleDisplay.textContent = '曲情報なし';

updateDropdown();
});

resetButton.addEventListener('click', () => {
localStorage.removeItem(EXCLUDED_KEY);
alert('除外リストをリセットしました。');
updateDropdown();
});

// メタデータ取得関数
async function updateMetadata(station) {
if (station.metaType === 'ICY' || !station.metaUrl) {
songTitleDisplay.textContent = '曲情報 (非対応)';
return;
}
try {
const proxyUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(station.metaUrl)}`;
const response = await fetch(proxyUrl);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);

const data = await response.json();
let title = '情報なし';

if (station.metaType === 'JSON') {
const metadata = JSON.parse(data.contents);
if (metadata.songtitle) title = metadata.songtitle;
else if (metadata.icestats && metadata.icestats.source) {
const source = Array.isArray(metadata.icestats.source) ? metadata.icestats.source[0] : metadata.icestats.source;
if (source && source.title) title = source.title;
}
} else if (station.metaType === '7.html') {
const bodyContent = data.contents.match(/<BODY>(.*)<\/BODY>/i);
if (bodyContent && bodyContent[1]) {
const parts = bodyContent[1].split(',');
if (parts.length > 6 && parts[6]) title = parts[6];
}
}
songTitleDisplay.textContent = title;
} catch (error) {
console.error('メタデータ取得失敗:', error);
songTitleDisplay.textContent = '曲情報 (取得失敗)';
}
}
});
</script>
</body>
</html>


使用変数

allStations
args
audioPlayer
bodyContent
charset
checkPlayability
checkResults
class
cleanup
content
controlsDiv
crossOrigin
data
display
e
excludeButton
excludedUrls
EXCLUDED_KEY
for
id
innerHTML
J1HITS&json
json
lang
loadingStatus
metadata
metadataInterval
metaType
mount
name
number
onError
onSuccess
option
parts
playableStations
playableUrls
playPauseButton
proxyUrl
r
resetButton
resolve
resolved
response
s
scale
selectedUrl
sid
songTitleDisplay
source
src
station
stationNameDisplay
stationSelect
style
testAudio
textContent
timeout
title
updateDropdown
updateMetadata -------( Function )
url
value
visibleStations
width