Res4ik / Yandex Music Downloader

// ==UserScript==
// @name              Yandex Music Downloader
// @name:ru           Загрузчик Яндекс.Музыки
// @namespace         https://openuserjs.org/users/Res4ik
// @version           1.0.0
// @description       Downloads tracks from Yandex.Music in high quality. Requires an active Yandex.Plus subscription and your YANDEX_TOKEN in the code.
// @description:ru    Скачивает треки с Яндекс.Музыки в высоком качестве. Требуется активная подписка Яндекс.Плюс и указание вашего YANDEX_TOKEN в коде.
// @author            Res4ik
// @copyright         2025, Res4ik
// @license           MIT
// @icon              https://www.google.com/s2/favicons?sz=64&domain=music.yandex.ru
// @homepageURL       https://openuserjs.org/scripts/Res4ik/Yandex_Music_Downloader
// @supportURL        https://openuserjs.org/scripts/Res4ik/Yandex_Music_Downloader/issues
// @updateURL         https://openuserjs.org/meta/Res4ik/Yandex_Music_Downloader.meta.js
// @downloadURL       https://openuserjs.org/install/Res4ik/Yandex_Music_Downloader.user.js
// @match             https://music.yandex*
// @grant             GM_xmlhttpRequest
// @grant             GM_registerMenuCommand
// @grant             GM_download
// @require           https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js
// @connect           api.music.yandex.net
// @connect           storage.mds.yandex.net
// @connect           avatars.yandex.net
// @connect           *strm.yandex.net
// @connect           strm.yandex.net
// @run-at            document-end
// ==/UserScript==

(function() {
    'use strict';

    console.log('Yandex Music Downloader запущен');

    // Глобальная переменная для контроля остановки скачивания
    let shouldStopDownload = false;
    let shouldStopScrolling = false;
    let isDownloading = false;

    // UI элементы
    let downloadFrame = null;
    let discoveredTracks = [];
    let trackIndexMap = new Map();
    let selectedTrackIds = new Set();

    const YANDEX_TOKEN = '';
    const SIGN_KEY = 'p93jhgh689SBReK6ghtw62';

    // Описываем типы блоков метаданных согласно спецификации FLAC
    const METADATA_BLOCK_TYPE = {
        STREAMINFO: 0,
        PADDING: 1,
        APPLICATION: 2,
        SEEKTABLE: 3,
        VORBIS_COMMENT: 4,
        CUESHEET: 5,
        PICTURE: 6,
    };

    // Флаг, указывающий, что это последний блок метаданных
    const LAST_METADATA_BLOCK_FLAG = 0x80;

    // Стандартный размер блока STREAMINFO в байтах
    const STREAMINFO_BLOCK_SIZE = 34;

    // Тип картинки "Обложка"
    const PICTURE_TYPE_FRONT_COVER = 3;

    // Размер заголовка блока метаданных FLAC
    const METADATA_BLOCK_HEADER_SIZE = 4;

    GM_registerMenuCommand('Скачать с текущей страницы', initiateDownload);

    function createDownloadFrame(isParsingMode = false) {
        if (downloadFrame) return;

        downloadFrame = document.createElement('div');
        downloadFrame.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            width: 320px;
            background: #fff;
            border: 2px solid #ffdb4d;
            border-radius: 8px;
            padding: 16px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            z-index: 10000;
            font-family: Arial, sans-serif;
            animation: slideIn 0.3s ease-out;
        `;

        downloadFrame.innerHTML = `
            <div style="font-size: 16px; font-weight: bold; margin-bottom: 12px; color: #333;">
                Скачивание треков
            </div>
            <div id="download-status" style="font-size: 14px; color: #666; margin-bottom: 8px;">
                Обнаружено треков: <span id="total-tracks">0</span>
                <button id="toggle-tracklist-btn" style="
                    margin-left: 8px;
                    padding: 2px 8px;
                    background: #f0f0f0;
                    border: 1px solid #ccc;
                    border-radius: 3px;
                    cursor: pointer;
                    font-size: 12px;
                ">▼</button>
            </div>
            <div id="tracklist-container" style="display: none; max-height: 300px; overflow-y: auto; margin-bottom: 12px; border: 1px solid #ddd; border-radius: 4px; padding: 8px; background: #f9f9f9;">
                <div style="margin-bottom: 8px; display: flex; gap: 8px;">
                    <button id="select-all-btn" style="padding: 4px 8px; background: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px;">Выбрать все</button>
                    <button id="deselect-all-btn" style="padding: 4px 8px; background: #ff3347; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px;">Снять все</button>
                </div>
                <div id="tracklist" style="font-size: 12px; color: #333;"></div>
            </div>
            <div id="download-progress" style="font-size: 14px; color: #666; margin-bottom: 12px; display: none;">
                Скачивается: <span id="current-track">0</span> из <span id="total-tracks-progress">0</span>
            </div>
            <button id="stop-download-btn" style="
                width: 100%;
                padding: 10px;
                background: #ff3347;
                color: white;
                border: none;
                border-radius: 4px;
                font-size: 14px;
                font-weight: bold;
                cursor: pointer;
                transition: background 0.2s;
            ">
                ${isParsingMode ? 'Остановить парсинг' : 'Скачать'}
            </button>
        `;

        document.body.appendChild(downloadFrame);

        const btn = document.getElementById('stop-download-btn');
        let clickHandlerAttached = false;
        btn.addEventListener('click', () => {
            if (isParsingMode && !clickHandlerAttached) {
                shouldStopScrolling = true;
                btn.textContent = 'Остановка...';
                btn.disabled = true;
            } else if (isDownloading) {
                shouldStopDownload = true;
                btn.textContent = 'Остановка...';
                btn.disabled = true;
            } else {
                clickHandlerAttached = true;
                startDownload();
            }
        });

        document.getElementById('toggle-tracklist-btn').addEventListener('click', () => {
            const container = document.getElementById('tracklist-container');
            const btn = document.getElementById('toggle-tracklist-btn');
            if (container.style.display === 'none') {
                container.style.display = 'block';
                btn.textContent = '▲';
            } else {
                container.style.display = 'none';
                btn.textContent = '▼';
            }
        });

        document.getElementById('select-all-btn').addEventListener('click', () => {
            const checkboxes = document.querySelectorAll('#tracklist input[type="checkbox"]');
            checkboxes.forEach(cb => {
                cb.checked = true;
                selectedTrackIds.add(cb.dataset.trackId);
            });
        });

        document.getElementById('deselect-all-btn').addEventListener('click', () => {
            const checkboxes = document.querySelectorAll('#tracklist input[type="checkbox"]');
            checkboxes.forEach(cb => {
                cb.checked = false;
                selectedTrackIds.delete(cb.dataset.trackId);
            });
        });
    }
    function updateTrackList() {
        const tracklistDiv = document.getElementById('tracklist');
        if (!tracklistDiv) return;

        tracklistDiv.innerHTML = '';
        discoveredTracks.forEach((track, index) => {
            const trackDiv = document.createElement('div');
            trackDiv.style.cssText = 'margin-bottom: 4px; display: flex; align-items: center;';

            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.checked = true;
            checkbox.dataset.trackId = track.id;
            checkbox.style.marginRight = '8px';
            selectedTrackIds.add(track.id);
            trackIndexMap.set(track.id, index + 1);

            checkbox.addEventListener('change', (e) => {
                if (e.target.checked) {
                    selectedTrackIds.add(track.id);
                } else {
                    selectedTrackIds.delete(track.id);
                }
            });

            const label = document.createElement('label');
            label.textContent = `${index + 1}. ${track.ariaLabel || 'Трек ' + track.id}`;
            label.style.cssText = 'cursor: pointer; user-select: none; color: #333;';
            label.addEventListener('click', () => {
                checkbox.checked = !checkbox.checked;
                checkbox.dispatchEvent(new Event('change'));
            });

            trackDiv.appendChild(checkbox);
            trackDiv.appendChild(label);
            tracklistDiv.appendChild(trackDiv);
        });
    }
    function updateFrameToDownloadMode() {
        const progressDiv = document.getElementById('download-progress');
        const btn = document.getElementById('stop-download-btn');

        progressDiv.style.display = 'block';
        btn.style.background = '#ff3347';
        btn.textContent = 'Остановка...';
        btn.disabled = false;

        isDownloading = true;
    }
    function removeDownloadFrame() {
        if (downloadFrame) {
            downloadFrame.style.animation = 'slideOut 0.3s ease-out';
            setTimeout(() => {
                if (downloadFrame && downloadFrame.parentNode) {
                    downloadFrame.parentNode.removeChild(downloadFrame);
                    downloadFrame = null;
                }
            }, 300);
        }
        shouldStopScrolling = false;
        isDownloading = false;
    }
    function initiateDownload() {
        const url = window.location.href;

        if (url.includes('/track/')) {
            handleTrackDownload(url);
        } else if (url.includes('/album/')) {
            handleAlbumDownload(url);
        } else if (url.includes('/playlist/') || url.includes('/playlists/')) {
            handlePlaylistDownload(url);
        } else {
            alert('Страница не является треком, альбомом или плейлистом.');
        }
    }
    function handleTrackDownload(url) {
        downloadCurrentTrack();
    }
    async function handleAlbumDownload(url) {
        try {
            const albumId = extractAlbumId(url);
            if (!albumId) {
                return;
            }

            const albumInfo = await getAlbumInfo(albumId);

            shouldStopDownload = false;
            shouldStopScrolling = false;
            createDownloadFrame(true);

            discoveredTracks = [];
            selectedTrackIds.clear();
            trackIndexMap.clear();

            albumInfo.tracks.forEach((track, index) => {
                discoveredTracks.push({
                    id: track.id,
                    ariaLabel: `${track.artists.map(a => a.name).join(', ')} ${track.title}`
                });
                selectedTrackIds.add(track.id);
                trackIndexMap.set(track.id, index + 1);
            });

            document.getElementById('total-tracks').textContent = albumInfo.tracks.length;
            updateTrackList();

            const btn = document.getElementById('stop-download-btn');
            btn.onclick = null;
            btn.textContent = 'Скачать';
            btn.style.background = '#4CAF50';
            btn.disabled = false;

            btn.addEventListener('click', () => {
                btn.textContent = 'Запуск...';
                btn.disabled = true;
                if (window.downloadStartResolver) {
                    window.downloadStartResolver();
                }
            });

            await waitForDownloadStart();

            const tracksToDownload = albumInfo.tracks.filter(track => selectedTrackIds.has(track.id));

            updateFrameToDownloadMode();
            shouldStopDownload = false;
            document.getElementById('total-tracks-progress').textContent = tracksToDownload.length;

            const artist = albumInfo.artists;
            const year = albumInfo.year;
            const albumTitle = albumInfo.title;
            const folderName = sanitizeFilename(`${artist} - ${year} - ${albumTitle}`);

            for (let i = 0; i < tracksToDownload.length; i++) {
                if (shouldStopDownload) {
                    break;
                }

                const track = tracksToDownload[i];
                document.getElementById('current-track').textContent = i + 1;

                try {
                    const originalIndex = trackIndexMap.get(track.id) || (i + 1);
                    await downloadSingleTrack(track.id, folderName, albumInfo.coverUri, originalIndex);
                } catch (error) {
                    console.error(`Ошибка при скачивании трека ${track.title}:`, error);
                }

                if (i < tracksToDownload.length - 1) {
                    await sleep(2000);
                }
            }

            removeDownloadFrame();
            if (!shouldStopDownload) {
            }

        } catch (error) {
            removeDownloadFrame();
            alert('Ошибка при скачивании альбома: ' + error.message);
        }
    }
    function extractAlbumId(url) {
        const match = url.match(/\/album\/(\d+)/);
        return match ? match[1] : null;
    }
    function getAlbumInfo(albumId) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `https://api.music.yandex.net/albums/${albumId}/with-tracks`,
                headers: {
                    'Authorization': `OAuth ${YANDEX_TOKEN}`,
                    'Accept': 'application/json',
                    'X-Requested-With': 'XMLHttpRequest'
                },
                anonymous: true,
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data.result) {
                            const album = data.result;
                            const tracks = album.volumes && album.volumes.length > 0 ? album.volumes[0] : [];

                            const info = {
                                id: album.id,
                                title: album.title,
                                artists: album.artists.map(a => a.name).join(', '),
                                year: album.year || new Date().getFullYear(),
                                coverUri: album.coverUri,
                                tracks: tracks
                            };
                            resolve(info);
                        } else {
                            reject(new Error('Альбом не найден'));
                        }
                    } catch (e) {
                        reject(new Error('Ошибка парсинга ответа альбома: ' + e.message));
                    }
                },
                onerror: function(error) {
                    reject(new Error('Ошибка запроса информации об альбоме'));
                }
            });
        });
    }
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    async function parsePlaylistFromPage() {
        try {
            shouldStopScrolling = false;
            createDownloadFrame(true);

            discoveredTracks = [];

            const scrollContainer = document.querySelector('[data-test-id="virtuoso-scroller"]');
            if (!scrollContainer) {
                throw new Error('Контейнер для прокрутки не найден');
            }

            const tracksContainer = document.querySelector('[data-test-id="virtuoso-item-list"]');
            if (!tracksContainer) {
                throw new Error('Контейнер треков не найден');
            }

            const tracksSet = new Set();
            const tracksData = [];

            const collectVisibleTracks = () => {
                const trackElements = tracksContainer.querySelectorAll('div[data-index]');
                let newTracksCount = 0;

                trackElements.forEach(trackElement => {
                    try {
                        let ariaLabel = '';
                        const trackCard = trackElement.querySelector('[aria-label]');
                        if (trackCard) {
                            ariaLabel = trackCard.getAttribute('aria-label') || '';
                        }

                        const trackLink = trackElement.querySelector('a[href*="/track/"]');
                        if (!trackLink) return;

                        const trackId = extractTrackId(trackLink.href);
                        if (!trackId) return;

                        if (tracksSet.has(trackId)) return;

                        tracksSet.add(trackId);
                        tracksData.push({
                            id: trackId,
                            ariaLabel: ariaLabel
                        });
                        newTracksCount++;

                        discoveredTracks = tracksData;
                        document.getElementById('total-tracks').textContent = tracksData.length;
                        updateTrackList();
                    } catch (error) {
                        console.error('Ошибка при обработке трека:', error);
                    }
                });

                return newTracksCount;
            };

            let previousTrackCount = 0;
            let scrollAttempts = 0;
            const maxScrollAttempts = 200;
            let noNewTracksCount = 0;

            while (scrollAttempts < maxScrollAttempts) {
                if (shouldStopScrolling) {
                    break;
                }

                const newTracks = collectVisibleTracks();
                const currentTrackCount = tracksData.length;

                if (currentTrackCount === previousTrackCount) {
                    noNewTracksCount++;
                } else {
                    noNewTracksCount = 0;
                }

                const footerElement = document.querySelector('footer');
                if (footerElement) {
                    const footerRect = footerElement.getBoundingClientRect();
                    const isFooterVisible = footerRect.top < window.innerHeight && footerRect.bottom > 0;

                    if (isFooterVisible) {
                        collectVisibleTracks();
                        break;
                    }
                }

                if (noNewTracksCount >= 5) {
                    break;
                }

                if (scrollContainer) {
                    const currentScroll = scrollContainer.scrollTop;
                    const scrollStep = 900;
                    scrollContainer.scrollTo({
                        top: currentScroll + scrollStep,
                        behavior: 'smooth'
                    });
                }

                await sleep(1500);

                previousTrackCount = currentTrackCount;
                scrollAttempts++;
            }

            if (scrollAttempts >= maxScrollAttempts) {
            }

            if (tracksData.length === 0) {
                throw new Error('Не удалось найти треки в плейлисте');
            }

            const btn = document.getElementById('stop-download-btn');
            btn.onclick = null;
            btn.textContent = 'Скачать';
            btn.style.background = '#4CAF50';
            btn.disabled = false;

            btn.addEventListener('click', () => {
                btn.textContent = 'Запуск...';
                btn.disabled = true;
                if (window.downloadStartResolver) {
                    window.downloadStartResolver();
                }
            });

            await waitForDownloadStart();

            return tracksData;

        } catch (error) {
            removeDownloadFrame();
            throw error;
        }
    }
    function waitForDownloadStart() {
        return new Promise((resolve) => {
            window.downloadStartResolver = resolve;
        });
    }
    function extractTrackId(url) {
        const match = url.match(/\/track\/(\d+)/);
        return match ? match[1] : null;
    }
    function extractPlaylistUuid(url) {
        const match = url.match(/\/playlists\/([a-zA-Z0-9\.\-]+)/);
        return match ? match[1] : null;
    }
    function getPlaylistInfoByUuid(playlistUuid) {
        return new Promise((resolve, reject) => {
            const domain = window.location.hostname.replace('music.', '');
            const apiUrl = `https://api.music.${domain}/playlist/${playlistUuid}?resumeStream=false&richTracks=true`;

            GM_xmlhttpRequest({
                method: 'GET',
                url: apiUrl,
                headers: {
                    'Authorization': `OAuth ${YANDEX_TOKEN}`,
                    'Accept': 'application/json',
                    'X-Requested-With': 'XMLHttpRequest'
                },
                anonymous: true,
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data.result) {
                            const playlist = data.result;
                            const tracks = playlist.tracks || [];

                            const info = {
                                title: playlist.title,
                                owner: playlist.owner ? playlist.owner.login : 'unknown',
                                coverUri: playlist.cover ? playlist.cover.uri : (playlist.ogImage ? playlist.ogImage.replace('%%', '400x400') : null),
                                tracks: tracks.map((item, index) => {
                                    const track = item.track;
                                    return {
                                        id: track.id,
                                        title: track.title,
                                        artists: track.artists.map(a => a.name).join(', '),
                                        ariaLabel: `${track.artists.map(a => a.name).join(', ')} - ${track.title}`,
                                        originalIndex: index + 1
                                    };
                                })
                            };
                            resolve(info);
                        } else {
                            reject(new Error('Плейлист не найден'));
                        }
                    } catch (e) {
                        reject(new Error('Ошибка парсинга ответа плейлиста: ' + e.message));
                    }
                },
                onerror: function(error) {
                    reject(new Error('Ошибка запроса информации о плейлисте'));
                }
            });
        });
    }
    async function handlePlaylistDownload(url) {
        try {
            if (url.includes('/playlists/') && !url.includes('/users/')) {
                const playlistUuid = extractPlaylistUuid(url);
                if (!playlistUuid) {
                    alert('Не удалось определить UUID плейлиста.');
                    return;
                }

                const playlistInfo = await getPlaylistInfoByUuid(playlistUuid);
                const folderName = sanitizeFilename(playlistInfo.title);

                shouldStopScrolling = false;
                createDownloadFrame(true);

                discoveredTracks = [];
                selectedTrackIds.clear();
                trackIndexMap.clear();

                playlistInfo.tracks.forEach((track) => {
                    discoveredTracks.push({
                        id: track.id,
                        ariaLabel: track.ariaLabel
                    });
                    selectedTrackIds.add(track.id);
                    trackIndexMap.set(track.id, track.originalIndex);
                });

                document.getElementById('total-tracks').textContent = playlistInfo.tracks.length;
                updateTrackList();

                const btn = document.getElementById('stop-download-btn');
                btn.onclick = null;
                btn.textContent = 'Скачать';
                btn.style.background = '#4CAF50';
                btn.disabled = false;

                btn.addEventListener('click', () => {
                    btn.textContent = 'Запуск...';
                    btn.disabled = true;
                    if (window.downloadStartResolver) {
                        window.downloadStartResolver();
                    }
                });

                await waitForDownloadStart();

                const tracksToDownload = discoveredTracks.filter(track => selectedTrackIds.has(track.id));

                updateFrameToDownloadMode();
                shouldStopDownload = false;
                document.getElementById('total-tracks-progress').textContent = tracksToDownload.length;

                for (let i = 0; i < tracksToDownload.length; i++) {
                    if (shouldStopDownload) {
                        break;
                    }

                    const track = tracksToDownload[i];
                    document.getElementById('current-track').textContent = i + 1;

                    try {
                        const originalIndex = trackIndexMap.get(track.id) || (i + 1);
                        await downloadSingleTrack(track.id, folderName, playlistInfo.coverUri, originalIndex);
                    } catch (error) {
                        console.error(`Ошибка при скачивании трека ${track.id}:`, error);
                    }

                    if (i < tracksToDownload.length - 1) {
                        await sleep(2000);
                    }
                }

                removeDownloadFrame();
            } else {

                const playlistData = extractPlaylistData(url);
                if (!playlistData) {
                    alert('Не удалось определить данные плейлиста.');
                    return;
                }

                const playlistInfo = await getPlaylistInfo(playlistData.owner, playlistData.kind);

                const playlistTitle = playlistInfo.title;
                const folderName = sanitizeFilename(playlistTitle);

                const tracksToDownload = playlistInfo.tracks;

                shouldStopDownload = false;
                isDownloading = true;
                createDownloadFrame(false);
                document.getElementById('total-tracks').textContent = tracksToDownload.length;
                document.getElementById('total-tracks-progress').textContent = tracksToDownload.length;
                document.getElementById('download-progress').style.display = 'block';

                for (let i = 0; i < tracksToDownload.length; i++) {
                    if (shouldStopDownload) {
                        break;
                    }

                    const track = tracksToDownload[i];
                    document.getElementById('current-track').textContent = i + 1;

                    try {
                        await downloadSingleTrack(track.id, folderName, playlistInfo.coverUri);
                    } catch (error) {
                        console.error(`Ошибка при скачивании трека ${track.title}:`, error);
                    }

                    if (i < tracksToDownload.length - 1) {
                        await sleep(2000);
                    }
                }

                removeDownloadFrame();
            }

        } catch (error) {
            removeDownloadFrame();
            alert('Ошибка при скачивании плейлиста: ' + error.message);
        }
    }
    function extractPlaylistData(url) {
        let match = url.match(/\/users\/([^\/]+)\/playlists\/(\d+)/);
        if (match) {
            return { owner: match[1], kind: match[2] };
        }

        match = url.match(/\/playlists\/([^\/]+)/);
        if (match) {
            return { owner: match[1], kind: null };
        }

        return null;
    }
    function getPlaylistInfo(owner, kind) {
        return new Promise((resolve, reject) => {
            const apiUrl = kind
                ? `https://api.music.yandex.net/users/${owner}/playlists/${kind}`
                : `https://api.music.yandex.net/playlists/${owner}`;

            GM_xmlhttpRequest({
                method: 'GET',
                url: apiUrl,
                headers: {
                    'Authorization': `OAuth ${YANDEX_TOKEN}`,
                    'Accept': 'application/json',
                    'X-Requested-With': 'XMLHttpRequest'
                },
                anonymous: true,
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data.result) {
                            const playlist = data.result;
                            const tracks = playlist.tracks || [];

                            const info = {
                                title: playlist.title,
                                owner: playlist.owner ? playlist.owner.login : owner,
                                coverUri: playlist.cover ? playlist.cover.uri : (playlist.ogImage ? playlist.ogImage.replace('%%', '400x400') : null),
                                tracks: tracks.map(t => t.track || t)
                            };
                            resolve(info);
                        } else {
                            reject(new Error('Плейлист не найден'));
                        }
                    } catch (e) {
                        reject(new Error('Ошибка парсинга ответа плейлиста: ' + e.message));
                    }
                },
                onerror: function(error) {
                    reject(new Error('Ошибка запроса информации о плейлисте'));
                }
            });
        });
    }
    function formatVorbisComment(vendorString, commentList) {
        const bufferArray = [];
        const vendorStringBuffer = Buffer.from(vendorString, 'utf8');
        const vendorLengthBuffer = Buffer.alloc(4);
        vendorLengthBuffer.writeUInt32LE(vendorStringBuffer.length, 0);
        bufferArray.push(vendorLengthBuffer);
        bufferArray.push(vendorStringBuffer);
        const commentListLengthBuffer = Buffer.alloc(4);
        commentListLengthBuffer.writeUInt32LE(commentList.length, 0);
        bufferArray.push(commentListLengthBuffer);
        commentList.forEach((comment) => {
            const commentBuffer = Buffer.from(comment, 'utf8');
            const commentLengthBuffer = Buffer.alloc(4);
            commentLengthBuffer.writeUInt32LE(commentBuffer.length, 0);
            bufferArray.push(commentLengthBuffer);
            bufferArray.push(commentBuffer);
        });
        return Buffer.concat(bufferArray);
    }
    class Metaflac {
        constructor(flac) {
            if (typeof flac !== 'string' && !Buffer.isBuffer(flac) && !(flac instanceof ArrayBuffer)) {
                throw new Error('Metaflac(flac) flac must be string, buffer or ArrayBuffer.');
            }
            this.flac = flac;
            this.buffer = null;
            this.marker = '';
            this.streamInfo = null;
            this.blocks = [];
            this.padding = null;
            this.vorbisComment = null;
            this.vendorString = '';
            this.tags = [];
            this.pictures = [];
            this.picturesSpecs = [];
            this.picturesDatas = [];
            this.framesOffset = 0;
            this.init();
        }

        init() {
            if (this.flac instanceof ArrayBuffer) {
                this.buffer = Buffer.from(this.flac);
            } else {
                this.buffer = Buffer.isBuffer(this.flac) ? this.flac : this.flac;
            }

            let offset = 0;
            const marker = this.buffer.slice(0, offset += 4).toString('ascii');
            if (marker !== 'fLaC') {
                throw new Error('The file does not appear to be a FLAC file.');
            }

            let blockType = 0;
            let isLastBlock = false;
            let blocksProcessed = 0;
            while (!isLastBlock && offset < this.buffer.length - 4) {
                blockType = this.buffer.readUInt8(offset++);
                isLastBlock = blockType > 128;
                blockType = blockType % 128;

                const blockLength = this.buffer.readUIntBE(offset, 3);
                offset += 3;

                if (offset + blockLength > this.buffer.length) {
                    break;
                }

                if (blockType === METADATA_BLOCK_TYPE.STREAMINFO) {
                    this.streamInfo = this.buffer.slice(offset, offset + blockLength);
                }

                if (blockType === METADATA_BLOCK_TYPE.PADDING) {
                    this.padding = this.buffer.slice(offset, offset + blockLength);
                }

                if (blockType === METADATA_BLOCK_TYPE.VORBIS_COMMENT) {
                    this.vorbisComment = this.buffer.slice(offset, offset + blockLength);
                    this.parseVorbisComment();
                }

                if (blockType === METADATA_BLOCK_TYPE.PICTURE) {
                    this.pictures.push(this.buffer.slice(offset, offset + blockLength));
                    this.parsePictureBlock();
                }

                if ([METADATA_BLOCK_TYPE.APPLICATION, METADATA_BLOCK_TYPE.SEEKTABLE, METADATA_BLOCK_TYPE.CUESHEET].includes(blockType)) {
                    this.blocks.push([blockType, this.buffer.slice(offset, offset + blockLength)]);
                }
                offset += blockLength;
                blocksProcessed++;

                if (blocksProcessed > 100) {
                    break;
                }
            }
            this.framesOffset = offset;
        }

        parseVorbisComment() {
            if (!this.vorbisComment || this.vorbisComment.length < 8) {
                return;
            }
            const vendorLength = this.vorbisComment.readUInt32LE(0) >>> 0;
            const vendorEnd = 4 + vendorLength;
            if (vendorEnd > this.vorbisComment.length) return;
            this.vendorString = this.vorbisComment.slice(4, vendorEnd).toString('utf8');
            if (vendorEnd + 4 > this.vorbisComment.length) return;
            const userCommentListLength = this.vorbisComment.readUInt32LE(vendorEnd);
            const userCommentListBuffer = this.vorbisComment.slice(vendorEnd + 4);
            for (let offset = 0; offset < userCommentListBuffer.length; ) {
                if (offset + 4 > userCommentListBuffer.length) break;
                const length = userCommentListBuffer.readUInt32LE(offset);
                offset += 4;
                if (offset + length > userCommentListBuffer.length) break;
                const comment = userCommentListBuffer.slice(offset, offset += length).toString('utf8');
                this.tags.push(comment);
            }
        }

        parsePictureBlock() {
            this.pictures.forEach(picture => {
                let offset = 0;
                const type = picture.readUInt32BE(offset);
                offset += 4;
                const mimeTypeLength = picture.readUInt32BE(offset);
                offset += 4;
                const mime = picture.slice(offset, offset + mimeTypeLength).toString('ascii');
                offset += mimeTypeLength;
                const descriptionLength = picture.readUInt32BE(offset);
                offset += 4;
                const description = picture.slice(offset, offset + descriptionLength).toString('utf8');
                offset += descriptionLength;
                const width = picture.readUInt32BE(offset);
                offset += 4;
                const height = picture.readUInt32BE(offset);
                offset += 4;
                const depth = picture.readUInt32BE(offset);
                offset += 4;
                const colors = picture.readUInt32BE(offset);
                offset += 4;
                const pictureDataLength = picture.readUInt32BE(offset);
                offset += 4;
                const pictureData = picture.slice(offset, offset + pictureDataLength);
                this.picturesSpecs.push({
                    type,
                    mime,
                    description,
                    width,
                    height,
                    depth,
                    colors,
                });
                this.picturesDatas.push(pictureData);
            });
        }
    }
    if (typeof Buffer === 'undefined') {
        window.Buffer = class Buffer extends Uint8Array {
            constructor(arg, encodingOrOffset, length) {
                if (typeof arg === 'number') {
                    super(arg);
                } else if (typeof arg === 'string') {
                    const encoding = encodingOrOffset || 'utf8';
                    if (encoding === 'utf8') {
                        const encoder = new TextEncoder();
                        const arr = encoder.encode(arg);
                        super(arr);
                    } else if (encoding === 'ascii') {
                        super(arg.length);
                        for (let i = 0; i < arg.length; i++) {
                            this[i] = arg.charCodeAt(i) & 0xFF;
                        }
                    } else if (encoding === 'hex') {
                        super(arg.length / 2);
                        for (let i = 0; i < arg.length; i += 2) {
                            this[i / 2] = parseInt(arg.substr(i, 2), 16);
                        }
                    }
                } else if (arg instanceof ArrayBuffer) {
                    super(arg, encodingOrOffset, length);
                } else if (ArrayBuffer.isView(arg)) {
                    super(arg.buffer, arg.byteOffset, arg.byteLength);
                } else {
                    super(arg);
                }
            }

            static alloc(size) {
                return new Buffer(size);
            }

            static from(arg, encoding) {
                return new Buffer(arg, encoding);
            }

            static concat(list) {
                const totalLength = list.reduce((acc, buf) => acc + buf.length, 0);
                const result = new Buffer(totalLength);
                let offset = 0;
                for (const buf of list) {
                    result.set(buf, offset);
                    offset += buf.length;
                }
                return result;
            }

            static isBuffer(obj) {
                return obj instanceof Buffer;
            }

            toString(encoding = 'utf8') {
                if (encoding === 'utf8') {
                    return new TextDecoder().decode(this);
                } else if (encoding === 'ascii') {
                    return String.fromCharCode(...this);
                } else if (encoding === 'hex') {
                    return Array.from(this).map(b => b.toString(16).padStart(2, '0')).join('');
                }
                return new TextDecoder().decode(this);
            }

            slice(start, end) {
                return new Buffer(super.slice(start, end));
            }

            readUInt8(offset) {
                return this[offset];
            }

            readUInt32LE(offset) {
                return this[offset] | (this[offset + 1] << 8) | (this[offset + 2] << 16) | (this[offset + 3] << 24);
            }

            readUInt32BE(offset) {
                return (this[offset] << 24) | (this[offset + 1] << 16) | (this[offset + 2] << 8) | this[offset + 3];
            }

            readUIntBE(offset, byteLength) {
                let val = 0;
                for (let i = 0; i < byteLength; i++) {
                    val = (val << 8) | this[offset + i];
                }
                return val;
            }

            writeUInt8(value, offset) {
                this[offset] = value & 0xFF;
            }

            writeUInt32LE(value, offset) {
                this[offset] = value & 0xFF;
                this[offset + 1] = (value >>> 8) & 0xFF;
                this[offset + 2] = (value >>> 16) & 0xFF;
                this[offset + 3] = (value >>> 24) & 0xFF;
            }

            writeUInt32BE(value, offset) {
                this[offset] = (value >>> 24) & 0xFF;
                this[offset + 1] = (value >>> 16) & 0xFF;
                this[offset + 2] = (value >>> 8) & 0xFF;
                this[offset + 3] = value & 0xFF;
            }

            writeUIntBE(value, offset, byteLength) {
                for (let i = byteLength - 1; i >= 0; i--) {
                    this[offset + i] = value & 0xFF;
                    value = value >>> 8;
                }
            }
        };
    }
    async function downloadCover(coverUri, size = '400x400') {
        if (!coverUri) {
            return null;
        }
        const coverUrl = `https://${coverUri.replace('%%', size)}`;
        try {
            return await downloadAudioData(coverUrl);
        } catch (error) {
            console.error('Не удалось скачать обложку', error);
            return null;
        }
    }
    function extractFlacFromMp4(mp4Data) {
        const view = new DataView(mp4Data);

        function readUint32BE(offset) {
            if (offset + 4 > mp4Data.byteLength) return 0;
            return view.getUint32(offset, false);
        }

        function readAtomType(offset) {
            if (offset + 4 > mp4Data.byteLength) return '';
            return String.fromCharCode(
                view.getUint8(offset + 0),
                view.getUint8(offset + 1),
                view.getUint8(offset + 2),
                view.getUint8(offset + 3)
            );
        }

        function findAtom(atomName, startOffset = 0, endOffset = mp4Data.byteLength) {
            let offset = startOffset;

            while (offset < endOffset - 8) {
                const atomSize = readUint32BE(offset);
                const atomType = readAtomType(offset + 4);

                if (atomSize < 8 || offset + atomSize > mp4Data.byteLength) {
                    break;
                }

                if (atomType === atomName) {
                    return { offset: offset, size: atomSize, dataOffset: offset + 8, dataSize: atomSize - 8 };
                }

                offset += atomSize;
            }
            return null;
        }

        const mdatAtom = findAtom('mdat');

        if (!mdatAtom) {
            return mp4Data;
        }

        const mdatData = new Uint8Array(mp4Data, mdatAtom.dataOffset, mdatAtom.dataSize);

        let flacOffset = -1;
        for (let i = 0; i <= mdatData.length - 4; i++) {
            if (mdatData[i] === 0x66 &&
                mdatData[i + 1] === 0x4C &&
                mdatData[i + 2] === 0x61 &&
                mdatData[i + 3] === 0x43) {
                flacOffset = i;
                break;
            }
        }

        if (flacOffset === -1) {
            return mp4Data.slice(mdatAtom.dataOffset, mdatAtom.dataOffset + mdatAtom.dataSize);
        }

        const afterFlac = flacOffset + 4;

        let metadataOffset = afterFlac;
        let skipBytes = 0;

        for (let i = 0; i < 100; i++) {
            const checkOffset = afterFlac + i;
            if (checkOffset + 4 > mdatData.length) break;

            const byte0 = mdatData[checkOffset];
            const byte1 = mdatData[checkOffset + 1];
            const byte2 = mdatData[checkOffset + 2];
            const byte3 = mdatData[checkOffset + 3];

            if ((byte0 === 0x00 || byte0 === 0x80) && byte1 === 0x00 && byte2 === 0x00 && byte3 === 0x22) {
                skipBytes = i;
                metadataOffset = checkOffset;
                break;
            }
        }

        const flacDataSize = mdatAtom.dataSize - flacOffset - 4 - skipBytes;
        const flacData = new Uint8Array(4 + flacDataSize);

        flacData[0] = 0x66; // 'f'
        flacData[1] = 0x4C; // 'L'
        flacData[2] = 0x61; // 'a'
        flacData[3] = 0x43; // 'C'

        flacData.set(mdatData.slice(metadataOffset, mdatAtom.dataSize), 4);

        return flacData.buffer;
    }
    function validateFlacData(data) {
        const view = new DataView(data);

        if (data.byteLength < 4) {
            return false;
        }

        const marker = String.fromCharCode(
            view.getUint8(0), view.getUint8(1),
            view.getUint8(2), view.getUint8(3)
        );

        return marker === 'fLaC';
    }
    function createVorbisComment(metadata) {
        const comments = [];

        if (metadata.title) comments.push(`TITLE=${metadata.title}`);
        if (metadata.artists) comments.push(`ARTIST=${metadata.artists}`);
        if (metadata.album) comments.push(`ALBUM=${metadata.album}`);
        if (metadata.year) comments.push(`DATE=${metadata.year}`);
        if (metadata.genre) comments.push(`GENRE=${metadata.genre}`);
        if (metadata.trackPosition) {
            comments.push(`TRACKNUMBER=${metadata.trackPosition.index}`);
        }

        const vendor = 'Yandex Music Downloader';
        const vendorBytes = new TextEncoder().encode(vendor);
        const vendorLength = new Uint8Array(4);
        new DataView(vendorLength.buffer).setUint32(0, vendorBytes.length, true);

        const commentCount = new Uint8Array(4);
        new DataView(commentCount.buffer).setUint32(0, comments.length, true);

        const commentBlocks = [];
        let commentBlocksSize = 0;

        for (const comment of comments) {
            const commentBytes = new TextEncoder().encode(comment);
            const commentLength = new Uint8Array(4);
            new DataView(commentLength.buffer).setUint32(0, commentBytes.length, true);

            const block = new Uint8Array(4 + commentBytes.length);
            block.set(commentLength, 0);
            block.set(commentBytes, 4);

            commentBlocks.push(block);
            commentBlocksSize += block.length;
        }

        const totalLength = vendorLength.length + vendorBytes.length +
                           commentCount.length + commentBlocksSize;

        const result = new Uint8Array(totalLength);
        let offset = 0;

        result.set(vendorLength, offset);
        offset += vendorLength.length;

        result.set(vendorBytes, offset);
        offset += vendorBytes.length;

        result.set(commentCount, offset);
        offset += commentCount.length;

        for (const block of commentBlocks) {
            result.set(block, offset);
            offset += block.length;
        }

        return result;
    }
    function createPictureBlock(coverBlob) {
        const coverData = new Uint8Array(coverBlob);

        let mime = 'image/jpeg';
        if (coverData[0] === 0x89 && coverData[1] === 0x50) {
            mime = 'image/png';
        }

        const mimeBytes = new TextEncoder().encode(mime);
        const descriptionBytes = new Uint8Array(0);

        const totalSize = 4 +
                         4 + mimeBytes.length +
                         4 + descriptionBytes.length +
                         4 +
                         4 +
                         4 +
                         4 +
                         4 + coverData.length;

        const result = new Uint8Array(totalSize);
        const view = new DataView(result.buffer);
        let offset = 0;

        view.setUint32(offset, 3, false);
        offset += 4;

        view.setUint32(offset, mimeBytes.length, false);
        offset += 4;

        result.set(mimeBytes, offset);
        offset += mimeBytes.length;

        view.setUint32(offset, 0, false);
        offset += 4;

        view.setUint32(offset, 0, false);
        offset += 4;

        view.setUint32(offset, 0, false);
        offset += 4;

        view.setUint32(offset, 24, false);
        offset += 4;

        view.setUint32(offset, 0, false);
        offset += 4;

        view.setUint32(offset, coverData.length, false);
        offset += 4;

        result.set(coverData, offset);

        return result;
    }
    class FlacFileBuilder {
        constructor() {
            this.blocks = [];
            this.audioFrames = null;
        }

        addStreamInfo(streamInfoBlock) {
            this.blocks.push({ type: METADATA_BLOCK_TYPE.STREAMINFO, data: streamInfoBlock });
        }

        addVorbisComment(metadata) {
            const comments = [];
            if (metadata.title) comments.push(`TITLE=${metadata.title}`);
            if (metadata.artists) comments.push(`ARTIST=${metadata.artists}`);
            if (metadata.album) comments.push(`ALBUM=${metadata.album}`);
            if (metadata.year) comments.push(`DATE=${metadata.year}`);
            if (metadata.genre) comments.push(`GENRE=${metadata.genre}`);
            if (metadata.trackPosition) comments.push(`TRACKNUMBER=${metadata.trackPosition.index}`);

            const vendorString = 'Yandex Music Downloader (Reforged)';
            const encoder = new TextEncoder();
            const vendorBytes = encoder.encode(vendorString);

            const commentListBytes = comments.map(c => {
                const commentBytes = encoder.encode(c);
                const lengthBytes = new Uint8Array(4);
                new DataView(lengthBytes.buffer).setUint32(0, commentBytes.length, true);
                return this.concatBuffers([lengthBytes, commentBytes]);
            });

            const totalCommentsLength = commentListBytes.reduce((sum, b) => sum + b.length, 0);
            const data = new Uint8Array(4 + vendorBytes.length + 4 + totalCommentsLength);
            const view = new DataView(data.buffer);

            let offset = 0;
            view.setUint32(offset, vendorBytes.length, true); offset += 4;
            data.set(vendorBytes, offset); offset += vendorBytes.length;
            view.setUint32(offset, comments.length, true); offset += 4;
            for (const block of commentListBytes) {
                data.set(block, offset);
                offset += block.length;
            }

            this.blocks.push({ type: METADATA_BLOCK_TYPE.VORBIS_COMMENT, data });
        }

        addPicture(coverBlob) {
            if (!coverBlob) return;

            const coverData = new Uint8Array(coverBlob);
            const mime = (coverData[0] === 0x89 && coverData[1] === 0x50) ? 'image/png' : 'image/jpeg';
            const encoder = new TextEncoder();
            const mimeBytes = encoder.encode(mime);
            const descriptionBytes = encoder.encode('Cover');

            const dataSize = 4 + 4 + mimeBytes.length + 4 + descriptionBytes.length + (4 * 4) + 4 + coverData.length;
            const data = new Uint8Array(dataSize);
            const view = new DataView(data.buffer);

            let offset = 0;
            view.setUint32(offset, PICTURE_TYPE_FRONT_COVER, false); offset += 4;
            view.setUint32(offset, mimeBytes.length, false); offset += 4;
            data.set(mimeBytes, offset); offset += mimeBytes.length;
            view.setUint32(offset, descriptionBytes.length, false); offset += 4;
            data.set(descriptionBytes, offset); offset += descriptionBytes.length;
            view.setUint32(offset, 0, false); offset += 4;
            view.setUint32(offset, 0, false); offset += 4;
            view.setUint32(offset, 24, false); offset += 4;
            view.setUint32(offset, 0, false); offset += 4;
            view.setUint32(offset, coverData.length, false); offset += 4;
            data.set(coverData, offset);

            this.blocks.push({ type: METADATA_BLOCK_TYPE.PICTURE, data });
        }

        setAudioFrames(audioFrames) {
            this.audioFrames = new Uint8Array(audioFrames);
        }

        build() {
            const flacMarker = new Uint8Array([0x66, 0x4C, 0x61, 0x43]); // 'fLaC'
            const builtBlocks = [];

            for (let i = 0; i < this.blocks.length; i++) {
                const block = this.blocks[i];
                const isLast = (i === this.blocks.length - 1);

                const header = new Uint8Array(METADATA_BLOCK_HEADER_SIZE);
                const headerView = new DataView(header.buffer);

                const blockType = block.type;
                const headerFirstByte = isLast ? (blockType | LAST_METADATA_BLOCK_FLAG) : blockType;

                headerView.setUint8(0, headerFirstByte);
                headerView.setUint32(0, headerView.getUint32(0) | (block.data.length & 0x00FFFFFF), false);

                builtBlocks.push(this.concatBuffers([header, block.data]));
            }

            return this.concatBuffers([flacMarker, ...builtBlocks, this.audioFrames]);
        }

        concatBuffers(buffers) {
            const totalLength = buffers.reduce((acc, val) => acc + val.length, 0);
            const result = new Uint8Array(totalLength);
            let offset = 0;
            for (const buffer of buffers) {
                result.set(buffer, offset);
                offset += buffer.length;
            }
            return result;
        }
    }
    function embedMetadataInFlac(flacData, metadata, coverBlob) {
        const data = new Uint8Array(flacData);

        if (data[0] !== 0x66 || data[1] !== 0x4C || data[2] !== 0x61 || data[3] !== 0x43) {
            console.error("Маркер 'fLaC' не найден.");
            return flacData;
        }

        let offset = 4;
        let streaminfoBlock = null;
        let streaminfoData = null;
        let isLastBlock = false;
        const MAX_METADATA_BLOCKS = 10;
        let blocksRead = 0;

        while (!isLastBlock && offset < data.length - METADATA_BLOCK_HEADER_SIZE && blocksRead < MAX_METADATA_BLOCKS) {
            const headerByte = data[offset];
            isLastBlock = (headerByte & LAST_METADATA_BLOCK_FLAG) !== 0;
            const blockType = headerByte & 0x7F;
            const blockLength = (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3];

            const blockStart = offset + 4;
            const blockEnd = blockStart + blockLength;

            if (blockEnd > data.length) {
                throw new Error('Обнаружен поврежденный мета-блок FLAC.');
            }

            if (blockType === METADATA_BLOCK_TYPE.STREAMINFO) {
                streaminfoBlock = data.slice(offset, blockEnd);
                streaminfoData = data.slice(blockStart, blockEnd);
            }

            offset = blockEnd;
            blocksRead++;
        }

        if (!streaminfoBlock) {
            throw new Error('Блок STREAMINFO не найден. Это невалидный FLAC-файл.');
        }

        const audioFrames = data.slice(offset);

        const builder = new FlacFileBuilder();

        builder.addStreamInfo(streaminfoData);
        builder.addVorbisComment(metadata);
        builder.addPicture(coverBlob);
        builder.setAudioFrames(audioFrames);

        const newFlacFile = builder.build();

        return newFlacFile.buffer;
    }
    async function downloadCurrentTrack() {
        try {
            const trackId = extractTrackId(window.location.href);
            if (!trackId) {
                alert('Не удалось определить ID трека. Откройте страницу трека.');
                return;
            }
            const trackInfo = await getTrackInfo(trackId);
            const downloadInfo = await getDownloadInfoV2(trackId);
            const audioData = await downloadAudioData(downloadInfo.url);
            let finalAudioData = audioData;

            if (downloadInfo.transport === 'encraw' && downloadInfo.key) {
                finalAudioData = decryptAudio(audioData, downloadInfo.key);
            } else {
            }

            const extension = getExtension(downloadInfo.codec);
            const filename = sanitizeFilename(`${trackInfo.artists} - ${trackInfo.title}.${extension}`);

            const coverData = await downloadCover(trackInfo.coverUri);

            let taggedAudioData = finalAudioData;

            if (extension === 'flac') {
                const firstBytes = new Uint8Array(finalAudioData).slice(0, 12);
                const isMp4 = (firstBytes[4] === 0x66 && firstBytes[5] === 0x74 &&
                               firstBytes[6] === 0x79 && firstBytes[7] === 0x70) || // ftyp
                              (firstBytes[0] === 0x00 && firstBytes[1] === 0x00 &&
                               firstBytes[2] === 0x00 && (firstBytes[3] === 0x1c || firstBytes[3] === 0x20));

                if (isMp4) {
                    finalAudioData = extractFlacFromMp4(finalAudioData);
                }

                if (!validateFlacData(finalAudioData)) {
                    throw new Error('Некорректные FLAC данные после извлечения');
                }

                taggedAudioData = embedMetadataInFlac(finalAudioData, trackInfo, coverData);
            } else {
                // Для других форматов (MP3, M4A) - сохраняем как есть
            }

            saveFile(taggedAudioData, filename, getMimeType(extension));

        } catch (error) {
            console.error('Ошибка при скачивании:', error);
            alert('Ошибка при скачивании трека: ' + error.message);
        }
    }
    async function downloadSingleTrack(trackId, folderPrefix = '', coverUriOverride = null, playlistIndex = null) {
        try {
            const trackInfo = await getTrackInfo(trackId);
            const downloadInfo = await getDownloadInfoV2(trackId);
            const audioData = await downloadAudioData(downloadInfo.url);
            let finalAudioData = audioData;

            if (downloadInfo.transport === 'encraw' && downloadInfo.key) {
                finalAudioData = decryptAudio(audioData, downloadInfo.key);
            }

            const extension = getExtension(downloadInfo.codec);

            let filename;
            if (folderPrefix) {
                let trackNumber;
                if (playlistIndex !== null) {
                    trackNumber = playlistIndex.toString().padStart(2, '0');
                } else {
                    trackNumber = trackInfo.trackPosition.index.toString().padStart(2, '0');
                }
                filename = sanitizeFilename(`${folderPrefix} - ${trackNumber} - ${trackInfo.title}.${extension}`);
            } else {
                filename = sanitizeFilename(`${trackInfo.artists} - ${trackInfo.title}.${extension}`);
            }

            const coverUri = coverUriOverride || trackInfo.coverUri;
            const coverData = await downloadCover(coverUri);

            let taggedAudioData = finalAudioData;

            if (extension === 'flac') {
                const firstBytes = new Uint8Array(finalAudioData).slice(0, 12);
                const isMp4 = (firstBytes[4] === 0x66 && firstBytes[5] === 0x74 &&
                               firstBytes[6] === 0x79 && firstBytes[7] === 0x70) ||
                              (firstBytes[0] === 0x00 && firstBytes[1] === 0x00 &&
                               firstBytes[2] === 0x00 && (firstBytes[3] === 0x1c || firstBytes[3] === 0x20));

                if (isMp4) {
                    finalAudioData = extractFlacFromMp4(finalAudioData);
                }

                if (!validateFlacData(finalAudioData)) {
                    throw new Error('Некорректные FLAC данные после извлечения');
                }

                taggedAudioData = embedMetadataInFlac(finalAudioData, trackInfo, coverData);
            }

            saveFile(taggedAudioData, filename, getMimeType(extension));

        } catch (error) {
            console.error('Ошибка при скачивании трека:', error);
            throw error;
        }
    }

    const style = document.createElement('style');
    style.textContent = `
        @keyframes slideIn {
            from { transform: translateX(400px); opacity: 0; }
            to { transform: translateX(0); opacity: 1; }
        }
        @keyframes slideOut {
            from { transform: translateX(0); opacity: 1; }
            to { transform: translateX(400px); opacity: 0; }
        }
    `;
    document.head.appendChild(style);

    function getTrackInfo(trackId) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `https://api.music.yandex.net/tracks/${trackId}`,
                headers: {
                    'Authorization': `OAuth ${YANDEX_TOKEN}`,
                    'Accept': 'application/json',
                    'X-Requested-With': 'XMLHttpRequest'
                },
                anonymous: true,
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data.result && data.result.length > 0) {
                            const track = data.result[0];
                            const info = {
                                id: track.id,
                                title: track.title,
                                artists: track.artists.map(a => a.name).join(', '),
                                album: track.albums && track.albums.length > 0 ? track.albums[0].title : 'Single',
                                year: track.albums && track.albums.length > 0 ? track.albums[0].year : new Date().getFullYear(),
                                coverUri: track.coverUri || (track.albums && track.albums.length > 0 ? track.albums[0].coverUri : null),
                                trackPosition: track.albums && track.albums.length > 0 ? track.albums[0].trackPosition : { index: 1, volume: 1 },
                                durationMs: track.durationMs || 0,
                                genre: track.albums && track.albums.length > 0 && track.albums[0].genre ? track.albums[0].genre : 'Unknown'
                            };
                            resolve(info);
                        } else {
                            reject(new Error('Трек не найден'));
                        }
                    } catch (e) {
                        reject(new Error('Ошибка парсинга ответа трека: ' + e.message));
                    }
                },
                onerror: function(error) {
                    reject(new Error('Ошибка запроса информации о треке'));
                }
            });
        });
    }
    function getDownloadInfoV2(trackId) {
        return new Promise((resolve, reject) => {
            const timestamp = Math.floor(Date.now() / 1000);
            const quality = 'lossless';
            const codecs = 'flac-mp4,flac,aac,he-aac,mp3,aac-mp4,he-aac-mp4';
            const transports = 'encraw,raw';
            const signPayload = `${timestamp}${trackId}${quality}${codecs.replace(/,/g, '')}${transports.replace(/,/g, '')}`;
            const hash = CryptoJS.HmacSHA256(signPayload, SIGN_KEY);
            const sign = CryptoJS.enc.Base64.stringify(hash).slice(0, -1);
            const params = new URLSearchParams({
                ts: timestamp,
                trackId: trackId,
                quality: quality,
                codecs: codecs,
                transports: transports,
                sign: sign
            });
            const url = `https://api.music.yandex.net/get-file-info?${params.toString()}`;
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                headers: {
                    'Authorization': `OAuth ${YANDEX_TOKEN}`,
                    'Accept': 'application/json',
                    'X-Requested-With': 'XMLHttpRequest',
                    'X-Yandex-Music-Client': 'YandexMusicAndroid/24023621'
                },
                anonymous: true,
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data.error) {
                            reject(new Error(`API Error: ${data.error.message || data.error.type}`));
                            return;
                        }
                        if (data.result.downloadInfo) {
                            const info = data.result.downloadInfo;
                            if (!info.urls || info.urls.length === 0) {
                                reject(new Error('Нет доступных URL для скачивания'));
                                return;
                            }

                            resolve({
                                url: info.urls[0],
                                codec: info.codec,
                                quality: info.quality,
                                bitrate: info.bitrate,
                                transport: info.transport,
                                key: info.key || null
                            });
                        } else {
                            reject(new Error('Информация для скачивания недоступна'));
                        }
                    } catch (e) {
                        reject(new Error('Ошибка парсинга информации для скачивания: ' + e.message));
                    }
                },
                onerror: function(error) {
                    reject(new Error('Ошибка запроса информации для скачивания'));
                }
            });
        });
    }
    function downloadAudioData(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                responseType: 'arraybuffer',
                headers: {
                    'Accept': '*/*',
                    'Referer': 'https://music.yandex.by/'
                },
                anonymous: true,
                onload: function(response) {
                    if (response.status === 200) {
                        resolve(response.response);
                    } else {
                        reject(new Error('Ошибка скачивания аудио: ' + response.status));
                    }
                },
                onerror: function(error) {
                    reject(new Error('Ошибка скачивания аудиоданных'));
                }
            });
        });
    }
    function decryptAudio(encryptedData, hexKey) {
        try {
            const key = CryptoJS.enc.Hex.parse(hexKey);
            const iv = CryptoJS.lib.WordArray.create([0, 0, 0, 0]);
            const encryptedWordArray = CryptoJS.lib.WordArray.create(new Uint8Array(encryptedData));
            const decrypted = CryptoJS.AES.decrypt(
                { ciphertext: encryptedWordArray },
                key,
                {
                    iv: iv,
                    mode: CryptoJS.mode.CTR,
                    padding: CryptoJS.pad.NoPadding
                }
            );
            const decryptedArray = new Uint8Array(decrypted.sigBytes);
            const words = decrypted.words;
            for (let i = 0; i < decrypted.sigBytes; i++) {
                decryptedArray[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
            }
            return decryptedArray.buffer;
        } catch (e) {
            console.error('Ошибка расшифровки:', e);
            throw new Error('Ошибка расшифровки аудио: ' + e.message);
        }
    }
    function getExtension(codec) {
        const extensions = {
            'flac': 'flac',
            'flac-mp4': 'flac',
            'aac': 'm4a',
            'aac-mp4': 'm4a',
            'he-aac': 'm4a',
            'he-aac-mp4': 'm4a',
            'mp3': 'mp3'
        };
        return extensions[codec] || 'mp3';
    }
    function getMimeType(extension) {
        const mimeTypes = {
            'flac': 'audio/flac',
            'm4a': 'audio/mp4',
            'mp3': 'audio/mpeg'
        };
        return mimeTypes[extension] || 'audio/mpeg';
    }
    function sanitizeFilename(filename) {
        const invalid = /[<>:"/\\|?*]/g;
        return filename.replace(invalid, '_');
    }
    function saveFile(data, filename, mimeType) {
        const blob = new Blob([data], { type: mimeType });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        setTimeout(() => {
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }, 100);
    }
})();