Pull to refresh

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

Reading time 6 min
Views 17K
Продолжим писать наше расширение для 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 и подсветку при наведении. В отличие от первой версии, сделаем нашу ссылку меньше,
чтобы она чаше помещалась в пространство, определенное для песни.


Вы, конечно, можете переопределить стили, если хотите.
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:\Droopy\work\habr\plugin.
Расширение должно появиться в списке. Включите его.



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



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

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



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

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

Articles