В прошлый раз мы изменяли наш раздел Мои Аудиозаписи так.
![]() |
![]() |
Но, в тот раз, у нашего расширения был существенный недостаток: оно не работало при переходе со страницы на страницу.
Если зайти на главную страницу, потом перейти в Мои Аудиозаписи, то ссылки у песен не появлялись.
Напомню, вконтакте при переходе со страницы на страницу не обновляет страницу в классическом понимании, а программно изменяет разметку страницы, и обновляет адресную строку. Это не является классическим браузерным переходом на новую страницу, и поэтому наше расширение не обновлялось.
Давайте это исправим.
Как и прежде, наше расширение будет состоять из трех файлов — файла описания (manifest.json), внедряемого js скрипта (vk_inject.js), и внедряемого файла стилей (vk_styles.css).
Вот главный файл расширения: 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 и подсветку при наведении. В отличие от первой версии, сделаем нашу ссылку меньше,
чтобы она чаше помещалась в пространство, определенное для песни.
![](http://habrastorage.org/files/23d/367/1b3/23d3671b3fc742a5969fe75fe5ef3fa6.png)
Вы, конечно, можете переопределить стили, если хотите.
.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.
(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» в адресную строку).
Включите Режим разработчика. Потом нажмите Загрузить распакованное расширение....
![Расширения](http://habrastorage.org/files/fee/67a/d43/fee67ad438934b82a974d79937bc51f6.png)
Выберите папку, куда вы сохранили эти три файла. В моем случае это D:\Droopy\work\habr\plugin.
Расширение должно появиться в списке. Включите его.
![](http://habrastorage.org/files/709/35b/466/70935b466baf4a92a4f0120680b237a0.png)
Давайте проверим, как оно работает. Для этого зайдем во вконтакте, выберем раздел Музыка в верхней панели.
![](http://habrastorage.org/files/72e/2a7/cc3/72e2a7cc3e864e5f85f6ec05fee28506.png)
Ура, ссылки «Скачать» появились! Причем, если мы начнем поиск аудиозаписей на этой же странице, то для каждой найденной песни тоже будет добавляться ссылка на скачивание. Расширение работает.
Но, как я уже говорил в прошлом посте, есть одна сложность с названием скачиваемой песни. Когда вы нажимаете на ссылку «Скачать», в диалоге сохранения файла вам будет предлагаться не то имя файла, которое было указано в атрибуте «download», а имя файла на сервере. Дело в том, что вконтакте хранит аудиозаписи на отдельном домене, и хром для этого случая будет использовать имя файла на сервере вместо предложенного в ссылке.
В багтрекере хрома сказано, что в этом случае нужно выбирать пункт Сохранить ссылку как в контекстном меню. Тогда нам будет предложено нормальное имя аудиозаписи.
![](http://habrastorage.org/files/ee0/059/a99/ee0059a998fe4868bb18a1d5b0cd661b.png)
Наше расширение готово. Для каждой аудиозаписи появляется ссылка на скачивание.
Так как оно распакованное (то есть в режиме разработки), при каждом перезапуске браузера будет предлагаться отключить его.
В принципе, так лучше и сделать, а включать его по необходимости, когда захотите скачать песни. Или можно загрузить его в Chrome webstore, чтобы использовать постоянно.
Комментарии (5)
Glebcha
10.04.2015 11:06Скорее не в «безымянную», а анонимную самовызывающуюся функцию.
Попробуй переписать с использованием паттернов для систематизации и лучшей читаемости как минимум.
У MO есть некоторые недостатки с которыми пришлось столкнуться когда я писал свое расширение еще год назад. Но плюсы оказались весомее — простой и одновременно мощный инструмент взамен прежних убогих Mutation Events.
С помощью MO запилил скробблинг, который в другом популярном расширении реализован в виде инъекции скрипта для работы с методами вконтактика (а подвязываться на них очень опасно — изменили именование и все перестало работать).
Можешь мой репозиторий посмотреть, форкнуть/ухватить что-то или скачать из Chrome store и посмотреть полную версию с манифестом и всем прочим.
trikadin
Кстати, реально сделать так, чтобы закачивалось с нужным именем. Буквально вчера допилил, и тут на эту статью наткнулся)
github.com/trikadin/getvk
qw1
К сожалению, конструкция
не работает в firefox, имя при скачивании будет 91900d6f408d.mp3.
Для greasemonkey скрипта я это обошёл так: в буфер обмена копируется название файла и в диалоге «Save As...» нужно просто нажать Ctrl+V
qw1
Есть ещё костыль для firefox: данные скачиваются в память через XMLHttpRequest, затем формируется ссылка с data-uri, в которой
download
работаетК сожалению, такой способ скачивает в обход браузерного менеджера закачек и не видно прогресса.
trikadin
Вся проблема с атрибутом download в кроссдоменной политике (вот здесь в пункте 4.8.3 можно почитать подробнее, как, зачем и почему так происходит). Я это решил запуском ссылки на скачку непосредственно из расширения (a.click()) и выставление для расширения permissions'ов для всех возможных сайтов. Возможно, для firefox-овых расширений можно что-то такое же замутить.