1
0

Neu: Spotify Stats

This commit is contained in:
Akamaru
2025-10-27 16:10:32 +01:00
parent 8836af2ef7
commit 03ed151ba6
4 changed files with 544 additions and 0 deletions

View File

@@ -185,6 +185,10 @@
<h2 class="tool-title">MyAnimeList Visualisierung</h2>
<p class="tool-description">Visualisiere deine MyAnimeList.net-Anime- und Manga-Listen als Grafiken und Statistiken.</p>
</a>
<a href="https://tools.ponywave.de/spotify_stats" class="tool-bubble">
<h2 class="tool-title">Spotify Stats</h2>
<p class="tool-description">Analysiere deine Spotify Streaming History und sehe deine Top-Tracks, Künstler und Alben.</p>
</a>
<a href="https://tools.ponywave.de/sys_info" class="tool-bubble">
<h2 class="tool-title">Systeminformationen</h2>
<p class="tool-description">Zeigt Infos zu Browser, Gerät, Betriebssystem, User-Agent und IPs an.</p>

View File

@@ -23,6 +23,7 @@ https://tools.ponywave.de/pinkie_timer
https://tools.ponywave.de/pokemon_quiz
https://tools.ponywave.de/shape_shifter
https://tools.ponywave.de/solitaire
https://tools.ponywave.de/spotify_stats
https://tools.ponywave.de/sys_info
https://tools.ponywave.de/text_cleaner
https://tools.ponywave.de/text_decoder

BIN
spotify_stats/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

539
spotify_stats/index.html Normal file
View File

@@ -0,0 +1,539 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spotify Stats - PonyWave Tools</title>
<meta property="og:title" content="Spotify Stats | PonyWave Tools">
<meta property="og:description" content="Spotify Streaming History lokal analysieren und Statistiken anzeigen">
<meta property="og:type" content="website">
<meta property="og:url" content="https://tools.ponywave.de/spotify_stats">
<meta property="og:image" content="https://tools.ponywave.de/spotify_stats/icon.png">
<link rel="icon" href="icon.png">
<!-- Umami Tracking -->
<script defer src="https://stats.ponywave.de/script" data-website-id="9ef713d2-adb9-4906-9df5-708d8a8b9131" data-tag="spotify_stats"></script>
<style>
:root {
--primary-color: #7F006E;
--secondary-color: #FF7FED;
--spotify-green: #1DB954;
--bg-gradient: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
--text-color: #e0e0e0;
--tool-bg: #2a2a2a;
--shadow-color: rgba(0, 0, 0, 0.5);
--footer-color: rgba(0, 0, 0, 0.8);
--border-color: #444;
--card-bg: #1e1e1e;
--card-header-bg: #252525;
--label-color: #888;
--button-hover: #CC65B5;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-image: var(--bg-gradient);
background-attachment: fixed;
color: var(--text-color);
min-height: 100vh;
display: flex;
flex-direction: column;
line-height: 1.6;
margin: 0;
padding: 20px 20px 70px 20px;
}
.container {
flex: 1;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
width: 100%;
}
.title {
text-align: center;
color: var(--secondary-color);
margin-bottom: 30px;
font-size: 2.5em;
text-shadow: 2px 2px 6px rgba(255, 127, 237, 0.66);
}
.upload-section {
background: var(--tool-bg);
border-radius: 15px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 4px 6px var(--shadow-color);
text-align: center;
}
.file-input-wrapper {
position: relative;
display: inline-block;
margin: 20px 0;
}
.file-input-label {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
padding: 15px 30px;
border-radius: 8px;
cursor: pointer;
font-size: 1.1em;
font-weight: 600;
transition: all 0.3s ease;
display: inline-block;
box-shadow: 0 2px 4px var(--shadow-color);
}
.file-input-label:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--shadow-color);
}
#fileInput {
display: none;
}
.file-name {
margin-top: 15px;
color: var(--secondary-color);
font-size: 1em;
font-weight: 500;
}
.stats-container {
display: none;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: var(--card-bg);
border-radius: 15px;
padding: 25px;
box-shadow: 0 4px 6px var(--shadow-color);
border: 1px solid var(--border-color);
}
.stat-card h3 {
color: var(--spotify-green);
margin-bottom: 15px;
font-size: 1.3em;
border-bottom: 2px solid var(--spotify-green);
padding-bottom: 10px;
}
.stat-value {
font-size: 2em;
font-weight: bold;
color: var(--secondary-color);
margin: 15px 0;
}
.stat-label {
color: var(--label-color);
font-size: 0.9em;
margin-bottom: 10px;
}
.top-list {
list-style: none;
padding: 0;
}
.top-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
margin-bottom: 10px;
background: var(--card-header-bg);
border-radius: 8px;
border-left: 3px solid var(--spotify-green);
}
.top-list-item .rank {
font-weight: bold;
color: var(--spotify-green);
min-width: 30px;
}
.top-list-item .name {
flex: 1;
margin: 0 15px;
word-break: break-word;
}
.top-list-item .count {
color: var(--secondary-color);
font-weight: 600;
white-space: nowrap;
}
.error-message {
background: #3d1e1e;
border: 1px solid #8b0000;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
color: #ff6b6b;
display: none;
}
.info-message {
background: rgba(127, 0, 110, 0.2);
border: 1px solid var(--primary-color);
border-radius: 8px;
padding: 15px;
margin: 15px 0;
color: var(--text-color);
}
.loading {
display: none;
text-align: center;
padding: 20px;
color: var(--spotify-green);
font-size: 1.2em;
}
.loading::after {
content: '...';
animation: dots 1.5s steps(4, end) infinite;
}
@keyframes dots {
0%, 20% { content: '.'; }
40% { content: '..'; }
60%, 100% { content: '...'; }
}
footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 15px 0;
background-color: var(--footer-color);
border-top: 1px solid var(--border-color);
text-align: center;
font-size: 14px;
color: var(--text-color);
z-index: 100;
}
footer a {
color: var(--secondary-color);
text-decoration: none;
font-weight: bold;
}
footer a:hover {
text-decoration: underline;
}
.heart {
color: #ff0000;
animation: heartbeat 1.5s infinite;
}
@keyframes heartbeat {
0% { transform: scale(1); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
@media (max-width: 768px) {
.title {
font-size: 2em;
}
.stats-grid {
grid-template-columns: 1fr;
}
.upload-section {
padding: 20px;
}
.top-list-item {
flex-direction: column;
align-items: flex-start;
gap: 5px;
}
.top-list-item .name {
margin: 5px 0;
}
}
</style>
</head>
<body>
<div class="container">
<h1 class="title">🎵 Spotify Stats</h1>
<div class="upload-section">
<h2 style="margin-bottom: 15px;">Streaming History hochladen</h2>
<p style="color: var(--label-color); margin-bottom: 10px;">
Lade deine Spotify Streaming History JSON-Datei hoch, um deine Statistiken zu sehen.
</p>
<div class="info-message">
Alle Daten werden nur lokal in deinem Browser verarbeitet. Es werden keine Daten hochgeladen.
</div>
<div class="file-input-wrapper">
<label for="fileInput" class="file-input-label">
JSON-Datei auswählen
</label>
<input type="file" id="fileInput" accept=".json" />
</div>
<div class="file-name" id="fileName"></div>
</div>
<div class="loading" id="loading">Analysiere deine Daten</div>
<div class="error-message" id="errorMessage"></div>
<div class="stats-container" id="statsContainer">
<div class="stats-grid">
<div class="stat-card">
<h3>📊 Übersicht</h3>
<div class="stat-label">Anzahl Streams</div>
<div class="stat-value" id="totalStreams">0</div>
<div class="stat-label">Gesamte Hörzeit</div>
<div class="stat-value" style="font-size: 1.5em;" id="totalTime">0h</div>
<div class="stat-label">Zeitraum</div>
<div style="color: var(--text-color); margin-top: 10px;" id="dateRange">-</div>
</div>
<div class="stat-card">
<h3>🎵 Top 10 Tracks</h3>
<ul class="top-list" id="topTracks"></ul>
</div>
<div class="stat-card">
<h3>🎤 Top 10 Künstler</h3>
<ul class="top-list" id="topArtists"></ul>
</div>
<div class="stat-card">
<h3>💿 Top 10 Alben</h3>
<ul class="top-list" id="topAlbums"></ul>
</div>
<div class="stat-card">
<h3>📻 Weitere Statistiken</h3>
<div class="stat-label">Geskippte Tracks</div>
<div class="stat-value" style="font-size: 1.5em;" id="skippedCount">0</div>
<div class="stat-label">Shuffle genutzt</div>
<div class="stat-value" style="font-size: 1.5em;" id="shuffleCount">0</div>
<div class="stat-label">Offline gehört</div>
<div class="stat-value" style="font-size: 1.5em;" id="offlineCount">0</div>
</div>
<div class="stat-card" id="podcastCard" style="display: none;">
<h3>🎙️ Top 10 Podcasts</h3>
<ul class="top-list" id="topPodcasts"></ul>
</div>
</div>
</div>
</div>
<footer>
<p>
<a href="https://tools.ponywave.de/">Zurück zur Startseite</a> |
&copy; <span id="currentYear"></span> Akamaru | Made with <span class="heart">❤️</span> by Claude
</p>
</footer>
<script>
// Jahr aktualisieren
document.getElementById('currentYear').textContent = new Date().getFullYear();
// File Input Handler
document.getElementById('fileInput').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
if (!file.name.toLowerCase().endsWith('.json')) {
showError('Bitte wähle eine JSON-Datei aus.');
return;
}
document.getElementById('fileName').textContent = `Ausgewählt: ${file.name}`;
readJSONFile(file);
}
});
function showError(message) {
const errorDiv = document.getElementById('errorMessage');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
document.getElementById('statsContainer').style.display = 'none';
document.getElementById('loading').style.display = 'none';
}
function hideError() {
document.getElementById('errorMessage').style.display = 'none';
}
function readJSONFile(file) {
document.getElementById('loading').style.display = 'block';
document.getElementById('statsContainer').style.display = 'none';
hideError();
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = JSON.parse(e.target.result);
if (!Array.isArray(data)) {
throw new Error('Die JSON-Datei muss ein Array enthalten.');
}
analyzeData(data);
document.getElementById('loading').style.display = 'none';
} catch (error) {
showError('Fehler beim Lesen der JSON-Datei: ' + error.message);
}
};
reader.onerror = function() {
showError('Fehler beim Lesen der Datei.');
};
reader.readAsText(file);
}
function analyzeData(data) {
if (data.length === 0) {
showError('Die JSON-Datei enthält keine Daten.');
return;
}
// Grundstatistiken
const totalStreams = data.length;
const totalMs = data.reduce((sum, item) => sum + (item.ms_played || 0), 0);
const totalHours = Math.round(totalMs / 1000 / 60 / 60);
const totalDays = Math.floor(totalHours / 24);
const remainingHours = totalHours % 24;
// Zeitraum
const dates = data.map(item => new Date(item.ts)).sort((a, b) => a - b);
const firstDate = dates[0].toLocaleDateString('de-DE');
const lastDate = dates[dates.length - 1].toLocaleDateString('de-DE');
// Top Tracks
const trackCounts = {};
const artistCounts = {};
const albumCounts = {};
const podcastCounts = {};
let skippedCount = 0;
let shuffleCount = 0;
let offlineCount = 0;
data.forEach(item => {
// Tracks
const trackName = item.master_metadata_track_name;
const artistName = item.master_metadata_album_artist_name;
const albumName = item.master_metadata_album_album_name;
if (trackName && artistName) {
const trackKey = `${trackName} - ${artistName}`;
trackCounts[trackKey] = (trackCounts[trackKey] || 0) + 1;
}
// Artists
if (artistName) {
artistCounts[artistName] = (artistCounts[artistName] || 0) + 1;
}
// Albums
if (albumName && artistName) {
const albumKey = `${albumName} - ${artistName}`;
albumCounts[albumKey] = (albumCounts[albumKey] || 0) + 1;
}
// Podcasts
const podcastName = item.episode_show_name;
if (podcastName) {
podcastCounts[podcastName] = (podcastCounts[podcastName] || 0) + 1;
}
// Weitere Stats
if (item.skipped) skippedCount++;
if (item.shuffle) shuffleCount++;
if (item.offline) offlineCount++;
});
// Display
document.getElementById('totalStreams').textContent = totalStreams.toLocaleString('de-DE');
let timeText = '';
if (totalDays > 0) {
timeText = `${totalDays}d ${remainingHours}h`;
} else {
timeText = `${totalHours}h`;
}
document.getElementById('totalTime').textContent = timeText;
document.getElementById('dateRange').textContent = `${firstDate} - ${lastDate}`;
// Top Lists
displayTopList('topTracks', trackCounts, 10);
displayTopList('topArtists', artistCounts, 10);
displayTopList('topAlbums', albumCounts, 10);
// Podcasts (nur anzeigen wenn vorhanden)
if (Object.keys(podcastCounts).length > 0) {
document.getElementById('podcastCard').style.display = 'block';
displayTopList('topPodcasts', podcastCounts, 10);
}
// Weitere Stats
document.getElementById('skippedCount').textContent = `${skippedCount} (${Math.round(skippedCount / totalStreams * 100)}%)`;
document.getElementById('shuffleCount').textContent = `${shuffleCount} (${Math.round(shuffleCount / totalStreams * 100)}%)`;
document.getElementById('offlineCount').textContent = `${offlineCount} (${Math.round(offlineCount / totalStreams * 100)}%)`;
// Show stats
document.getElementById('statsContainer').style.display = 'block';
}
function displayTopList(elementId, counts, limit) {
const sorted = Object.entries(counts)
.sort((a, b) => b[1] - a[1])
.slice(0, limit);
const listElement = document.getElementById(elementId);
listElement.innerHTML = '';
if (sorted.length === 0) {
listElement.innerHTML = '<li style="padding: 12px; color: var(--label-color);">Keine Daten vorhanden</li>';
return;
}
sorted.forEach(([name, count], index) => {
const li = document.createElement('li');
li.className = 'top-list-item';
li.innerHTML = `
<span class="rank">${index + 1}.</span>
<span class="name">${escapeHtml(name)}</span>
<span class="count">${count}×</span>
`;
listElement.appendChild(li);
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>