Browse Source

Initial commit

master
Denis Tereshkin 11 months ago
commit
33e4f8cac3
  1. 29
      app.py
  2. BIN
      static/audio/blat_vim.mp3
  3. BIN
      static/audio/buddies_and_discord.mp3
  4. BIN
      static/audio/buddies_and_multicast.mp3
  5. BIN
      static/audio/buddies_and_vim.mp3
  6. BIN
      static/audio/cannon.mp3
  7. BIN
      static/audio/closed_contour.mp3
  8. BIN
      static/audio/dev_anthem.mp3
  9. BIN
      static/audio/developer_in_vim.mp3
  10. BIN
      static/audio/digital_thug_life.mp3
  11. BIN
      static/audio/fake_licenses.mp3
  12. BIN
      static/audio/hacker_and_ltpx.mp3
  13. BIN
      static/audio/late_evening.mp3
  14. BIN
      static/audio/ltp-x.mp3
  15. BIN
      static/audio/multicast_case.mp3
  16. BIN
      static/audio/poisoned_mellon.mp3
  17. BIN
      static/audio/refactoring.mp3
  18. BIN
      static/audio/rfc3376.mp3
  19. BIN
      static/audio/shampoo.mp3
  20. BIN
      static/audio/shtiblets.mp3
  21. BIN
      static/audio/switch_and_arps.mp3
  22. BIN
      static/audio/thieves_and_hackers.mp3
  23. BIN
      static/audio/traffic.mp3
  24. BIN
      static/audio/vim_trap.mp3
  25. BIN
      static/covers/blat_vim.jpeg
  26. BIN
      static/covers/buddies_and_discord.jpeg
  27. BIN
      static/covers/buddies_and_multicast.jpeg
  28. BIN
      static/covers/buddies_and_vim.jpeg
  29. BIN
      static/covers/cannon.jpeg
  30. BIN
      static/covers/closed_contour.jpeg
  31. BIN
      static/covers/dev_anthem.jpeg
  32. BIN
      static/covers/developer_in_vim.jpeg
  33. BIN
      static/covers/digital_thug_life.jpg
  34. BIN
      static/covers/digital_thug_life.webp
  35. BIN
      static/covers/fake_licenses.jpeg
  36. BIN
      static/covers/hacker_and_ltpx.jpeg
  37. BIN
      static/covers/late_evening.jpeg
  38. BIN
      static/covers/ltp-x.jpeg
  39. BIN
      static/covers/multicast_case.jpeg
  40. BIN
      static/covers/poisoned_watermellon.jpeg
  41. BIN
      static/covers/refactoring.jpeg
  42. BIN
      static/covers/rfc3376.jpeg
  43. BIN
      static/covers/shampoo.jpg
  44. BIN
      static/covers/shtiblets.jpeg
  45. BIN
      static/covers/switch_and_arps.jpeg
  46. BIN
      static/covers/thieves_and_hackers.jpeg
  47. BIN
      static/covers/traffic.jpeg
  48. BIN
      static/covers/vim_trap.jpeg
  49. 91
      static/css/styles.css
  50. 118
      static/js/player.js
  51. 70
      templates/index.html
  52. 163
      tracks.json

29
app.py

@ -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)

BIN
static/audio/blat_vim.mp3

Binary file not shown.

BIN
static/audio/buddies_and_discord.mp3

Binary file not shown.

BIN
static/audio/buddies_and_multicast.mp3

Binary file not shown.

BIN
static/audio/buddies_and_vim.mp3

Binary file not shown.

BIN
static/audio/cannon.mp3

Binary file not shown.

BIN
static/audio/closed_contour.mp3

Binary file not shown.

BIN
static/audio/dev_anthem.mp3

Binary file not shown.

BIN
static/audio/developer_in_vim.mp3

Binary file not shown.

BIN
static/audio/digital_thug_life.mp3

Binary file not shown.

BIN
static/audio/fake_licenses.mp3

Binary file not shown.

BIN
static/audio/hacker_and_ltpx.mp3

Binary file not shown.

BIN
static/audio/late_evening.mp3

Binary file not shown.

BIN
static/audio/ltp-x.mp3

Binary file not shown.

BIN
static/audio/multicast_case.mp3

Binary file not shown.

BIN
static/audio/poisoned_mellon.mp3

Binary file not shown.

BIN
static/audio/refactoring.mp3

Binary file not shown.

BIN
static/audio/rfc3376.mp3

Binary file not shown.

BIN
static/audio/shampoo.mp3

Binary file not shown.

BIN
static/audio/shtiblets.mp3

Binary file not shown.

BIN
static/audio/switch_and_arps.mp3

Binary file not shown.

BIN
static/audio/thieves_and_hackers.mp3

Binary file not shown.

BIN
static/audio/traffic.mp3

Binary file not shown.

BIN
static/audio/vim_trap.mp3

Binary file not shown.

BIN
static/covers/blat_vim.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

BIN
static/covers/buddies_and_discord.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

BIN
static/covers/buddies_and_multicast.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

BIN
static/covers/buddies_and_vim.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

BIN
static/covers/cannon.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

BIN
static/covers/closed_contour.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

BIN
static/covers/dev_anthem.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

BIN
static/covers/developer_in_vim.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
static/covers/digital_thug_life.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
static/covers/digital_thug_life.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
static/covers/fake_licenses.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

BIN
static/covers/hacker_and_ltpx.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

BIN
static/covers/late_evening.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

BIN
static/covers/ltp-x.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

BIN
static/covers/multicast_case.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

BIN
static/covers/poisoned_watermellon.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
static/covers/refactoring.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

BIN
static/covers/rfc3376.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

BIN
static/covers/shampoo.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
static/covers/shtiblets.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
static/covers/switch_and_arps.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

BIN
static/covers/thieves_and_hackers.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

BIN
static/covers/traffic.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 KiB

BIN
static/covers/vim_trap.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

91
static/css/styles.css

@ -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;
}

118
static/js/player.js

@ -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;
});

70
templates/index.html

@ -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>

163
tracks.json

@ -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"
}
]
Loading…
Cancel
Save