NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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);
}
})();