Starchik1 commited on
Commit
6bab515
·
verified ·
1 Parent(s): 260a9fd

Upload 7 files

Browse files
app.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, send_file, jsonify, request, redirect, url_for, flash, abort
2
+ from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
3
+ from flask_sqlalchemy import SQLAlchemy
4
+ from werkzeug.security import generate_password_hash, check_password_hash
5
+ from werkzeug.utils import secure_filename
6
+ from pathlib import Path
7
+ import os
8
+
9
+ app = Flask(__name__)
10
+ app.config['SECRET_KEY'] = 'your-secret-key'
11
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
12
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
13
+
14
+ # Папка для хранения музыкальных файлов
15
+ MUSIC_FOLDER = Path('static/music')
16
+ app.config['MUSIC_FOLDER'] = MUSIC_FOLDER
17
+
18
+ # Инициализация базы данных и менеджера авторизации
19
+ db = SQLAlchemy(app)
20
+ login_manager = LoginManager(app)
21
+ login_manager.login_view = 'login'
22
+
23
+ # Модель пользователя
24
+ class User(UserMixin, db.Model):
25
+ id = db.Column(db.Integer, primary_key=True)
26
+ username = db.Column(db.String(80), unique=True, nullable=False)
27
+ password_hash = db.Column(db.String(120), nullable=False)
28
+ favorite_tracks = db.relationship('FavoriteTrack', backref='user', lazy=True)
29
+
30
+ # Модель избранных треков
31
+ class FavoriteTrack(db.Model):
32
+ id = db.Column(db.Integer, primary_key=True)
33
+ track_name = db.Column(db.String(200), nullable=False)
34
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
35
+ created_at = db.Column(db.DateTime, default=db.func.current_timestamp())
36
+
37
+ @login_manager.user_loader
38
+ def load_user(user_id):
39
+ return User.query.get(int(user_id))
40
+
41
+ # Создаем папку для музыки, если её нет
42
+ if not os.path.exists(MUSIC_FOLDER):
43
+ os.makedirs(MUSIC_FOLDER)
44
+
45
+ @app.route('/register', methods=['GET', 'POST'])
46
+ def register():
47
+ if request.method == 'POST':
48
+ username = request.form.get('username')
49
+ password = request.form.get('password')
50
+
51
+ if User.query.filter_by(username=username).first():
52
+ flash('Пользователь с таким именем уже существует')
53
+ return redirect(url_for('register'))
54
+
55
+ user = User(username=username, password_hash=generate_password_hash(password))
56
+ db.session.add(user)
57
+ db.session.commit()
58
+
59
+ return redirect(url_for('login'))
60
+ return render_template('register.html')
61
+
62
+ @app.route('/login', methods=['GET', 'POST'])
63
+ def login():
64
+ if request.method == 'POST':
65
+ username = request.form.get('username')
66
+ password = request.form.get('password')
67
+ user = User.query.filter_by(username=username).first()
68
+
69
+ if user and check_password_hash(user.password_hash, password):
70
+ login_user(user)
71
+ return redirect(url_for('index'))
72
+ flash('Неверное имя пользователя или пароль')
73
+ return render_template('login.html')
74
+
75
+ @app.route('/logout')
76
+ @login_required
77
+ def logout():
78
+ logout_user()
79
+ return redirect(url_for('index'))
80
+
81
+ @app.route('/')
82
+ def index():
83
+ music_files = [f for f in os.listdir(MUSIC_FOLDER) if f.endswith(('.mp3', '.wav', '.m4a', '.webm'))]
84
+ favorites = []
85
+ if current_user.is_authenticated:
86
+ # Получаем избранные треки, отсортированные по дате добавления (новые сверху)
87
+ favorite_tracks = FavoriteTrack.query.filter_by(user_id=current_user.id).order_by(FavoriteTrack.created_at.desc()).all()
88
+ favorites = [track.track_name for track in favorite_tracks]
89
+ return render_template('index.html', music_files=music_files, favorites=favorites)
90
+
91
+ @app.route('/play/<filename>')
92
+ def play_music(filename):
93
+ try:
94
+ return send_file(MUSIC_FOLDER / filename)
95
+ except Exception as e:
96
+ return str(e)
97
+
98
+ @app.route('/tracks')
99
+ def get_tracks():
100
+ music_files = [f for f in os.listdir(MUSIC_FOLDER) if f.endswith(('.mp3', '.wav', '.m4a', '.webm'))]
101
+ return jsonify(music_files)
102
+
103
+ @app.route('/favorite/<filename>', methods=['POST'])
104
+ @login_required
105
+ def toggle_favorite(filename):
106
+ track = FavoriteTrack.query.filter_by(user_id=current_user.id, track_name=filename).first()
107
+ if track:
108
+ db.session.delete(track)
109
+ db.session.commit()
110
+ return jsonify({'status': 'removed'})
111
+ else:
112
+ # Добавляем трек в избранное с текущим временем
113
+ from datetime import datetime
114
+ new_favorite = FavoriteTrack(track_name=filename, user_id=current_user.id, created_at=datetime.now())
115
+ db.session.add(new_favorite)
116
+ db.session.commit()
117
+ return jsonify({'status': 'added'})
118
+
119
+ @app.route('/favorites')
120
+ @login_required
121
+ def get_favorites():
122
+ # Получаем избранные треки, отсортированные по дате добавления (новые сверху)
123
+ favorite_tracks = FavoriteTrack.query.filter_by(user_id=current_user.id).order_by(FavoriteTrack.created_at.desc()).all()
124
+ favorites = [track.track_name for track in favorite_tracks]
125
+ return jsonify(favorites)
126
+
127
+ @app.route('/upload', methods=['POST'])
128
+ @login_required
129
+ def upload_music():
130
+ if 'file' not in request.files:
131
+ return jsonify({'status': 'error', 'message': 'Файл не найден'}), 400
132
+
133
+ file = request.files['file']
134
+ if file.filename == '':
135
+ return jsonify({'status': 'error', 'message': 'Файл не выбран'}), 400
136
+
137
+ if file and file.filename.endswith(('.mp3', '.wav', '.m4a', '.webm')):
138
+ filename = secure_filename(file.filename)
139
+ # Проверяем, существует ли файл с таким именем
140
+ if os.path.exists(os.path.join(MUSIC_FOLDER, filename)):
141
+ # Добавляем временную метку к имени файла, чтобы избежать перезаписи
142
+ from datetime import datetime
143
+ timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
144
+ name, ext = os.path.splitext(filename)
145
+ filename = f"{name}_{timestamp}{ext}"
146
+
147
+ file.save(os.path.join(MUSIC_FOLDER, filename))
148
+ return jsonify({'status': 'success', 'filename': filename})
149
+ else:
150
+ return jsonify({'status': 'error', 'message': 'Недопустимый формат файла'}), 400
151
+
152
+ if __name__ == '__main__':
153
+ with app.app_context():
154
+ db.create_all()
155
+ app.run(host='0.0.0.0', port=5000, debug=True)
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ flask==2.2.5
2
+ flask-sqlalchemy==2.5.1
3
+ flask-login==0.6.2
4
+ pathlib==1.0.1
5
+ werkzeug==2.3.7
6
+ flask-session==0.8.0
7
+ sqlalchemy==1.4.46
static/css/style.css ADDED
@@ -0,0 +1,713 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
9
+ background-color: #121212;
10
+ min-height: 100vh;
11
+ display: flex;
12
+ justify-content: center;
13
+ align-items: center;
14
+ color: #fff;
15
+ }
16
+
17
+ .player-container {
18
+ max-width: 1200px;
19
+ width: 100%;
20
+ margin: 0 auto;
21
+ padding: 24px;
22
+ background-color: #181818;
23
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
24
+ border-radius: 8px;
25
+ }
26
+
27
+ .header {
28
+ display: flex;
29
+ justify-content: space-between;
30
+ align-items: center;
31
+ margin-bottom: 24px;
32
+ padding-bottom: 16px;
33
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
34
+ gap: 24px;
35
+ }
36
+
37
+ .search-container {
38
+ position: relative;
39
+ flex: 1;
40
+ max-width: 400px;
41
+ }
42
+
43
+ .search-container input {
44
+ width: 100%;
45
+ padding: 12px 40px 12px 16px;
46
+ background-color: #282828;
47
+ border: 1px solid rgba(255, 255, 255, 0.1);
48
+ border-radius: 500px;
49
+ color: #fff;
50
+ font-size: 0.9rem;
51
+ transition: all 0.3s;
52
+ }
53
+
54
+ .search-container input:focus {
55
+ outline: none;
56
+ border-color: #1DB954;
57
+ box-shadow: 0 0 0 2px rgba(29, 185, 84, 0.3);
58
+ }
59
+
60
+ .search-container .search-icon {
61
+ position: absolute;
62
+ right: 16px;
63
+ top: 50%;
64
+ transform: translateY(-50%);
65
+ color: #b3b3b3;
66
+ pointer-events: none;
67
+ }
68
+
69
+ .auth-buttons {
70
+ display: flex;
71
+ gap: 16px;
72
+ align-items: center;
73
+ }
74
+
75
+ .auth-link {
76
+ text-decoration: none;
77
+ color: #fff;
78
+ padding: 8px 32px;
79
+ border-radius: 500px;
80
+ font-weight: 700;
81
+ letter-spacing: 0.1em;
82
+ text-transform: uppercase;
83
+ font-size: 0.8rem;
84
+ transition: all 0.3s;
85
+ background-color: transparent;
86
+ border: 1px solid rgba(255, 255, 255, 0.3);
87
+ }
88
+
89
+ .auth-link:hover {
90
+ border-color: #fff;
91
+ transform: scale(1.04);
92
+ }
93
+
94
+ .username {
95
+ margin-right: 16px;
96
+ color: #fff;
97
+ font-weight: 500;
98
+ }
99
+
100
+ .auth-container {
101
+ max-width: 450px;
102
+ margin: 0 auto;
103
+ padding: 40px;
104
+ background-color: #181818;
105
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
106
+ border-radius: 8px;
107
+ }
108
+
109
+ .auth-form {
110
+ display: flex;
111
+ flex-direction: column;
112
+ gap: 24px;
113
+ }
114
+
115
+ .form-group {
116
+ display: flex;
117
+ flex-direction: column;
118
+ gap: 8px;
119
+ }
120
+
121
+ .form-group label {
122
+ color: #b3b3b3;
123
+ font-size: 0.875rem;
124
+ font-weight: 500;
125
+ }
126
+
127
+ .form-group input {
128
+ padding: 12px;
129
+ border: 1px solid rgba(255, 255, 255, 0.1);
130
+ border-radius: 4px;
131
+ background-color: #282828;
132
+ color: #fff;
133
+ font-size: 1rem;
134
+ transition: all 0.3s;
135
+ }
136
+
137
+ .form-group input:focus {
138
+ outline: none;
139
+ border-color: #1DB954;
140
+ box-shadow: 0 0 0 2px rgba(29, 185, 84, 0.3);
141
+ }
142
+
143
+ .auth-btn {
144
+ padding: 14px;
145
+ background-color: #1DB954;
146
+ color: #fff;
147
+ border: none;
148
+ border-radius: 500px;
149
+ cursor: pointer;
150
+ font-weight: 700;
151
+ font-size: 1rem;
152
+ letter-spacing: 0.1em;
153
+ text-transform: uppercase;
154
+ transition: all 0.3s;
155
+ }
156
+
157
+ .auth-btn:hover {
158
+ background-color: #1ed760;
159
+ transform: scale(1.04);
160
+ }
161
+
162
+ .alert {
163
+ padding: 12px;
164
+ margin-bottom: 20px;
165
+ background-color: rgba(255, 107, 107, 0.1);
166
+ color: #ff6b6b;
167
+ border: 1px solid rgba(255, 107, 107, 0.2);
168
+ border-radius: 4px;
169
+ }
170
+
171
+ .track-item {
172
+ display: flex;
173
+ justify-content: space-between;
174
+ align-items: center;
175
+ padding: 12px 16px;
176
+ margin: 4px 0;
177
+ background-color: transparent;
178
+ color: #b3b3b3;
179
+ border-radius: 4px;
180
+ cursor: pointer;
181
+ transition: all 0.3s;
182
+ }
183
+
184
+ .track-item:hover {
185
+ background-color: rgba(255, 255, 255, 0.1);
186
+ color: #fff;
187
+ }
188
+
189
+ .track-item.active {
190
+ background-color: rgba(29, 185, 84, 0.2);
191
+ color: #fff;
192
+ }
193
+
194
+ .favorite-btn {
195
+ background: none;
196
+ border: none;
197
+ color: #b3b3b3;
198
+ cursor: pointer;
199
+ padding: 8px;
200
+ transition: all 0.3s;
201
+ }
202
+
203
+ .favorite-btn:hover {
204
+ color: #1DB954;
205
+ transform: scale(1.2);
206
+ }
207
+
208
+ .favorite-btn.active {
209
+ color: #1DB954;
210
+ }
211
+
212
+ #shuffle-btn.active {
213
+ color: #1DB954;
214
+ }
215
+
216
+ h1 {
217
+ text-align: center;
218
+ margin-bottom: 32px;
219
+ font-size: 2rem;
220
+ font-weight: 700;
221
+ background: linear-gradient(to right, #1DB954, #1ed760);
222
+ -webkit-background-clip: text;
223
+ -webkit-text-fill-color: transparent;
224
+ }
225
+
226
+ .playlist-tabs {
227
+ display: flex;
228
+ gap: 16px;
229
+ margin-bottom: 24px;
230
+ }
231
+
232
+ .tab-btn {
233
+ padding: 12px 24px;
234
+ background: transparent;
235
+ border: 1px solid rgba(255, 255, 255, 0.1);
236
+ border-radius: 500px;
237
+ color: #b3b3b3;
238
+ cursor: pointer;
239
+ font-weight: 600;
240
+ transition: all 0.3s;
241
+ }
242
+
243
+ .tab-btn:hover {
244
+ color: #fff;
245
+ border-color: rgba(255, 255, 255, 0.3);
246
+ }
247
+
248
+ .tab-btn.active {
249
+ background: #1DB954;
250
+ color: #fff;
251
+ border-color: #1DB954;
252
+ }
253
+
254
+ .tab-content {
255
+ display: none;
256
+ }
257
+
258
+ .tab-content.active {
259
+ display: block;
260
+ animation: fadeIn 0.3s ease-in-out;
261
+ }
262
+
263
+ /* Стили для загрузки файлов */
264
+ #upload-container {
265
+ padding: 20px 0;
266
+ }
267
+
268
+ .upload-area {
269
+ border: 2px dashed rgba(255, 255, 255, 0.2);
270
+ border-radius: 8px;
271
+ padding: 40px 20px;
272
+ text-align: center;
273
+ transition: all 0.3s ease;
274
+ background-color: rgba(255, 255, 255, 0.05);
275
+ cursor: pointer;
276
+ }
277
+
278
+ .upload-area.highlight {
279
+ border-color: #3498db;
280
+ background-color: rgba(52, 152, 219, 0.1);
281
+ }
282
+
283
+ .upload-area:hover, .upload-area.dragover {
284
+ border-color: #1DB954;
285
+ background-color: rgba(29, 185, 84, 0.1);
286
+ }
287
+
288
+ .upload-icon {
289
+ font-size: 48px;
290
+ color: rgba(255, 255, 255, 0.6);
291
+ margin-bottom: 16px;
292
+ }
293
+
294
+ .upload-text {
295
+ margin-bottom: 16px;
296
+ color: rgba(255, 255, 255, 0.8);
297
+ }
298
+
299
+ .upload-button {
300
+ display: inline-block;
301
+ padding: 10px 24px;
302
+ background-color: #1DB954;
303
+ color: white;
304
+ border-radius: 500px;
305
+ font-weight: 700;
306
+ cursor: pointer;
307
+ transition: all 0.3s;
308
+ margin-bottom: 16px;
309
+ }
310
+
311
+ .upload-button:hover {
312
+ transform: scale(1.05);
313
+ background-color: #1ed760;
314
+ }
315
+
316
+ .upload-info {
317
+ font-size: 0.8rem;
318
+ color: rgba(255, 255, 255, 0.5);
319
+ }
320
+
321
+ .upload-progress-container {
322
+ margin-top: 24px;
323
+ }
324
+
325
+ .upload-list {
326
+ display: flex;
327
+ flex-direction: column;
328
+ gap: 12px;
329
+ }
330
+
331
+ .upload-item {
332
+ background-color: rgba(255, 255, 255, 0.05);
333
+ border-radius: 4px;
334
+ padding: 12px 16px;
335
+ display: flex;
336
+ align-items: center;
337
+ justify-content: space-between;
338
+ }
339
+
340
+ .upload-item-name {
341
+ flex: 1;
342
+ white-space: nowrap;
343
+ overflow: hidden;
344
+ text-overflow: ellipsis;
345
+ margin-right: 16px;
346
+ }
347
+
348
+ .upload-item-progress {
349
+ width: 150px;
350
+ height: 6px;
351
+ background-color: rgba(255, 255, 255, 0.1);
352
+ border-radius: 3px;
353
+ overflow: hidden;
354
+ margin-right: 16px;
355
+ }
356
+
357
+ .upload-item-progress-bar {
358
+ height: 100%;
359
+ background-color: #1DB954;
360
+ width: 0%;
361
+ transition: width 0.3s ease;
362
+ }
363
+
364
+ .upload-item-status {
365
+ font-size: 0.8rem;
366
+ color: rgba(255, 255, 255, 0.7);
367
+ width: 80px;
368
+ text-align: right;
369
+ }
370
+
371
+ .upload-item-status.success {
372
+ color: #1DB954;
373
+ }
374
+
375
+ .upload-item-status.error {
376
+ color: #ff5252;
377
+ }
378
+
379
+ @keyframes fadeIn {
380
+ from { opacity: 0; }
381
+ to { opacity: 1; }
382
+ }
383
+
384
+ .current-track {
385
+ margin-bottom: 32px;
386
+ padding: 24px;
387
+ background-color: rgba(255, 255, 255, 0.03);
388
+ border-radius: 8px;
389
+ }
390
+
391
+ .track-info {
392
+ text-align: center;
393
+ margin-bottom: 24px;
394
+ }
395
+
396
+ .progress-container {
397
+ margin-bottom: 16px;
398
+ }
399
+
400
+ .progress-bar {
401
+ background: rgba(255, 255, 255, 0.1);
402
+ height: 4px;
403
+ border-radius: 2px;
404
+ cursor: pointer;
405
+ position: relative;
406
+ overflow: hidden;
407
+ }
408
+
409
+ .progress {
410
+ background: #1DB954;
411
+ width: 0;
412
+ height: 100%;
413
+ border-radius: 2px;
414
+ transition: width 0.1s linear;
415
+ }
416
+
417
+ .time-info {
418
+ display: flex;
419
+ justify-content: space-between;
420
+ font-size: 0.8rem;
421
+ color: #b3b3b3;
422
+ margin-top: 8px;
423
+ }
424
+
425
+ .controls {
426
+ display: flex;
427
+ justify-content: center;
428
+ align-items: center;
429
+ gap: 24px;
430
+ margin-bottom: 32px;
431
+ }
432
+
433
+ .control-btn {
434
+ background: none;
435
+ border: none;
436
+ color: #b3b3b3;
437
+ cursor: pointer;
438
+ font-size: 1.5rem;
439
+ width: 40px;
440
+ height: 40px;
441
+ border-radius: 50%;
442
+ display: flex;
443
+ align-items: center;
444
+ justify-content: center;
445
+ transition: all 0.3s;
446
+ }
447
+
448
+ .control-btn:hover {
449
+ color: #fff;
450
+ transform: scale(1.1);
451
+ }
452
+
453
+ .control-btn.play-pause {
454
+ background-color: #1DB954;
455
+ color: #fff;
456
+ width: 56px;
457
+ height: 56px;
458
+ font-size: 2rem;
459
+ }
460
+
461
+ .control-btn.play-pause:hover {
462
+ background-color: #1ed760;
463
+ transform: scale(1.08);
464
+ }
465
+
466
+ .volume-control {
467
+ display: flex;
468
+ align-items: center;
469
+ gap: 12px;
470
+ }
471
+
472
+ #volume {
473
+ width: 100px;
474
+ cursor: pointer;
475
+ -webkit-appearance: none;
476
+ background: rgba(255, 255, 255, 0.1);
477
+ height: 4px;
478
+ border-radius: 2px;
479
+ }
480
+
481
+ #volume::-webkit-slider-thumb {
482
+ -webkit-appearance: none;
483
+ width: 12px;
484
+ height: 12px;
485
+ border-radius: 50%;
486
+ background: #fff;
487
+ cursor: pointer;
488
+ transition: all 0.3s;
489
+ }
490
+
491
+ #volume::-webkit-slider-thumb:hover {
492
+ background: #1DB954;
493
+ transform: scale(1.2);
494
+ }
495
+
496
+ .playlist {
497
+ background-color: rgba(255, 255, 255, 0.03);
498
+ border-radius: 8px;
499
+ padding: 24px;
500
+ }
501
+
502
+ .playlist-tabs {
503
+ display: flex;
504
+ gap: 16px;
505
+ margin-bottom: 24px;
506
+ }
507
+
508
+ .tab-btn {
509
+ background: none;
510
+ border: none;
511
+ color: #b3b3b3;
512
+ cursor: pointer;
513
+ font-size: 1rem;
514
+ font-weight: 700;
515
+ padding: 8px 16px;
516
+ border-radius: 500px;
517
+ transition: all 0.3s;
518
+ }
519
+
520
+ .tab-btn:hover {
521
+ color: #fff;
522
+ }
523
+
524
+ .tab-btn.active {
525
+ background-color: #1DB954;
526
+ color: #fff;
527
+ }
528
+
529
+ .tab-content {
530
+ display: none;
531
+ max-height: 400px;
532
+ overflow-y: auto;
533
+ padding-right: 8px;
534
+ }
535
+
536
+ .tab-content.active {
537
+ display: block;
538
+ }
539
+
540
+ .tab-content::-webkit-scrollbar {
541
+ width: 8px;
542
+ }
543
+
544
+ .tab-content::-webkit-scrollbar-track {
545
+ background: rgba(255, 255, 255, 0.1);
546
+ border-radius: 4px;
547
+ }
548
+
549
+ .tab-content::-webkit-scrollbar-thumb {
550
+ background: rgba(255, 255, 255, 0.3);
551
+ border-radius: 4px;
552
+ }
553
+
554
+ .tab-content::-webkit-scrollbar-thumb:hover {
555
+ background: rgba(255, 255, 255, 0.4);
556
+ }
557
+
558
+ .playlist h2 {
559
+ margin-bottom: 16px;
560
+ font-size: 1.5rem;
561
+ font-weight: 700;
562
+ color: #fff;
563
+ }
564
+
565
+ #track-list {
566
+ list-style: none;
567
+ max-height: 400px;
568
+ overflow-y: auto;
569
+ padding-right: 8px;
570
+ }
571
+
572
+ #track-list::-webkit-scrollbar {
573
+ width: 8px;
574
+ }
575
+
576
+ #track-list::-webkit-scrollbar-track {
577
+ background: rgba(255, 255, 255, 0.1);
578
+ border-radius: 4px;
579
+ }
580
+
581
+ #track-list::-webkit-scrollbar-thumb {
582
+ background: rgba(255, 255, 255, 0.3);
583
+ border-radius: 4px;
584
+ }
585
+
586
+ #track-list::-webkit-scrollbar-thumb:hover {
587
+ background: rgba(255, 255, 255, 0.4);
588
+ }
589
+
590
+ /* Медиа-запросы для адаптивного дизайна */
591
+ @media screen and (max-width: 768px) {
592
+ .player-container {
593
+ padding: 16px;
594
+ max-width: 100%;
595
+ border-radius: 0;
596
+ }
597
+
598
+ .header {
599
+ flex-direction: column;
600
+ gap: 16px;
601
+ align-items: stretch;
602
+ }
603
+
604
+ .search-container {
605
+ max-width: 100%;
606
+ }
607
+
608
+ .auth-buttons {
609
+ justify-content: center;
610
+ }
611
+
612
+ .auth-link {
613
+ padding: 8px 16px;
614
+ font-size: 0.7rem;
615
+ }
616
+
617
+ .controls {
618
+ gap: 16px;
619
+ }
620
+
621
+ .control-btn {
622
+ width: 36px;
623
+ height: 36px;
624
+ font-size: 1.2rem;
625
+ }
626
+
627
+ .control-btn.play-pause {
628
+ width: 48px;
629
+ height: 48px;
630
+ font-size: 1.5rem;
631
+ }
632
+
633
+ .playlist-tabs {
634
+ flex-wrap: wrap;
635
+ }
636
+
637
+ .tab-btn {
638
+ flex: 1;
639
+ text-align: center;
640
+ padding: 8px 12px;
641
+ }
642
+
643
+ .tab-content {
644
+ max-height: 300px;
645
+ }
646
+ }
647
+
648
+ @media screen and (max-width: 480px) {
649
+ h1 {
650
+ font-size: 1.5rem;
651
+ margin-bottom: 16px;
652
+ }
653
+
654
+ .player-container {
655
+ padding: 12px;
656
+ }
657
+
658
+ .current-track {
659
+ padding: 16px;
660
+ margin-bottom: 16px;
661
+ }
662
+
663
+ .controls {
664
+ gap: 12px;
665
+ }
666
+
667
+ .control-btn {
668
+ width: 32px;
669
+ height: 32px;
670
+ font-size: 1rem;
671
+ }
672
+
673
+ .control-btn.play-pause {
674
+ width: 42px;
675
+ height: 42px;
676
+ font-size: 1.3rem;
677
+ }
678
+
679
+ .volume-control {
680
+ display: flex;
681
+ flex-direction: column;
682
+ align-items: center;
683
+ gap: 8px;
684
+ }
685
+
686
+ #volume {
687
+ width: 80px;
688
+ }
689
+
690
+ .track-item {
691
+ padding: 10px 12px;
692
+ font-size: 0.9rem;
693
+ }
694
+
695
+ .auth-container {
696
+ padding: 20px;
697
+ max-width: 100%;
698
+ }
699
+
700
+ .auth-form {
701
+ gap: 16px;
702
+ }
703
+
704
+ .form-group input {
705
+ padding: 10px;
706
+ font-size: 0.9rem;
707
+ }
708
+
709
+ .auth-btn {
710
+ padding: 12px;
711
+ font-size: 0.9rem;
712
+ }
713
+ }
static/js/player.js ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class MusicPlayer {
2
+ constructor() {
3
+ this.audio = new Audio();
4
+ this.currentTrackIndex = 0;
5
+ this.isPlaying = false;
6
+ this.isShuffled = false;
7
+ this.shuffledIndexes = [];
8
+
9
+ // Элементы управления
10
+ this.playBtn = document.getElementById('play-btn');
11
+ this.prevBtn = document.getElementById('prev-btn');
12
+ this.nextBtn = document.getElementById('next-btn');
13
+ this.shuffleBtn = document.getElementById('shuffle-btn');
14
+ this.volumeControl = document.getElementById('volume');
15
+ this.progressBar = document.getElementById('progress-bar');
16
+ this.progress = document.getElementById('progress');
17
+ this.currentTimeSpan = document.getElementById('current-time');
18
+ this.durationSpan = document.getElementById('duration');
19
+ this.currentTrackName = document.getElementById('current-track-name');
20
+ this.trackList = document.getElementById('track-list');
21
+ this.likedList = document.getElementById('liked-list');
22
+ this.tabButtons = document.querySelectorAll('.tab-btn');
23
+ this.searchInput = document.getElementById('search-input');
24
+ this.dropArea = document.getElementById('drop-area');
25
+ this.fileInput = document.getElementById('file-input');
26
+
27
+ this.tracks = Array.from(this.trackList.getElementsByClassName('track-item'));
28
+ this.allTracks = [...this.tracks]; // Сохраняем копию всех треков
29
+ this.favoriteButtons = document.querySelectorAll('.favorite-btn');
30
+
31
+ this.initEventListeners();
32
+ }
33
+
34
+ initEventListeners() {
35
+ // Поиск треков
36
+ this.searchInput.addEventListener('input', (e) => this.searchTracks(e.target.value));
37
+
38
+ // Кнопки управления
39
+ this.playBtn.addEventListener('click', () => this.togglePlay());
40
+ this.prevBtn.addEventListener('click', () => this.playPrevious());
41
+ this.nextBtn.addEventListener('click', () => this.playNext());
42
+ this.shuffleBtn.addEventListener('click', () => this.toggleShuffle());
43
+
44
+ // Обработчики для drag-and-drop загрузки файлов
45
+ if (this.dropArea && this.fileInput) {
46
+ // Предотвращаем стандартное поведение браузера при перетаскивании файлов
47
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
48
+ this.dropArea.addEventListener(eventName, (e) => {
49
+ e.preventDefault();
50
+ e.stopPropagation();
51
+ });
52
+ });
53
+
54
+ // Добавляем визуальные эффекты при перетаскивании
55
+ ['dragenter', 'dragover'].forEach(eventName => {
56
+ this.dropArea.addEventListener(eventName, () => {
57
+ this.dropArea.classList.add('highlight');
58
+ });
59
+ });
60
+
61
+ ['dragleave', 'drop'].forEach(eventName => {
62
+ this.dropArea.addEventListener(eventName, () => {
63
+ this.dropArea.classList.remove('highlight');
64
+ });
65
+ });
66
+
67
+ // Обработка события drop для загрузки файлов
68
+ this.dropArea.addEventListener('drop', (e) => {
69
+ const files = e.dataTransfer.files;
70
+ if (files.length) {
71
+ this.uploadFiles(files);
72
+ }
73
+ });
74
+
75
+ // Обработка выбора файлов через кнопку
76
+ this.fileInput.addEventListener('change', (e) => {
77
+ const files = e.target.files;
78
+ if (files.length) {
79
+ this.uploadFiles(files);
80
+ }
81
+ });
82
+ }
83
+
84
+ // Обработчики для вкладок
85
+ this.tabButtons.forEach(button => {
86
+ button.addEventListener('click', () => {
87
+ // Удаляем класс active у всех кнопок и контента
88
+ this.tabButtons.forEach(btn => btn.classList.remove('active'));
89
+ document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
90
+
91
+ // Добавляем класс active текущей кнопке и соответствующему контенту
92
+ button.classList.add('active');
93
+ const tabName = button.dataset.tab;
94
+ const content = document.querySelector(`.tab-content[data-tab="${tabName}"]`);
95
+ if (content) {
96
+ content.classList.add('active');
97
+ // Обновляем список треков
98
+ this.tracks = Array.from(content.getElementsByClassName('track-item'));
99
+ // Переназначаем обработчики событий для треков
100
+ this.tracks.forEach((track, index) => {
101
+ track.addEventListener('click', (e) => {
102
+ if (!e.target.closest('.favorite-btn')) {
103
+ this.currentTrackIndex = index;
104
+ this.loadAndPlayTrack();
105
+ }
106
+ });
107
+ });
108
+ }
109
+ });
110
+ });
111
+
112
+ // Управление громкостью
113
+ this.volumeControl.addEventListener('input', (e) => {
114
+ this.audio.volume = e.target.value;
115
+ });
116
+
117
+ // Прогресс воспроизведения
118
+ this.progressBar.addEventListener('click', (e) => {
119
+ const rect = this.progressBar.getBoundingClientRect();
120
+ const percent = (e.clientX - rect.left) / rect.width;
121
+ this.audio.currentTime = percent * this.audio.duration;
122
+ });
123
+
124
+ // События аудио
125
+ this.audio.addEventListener('timeupdate', () => this.updateProgress());
126
+ this.audio.addEventListener('ended', () => this.playNext());
127
+
128
+ // Клики по трекам
129
+ this.tracks.forEach((track, index) => {
130
+ track.addEventListener('click', (e) => {
131
+ if (!e.target.closest('.favorite-btn')) {
132
+ this.currentTrackIndex = index;
133
+ this.loadAndPlayTrack();
134
+ }
135
+ });
136
+ });
137
+
138
+ // Обработчики для кнопок избранного
139
+ this.favoriteButtons.forEach(btn => {
140
+ btn.addEventListener('click', async (e) => {
141
+ e.preventDefault();
142
+ const track = btn.dataset.track;
143
+ const response = await fetch(`/favorite/${track}`, {
144
+ method: 'POST',
145
+ headers: {
146
+ 'Content-Type': 'application/json'
147
+ }
148
+ });
149
+
150
+ const result = await response.json();
151
+ if (result.status === 'added') {
152
+ btn.classList.add('active');
153
+ } else {
154
+ btn.classList.remove('active');
155
+ }
156
+ });
157
+ });
158
+ }
159
+
160
+ togglePlay() {
161
+ if (this.isPlaying) {
162
+ this.pause();
163
+ } else {
164
+ if (this.audio.src) {
165
+ this.play();
166
+ } else if (this.tracks.length > 0) {
167
+ this.loadAndPlayTrack();
168
+ }
169
+ }
170
+ }
171
+
172
+ play() {
173
+ this.audio.play();
174
+ this.isPlaying = true;
175
+ this.playBtn.innerHTML = '<i class="fas fa-pause"></i>';
176
+ this.tracks[this.currentTrackIndex].classList.add('active');
177
+ }
178
+
179
+ pause() {
180
+ this.audio.pause();
181
+ this.isPlaying = false;
182
+ this.playBtn.innerHTML = '<i class="fas fa-play"></i>';
183
+ }
184
+
185
+ playPrevious() {
186
+ if (this.tracks.length === 0) return;
187
+ this.tracks[this.currentTrackIndex].classList.remove('active');
188
+
189
+ if (this.isShuffled) {
190
+ const currentShuffleIndex = this.shuffledIndexes.indexOf(this.currentTrackIndex);
191
+ const prevShuffleIndex = (currentShuffleIndex - 1 + this.tracks.length) % this.tracks.length;
192
+ this.currentTrackIndex = this.shuffledIndexes[prevShuffleIndex];
193
+ } else {
194
+ this.currentTrackIndex = (this.currentTrackIndex - 1 + this.tracks.length) % this.tracks.length;
195
+ }
196
+
197
+ this.loadAndPlayTrack();
198
+ }
199
+
200
+ playNext() {
201
+ if (this.tracks.length === 0) return;
202
+ this.tracks[this.currentTrackIndex].classList.remove('active');
203
+
204
+ if (this.isShuffled) {
205
+ const currentShuffleIndex = this.shuffledIndexes.indexOf(this.currentTrackIndex);
206
+ const nextShuffleIndex = (currentShuffleIndex + 1) % this.tracks.length;
207
+ this.currentTrackIndex = this.shuffledIndexes[nextShuffleIndex];
208
+ } else {
209
+ this.currentTrackIndex = (this.currentTrackIndex + 1) % this.tracks.length;
210
+ }
211
+
212
+ this.loadAndPlayTrack();
213
+ }
214
+
215
+ loadAndPlayTrack() {
216
+ if (!this.tracks || this.tracks.length === 0 || this.currentTrackIndex >= this.tracks.length) {
217
+ console.error('No tracks available or invalid track index');
218
+ return;
219
+ }
220
+
221
+ const track = this.tracks[this.currentTrackIndex];
222
+ if (!track || !track.dataset.src) {
223
+ console.error('Invalid track data');
224
+ return;
225
+ }
226
+
227
+ const trackSrc = track.dataset.src;
228
+ this.audio.src = trackSrc;
229
+ this.currentTrackName.textContent = track.textContent.trim();
230
+
231
+ // Удаляем класс active у всех треков в обеих вкладках
232
+ document.querySelectorAll('.track-item').forEach(t => t.classList.remove('active'));
233
+ // Добавляем класс active текущему треку
234
+ track.classList.add('active');
235
+
236
+ this.play();
237
+ }
238
+
239
+ updateProgress() {
240
+ const duration = this.audio.duration;
241
+ const currentTime = this.audio.currentTime;
242
+
243
+ if (duration) {
244
+ // Обновляем прогресс-бар
245
+ const progressPercent = (currentTime / duration) * 100;
246
+ this.progress.style.width = progressPercent + '%';
247
+
248
+ // Обновляем время
249
+ this.currentTimeSpan.textContent = this.formatTime(currentTime);
250
+ this.durationSpan.textContent = this.formatTime(duration);
251
+ }
252
+ }
253
+
254
+ formatTime(seconds) {
255
+ const minutes = Math.floor(seconds / 60);
256
+ const remainingSeconds = Math.floor(seconds % 60);
257
+ return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
258
+ }
259
+
260
+ toggleShuffle() {
261
+ this.isShuffled = !this.isShuffled;
262
+
263
+ if (this.isShuffled) {
264
+ // Активируем кнопку shuffle
265
+ this.shuffleBtn.classList.add('active');
266
+
267
+ // Создаем массив индексов и перемешиваем его
268
+ this.shuffledIndexes = Array.from({ length: this.tracks.length }, (_, i) => i);
269
+ this.shuffleArray(this.shuffledIndexes);
270
+
271
+ // Убедимся, что текущий трек будет первым в перемешанном списке
272
+ const currentIndex = this.shuffledIndexes.indexOf(this.currentTrackIndex);
273
+ if (currentIndex !== -1) {
274
+ // Меняем местами текущий индекс с первым элементом
275
+ [this.shuffledIndexes[0], this.shuffledIndexes[currentIndex]] =
276
+ [this.shuffledIndexes[currentIndex], this.shuffledIndexes[0]];
277
+ }
278
+ } else {
279
+ // Деактивируем кнопку shuffle
280
+ this.shuffleBtn.classList.remove('active');
281
+ }
282
+ }
283
+
284
+ shuffleArray(array) {
285
+ // Алгоритм Фишера-Йейтса для перемешивания массива
286
+ for (let i = array.length - 1; i > 0; i--) {
287
+ const j = Math.floor(Math.random() * (i + 1));
288
+ [array[i], array[j]] = [array[j], array[i]];
289
+ }
290
+ return array;
291
+ }
292
+
293
+ searchTracks(query) {
294
+ query = query.toLowerCase();
295
+ const activeTab = document.querySelector('.tab-content.active');
296
+ const tracksList = activeTab.getElementsByClassName('track-item');
297
+
298
+ Array.from(tracksList).forEach(track => {
299
+ const trackName = track.querySelector('.track-name').textContent.toLowerCase();
300
+ if (trackName.includes(query)) {
301
+ track.style.display = '';
302
+ } else {
303
+ track.style.display = 'none';
304
+ }
305
+ });
306
+ }
307
+
308
+ async uploadFiles(files) {
309
+ const uploadList = document.getElementById('upload-list');
310
+ const uploadProgressContainer = document.getElementById('upload-progress-container');
311
+
312
+ if (uploadProgressContainer) {
313
+ uploadProgressContainer.style.display = 'block';
314
+ }
315
+
316
+ if (uploadList) {
317
+ uploadList.innerHTML = '';
318
+ }
319
+
320
+ // Перебираем все файлы и загружаем их на сервер
321
+ for (const file of files) {
322
+ // Проверяем, что файл имеет допустимый формат
323
+ if (!file.name.match(/\.(mp3|wav|m4a|webm)$/i)) {
324
+ this.showUploadStatus(file.name, 'Недопустимый формат файла', 'error');
325
+ continue;
326
+ }
327
+
328
+ // Создаем объект FormData для отправки файла
329
+ const formData = new FormData();
330
+ formData.append('file', file);
331
+
332
+ try {
333
+ // Отображаем статус загрузки
334
+ this.showUploadStatus(file.name, 'Загрузка...', 'loading');
335
+
336
+ // Отправляем файл на сервер
337
+ const response = await fetch('/upload', {
338
+ method: 'POST',
339
+ body: formData
340
+ });
341
+
342
+ const result = await response.json();
343
+
344
+ if (result.status === 'success') {
345
+ this.showUploadStatus(file.name, 'Загружено успешно', 'success');
346
+
347
+ // Добавляем новый трек в список без перезагрузки страницы
348
+ this.addTrackToList(result.filename);
349
+ } else {
350
+ this.showUploadStatus(file.name, result.message || 'Ошибка загрузки', 'error');
351
+ }
352
+ } catch (error) {
353
+ console.error('Ошибка загрузки файла:', error);
354
+ this.showUploadStatus(file.name, 'Ошибка загрузки', 'error');
355
+ }
356
+ }
357
+ }
358
+
359
+ showUploadStatus(filename, message, status) {
360
+ const uploadList = document.getElementById('upload-list');
361
+ if (!uploadList) return;
362
+
363
+ const statusItem = document.createElement('div');
364
+ statusItem.className = `upload-status ${status}`;
365
+ statusItem.innerHTML = `
366
+ <span class="filename">${filename}</span>
367
+ <span class="message">${message}</span>
368
+ `;
369
+
370
+ uploadList.appendChild(statusItem);
371
+ }
372
+
373
+ addTrackToList(filename) {
374
+ // Добавляем новый трек в список всех треков
375
+ const trackList = document.getElementById('track-list');
376
+ if (!trackList) return;
377
+
378
+ const newTrack = document.createElement('li');
379
+ newTrack.className = 'track-item';
380
+ newTrack.dataset.src = `/play/${filename}`;
381
+
382
+ newTrack.innerHTML = `
383
+ <span class="track-name">${filename}</span>
384
+ <button class="favorite-btn" data-track="${filename}">
385
+ <i class="fas fa-heart"></i>
386
+ </button>
387
+ `;
388
+
389
+ // Добавляем обработчик клика для нового трека
390
+ newTrack.addEventListener('click', (e) => {
391
+ if (!e.target.closest('.favorite-btn')) {
392
+ this.currentTrackIndex = this.tracks.length;
393
+ this.tracks.push(newTrack);
394
+ this.loadAndPlayTrack();
395
+ }
396
+ });
397
+
398
+ // Добавляем обработчик для кнопки избранного
399
+ const favoriteBtn = newTrack.querySelector('.favorite-btn');
400
+ favoriteBtn.addEventListener('click', async (e) => {
401
+ e.preventDefault();
402
+ const track = favoriteBtn.dataset.track;
403
+ const response = await fetch(`/favorite/${track}`, {
404
+ method: 'POST',
405
+ headers: {
406
+ 'Content-Type': 'application/json'
407
+ }
408
+ });
409
+
410
+ const result = await response.json();
411
+ if (result.status === 'added') {
412
+ favoriteBtn.classList.add('active');
413
+ } else {
414
+ favoriteBtn.classList.remove('active');
415
+ }
416
+ });
417
+
418
+ trackList.appendChild(newTrack);
419
+
420
+ // Обновляем список треков
421
+ this.tracks = Array.from(trackList.getElementsByClassName('track-item'));
422
+ }
423
+ }
424
+
425
+ // Инициализация плеера
426
+ document.addEventListener('DOMContentLoaded', () => {
427
+ const player = new MusicPlayer();
428
+ });
templates/index.html ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Pulse</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
8
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
9
+ </head>
10
+ <body>
11
+ <div class="player-container">
12
+ <div class="header">
13
+ <h1>Pulse</h1>
14
+ <div class="search-container">
15
+ <input type="text" id="search-input" placeholder="Поиск треков...">
16
+ <i class="fas fa-search search-icon"></i>
17
+ </div>
18
+ <div class="auth-buttons">
19
+ {% if current_user.is_authenticated %}
20
+ <span class="username">{{ current_user.username }}</span>
21
+ <a href="{{ url_for('logout') }}" class="auth-link">Выйти</a>
22
+ {% else %}
23
+ <a href="{{ url_for('login') }}" class="auth-link">Войти</a>
24
+ <a href="{{ url_for('register') }}" class="auth-link">Регистрация</a>
25
+ {% endif %}
26
+ </div>
27
+ </div>
28
+
29
+ <div class="current-track">
30
+ <div class="track-info">
31
+ <span id="current-track-name">Выберите трек</span>
32
+ </div>
33
+ <div class="progress-container">
34
+ <div class="progress-bar" id="progress-bar">
35
+ <div class="progress" id="progress"></div>
36
+ </div>
37
+ <div class="time-info">
38
+ <span id="current-time">0:00</span>
39
+ <span id="duration">0:00</span>
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+ <div class="controls">
45
+ <button id="prev-btn" class="control-btn">
46
+ <i class="fas fa-step-backward"></i>
47
+ </button>
48
+ <button id="play-btn" class="control-btn">
49
+ <i class="fas fa-play"></i>
50
+ </button>
51
+ <button id="next-btn" class="control-btn">
52
+ <i class="fas fa-step-forward"></i>
53
+ </button>
54
+ <button id="shuffle-btn" class="control-btn">
55
+ <i class="fas fa-random"></i>
56
+ </button>
57
+ <div class="volume-control">
58
+ <i class="fas fa-volume-up"></i>
59
+ <input type="range" id="volume" min="0" max="1" step="0.1" value="1">
60
+ </div>
61
+ </div>
62
+
63
+ <div class="playlist">
64
+ <div class="playlist-tabs">
65
+ <button class="tab-btn active" data-tab="all">Плейлист</button>
66
+ <button class="tab-btn" data-tab="liked">Понравившиеся</button>
67
+ {% if current_user.is_authenticated %}
68
+ <button class="tab-btn" data-tab="upload">Добавить трек</button>
69
+ {% endif %}
70
+ </div>
71
+ <ul id="track-list" class="tab-content active" data-tab="all">
72
+ {% for music_file in music_files %}
73
+ <li class="track-item" data-src="{{ url_for('play_music', filename=music_file) }}">
74
+ <span class="track-name">{{ music_file }}</span>
75
+ {% if current_user.is_authenticated %}
76
+ <button class="favorite-btn {% if music_file in favorites %}active{% endif %}" data-track="{{ music_file }}">
77
+ <i class="fas fa-heart"></i>
78
+ </button>
79
+ {% endif %}
80
+ </li>
81
+ {% endfor %}
82
+ </ul>
83
+ <ul id="liked-list" class="tab-content" data-tab="liked">
84
+ {% for music_file in favorites %}
85
+ <li class="track-item" data-src="{{ url_for('play_music', filename=music_file) }}">
86
+ <span class="track-name">{{ music_file }}</span>
87
+ {% if current_user.is_authenticated %}
88
+ <button class="favorite-btn active" data-track="{{ music_file }}">
89
+ <i class="fas fa-heart"></i>
90
+ </button>
91
+ {% endif %}
92
+ </li>
93
+ {% endfor %}
94
+ </ul>
95
+ {% if current_user.is_authenticated %}
96
+ <div id="upload-container" class="tab-content" data-tab="upload">
97
+ <div class="upload-area" id="drop-area">
98
+ <div class="upload-icon">
99
+ <i class="fas fa-cloud-upload-alt"></i>
100
+ </div>
101
+ <p class="upload-text">Перетащите аудиофайлы сюда или</p>
102
+ <label for="file-input" class="upload-button">Выберите файлы</label>
103
+ <input type="file" id="file-input" accept="audio/*" multiple style="display: none;">
104
+ <p class="upload-info">Поддерживаемые форматы: MP3, WAV, M4A, WEBM</p>
105
+ </div>
106
+ <div class="upload-progress-container" id="upload-progress-container" style="display: none;">
107
+ <div class="upload-list" id="upload-list"></div>
108
+ </div>
109
+ </div>
110
+ {% endif %}
111
+ </div>
112
+ </div>
113
+
114
+ <script src="{{ url_for('static', filename='js/player.js') }}"></script>
115
+ </body>
116
+ </html>
templates/login.html ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Вход</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
8
+ </head>
9
+ <body>
10
+ <div class="auth-container">
11
+ <h1>Вход</h1>
12
+ {% with messages = get_flashed_messages() %}
13
+ {% if messages %}
14
+ {% for message in messages %}
15
+ <div class="alert">{{ message }}</div>
16
+ {% endfor %}
17
+ {% endif %}
18
+ {% endwith %}
19
+ <form method="POST" class="auth-form">
20
+ <div class="form-group">
21
+ <label for="username">Имя пользователя:</label>
22
+ <input type="text" id="username" name="username" required>
23
+ </div>
24
+ <div class="form-group">
25
+ <label for="password">Пароль:</label>
26
+ <input type="password" id="password" name="password" required>
27
+ </div>
28
+ <button type="submit" class="auth-btn">Войти</button>
29
+ </form>
30
+ <p>Нет аккаунта? <a href="{{ url_for('register') }}">Зарегистрироваться</a></p>
31
+ </div>
32
+ </body>
33
+ </html>
templates/register.html ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Регистрация</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
8
+ </head>
9
+ <body>
10
+ <div class="auth-container">
11
+ <h1>Регистрация</h1>
12
+ {% with messages = get_flashed_messages() %}
13
+ {% if messages %}
14
+ {% for message in messages %}
15
+ <div class="alert">{{ message }}</div>
16
+ {% endfor %}
17
+ {% endif %}
18
+ {% endwith %}
19
+ <form method="POST" class="auth-form">
20
+ <div class="form-group">
21
+ <label for="username">Имя пользователя:</label>
22
+ <input type="text" id="username" name="username" required>
23
+ </div>
24
+ <div class="form-group">
25
+ <label for="password">Пароль:</label>
26
+ <input type="password" id="password" name="password" required>
27
+ </div>
28
+ <button type="submit" class="auth-btn">Зарегистрироваться</button>
29
+ </form>
30
+ <p>Уже есть аккаунт? <a href="{{ url_for('login') }}">Войти</a></p>
31
+ </div>
32
+ </body>
33
+ </html>