Project structure:

LitMusic/
├── static/
│   ├── icons/
│   │   ├── arrow.webp
│   │   ├── audio.webp
│   │   ├── close.webp
│   │   ├── copy.webp
│   │   ├── description.webp
│   │   ├── finder.webp
│   │   ├── pause.webp
│   │   ├── play.webp
│   │   └── text.webp
│   ├── tracks/
│   ├── app.js
│   └── styles.css
├── templates/
│   └── index.html
├── Dockerfile
├── __upload_new_db.py
├── app.py
├── database.db
├── database.py
└── requirements.txt


Project files:

app.py
```py
# app.py
import shutil
import sqlite3
import zipfile
from io import BytesIO

from flask import Flask, request, jsonify, send_from_directory, render_template, send_file, url_for
from flask_cors import CORS
from database import Database
import os
import uuid
from werkzeug.utils import secure_filename

app = Flask(__name__, static_folder='static', template_folder='templates')
CORS(app)

db = Database()

# Убедимся, что папка для хранения треков существует
TRACKS_DIR = os.path.join(app.static_folder, 'tracks')
os.makedirs(TRACKS_DIR, exist_ok=True)

# Ограничение для допустимых расширений файлов
ALLOWED_EXTENSIONS = {'mp3', 'wav', 'aac', 'flac', 'ogg', 'm4a'}

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


# Главная страница
@app.route('/')
def index():
    return render_template('index.html')

# 1. Получение всех треков (с пагинацией)
@app.route('/tracks', methods=['GET'])
def get_tracks():
    page = request.args.get('page', 1, type=int)
    per_page = 1000
    name = request.args.get('name', type=str)

    if name:
        tracks, total = db.search_tracks_paginated(
            name=name,
            page=page,
            per_page=per_page
        )
    else:
        tracks, total = db.get_tracks(page, per_page)

    return jsonify({'tracks': tracks, 'total': total})

# 2. Поиск треков
@app.route('/tracks/search', methods=['GET'])
def search_tracks():
    description = request.args.get('description', '', type=str)
    tags = request.args.getlist('tags')
    tracks = db.search_tracks(description, tags)
    return jsonify({'tracks': tracks})

# 3. Добавление нового трека с загрузкой файла
@app.route('/tracks', methods=['POST'])
def add_track():
    # Получение оригинальной ссылки, если она существует
    original_url = request.form.get('url')  # Сделали необязательным

    # Проверяем наличие файла в запросе
    if 'file' not in request.files:
        return jsonify({'error': 'Файл трека обязателен.'}), 400

    file = request.files['file']

    # Получаем другие поля формы
    name = request.form.get('name')
    description = request.form.get('description')
    tags = request.form.getlist('tags')  # Ожидаем, что теги передаются как список

    # Проверяем обязательные поля
    if not name:
        return jsonify({'error': 'Название трека обязательно.'}), 400

    if file.filename == '':
        return jsonify({'error': 'Выберите файл для загрузки.'}), 400

    if file and allowed_file(file.filename):
        # Обезопасим имя файла
        filename = secure_filename(file.filename)
        # Генерируем уникальное имя файла
        unique_filename = f"{uuid.uuid4()}_{filename}"
        file_path = os.path.join(TRACKS_DIR, unique_filename)
        file.save(file_path)

        # URL для доступа к файлу
        local_url = url_for('static', filename=f"tracks/{unique_filename}")

        # Добавляем трек в базу данных
        track_id = db.add_track(name, description, local_url, original_url, tags)

        return jsonify({'message': 'Трек успешно добавлен.', 'track_id': track_id}), 201
    else:
        return jsonify({'error': 'Недопустимый тип файла. Поддерживаемые форматы: mp3, wav, aac, flac, ogg, m4a.'}), 400

# 4. Удаление трека
@app.route('/tracks/<int:track_id>', methods=['DELETE'])
def delete_track(track_id):
    # Получаем трек по ID
    track = db.get_track_by_id(track_id)
    if not track:
        return jsonify({'error': 'Трек не найден.'}), 404

    # Удаляем трек из базы данных
    db.delete_track(track_id)

    # Удаляем локальный файл
    file_path = os.path.join(app.root_path, track['url'].lstrip('/'))
    if os.path.exists(file_path):
        try:
            os.remove(file_path)
        except Exception as e:
            # Можно логировать ошибку или обработать иначе
            pass

    return jsonify({'message': 'Трек успешно удален.'}), 200

@app.route('/get_db')
def download_db():
    try:
        memory_file = BytesIO()
        with zipfile.ZipFile(memory_file, 'w', zipfile.ZIP_DEFLATED) as zipf:
            # Добавляем базу данных в архив
            zipf.write(db.db_path, arcname='tracks.db')

            # Добавляем файлы треков в архив
            for root, _, files in os.walk(TRACKS_DIR):
                for file in files:
                    zipf.write(os.path.join(root, file), arcname=os.path.relpath(os.path.join(root, file), app.static_folder))

        memory_file.seek(0)

        return send_file(memory_file, as_attachment=True, download_name="tracks_backup.zip", mimetype="application/zip")

    except Exception as e:
        print(f"Error creating backup: {e}")
        return jsonify({'error': 'Ошибка при создании бэкапа.'}), 500

@app.route('/set_db', methods=['POST'])
def upload_db():
    print(f"upload_db")
    if 'db_file' not in request.files:
        print("11111")
        return jsonify({'error': 'Файл базы данных не найден.'}), 400
    print("22222222")
    file = request.files['db_file']
    if file.filename == '':
        return jsonify({'error': 'Файл не выбран.'}), 400

    temp_dir = os.path.join(app.instance_path, f"temp_{uuid.uuid4()}")  # instance_path более безопасный
    os.makedirs(temp_dir, exist_ok=True)

    temp_db_path = None

    try:
        file.save(os.path.join(temp_dir, "temp_backup.zip"))

        with zipfile.ZipFile(os.path.join(temp_dir, "temp_backup.zip"), 'r') as zip_ref:
            zip_ref.extractall(temp_dir)

        temp_db_path = os.path.join(temp_dir, 'tracks.db')

        # Подключаемся к временной базе данных
        temp_conn = sqlite3.connect(temp_db_path)
        temp_cursor = temp_conn.cursor()

        def get_tags_from_temp_db(temp_cursor, track_name):
            temp_cursor.execute('''
                SELECT t.name, g.name
                FROM tracks AS t
                INNER JOIN track_tags AS tt ON t.id = tt.track_id
                INNER JOIN tags AS g ON tt.tag_id = g.id
                WHERE t.name = ?
            ''', (track_name,))
            tags = [tag[1] for tag in temp_cursor.fetchall() if tag[0] == track_name]
            return tags

        temp_cursor.execute("SELECT name, description, url, original_url FROM tracks")
        tracks = temp_cursor.fetchall()

        # Перемещаем треки в static/tracks
        temp_tracks_dir = os.path.join(temp_dir, 'tracks')
        if os.path.exists(temp_tracks_dir):
            for filename in os.listdir(temp_tracks_dir):
                shutil.move(os.path.join(temp_tracks_dir, filename), TRACKS_DIR)

        for track in tracks:
            # обновляем url, чтобы он указывал на правильное место
            track_url = track[2]  # url из временной БД
            track_filename = os.path.basename(track_url.lstrip('/tracks/'))  # достаем имя файла из url
            new_track_url = url_for('static', filename=f"tracks/{track_filename}")  # Изменено на /static/tracks/
            new_track_id = db.add_track(track[0], track[1], new_track_url, track[3], [])  # используем новый url
            tags = get_tags_from_temp_db(temp_cursor, track[0])
            if tags:
                db.add_tags_to_track(new_track_id, tags)

        temp_conn.close()

        return jsonify({'message': 'Данные из базы данных успешно загружены.'}), 200

    except sqlite3.Error as e:
        return jsonify({'error': f'Ошибка SQLite: {e}'}), 400  # более информативное сообщение
    except Exception as e:
        import traceback
        print(traceback.format_exc())
        return jsonify({'error': f'Ошибка при загрузке данных: {e}'}), 500
    finally:
        # Удалить временную директорию и все ее содержимое
        if temp_dir:
            try:
                shutil.rmtree(temp_dir)
            except OSError as e:
                print("Error: %s - %s." % (e.filename, e.strerror))

# Обработка ошибки превышения размера файла
@app.errorhandler(413)
def request_entity_too_large(error):
    return jsonify({'error': 'Файл слишком большой. Максимальный размер: 100MB.'}), 413

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=6390)
```


database.py
```py
# database.py

import sqlite3

class Database:
    def __init__(self, db_path='database.db'):
        self.db_path = db_path
        self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
        self.init_db()

    def init_db(self):
        cursor = self.conn.cursor()
        # Создание таблицы треков с полем original_url
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS tracks (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                description TEXT,
                url TEXT NOT NULL,
                original_url TEXT
            )
        ''')
        # Создание таблицы тегов
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS tags (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL UNIQUE
            )
        ''')
        # Создание связующей таблицы track_tags
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS track_tags (
                track_id INTEGER,
                tag_id INTEGER,
                FOREIGN KEY(track_id) REFERENCES tracks(id),
                FOREIGN KEY(tag_id) REFERENCES tags(id),
                PRIMARY KEY(track_id, tag_id)
            )
        ''')
        self.conn.commit()

    # Обновленный метод добавления трека
    def add_track(self, name, description, url, original_url, tags):
        cursor = self.conn.cursor()
        cursor.execute('INSERT INTO tracks (name, description, url, original_url) VALUES (?, ?, ?, ?)',
                       (name, description, url, original_url))
        track_id = cursor.lastrowid
        for tag_name in tags:
            # Проверяем, существует ли тег
            cursor.execute('SELECT id FROM tags WHERE name = ?', (tag_name,))
            tag_row = cursor.fetchone()
            if tag_row:
                tag_id = tag_row[0]
            else:
                cursor.execute('INSERT INTO tags (name) VALUES (?)', (tag_name,))
                tag_id = cursor.lastrowid
            # Добавляем связь трека и тега
            cursor.execute('INSERT OR IGNORE INTO track_tags (track_id, tag_id) VALUES (?, ?)', (track_id, tag_id))
        self.conn.commit()
        return track_id

    # Метод для обновления URL трека
    def update_track_url(self, track_id, url):
        cursor = self.conn.cursor()
        cursor.execute('UPDATE tracks SET url = ? WHERE id = ?', (url, track_id))
        self.conn.commit()

    # Метод для получения трека по ID
    def get_track_by_id(self, track_id):
        cursor = self.conn.cursor()
        cursor.execute('SELECT * FROM tracks WHERE id = ?', (track_id,))
        row = cursor.fetchone()
        if row:
            track = {
                'id': row[0],
                'name': row[1],
                'description': row[2],
                'url': row[3],
                'original_url': row[4],
                'tags': self.get_tags_for_track(row[0])
            }
            return track
        return None

    # Метод для получения всех треков с пагинацией
    def get_tracks(self, page=1, per_page=10):
        cursor = self.conn.cursor()
        offset = (page - 1) * per_page
        cursor.execute('SELECT * FROM tracks LIMIT ? OFFSET ?', (per_page, offset))
        tracks_rows = cursor.fetchall()
        tracks = []
        for row in tracks_rows:
            track = {
                'id': row[0],
                'name': row[1],
                'description': row[2],
                'url': row[3],
                'original_url': row[4],
                'tags': self.get_tags_for_track(row[0])
            }
            tracks.append(track)
        # Получаем общее количество треков
        cursor.execute('SELECT COUNT(*) FROM tracks')
        total = cursor.fetchone()[0]
        return tracks, total

    # Метод для поиска треков по описанию и тегам
    def search_tracks(self, description='', tags=[]):
        cursor = self.conn.cursor()
        query = 'SELECT DISTINCT t.id, t.name, t.description, t.url, t.original_url FROM tracks t'
        params = []
        if tags:
            query += ' INNER JOIN track_tags tt ON t.id = tt.track_id INNER JOIN tags g ON tt.tag_id = g.id'
        query += ' WHERE 1=1'
        if description:
            query += ' AND t.description LIKE ?'
            params.append(f'%{description}%')
        if tags:
            query += ' AND g.name IN ({seq})'.format(seq=','.join(['?']*len(tags)))
            params.extend(tags)
        cursor.execute(query, params)
        tracks_rows = cursor.fetchall()
        tracks = []
        for row in tracks_rows:
            track = {
                'id': row[0],
                'name': row[1],
                'description': row[2],
                'url': row[3],
                'original_url': row[4],
                'tags': self.get_tags_for_track(row[0])
            }
            tracks.append(track)
        return tracks

    # Метод для получения тегов трека
    def get_tags_for_track(self, track_id):
        cursor = self.conn.cursor()
        cursor.execute('''
            SELECT g.name FROM tags g
            INNER JOIN track_tags tt ON g.id = tt.tag_id
            WHERE tt.track_id = ?
        ''', (track_id,))
        tags = [row[0] for row in cursor.fetchall()]
        return tags

    # Метод для удаления трека
    def delete_track(self, track_id):
        cursor = self.conn.cursor()
        # Удаляем связи из track_tags
        cursor.execute('DELETE FROM track_tags WHERE track_id = ?', (track_id,))
        # Удаляем сам трек
        cursor.execute('DELETE FROM tracks WHERE id = ?', (track_id,))
        self.conn.commit()

    def search_tracks_paginated(self, name='', page=1, per_page=10):
        cursor = self.conn.cursor()

        # Base query
        query = '''
            SELECT DISTINCT t.id, t.name, t.url, t.original_url
            FROM tracks t
        '''
        params = []


        # Where conditions
        query += ' WHERE 1=1'
        if name:
            query += ' AND t.name LIKE ?'
            params.append(f'%{name}%')

        # Pagination
        query += ' LIMIT ? OFFSET ?'
        params.extend([per_page, (page - 1) * per_page])

        # Execute search query
        cursor.execute(query, params)
        tracks_rows = cursor.fetchall()
        tracks = []
        for row in tracks_rows:
            track = {
                'id': row[0],
                'name': row[1],
                'url': row[2],
                'original_url': row[3],
                'tags': self.get_tags_for_track(row[0])
            }
            tracks.append(track)

        # Count total matching records for pagination
        count_query = 'SELECT COUNT(DISTINCT t.id) FROM tracks t'
        count_params = []

        count_query += ' WHERE 1=1'
        if name:
            count_query += ' AND t.name LIKE ?'
            count_params.append(f'%{name}%')

        cursor.execute(count_query, count_params)
        total = cursor.fetchone()[0]

        return tracks, total

    def add_tags_to_track(self, track_id, tags):
        cursor = self.conn.cursor()
        for tag_name in tags:
            cursor.execute('SELECT id FROM tags WHERE name = ?', (tag_name,))
            tag_row = cursor.fetchone()
            if tag_row:
                tag_id = tag_row[0]
            else:
                cursor.execute('INSERT INTO tags (name) VALUES (?)', (tag_name,))
                tag_id = cursor.lastrowid
            cursor.execute('INSERT OR IGNORE INTO track_tags (track_id, tag_id) VALUES (?, ?)', (track_id, tag_id))
        self.conn.commit()

    def reconnect(self):
        self.conn.close()
        self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
        self.cursor = self.conn.cursor() #  Важно: обновить cursor
```


requirements.txt
```txt
Flask
Flask-Cors
requests
```


__upload_new_db.py
```py
import requests

# Путь к zip-архиву
zip_path = "C:/Users/matve/Videos/getshit/tracks_backup (1).zip"  # Замените на свой путь

# Отправляем запрос
with open(zip_path, 'rb') as f:
    files = {'db_file': ('tracks_backup.zip', f, 'application/zip')}
    try:
        r = requests.post('http://185.106.94.81:6390/set_db', files=files)
        r.raise_for_status()
        print(r.text)
    except requests.exceptions.RequestException as e:
        print(f"Error during upload: {e}")
```


static/app.js
```js
// static/app.js

$(document).ready(function () {
    $(".litmusic-text").css("transform", "scale(1.5)");

    setTimeout(function () {
        $(".intro-container").css("pointer-events", "none");
        $(".intro-container").animate({opacity: 0}, 500, function () {
            $(this).remove();
        });
    }, 1000); // Задержка, чтобы анимация transform завершилась

    const $tracksContainer = $('#tracks-container');
    const $addTrackButton = $('#add-track-button');
    const $addTrackDialog = $('#add-track-dialog');
    const $closeAddDialog = $('#close-add-dialog');
    const $addTrackForm = $('#add-track-form');
    const $tagsContainer = $('#tags-container');
    const $addTagButton = $('#add-tag-button');
    const $audioPlayer = $('#audio-player');
    const $audioControl = $('#audio-control');
    const $trackTitle = $('#track-title');
    const $togglePlayerButton = $('#toggle-player-button');
    const $descriptionDialog = $('#description-dialog');
    const $closeDescriptionDialog = $('#close-description-dialog');
    const $descriptionTitle = $('#description-title');
    const $descriptionText = $('#description-text');
    const $searchForm = $('#search-form');
    const $clearSearchButton = $('#clear-search');
    const $paginationControls = $('#pagination-controls');
    const $fileDropArea = $('#file-drop-area');
    const $trackFileInput = $('#track-file');
    const $selectedFileInfo = $('#selected-file-info');
    const $selectedFileName = $('#selected-file-name');
    const $removeSelectedFile = $('#remove-selected-file');
    const $globalOverlay = $('#global-overlay');

    let currentPage = 1;
    const perPage = 10;
    let totalTracks = 0;
    let currentSearch = {
        name: '',
        description: '',
        tags: []
    };
    const animationSpeed = 300; // миллисекунд

    // Переменные для отслеживания текущего трека
    let currentTrackId = null;
    let $currentPlayButton = null;

    // Глобальная переменная для хранения всех треков
    let allTracks = [];

    // --- Функции ---
    function loadTracks(page = 1, search = {}) {
        currentPage = page;
        let url = `/tracks?page=${page}`;
        if (search.name) {
            url += `&name=${encodeURIComponent(search.name)}`;
        }
        if (search.description) {
            url += `&description=${encodeURIComponent(search.description)}`;
        }
        if (search.tags && search.tags.length > 0) {
            search.tags.forEach(tag => {
                url += `&tags=${encodeURIComponent(tag)}`;
            });
        }

        $.getJSON(url, function (data) {
            $tracksContainer.empty();
            totalTracks = data.total;
            allTracks = data.tracks; // Сохраняем загруженные треки
            if (data.tracks.length === 0) {
                $tracksContainer.append('<p>Треки не найдены.</p>');
            } else {
                $.each(data.tracks, function (index, track) {
                    const $trackTile = $('<div class="track-tile"></div>');
                    const $topDiv = $('<div class="top-div"></div>');
                    const $buttons = $('<div class="buttons"></div>');
                    const $title = $('<h3>' + track.name + '</h3>');

                    // Создаем кнопку Play/Pause с data-track-id
                    const $playPauseButton = $(`
                        <button class="play-pause-button" data-track-id="${track.id}">
                            <img src="../static/icons/play.webp" alt="Play">
                        </button>
                    `).click(function () {
                        togglePlayPause(track, $(this));
                    });

                    // Добавляем кнопку копирования только если original_url присутствует
                    if (track.original_url) {
                        const $copyButton = $('<button><img src="../static/icons/copy.webp" alt="Copy"></button>').click(function () {
                            copyToClipboard(track.original_url); // Копируем оригинальную ссылку
                        });
                        $buttons.append($copyButton);
                    }

                    const $descriptionButton = $('<button><img src="../static/icons/description.webp" alt="Description"></button>').click(function () {
                        showDescription(track);
                    });
                    const $deleteButton = $('<button><img src="../static/icons/close.webp" alt="Delete"></button>').click(function () {
                        deleteTrack(track.id, track.name);
                    });

                    $topDiv.append($playPauseButton).append($title);
                    $buttons.append($descriptionButton).append($deleteButton);
                    $trackTile.append($topDiv).append($buttons);
                    $tracksContainer.append($trackTile);
                });
            }
            renderPagination();
        }).fail(function (jqxhr, textStatus, error) {
            console.error('Ошибка при загрузке треков:', error);
            alert('Не удалось загрузить треки.');
        });
    }

    function togglePlayPause(track, $button) {
        if (currentTrackId === track.id) {
            // Если тот же трек, переключаем воспроизведение/пауза
            if ($audioControl[0].paused) {
                $audioControl[0].play();
                $button.find('img').attr('src', '../static/icons/pause.webp');
            } else {
                $audioControl[0].pause();
                $button.find('img').attr('src', '../static/icons/play.webp');
            }
        } else {
            // Если другой трек
            // Останавливаем текущий трек, если есть
            if (currentTrackId !== null && $currentPlayButton !== null) {
                $currentPlayButton.find('img').attr('src', '../static/icons/play.webp');
            }

            // Устанавливаем новый трек
            currentTrackId = track.id;
            $currentPlayButton = $button;
            $audioControl.attr('src', track.url);
            $trackTitle.text(track.name);
            $audioControl[0].play();  // Воспроизведение

            // Меняем иконку кнопки
            $button.find('img').attr('src', '../static/icons/pause.webp');
        }
        maximizePlayer();
    }

    function maximizePlayer() {
        $audioPlayer.removeClass('minimized').addClass('maximized');
        $togglePlayerButton.addClass('rotated');
    }

    function minimizePlayer() {
        $audioPlayer.addClass('minimized').removeClass('maximized');
        $togglePlayerButton.removeClass('rotated');
    }

    function showDescription(track) {
        $descriptionTitle.text(track.name);
        $descriptionText.text(track.description || 'Нет описания.');
        $descriptionDialog.fadeIn(animationSpeed);
    }

    /**
     * Функция для отображения уведомления.
     * @param {string} message - Текст уведомления.
     */
    function showNotification(message) {
        const $notification = $('<div class="notification"></div>').text(message);
        $('#notifications-container').append($notification);

        // Запустить анимацию появления
        setTimeout(() => {
            $notification.addClass('show');
        }, 10); // Небольшая задержка для корректного срабатывания CSS

        // Удалить уведомление через 1.5 секунды
        setTimeout(() => {
            $notification.removeClass('show').addClass('hide');
            // Удалить элемент из DOM после завершения анимации
            $notification.on('transitionend', () => {
                $notification.remove();
            });
        }, 1500);
    }

    function copyToClipboard(text) {
        if (navigator.clipboard) {
            navigator.clipboard.writeText(text).then(function() {
                showNotification('Ссылка скопирована!');
            }).catch(function(err) {
                console.error('Не удалось скопировать:', err);
                fallbackCopy(text); // Вызываем fallback, если Clipboard API не сработал
            });
        } else {
            fallbackCopy(text);
        }
    }

    function fallbackCopy(text) {
        const textArea = document.createElement('textarea');
        textArea.value = text;
        document.body.appendChild(textArea);
        textArea.focus();
        textArea.select();

        try {
            const successful = document.execCommand('copy');
            if (successful) {
                showNotification('Ссылка скопирована!');
            } else {
                showNotification('Не удалось скопировать ссылку.');
            }
        } catch (err) {
            console.error('Ошибка при копировании fallback:', err);
            showNotification('Не удалось скопировать ссылку.');
        }

        document.body.removeChild(textArea);
    }

    function deleteTrack(trackId, trackName) {
        if (confirm(`Вы уверены, что хотите удалить трек "${trackName}"?`)) {
            $.ajax({
                url: `/tracks/${trackId}`,
                type: 'DELETE',
                success: function (data) {
                    if (data.error) {
                        showNotification('Ошибка: ' + data.error);
                    } else {
                        showNotification(data.message);
                        // Если удаляется текущий воспроизводимый трек, сбрасываем аудио
                        if (currentTrackId === trackId) {
                            $audioControl[0].pause();
                            $audioControl.attr('src', '');
                            $trackTitle.text('');
                            if ($currentPlayButton !== null) {
                                $currentPlayButton.find('img').attr('src', '../static/icons/play.webp');
                                $currentPlayButton = null;
                            }
                            currentTrackId = null;
                        }
                        loadTracks(currentPage, currentSearch);
                    }
                },
                error: function (jqxhr, textStatus, error) {
                    console.error('Ошибка при удалении трека:', error);
                    showNotification('Что-то пошло не так.');
                }
            });
        }
    }

    // Рендеринг элементов пагинации
    function renderPagination() {
        const totalPages = Math.ceil(totalTracks / perPage);
        $paginationControls.empty();

        if (totalPages <= 1) return;

        // Кнопка "Предыдущая"
        const $prevButton = $('<button>').text('<< Предыдущая').prop('disabled', currentPage === 1).click(function () {
            if (currentPage > 1) {
                loadTracks(currentPage - 1, currentSearch);
            }
        });
        $paginationControls.append($prevButton);

        // Номера страниц (отображать максимум 5)
        const maxPagesToShow = 5;
        let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
        let endPage = startPage + maxPagesToShow - 1;
        if (endPage > totalPages) {
            endPage = totalPages;
            startPage = Math.max(1, endPage - maxPagesToShow + 1);
        }

        for (let i = startPage; i <= endPage; i++) {
            const $pageButton = $('<button>').text(i).addClass(i === currentPage ? 'active' : '').click(function () {
                loadTracks(i, currentSearch);
            });
            $paginationControls.append($pageButton);
        }

        // Кнопка "Следующая"
        const $nextButton = $('<button>').text('Следующая >>').prop('disabled', currentPage === totalPages).click(function () {
            if (currentPage < totalPages) {
                loadTracks(currentPage + 1, currentSearch);
            }
        });
        $paginationControls.append($nextButton);
    }

    // --- Обработчики Событий ---

    // Открытие диалога добавления трека
    $addTrackButton.click(function () {
        $addTrackDialog.fadeIn(animationSpeed);
    });
    $closeAddDialog.click(function () {
        $addTrackDialog.fadeOut(animationSpeed);
    });

    // Закрытие диалогового окна описания
    $closeDescriptionDialog.click(function () {
        $descriptionDialog.fadeOut(animationSpeed);
    });

    // Закрытие модальных окон при клике вне их
    $(window).click(function (event) {
        if (event.target == $addTrackDialog[0]) {
            $addTrackDialog.fadeOut(animationSpeed);
        }
        if (event.target == $descriptionDialog[0]) {
            $descriptionDialog.fadeOut(animationSpeed);
        }
    });

    // Добавление нового тега
    $addTagButton.click(function () {
        const $tagItem = $('<div class="tag-item"></div>');
        const $tagInput = $('<input type="text" name="tags">');
        const $removeButton = $('<button type="button"><img src="../static/icons/close.webp" alt="Удалить тег"></button>').click(function () {
            $tagItem.remove();
        });
        $tagItem.append($tagInput).append($removeButton);
        $tagsContainer.append($tagItem);
    });

    // Обработка выбора файла через input
    $trackFileInput.on('change', function () {
        const file = this.files[0];
        if (file) {
            $selectedFileName.text(file.name);
            $selectedFileInfo.removeClass('hidden');
        } else {
            $selectedFileInfo.addClass('hidden');
            $selectedFileName.text('');
        }
    });

    // Обработка удаления выбранного файла
    $removeSelectedFile.click(function () {
        $trackFileInput.val(''); // Очищаем выбранный файл

        // Создаем пустой DataTransfer для очистки FileList
        const dataTransfer = new DataTransfer();
        $trackFileInput[0].files = dataTransfer.files;

        $selectedFileName.text('');
        $selectedFileInfo.addClass('hidden');
    });

    // Отправка формы добавления трека с файлом
    $addTrackForm.submit(function (event) {
        event.preventDefault();

        const formData = new FormData(this);

        // Валидация: проверяем наличие файла
        if (!formData.get('file')) {
            showNotification('Файл трека обязателен.');
            return;
        }

        $.ajax({
            url: '/tracks',
            type: 'POST',
            data: formData,
            processData: false, // Не обрабатываем данные
            contentType: false, // Не устанавливаем contentType
            success: function (data) {
                if (data.error) {
                    showNotification('Ошибка: ' + data.error);
                } else {
                    showNotification('Трек успешно добавлен!');
                    $addTrackDialog.fadeOut(animationSpeed);
                    $addTrackForm[0].reset();
                    $tagsContainer.empty();
                    $selectedFileInfo.addClass('hidden');
                    loadTracks(1, {}); // Перезагрузка первой страницы без поиска
                }
            },
            error: function (jqxhr, textStatus, error) {
                console.error('Ошибка при добавлении трека:', error);
                const response = jqxhr.responseJSON;
                if (response && response.error) {
                    showNotification('Ошибка: ' + response.error);
                } else {
                    showNotification('Что-то пошло не так.');
                }
            }
        });
    });

    // Обработка dragover и dragleave для file-drop-area
    $fileDropArea.on('dragover', function (e) {
        e.preventDefault();
        e.stopPropagation();
        $(this).addClass('hover');
    });

    $fileDropArea.on('dragleave', function (e) {
        e.preventDefault();
        e.stopPropagation();
        $(this).removeClass('hover');
    });

    $fileDropArea.on('drop', function (e) {
        e.preventDefault();
        e.stopPropagation();
        $(this).removeClass('hover');

        const files = e.originalEvent.dataTransfer.files;
        if (files.length > 0) {
            // Создаем объект DataTransfer и добавляем файл
            const dataTransfer = new DataTransfer();
            dataTransfer.items.add(files[0]);

            // Устанавливаем созданный FileList в input
            $trackFileInput[0].files = dataTransfer.files;

            // Отображаем информацию о выбранном файле
            $selectedFileName.text(files[0].name);
            $selectedFileInfo.removeClass('hidden');
        }
    });

    // Переключение состояния проигрывателя
    $togglePlayerButton.click(function () {
        $audioPlayer.toggleClass('minimized maximized');
        $togglePlayerButton.toggleClass('rotated');
    });

    // Копирование ссылки в проигрывателе (если реализовано)
    // Если есть элемент для копирования ссылки в плеере, реализуйте соответствующую логику здесь
    // Например:
    /*
    $copyLinkButton.click(function () {
        const trackId = $(this).closest('.track-tile').find('.play-pause-button').data('track-id');
        const track = getTrackById(trackId);
        if (track && track.original_url) {
            copyToClipboard(track.original_url);
        } else {
            showNotification('Нет ссылки для копирования.');
        }
    });
    */

    // Отправка формы поиска
    $searchForm.submit(function (event) {
        event.preventDefault();
        const name = $('#search-name').val().trim();

        currentSearch = {name};
        loadTracks(1, currentSearch);
    });

    // Очистка поиска
    $clearSearchButton.click(function () {
        $searchForm[0].reset();
        currentSearch = {name: '', description: '', tags: []};
        loadTracks(1, currentSearch);
    });

    // Обработчик ввода в поисковой строке
    $('#search-name').on('input', function () {
        const name = $(this).val().trim();
        currentSearch = {name};
        loadTracks(1, currentSearch);
    });

    // Обработчик окончания воспроизведения трека
    $audioControl.on('ended', function () {
        if (currentTrackId !== null && $currentPlayButton !== null) {
            $currentPlayButton.find('img').attr('src', '../static/icons/play.webp');
            currentTrackId = null;
            $currentPlayButton = null;
        }
        minimizePlayer();
    });

    // Обработчик события воспроизведения трека
    $audioControl.on('play', function () {
        if (currentTrackId !== null && $currentPlayButton !== null) {
            $currentPlayButton.find('img').attr('src', '../static/icons/pause.webp');
        }
    });

    // Обработчик события паузы трека
    $audioControl.on('pause', function () {
        if (currentTrackId !== null && $currentPlayButton !== null) {
            $currentPlayButton.find('img').attr('src', '../static/icons/play.webp');
        }
    });

    // Функции для обработки глобального перетаскивания файлов
    let dragCounter = 0; // Добавляем счетчик перетаскиваний

    function showGlobalOverlay() {
        $globalOverlay.removeClass('hidden').addClass('fade-in').removeClass('fade-out');
    }

    function hideGlobalOverlay() {
        $globalOverlay.removeClass('fade-in').addClass('fade-out');
        // После анимации скрываем
        setTimeout(() => {
            $globalOverlay.removeClass('fade-out').addClass('hidden');
        }, 300);
    }

    // Обработка dragenter
    $(document).on('dragenter', function (e) {
        e.preventDefault();
        e.stopPropagation();
        dragCounter++;
        showGlobalOverlay();
    });

    // Обработка dragleave
    $(document).on('dragleave', function (e) {
        e.preventDefault();
        e.stopPropagation();
        dragCounter--;
        if (dragCounter === 0) {
            hideGlobalOverlay();
        }
    });

    // Обработка dragover
    $(document).on('dragover', function (e) {
        e.preventDefault();
        e.stopPropagation();
        // Можно дополнительные действия при перетаскивании
    });

    // Обработка drop
    $(document).on('drop', function (e) {
        e.preventDefault();
        e.stopPropagation();
        dragCounter = 0;
        hideGlobalOverlay();

        const files = e.originalEvent.dataTransfer.files;
        if (files.length > 0) {
            // Проверяем, является ли первый файл аудио
            const audioFile = Array.from(files).find(file => file.type.startsWith('audio/'));
            if (audioFile) {
                $addTrackDialog.fadeIn(animationSpeed);
                // Создаем объект DataTransfer и добавляем файл
                const dataTransfer = new DataTransfer();
                dataTransfer.items.add(audioFile);

                // Устанавливаем созданный FileList в input
                $trackFileInput[0].files = dataTransfer.files;

                // Отображаем информацию о выбранном файле
                $selectedFileName.text(audioFile.name);
                $selectedFileInfo.removeClass('hidden');
            } else {
                showNotification('Пожалуйста, перетащите аудио-файл.');
            }
        }
    });

    // Получение трека по ID (для копирования ссылки)
    function getTrackById(trackId) {
        return allTracks.find(track => track.id === trackId) || null;
    }

    // Первоначальная загрузка треков
    loadTracks();
});
```


templates/index.html
```html
<!-- templates/index.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>LitMusic</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- Стили -->
    <link rel="stylesheet" href="../static/styles.css">
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto+Slab:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
</head>
<body>

<div class="intro-container">
    <div class="litmusic-text">LitMusic</div>
</div>

<div id="notifications-container"></div>
<!-- Верхняя панель -->
<div class="top-panel">
    <header>
        <h1>LitMusic</h1>
        <button id="add-track-button">Добавить Трек</button>
    </header>

    <!-- Поисковая панель -->
    <div id="search-container">
        <form id="search-form">
            <input type="text" id="search-name" placeholder="Поиск" name="name" autocomplete="off">
            <button type="submit" class="small-button"><img src="../static/icons/finder.webp" alt="Поиск"></button>
            <button type="button" class="small-button" id="clear-search"><img src="../static/icons/text.webp" alt="Очистить"></button>
        </form>
    </div>
</div>


<!-- Список треков -->
<main id="tracks-container"></main>

<!-- Контейнер для пагинации -->
<!--<div id="pagination-controls"></div> -->

<!-- Проигрыватель -->
<div id="audio-player" class="minimized">
    <div id="player-header">
        <span id="now-playing">Сейчас играет: <span id="track-title"></span></span>
        <button class="small-button" id="toggle-player-button"><img src="../static/icons/arrow.webp"></button>
    </div>
    <audio id="audio-control" controls controlsList="nodownload nofullscreen noremoteplayback noplaybackrate"></audio>
</div>

<!-- Диалоговое окно добавления трека -->
<div id="add-track-dialog" class="modal">
    <div class="modal-content">
        <span class="close-button" id="close-add-dialog">&times;</span>
        <h2>Добавить Новый Трек</h2>
        <form id="add-track-form">
            <label for="track-name">Название Трека:</label>
            <input type="text" id="track-name" name="name" required>

            <label for="track-description">Описание:</label>
            <textarea id="track-description" name="description"></textarea>

            <label for="track-url">Ссылка на трек (необязательно):</label>
            <input type="url" id="track-url" name="url" placeholder="https://example.com/track">

            <label>Теги:</label>
            <div id="tags-container">
                <!-- Контейнер тегов -->
            </div>
            <button type="button" class="small-button" id="add-tag-button">+</button>

            <!-- Новый блок для загрузки файла -->
            <label for="track-file">Файл Трека:</label>
            <div id="file-drop-area">
                <p>Перетащите сюда файл или нажмите для выбора</p>
                <input type="file" id="track-file" name="file" accept="audio/*" required>
            </div>

            <!-- Добавленный блок для отображения выбранного файла -->
            <div id="selected-file-info" class="hidden">
                <img src="../static/icons/audio.webp">
                <span id="selected-file-name"></span>
                <button type="button" class="small-button" id="remove-selected-file">&times;</button>
            </div>

            <button class="submit-btn" type="submit">Добавить</button>
        </form>
    </div>
</div>

<!-- Диалоговое окно описания -->
<div id="description-dialog" class="modal">
    <div class="modal-content">
        <span class="close-button" id="close-description-dialog">&times;</span>
        <h2 id="description-title"></h2>
        <p id="description-text"></p>
    </div>
</div>

<!-- Overlay для глобального перетаскивания файлов -->
<div id="global-overlay" class="overlay hidden">
    <div class="overlay-content">
        <p>Отпустите файл, чтобы добавить трек</p>
    </div>
</div>

<!-- Скрипты -->
<script
  src="https://code.jquery.com/jquery-3.7.1.js"
  integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4="
  crossorigin="anonymous"></script>
<script src="../static/app.js"></script>

</body>
</html>
```


Привет, сделай так, чтобы на сайте была кнопка сверху в топбаре "Админ". Когда на нее нажимаешь, то открывается /login_admin страница, содержащая текстовое с паролем поле и кнопку "Войти". Как только на нее нажимаешь, то введенный код будет проверяться с заданным заранее на сервере. По умолчанию - это "viktortop4ik". Если код верных, то нам в coockie запушется специальный jwt ключ, который будет нужен для того, чтобы: удалять треки, добавлять треки.
Сделай так, что если у нас нет куки/он ложный, то кнопки с удалением вообще не будет, а также не будет кнопки с добавлением трека.