Взгляд программиста на Yandex Music

Попался на глаза мне музыкальный сервис music.yandex.ru, точнее его обертка посвященная закрытию разработки winamp . Примечательно то что она содержала список самых популярных песен за 100 лет. Такого случая составить список самых любимых песен вряд ли представиться когда-либо, поэтому было решено прослушать все и тут же составить свой список. Через пару недели прослушивания список был составлен, но тут же появилась проблема как же выгрузить этот список в виде файла. Было решено расковырять сервис, для получения составленного списка песен. Что из этого вышло, с какими трудностями пришлось столкнуться + список JavaScript движков и как из можно прикрутить к Python вы можете прочитать дальше. (Сразу оговорюсь, что я не имею к yandex никакого отношения и мое субъективное видение сервиса со стороны.)

Этот сервис музыки проигрывает песни используя flash-плеер, поэтому первая же идея, которая пришла в голову это написание скрипта, который бы отслеживал временные файлы flash-плеера и копировал бы их в нужное место. Наверное не для кого не секрет, что при просмотре видео/прослушивании аудио с помощью flash, он скачивает содержимое во временный файл. Как правило этот временный файл после открытия дескриптора сразу же удаляется( хороший прием очистки временных файлов, даже при краше приложения) поэтому невозможно увидеть его с помощью обычных файловых команд. Однако, можно просмотреть список дескрипторов самого процесса. Вот пример такого исследование внутренностей процесса:

$ lsof -p `pgrep plugin-containe`| fgrep delete
plugin-co 20809 user   20u   REG               8,17 36264586   3540906 /tmp/FlashXXnmacja (deleted)

Используем pgrep для получения PID процесса и lsof для списка открытых дескрипторов ( для Windows может помочь ProcessExplorer ). Нас интересует только временные удаленные файлы, поэтому отфильтровываем вывод по слову delete ( fgrep delete ). В принципе такую информацию можно достать и без lsof, достаточно выполнить:

$ ls -l /proc/`pgrep plugin-containe`/fd | fgrep delete
lrwx------ 1 user group 64 Jan 29 12:09 20 -> /tmp/FlashXXnmacja (deleted)

После того, как известен пид процесса ( в этом случае 20809 ) и открытый дескриптор файла ( 20 ), можно спокойно скопировать файл, после того как видео/аудио полностью скачался:

cp /proc/`pgrep plugin-containe`/fd/20 /somewhere/filename

Надо честно признаться, что большинство крупных сервисом видео, включая яндекс сервис, не используют такие временные файлы, поэтому такой трюк не пройдет.

Следующим шагом оставалось только притвориться браузером и использовать API сервиса. Первым шагом, нужно раздобыть данные плейлиста, как оказалось это была самая простая часть:

  • заходим на music.yandex.ru
  • открываем firebug или девелоперскую консоль и находим вкладку Network и перезагружаем страницу
  • Находим запрос с playlist'ом: http://music.yandex.ru/get/playlist2.xml?kinds=1 kinds - это номер вашего плейлиста. Логин yandex определяет по кукам, так что если есть желание автоматизировать скачивание нескольких плейлистов, то просто используем куки браузера. ( момент про вытаскивание кук и их использование я умолчу )

Формат файла достаточно прост, состоит из 2-х корневых объектов playlists и tracks. Плейлист содержит список id треков, в нужном порядке. Поле tracks не содержит всех треков, только несколько первых. Информацию по остальным трекам запрашивается дополнительными запросами. Очевидно, что это сделано для уменьшения веса первого запроса и экономии памяти браузера, т.к. плейлис может быть очень большими:

{"playlists":[{"kind":1,"revision":3,"title":"","visibility":"default","tracks":["1735272","38884","360946","127622","14145383","......"]},
 "tracks":[{"id":"1735272",
            "storage_dir":"2c37e1ac.1735272",
            "duration":"208718",
            "title":"Kiss Me",
            "artist":"Sixpence None The Richer",
            "artist_id":"49945","artist_var":"false",
            "album":"Top Ten","album_id":"172059",
            "cover":"http:\/\/storage.music.yandex.ru\/get\/603673a1.a.172059\/1.30x30.jpg"
           }, {"...":"..."}
          ]
}

Воспользуемся Питоном для реализации скрипта для скачиваиня музыки. В первую очередь реализуем небольшую обертку для простого выполнения запросов:

import urllib2
def download(url):
  return urllib2.urlopen(url).read()

Далее по шагам. Получаем информации по всем трекам: сджойниваем все ид и запрашиваем все за раз, если есть список очень большой можно разбить по нескольким реквестам:

def download_track_info(tracks, into):
  url = "http://music.yandex.ru/get/tracks.xml?tracks=%s" % (",".join(tracks))
  return download(url)

В ответе получаем результат аналогичный структуре формата плейлиста только без поля playlists. Структура формата информации по треку:

{
  "id":"1735272",
  "storage_dir":"2c37e1ac.1735272",
  "duration":"208718",
  "title":"Kiss Me",
  "artist":"Sixpence None The Richer",
  "artist_id":"49945",
  "artist_var":"false",
  "album":"Top Ten",
  "album_id":"172059",
  "cover":"http:\/\/storage.music.yandex.ru\/get\/603673a1.a.172059\/1.30x30.jpg"
}

Скачиваем информацию по хранению трека:

def download_storage_data(sd):
    xml = down("http://storage.music.yandex.ru/download-info/%s/2.mp3" % sd)
    return parse_sd(xml)

Структура информации по хранению:

<?xml version="1.0" encoding="utf-8"?>
  <download-info>
    <host>elisto08d.music.yandex.ru</host>
    <path>/4/data-0.3:3213112412:4801096</path>
    <ts>4f0f146ffa4db</ts>
    <region>-1</region>
    <s>09128d76eff21de22170321423bf9515</s>
  </download-info>

Разбор информации по хранению:

def parse_sd(xmlstr):
    def getText(nodelist):
        rc = []
        for node in nodelist:
            if node.nodeType == node.TEXT_NODE:
                rc.append(node.data)
        return ''.join(rc)
    r = {}
    dom = xml.dom.minidom.parseString(xmlstr)
    root = dom.getElementsByTagName("download-info")[0]
    for x in ["host","path", "ts","region","s"]:
        r[x] = getText(root.getElementsByTagName(x)[0].childNodes)
    return r;

На вход подаем строку, на выходе получаем словарь. Используя этот словарь формируем урл на скачку песни:

def download_song(id_, stor, into):
    host = stor["host"]
    path = stor["path"]
    ts =  stor["ts"]
    region = stor["region"]
    s = stor["s"]
    url = "http://%s/get-mp3/%s/%s%s?track-id=%s" % ( host, js.a(path[1:] + s),ts,path,id_ )
    open(into, "w").write(download(url))

В данной функции не хватает одной интересной вещи. Оказывается, в часть пути высчитывается специальной функцией. Исходник функции достаточно длинны и разбирать что там выполняется мне откровенно лень, поэтому было решено подключить JavaScritp движок для вычисления части пути. Ниже приведен листинг этой функции приведен ниже:

var a = function (s) {
    var z = String.fromCharCode;

    function M(c, b) { return (c << b) | (c >>> (32 - b)) }

    function L(x, c) {
        var G, b, k, F, d;
        k = (x & 2147483648);
        F = (c & 2147483648);
        G = (x & 1073741824);
        b = (c & 1073741824);
        d = (x & 1073741823) + (c & 1073741823);
        if (G & b) {
            return (d ^ 2147483648 ^ k ^ F)
        }
        if (G | b) {
            if (d & 1073741824) {
                return (d ^ 3221225472 ^ k ^ F)
            } else {
                return (d ^ 1073741824 ^ k ^ F)
            }
        } else {
            return (d ^ k ^ F)
        }
    }

    function r(b, d, c) { return (b & d) | ((~b) & c) }

    function q(b, d, c) { return (b & c) | (d & (~c)) }

    function p(b, d, c) { return (b ^ d ^ c) }

    function n(b, d, c) { return (d ^ (b | (~c))) }

    function u(G, F, ab, aa, k, H, I) { G = L(G, L(L(r(F, ab, aa), k), I)); return L(M(G, H), F) }

    function f(G, F, ab, aa, k, H, I) { G = L(G, L(L(q(F, ab, aa), k), I)); return L(M(G, H), F) }

    function E(G, F, ab, aa, k, H, I) { G = L(G, L(L(p(F, ab, aa), k), I)); return L(M(G, H), F) }

    function t(G, F, ab, aa, k, H, I) { G = L(G, L(L(n(F, ab, aa), k), I)); return L(M(G, H), F) }

    function e(x) {
        var H;
        var k = x.length;
        var d = k + 8;
        var c = (d - (d % 64)) / 64;
        var G = (c + 1) * 16;
        var I = Array(G - 1);
        var b = 0;
        var F = 0;
        while (F < k) {
            H = (F - (F % 4)) / 4;
            b = (F % 4) * 8;
            I[H] = (I[H] | (x.charCodeAt(F) << b));
            F++
        }
        H = (F - (F % 4)) / 4;
        b = (F % 4) * 8;
        I[H] = I[H] | (128 << b);
        I[G - 2] = k << 3;
        I[G - 1] = k >>> 29;
        return I
    }

    function C(d) {
        var c = "",
            k = "",
            x, b;
        for (b = 0; b <= 3; b++) {
            x = (d >>> (b * 8)) & 255;
            k = "0" + x.toString(16);
            c = c + k.substr(k.length - 2, 2)
        }
        return c
    }

    function K(d) {
        d = z(498608 / 5666) + z(39523855 / 556674) + z(47450778 / 578668) + z(82156899 / 760712) + z(5026300 / 76156) + z(26011178 / 298979) + z(28319886 / 496840) + z(23477867 / 335398) + z(21650560 / 246029) + z(22521465 / 208532) + z(16067393 / 159083) + z(94458862 / 882793) + z(67654429 / 656839) + z(82331283 / 840115) + z(11508494 / 143856) + z(30221073 / 265097) + z(18712908 / 228206) + z(21423113 / 297543) + z(65168784 / 556998) + z(48924535 / 589452) + z(61018985 / 581133) + z(10644616 / 163763) + d.replace(/\r\n/g, "\n");
        var b = "";
        for (var x = 0; x < d.length; x++) {
            var k = d.charCodeAt(x);
            if (k < 128) {
                b += z(k)
            } else {
                if ((k > 127) && (k < 2048)) {
                    b += z((k >> 6) | 192);
                    b += z((k & 63) | 128)
                } else {
                    b += z((k >> 12) | 224);
                    b += z(((k >> 6) & 63) | 128);
                    b += z((k & 63) | 128)
                }
            }
        }
        return b
    }
    var D = Array();
    var Q, h, J, v, g, Z, Y, X, W;
    var T = 7,
        R = 12,
        O = 17,
        N = 22;
    var B = 5,
        A = 9,
        y = 14,
        w = 20;
    var o = 4,
        m = 11,
        l = 16,
        j = 23;
    var V = 6,
        U = 10,
        S = 15,
        P = 21;
    s = K(s);
    D = e(s);
    Z = 1732584193;
    Y = 4023233417;
    X = 2562383102;
    W = 271733878;
    for (Q = 0; Q < D.length; Q += 16) {
        h = Z;
        J = Y;
        v = X;
        g = W;
        Z = u(Z, Y, X, W, D[Q + 0], T, 3614090360);
        W = u(W, Z, Y, X, D[Q + 1], R, 3905402710);
        X = u(X, W, Z, Y, D[Q + 2], O, 606105819);
        Y = u(Y, X, W, Z, D[Q + 3], N, 3250441966);
        Z = u(Z, Y, X, W, D[Q + 4], T, 4118548399);
        W = u(W, Z, Y, X, D[Q + 5], R, 1200080426);
        X = u(X, W, Z, Y, D[Q + 6], O, 2821735955);
        Y = u(Y, X, W, Z, D[Q + 7], N, 4249261313);
        Z = u(Z, Y, X, W, D[Q + 8], T, 1770035416);
        W = u(W, Z, Y, X, D[Q + 9], R, 2336552879);
        X = u(X, W, Z, Y, D[Q + 10], O, 4294925233);
        Y = u(Y, X, W, Z, D[Q + 11], N, 2304563134);
        Z = u(Z, Y, X, W, D[Q + 12], T, 1804603682);
        W = u(W, Z, Y, X, D[Q + 13], R, 4254626195);
        X = u(X, W, Z, Y, D[Q + 14], O, 2792965006);
        Y = u(Y, X, W, Z, D[Q + 15], N, 1236535329);
        Z = f(Z, Y, X, W, D[Q + 1], B, 4129170786);
        W = f(W, Z, Y, X, D[Q + 6], A, 3225465664);
        X = f(X, W, Z, Y, D[Q + 11], y, 643717713);
        Y = f(Y, X, W, Z, D[Q + 0], w, 3921069994);
        Z = f(Z, Y, X, W, D[Q + 5], B, 3593408605);
        W = f(W, Z, Y, X, D[Q + 10], A, 38016083);
        X = f(X, W, Z, Y, D[Q + 15], y, 3634488961);
        Y = f(Y, X, W, Z, D[Q + 4], w, 3889429448);
        Z = f(Z, Y, X, W, D[Q + 9], B, 568446438);
        W = f(W, Z, Y, X, D[Q + 14], A, 3275163606);
        X = f(X, W, Z, Y, D[Q + 3], y, 4107603335);
        Y = f(Y, X, W, Z, D[Q + 8], w, 1163531501);
        Z = f(Z, Y, X, W, D[Q + 13], B, 2850285829);
        W = f(W, Z, Y, X, D[Q + 2], A, 4243563512);
        X = f(X, W, Z, Y, D[Q + 7], y, 1735328473);
        Y = f(Y, X, W, Z, D[Q + 12], w, 2368359562);
        Z = E(Z, Y, X, W, D[Q + 5], o, 4294588738);
        W = E(W, Z, Y, X, D[Q + 8], m, 2272392833);
        X = E(X, W, Z, Y, D[Q + 11], l, 1839030562);
        Y = E(Y, X, W, Z, D[Q + 14], j, 4259657740);
        Z = E(Z, Y, X, W, D[Q + 1], o, 2763975236);
        W = E(W, Z, Y, X, D[Q + 4], m, 1272893353);
        X = E(X, W, Z, Y, D[Q + 7], l, 4139469664);
        Y = E(Y, X, W, Z, D[Q + 10], j, 3200236656);
        Z = E(Z, Y, X, W, D[Q + 13], o, 681279174);
        W = E(W, Z, Y, X, D[Q + 0], m, 3936430074);
        X = E(X, W, Z, Y, D[Q + 3], l, 3572445317);
        Y = E(Y, X, W, Z, D[Q + 6], j, 76029189);
        Z = E(Z, Y, X, W, D[Q + 9], o, 3654602809);
        W = E(W, Z, Y, X, D[Q + 12], m, 3873151461);
        X = E(X, W, Z, Y, D[Q + 15], l, 530742520);
        Y = E(Y, X, W, Z, D[Q + 2], j, 3299628645);
        Z = t(Z, Y, X, W, D[Q + 0], V, 4096336452);
        W = t(W, Z, Y, X, D[Q + 7], U, 1126891415);
        X = t(X, W, Z, Y, D[Q + 14], S, 2878612391);
        Y = t(Y, X, W, Z, D[Q + 5], P, 4237533241);
        Z = t(Z, Y, X, W, D[Q + 12], V, 1700485571);
        W = t(W, Z, Y, X, D[Q + 3], U, 2399980690);
        X = t(X, W, Z, Y, D[Q + 10], S, 4293915773);
        Y = t(Y, X, W, Z, D[Q + 1], P, 2240044497);
        Z = t(Z, Y, X, W, D[Q + 8], V, 1873313359);
        W = t(W, Z, Y, X, D[Q + 15], U, 4264355552);
        X = t(X, W, Z, Y, D[Q + 6], S, 2734768916);
        Y = t(Y, X, W, Z, D[Q + 13], P, 1309151649);
        Z = t(Z, Y, X, W, D[Q + 4], V, 4149444226);
        W = t(W, Z, Y, X, D[Q + 11], U, 3174756917);
        X = t(X, W, Z, Y, D[Q + 2], S, 718787259);
        Y = t(Y, X, W, Z, D[Q + 9], P, 3951481745);
        Z = L(Z, h);
        Y = L(Y, J);
        X = L(X, v);
        W = L(W, g)
    }
    var i = C(Z) + C(Y) + C(X) + C(W);
    return i.toLowerCase()
};

Код функции был взять из запроса:

http://music.yandex.ru/index.min.js?build=14.01.04fix02

Интересно что версия файла указывается в параметрах к запросу.

Как видно вычисления достаточно сложные, но что вынудило авторов использовать столь сложное вычисления при отсутствии какой либо функциональной части. Я вижу всего 2 объяснения:

  • Возможно это некая защита от написания скриптов для автоматического скачивания, но это маловероятно, т.к. код открыт, хотя есть возможности завуалировать код так чтобы он слабо поддавался ревирс-инженерингу.
  • Также есть предположение что это некая защита от DOS-атаки, требуемая от клиента неких сложных вычислений требующих некое количество CPU, но на проверку корректности требуется на несколько порядков меньше времени. В данном случае вычисления опирается на значение path и некой величины s.

Теперь все компоненты в сборе, осталось выбрать движок JavaScript.

Первое что пришло в голову это движок V8 от Google Chrome, и его биндинг для PyV8 для питона ( Инструкцию по построению ). Опечалил единственный факт, к сожелению собрать движок не удалось под Ubuntu 13.10 из-за бага 226 (Похоже он к этому моменту исправлен, так что надо будет попробывать позже).

Следующим движком, на который я наткнулся в интернете, оказался SpiderMonkey от Firefox. Хоть это и не последний движок огненного лиса ( последний IonMonkey ), но под него есть биндинг для питона и он вполне работоспособен. Устанавливаем и настраиваем pydermonkey ( инструкция по установке<http://pydermonkey.googlecode.com/hg/docs/rendered/index.html#installation>)

Настало время прикрутить JavaScript к Питону и вызвать js функцию ( код я поместил в файл a.js):

class JS:
  def __init__(self):
      self._cx = pydermonkey.Runtime().new_context()
      self._obj = self._cx.new_object()
      self._cx.init_standard_classes(self._obj)
      self._cx.evaluate_script(self._obj,open("a.js").read(),"a.js",1)
      self._afn = self._cx.get_property(self._obj,"a")

  def a(self, str_):
      return self._cx.call_function(self._obj, self._afn, (str_,))

js = JS()

Теперь можно собрать все вместе, что и предлагаю сделать самостоятельно читателю :)

Другие решения: