Пишем расширение для Chrome «загрузка аудиозаписей с Вконтакте», часть 2

в 19:37, , рубрики: Google Chrome, html, javascript, Вконтакте

Продолжим писать наше расширение для Chrome, которое добавляет ссылку «Скачать» для каждой аудиозаписи вконтакте.
В прошлый раз мы изменяли наш раздел Мои Аудиозаписи так.

Оригинал Результат

Но, в тот раз, у нашего расширения был существенный недостаток: оно не работало при переходе со страницы на страницу.
Если зайти на главную страницу, потом перейти в Мои Аудиозаписи, то ссылки у песен не появлялись.
Напомню, вконтакте при переходе со страницы на страницу не обновляет страницу в классическом понимании, а программно изменяет разметку страницы, и обновляет адресную строку. Это не является классическим браузерным переходом на новую страницу, и поэтому наше расширение не обновлялось.
Давайте это исправим.

Как и прежде, наше расширение будет состоять из трех файлов — файла описания (manifest.json), внедряемого js скрипта (vk_inject.js), и внедряемого файла стилей (vk_styles.css).

Вот главный файл расширения: manifest.json. В нем содержится дескриптор расширения и ссылки на внедряемые файлы.

manifest.json

{
	"manifest_version": 2,

	"name": "Загрузчик музыки из Вконтакте",
	"description": "Позволяет вам загрузить музыку из социальной сети Вконтакте.",
	"version": "2.0",

	"content_scripts": [{
		"matches": ["*://vk.com/*"],
		"js": ["vk_inject.js"],
		"css": ["vk_styles.css"]
	}]
}

Тег «content_scripts» в манифесте определяет, какие js и css файлы будут внедрены в страницу.
Наше расширение будет встраивать файлы vk_inject.js и vk_styles.css в каждую страницу вконтакте — http://vk.com/* или https://vk.com/*.

Файл стилей (vk_styles.css) содержит стили для для внедряемой ссылки. Ссылка будет иметь css класс downloadLink.
Обязательно нужно следить, чтобы класс не пересекался со стилями исходной страницы.
Сделаем для нашей ссылки border и подсветку при наведении. В отличие от первой версии, сделаем нашу ссылку меньше,
чтобы она чаше помещалась в пространство, определенное для песни.

Пишем расширение для Chrome «загрузка аудиозаписей с Вконтакте», часть 2 - 3
Вы, конечно, можете переопределить стили, если хотите.

vk_styles.css

.downloadLink {
	float: right;
	cursor: copy;
	border: 1px dotted #CED8DB;
	border-radius: 2px;
	padding: 0 4px;
}

.downloadLink:hover {
	background-color: #d0e6ff;
	border-color: #9DA5AE;
}

Все основные действия расширения, будут происходить во внедряемом коде vk_inject.js.
Итак, что будем делать:

Для каждой песни в списке аудиозаписей мы внедрим по ссылке «Скачать».

Мы будем искать на странице элементы с id 'pad_playlist', 'pad_search_list', 'initial_list', 'search_list', 'choose_audio_rows'.
Именно в них находятся списки аудиозаписей. Но, каждый из элементов может изначально присутствовать на странице, так и
динамически создаваться/удаляться. Поэтому нам нужно следить за добавлением элементов в DOM страницы.

Наш внедряемый скрипт исполняется в отдельной виртуальной машине, и не может взаимодействовать со скриптом на станице.
Поэтому мы не можем переопределять исходные функции или как-то иначе перехватывать js код на исходной странице.
Но, оба эти скрипта разделяют DOM-дерево. Так что мы будем следить за обновлениями DOM элементов списка с помощью MutationObserver.

vk_inject.js

(function (){	// Обернем все в безымянную функцию, чтобы не создавать глобальных переменных
	// Этот observer будет следить за добавлением аудиозаписей в найденные списки аудиозаписей
	var trackObserver = new MutationObserver(listModified);

	// Первоначально, проверим, не существуют ли уже списки аудиозаписей на странице
	var list_ids = ['pad_playlist', 'pad_search_list', 'initial_list', 'search_list', 'choose_audio_rows'];
	for (var i= 0 ; i < list_ids.length; i++)
	{
		var list = document.getElementById(list_ids[i]);
		if (list)
		{
			// добавим ссылки "Скачать" ко всем записям, и будем следить за изменениями с помощью trackObserver
			listFound(list);
		}
	}
	// отдельно ищем результат поиска аудиозаписей, потому что там нужно проверить css класс
	list = document.getElementById('results');
	if (list && list.classList.contains('audio_results'))
	{
		listFound(list);
	}

	// Создадим observer для нотификаций о создании новых элементов на странице
	var listObserver = new MutationObserver(elementAdded);
	// и следим за body, когда новые списки аудиозаписей добавятся
	listObserver.observe(document.body, {childList: true, subtree: true});

	// вызывается при любой модификации DOM страницы
	function elementAdded(mutations)
	{
		for (var i = 0; i < mutations.length; i++)
		{
			var added = mutations[i].addedNodes;
			// просмотрим добавленные элементы на предмет списка аудиозаписей
			for (var j = 0; j < added.length; j++)
			{
				findAudioLists(added[j]);
			}
		}
	}

	// рекурсивная функция проходит по добавленным элементам и ищет в них списки аудиозаписей
	function findAudioLists(node)
	{
		if (node.id)	// у списка должно быть id
		{
			for (var i = 0; i < list_ids.length; i++)	// смотрим, совпадает ли id с искомыми
			{
				if (list_ids[i] == node.id)
				{
					listFound(node);
					return;	// не будем искать внутри уже найденного списка
				}
			}
			if (node.id == 'results')	// отдельно будем искать '#results.audio_results' - результаты поиска
			{
				if (node.classList.contains('audio_results'))
				{
					listFound(node);
					return;
				}
			}
		}
		// пройдемся по дереву добавленного элемента
		var child = node.firstElementChild;
		while (child)
		{
			findAudioLists(child);	// вызываем рекурсивно для всех дочерних элементов
			child = child.nextElementSibling;
		}
	}

	// найден один из списков, в котором содержатся аудиозаписи
	function listFound(listNode)
	{
		if (listNode.children.length)	// в новом списке уже есть аудиозаписи
		{
			for (var j = 0; j < listNode.children.length; j++)
			{
				addDownloadLink(listNode.children[j]);	// добавим в каждую по ссылке "Скачать"
			}
		}
		trackObserver.observe(listNode, {childList: true});	// следим за добавлением новых записей -> listModified()
	}

	// вызывается, когда в список песен добавляются (или удаляются) элементы
	function listModified(mutations)
	{
		for (var i = 0; i < mutations.length; i++)
		{
			var mut = mutations[i];
			// пройдем по добавленным песням
			for (var j = 0; j < mut.addedNodes.length; j++)
			{
				addDownloadLink(mut.addedNodes[j]);
			}
			// удаленныые записи - mut.removedNodes игнорируем
		}
	}

	// Добавляет ссылку "Скачать" к разметке песни
	function addDownloadLink(row)
	{
		// новый элемент-аудиозапись может иметь различную разметку, в зависимости от того, куда добавляется
		if (!row.classList.contains('audio'))
		{
			// возможно, это элемент из списка "Прикрепить аудиозапись"
			row = row.querySelector('div.audio');	// внутри него содержится 'div.audio', с которым мы будем работать
			if (!row)
			{
				return;
			}
		}
		var titleNode = row.querySelector('div.title_wrap');	// Исполнитель песни + название
		if (!titleNode)	// если ничего не находим - выйдем (может, разметка была изменена?)
		{
			return;
		}
		// может, наша ссылка уже есть? Так бывает, если вконтакте перемещает список из одного элемента в другой
		if (titleNode.querySelector('a.downloadLink'))
		{
			return;	// ссылка уже была добавлена ранее
		}
		var input = row.querySelector('div.play_btn > input');	// найдем input, в котором хранится url
		if (!input)
		{
			input = row.querySelector('div.play_btn_wrap + input');	// проверим другой способ разметки
			if (!input)
			{
				return;	// не та разметка
			}
		}
		var ref = input.getAttribute('value');	// сам URL
		ref = ref.substr(0, ref.indexOf('?'));	// обрежем все после '?', чтобы оставить только ссылку на mp3

		var link = document.createElement('a');
		link.className = 'downloadLink';	// Добавим класс 'downloadLink' для нашей ссылки
		link.textContent = "^";
		link.setAttribute('title', "Скачать");
		link.setAttribute('download', titleNode.textContent + '.mp3');	// Имя файла для загрузки
		link.setAttribute('href', ref);
		link.addEventListener('click', function(event){	// при клике на нашу ссылку, отменим запуск проигрывателя
			event.stopPropagation();
		});
		titleNode.appendChild(link);
	}
})();

Устанавливаем расширение

Итак, наши три файла готовы.
Вы можете скопировать их из поста или загрузить архивом.
В хроме войдите на страницу настроек, выберите вкладку Расширения (или просто введите «chrome://extensions» в адресную строку).
Включите Режим разработчика. Потом нажмите Загрузить распакованное расширение....

Расширения

Выберите папку, куда вы сохранили эти три файла. В моем случае это D:Droopyworkhabrplugin.
Расширение должно появиться в списке. Включите его.

Пишем расширение для Chrome «загрузка аудиозаписей с Вконтакте», часть 2 - 5

Давайте проверим, как оно работает. Для этого зайдем во вконтакте, выберем раздел Музыка в верхней панели.

Пишем расширение для Chrome «загрузка аудиозаписей с Вконтакте», часть 2 - 6

Ура, ссылки «Скачать» появились! Причем, если мы начнем поиск аудиозаписей на этой же странице, то для каждой найденной песни тоже будет добавляться ссылка на скачивание. Расширение работает.

Но, как я уже говорил в прошлом посте, есть одна сложность с названием скачиваемой песни. Когда вы нажимаете на ссылку «Скачать», в диалоге сохранения файла вам будет предлагаться не то имя файла, которое было указано в атрибуте «download», а имя файла на сервере. Дело в том, что вконтакте хранит аудиозаписи на отдельном домене, и хром для этого случая будет использовать имя файла на сервере вместо предложенного в ссылке.
В багтрекере хрома сказано, что в этом случае нужно выбирать пункт Сохранить ссылку как в контекстном меню. Тогда нам будет предложено нормальное имя аудиозаписи.

Пишем расширение для Chrome «загрузка аудиозаписей с Вконтакте», часть 2 - 7

Наше расширение готово. Для каждой аудиозаписи появляется ссылка на скачивание.

Так как оно распакованное (то есть в режиме разработки), при каждом перезапуске браузера будет предлагаться отключить его.
В принципе, так лучше и сделать, а включать его по необходимости, когда захотите скачать песни. Или можно загрузить его в Chrome webstore, чтобы использовать постоянно.

Автор: 4dro

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js