@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
from flask import Flask, render_template, send_file, jsonify |
||||
import json |
||||
import os |
||||
|
||||
app = Flask(__name__) |
||||
|
||||
# Configuration |
||||
MUSIC_DIR = 'static/audio' |
||||
COVERS_DIR = 'static/covers' |
||||
|
||||
@app.route('/') |
||||
def index(): |
||||
return render_template('index.html') |
||||
|
||||
@app.route('/tracks') |
||||
def get_tracks(): |
||||
with open('tracks.json') as f: |
||||
return jsonify(json.load(f)) |
||||
|
||||
@app.route('/audio/<path:filename>') |
||||
def serve_audio(filename): |
||||
return send_file(os.path.join(MUSIC_DIR, filename)) |
||||
|
||||
@app.route('/cover/<path:filename>') |
||||
def serve_cover(filename): |
||||
return send_file(os.path.join(COVERS_DIR, filename)) |
||||
|
||||
if __name__ == '__main__': |
||||
app.run(debug=True) |
||||
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 567 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 468 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 373 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 604 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 323 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 587 KiB |
|
After Width: | Height: | Size: 221 KiB |
@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
|
||||
body { |
||||
background: linear-gradient(135deg, #1a1a1a 0%, #0a0a0a 100%); |
||||
} |
||||
|
||||
.player-card { |
||||
border-radius: 15px !important; |
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3); |
||||
position: sticky; |
||||
top: 20px; |
||||
} |
||||
|
||||
.playlist { |
||||
max-height: calc(100vh - 40px); |
||||
padding-bottom: 20px; |
||||
} |
||||
|
||||
.btn-outline-light:hover { |
||||
background-color: rgba(255,255,255,0.1); |
||||
} |
||||
|
||||
.list-group-item { |
||||
background-color: transparent !important; |
||||
color: white !important; |
||||
border-color: #333 !important; |
||||
cursor: pointer; |
||||
transition: all 0.2s; |
||||
} |
||||
|
||||
.list-group-item:hover { |
||||
background-color: rgba(255,255,255,0.05) !important; |
||||
} |
||||
|
||||
.list-group-item.active { |
||||
background-color: rgba(255,255,255,0.1) !important; |
||||
border-color: #666 !important; |
||||
} |
||||
|
||||
.sticky-column { |
||||
position: -webkit-sticky; |
||||
position: sticky; |
||||
top: 0; |
||||
height: 100vh; |
||||
overflow-y: auto; |
||||
} |
||||
|
||||
.scrollable-column { |
||||
height: 100vh; |
||||
overflow-y: auto; |
||||
} |
||||
|
||||
#progress-container { |
||||
transition: all 0.2s; |
||||
} |
||||
|
||||
#progress-container:hover { |
||||
height: 10px !important; |
||||
} |
||||
|
||||
#progress-bar { |
||||
transition: width 0.1s linear; |
||||
} |
||||
|
||||
/* Mobile responsiveness */ |
||||
@media (max-width: 768px) { |
||||
.sticky-column { |
||||
height: auto; |
||||
position: relative; |
||||
} |
||||
|
||||
.scrollable-column { |
||||
height: 50vh; |
||||
} |
||||
} |
||||
|
||||
/* Custom Scrollbar */ |
||||
::-webkit-scrollbar { |
||||
width: 8px; |
||||
} |
||||
|
||||
::-webkit-scrollbar-track { |
||||
background: #1a1a1a; |
||||
} |
||||
|
||||
::-webkit-scrollbar-thumb { |
||||
background: #333; |
||||
border-radius: 4px; |
||||
} |
||||
|
||||
::-webkit-scrollbar-thumb:hover { |
||||
background: #444; |
||||
} |
||||
@ -0,0 +1,118 @@
@@ -0,0 +1,118 @@
|
||||
let currentTrackIndex = 0; |
||||
let tracks = []; |
||||
const audio = new Audio(); |
||||
let isPlaying = false; |
||||
|
||||
// DOM Elements
|
||||
const playBtn = document.getElementById('playBtn'); |
||||
const trackList = document.getElementById('track-list'); |
||||
const currentTimeElem = document.getElementById('current-time'); |
||||
const durationElem = document.getElementById('duration-time'); |
||||
const progressContainer = document.getElementById('progress-container'); |
||||
const progressBar = document.getElementById('progress-bar'); |
||||
|
||||
// Fetch tracks and initialize
|
||||
fetch('/tracks') |
||||
.then(response => response.json()) |
||||
.then(data => { |
||||
tracks = data; |
||||
renderPlaylist(); |
||||
loadTrack(currentTrackIndex); |
||||
}); |
||||
|
||||
function renderPlaylist() { |
||||
trackList.innerHTML = tracks.map((track, index) => ` |
||||
<div class="list-group-item ${index === 0 ? 'active' : ''}" |
||||
onclick="playTrack(${index})"> |
||||
${track.title} <span class="text-muted float-end">${track.artist}</span> |
||||
<span>${track.explicit === 'true' ? '<i class="bi bi-exclamation-circle" title="Explicit language"></i>' : ''} </span> |
||||
</div> |
||||
`).join('');
|
||||
} |
||||
|
||||
function playTrack(index) { |
||||
currentTrackIndex = index; |
||||
const items = document.querySelectorAll('.list-group-item') |
||||
items.forEach(item => item.classList.remove('active')); |
||||
items[index].classList.add('active'); |
||||
items[index].scrollIntoView( { behavior: 'smooth', block: 'nearest'}) |
||||
|
||||
loadTrack(index); |
||||
audio.play(); |
||||
isPlaying = true; |
||||
updatePlayButton(); |
||||
} |
||||
|
||||
function loadTrack(index) { |
||||
const track = tracks[index]; |
||||
audio.src = `/audio/${track.file}`; |
||||
document.getElementById('cover').src = `/cover/${track.cover}`; |
||||
document.getElementById('song-title').textContent = track.title; |
||||
document.getElementById('song-artist').textContent = track.artist; |
||||
} |
||||
|
||||
function togglePlay() { |
||||
if (isPlaying) { |
||||
audio.pause(); |
||||
} else { |
||||
audio.play(); |
||||
} |
||||
isPlaying = !isPlaying; |
||||
updatePlayButton(); |
||||
} |
||||
|
||||
function updatePlayButton() { |
||||
const icon = playBtn.querySelector('i'); |
||||
icon.classList.toggle('bi-play-fill', !isPlaying); |
||||
icon.classList.toggle('bi-pause-fill', isPlaying); |
||||
} |
||||
|
||||
function nextTrack() { |
||||
currentTrackIndex = (currentTrackIndex + 1) % tracks.length; |
||||
playTrack(currentTrackIndex); |
||||
} |
||||
|
||||
function previousTrack() { |
||||
currentTrackIndex = (currentTrackIndex - 1 + tracks.length) % tracks.length; |
||||
playTrack(currentTrackIndex); |
||||
} |
||||
|
||||
// Time formatting
|
||||
function formatTime(seconds) { |
||||
const minutes = Math.floor(seconds / 60); |
||||
seconds = Math.floor(seconds % 60); |
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`; |
||||
} |
||||
|
||||
// Audio event listeners
|
||||
audio.addEventListener('timeupdate', () => { |
||||
const progressPercent = (audio.currentTime / audio.duration) * 100; |
||||
progressBar.style.width = `${progressPercent}%`; |
||||
|
||||
currentTimeElem.textContent = formatTime(audio.currentTime); |
||||
durationElem.textContent = formatTime(duration); |
||||
}); |
||||
|
||||
audio.addEventListener('loadedmetadata', () => { |
||||
durationElem.textContent = formatTime(audio.duration); |
||||
}); |
||||
|
||||
audio.addEventListener('ended', nextTrack); |
||||
audio.addEventListener('play', () => { |
||||
isPlaying = true; |
||||
updatePlayButton(); |
||||
}); |
||||
audio.addEventListener('pause', () => { |
||||
isPlaying = false; |
||||
updatePlayButton(); |
||||
}); |
||||
|
||||
progressContainer.addEventListener('click', (e) => { |
||||
if (!audio.duration) return; |
||||
|
||||
const rect = progressContainer.getBoundingClientRect(); |
||||
const x = e.clientX - rect.left; |
||||
const percentage = x / progressContainer.offsetWidth; |
||||
const seekTime = percentage * audio.duration; |
||||
audio.currentTime = seekTime; |
||||
}); |
||||
@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html> |
||||
<html lang="en"> |
||||
<head> |
||||
<meta charset="UTF-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
<title>Нейрошансон и не только</title> |
||||
<!-- Bootstrap 5 CSS --> |
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> |
||||
<!-- Bootstrap Icons --> |
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css"> |
||||
<!-- Custom CSS --> |
||||
<link rel="stylesheet" href="/static/css/styles.css"> |
||||
</head> |
||||
<body class="bg-dark text-light"> |
||||
<div class="container-fluid min-vh-100 justify-content-center"> |
||||
<div class="row g-0"> |
||||
<div class="col-md-4 p-4 sticky-column"> |
||||
<div class="player-card card bg-dark border-secondary mx-auto" style="max-width: 400px;"> |
||||
<!-- Album Art --> |
||||
<img id="cover" src="" class="card-img-top" alt="Album Cover" style="border-radius: 15px 15px 0 0;"> |
||||
|
||||
<!-- Song Info --> |
||||
<div class="card-body text-center"> |
||||
<h3 id="song-title" class="card-title mb-1">-</h3> |
||||
<p id="song-artist" class="text-muted mb-4">-</p> |
||||
|
||||
<!-- Progress Bar --> |
||||
<div class="progress bg-transparent mb-3" id="progress-container" style="height: 3px; cursor: pointer;"> |
||||
<div id="progress-bar" class="progress-bar bg-primary" role="progressbar" style="width: 0%"></div> |
||||
</div> |
||||
|
||||
<!-- Time Display --> |
||||
<div class="d-flex justify-content-between small text-muted mb-4"> |
||||
<span id="current-time">0:00</span> |
||||
<span id="duration-time">0:00</span> |
||||
</div> |
||||
|
||||
<!-- Controls --> |
||||
<div class="d-flex justify-content-center gap-3"> |
||||
<button onclick="previousTrack()" class="btn btn-outline-light btn-lg rounded-circle"> |
||||
<i class="bi bi-skip-start-fill"></i> |
||||
</button> |
||||
<button onclick="togglePlay()" id="playBtn" class="btn btn-light btn-lg rounded-circle px-4"> |
||||
<i class="bi bi-play-fill"></i> |
||||
</button> |
||||
<button onclick="nextTrack()" class="btn btn-outline-light btn-lg rounded-circle"> |
||||
<i class="bi bi-skip-end-fill"></i> |
||||
</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- Playlist --> |
||||
<div class="col-md-8 p-4 scrollable-column"> |
||||
<div class="playlist" style="max-width: 600px;"> |
||||
<h5 class="mb-3">Playlist</h5> |
||||
<div id="track-list" class="list-group bg-transparent"> |
||||
<!-- Dynamic track list --> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- Scripts --> |
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> |
||||
<script src="/static/js/player.js"></script> |
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,163 @@
@@ -0,0 +1,163 @@
|
||||
[ |
||||
{ |
||||
"file": "switch_and_arps.mp3", |
||||
"title": "Switch и Арпы", |
||||
"artist": "Suno", |
||||
"cover": "switch_and_arps.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "thieves_and_hackers.mp3", |
||||
"title": "Воры и Хакеры", |
||||
"artist": "Suno", |
||||
"cover": "thieves_and_hackers.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "buddies_and_discord.mp3", |
||||
"title": "Пацаны и Discord", |
||||
"artist": "Suno/Gemma2", |
||||
"cover": "buddies_and_discord.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "blat_vim.mp3", |
||||
"title": "Блатной Vim", |
||||
"artist": "Suno", |
||||
"cover": "blat_vim.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "buddies_and_vim.mp3", |
||||
"title": "Пацаны и Vim", |
||||
"artist": "Suno", |
||||
"cover": "buddies_and_vim.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "fake_licenses.mp3", |
||||
"title": "Палёные Лицензии", |
||||
"artist": "Suno/Gemma2", |
||||
"cover": "fake_licenses.jpeg", |
||||
"explicit": "true" |
||||
}, |
||||
{ |
||||
"file": "hacker_and_ltpx.mp3", |
||||
"title": "Хакер и LTP-X", |
||||
"artist": "Suno/Gemma2", |
||||
"cover": "hacker_and_ltpx.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "shtiblets.mp3", |
||||
"title": "Штиблеты в Лесу", |
||||
"artist": "Suno", |
||||
"cover": "shtiblets.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "digital_thug_life.mp3", |
||||
"title": "Цифровой Блатняк (Digital Thug Life)", |
||||
"artist": "Riffusion", |
||||
"cover": "digital_thug_life.jpg", |
||||
"explicit": "true" |
||||
}, |
||||
{ |
||||
"file": "buddies_and_multicast.mp3", |
||||
"title": "Кореша и Мультикаст", |
||||
"artist": "Suno", |
||||
"cover": "buddies_and_multicast.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "cannon.mp3", |
||||
"title": "Пушка Рикошет", |
||||
"artist": "Suno", |
||||
"cover": "cannon.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "closed_contour.mp3", |
||||
"title": "Закрытый Контур", |
||||
"artist": "Suno", |
||||
"cover": "closed_contour.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "dev_anthem.mp3", |
||||
"title": "Гимн Разработчика xPON", |
||||
"artist": "Suno", |
||||
"cover": "dev_anthem.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "developer_in_vim.mp3", |
||||
"title": "Программист в Ловушке", |
||||
"artist": "Suno", |
||||
"cover": "developer_in_vim.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "late_evening.mp3", |
||||
"title": "Поздний Вечер", |
||||
"artist": "Suno", |
||||
"cover": "late_evening.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "ltp-x.mp3", |
||||
"title": "LTP-X", |
||||
"artist": "Suno", |
||||
"cover": "ltp-x.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "multicast_case.mp3", |
||||
"title": "Дело о Потерянном Мультикасте", |
||||
"artist": "Suno", |
||||
"cover": "multicast_case.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "poisoned_mellon.mp3", |
||||
"title": "Отравленный Арбуз", |
||||
"artist": "Suno", |
||||
"cover": "poisoned_watermellon.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "refactoring.mp3", |
||||
"title": "Рефакторинг Боли", |
||||
"artist": "Suno", |
||||
"cover": "refactoring.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "rfc3376.mp3", |
||||
"title": "RFC3376", |
||||
"artist": "Suno", |
||||
"cover": "rfc3376.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "traffic.mp3", |
||||
"title": "Пробки и Крик", |
||||
"artist": "Suno", |
||||
"cover": "traffic.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "vim_trap.mp3", |
||||
"title": "В ловушке Vim'a", |
||||
"artist": "Suno", |
||||
"cover": "vim_trap.jpeg", |
||||
"explicit": "false" |
||||
}, |
||||
{ |
||||
"file": "shampoo.mp3", |
||||
"title": "Шампунь", |
||||
"artist": "Suno", |
||||
"cover": "shampoo.jpg", |
||||
"explicit": "false" |
||||
} |
||||
] |
||||