Свой сервер обложек на Python для интернет-радио

    image

    Я перфекционист который любит во всём порядок. Больше всего меня радует когда вещи работают именно так, как они должны работать (в моём, разумеется, понимании). А ещё у меня уже давно есть своё персональное интернет-радио на базе IceCast-KH + LiquidSoap. И много лет мне не давал спокойно спать тот факт, что сервера потокового радиовещания не умеют отдавать обложки (artwork) проигрываемых треков в потоке. Да и не только в потоке — вообще никак не умеют. Я и на IceCast-KH (форк от IceCast2) перешёл только из-за одной его убер-фичи — он умеет отдавать mp3-тэги внутри flv потока (это нужно для отображения исполняемого трека при онлайн воспроизведении на сайте через флэш-плеер). И теперь пришло время закрыть последний вопрос — отдачу обложек проигрываемых треков — и успокоиться. Поскольку готовых решений не нашлось, я не придумал ничего лучше, чем написать свой сервер обложек для .mp3 файлов. Как? Добро пожаловать под кат.

    Предыстория


    Радио я обычно слушаю в машине, на 2-din магнитоле на базе Android 4.4 KitKat (а дома на планшете под тем же Андроидом). Для прослушивания, после долгого и вдумчивого перебора существующих программ, была выбрана XiiaLive, в основном за то, что она умеет в пользовательские радиостанции (такая банальная, казалось бы фича, но не поддерживается большинством плееров потокового радио — вот тебе каталог ShoutCast/Uber Stations — выбирай и слушай что дают), а также за то, что умеет подкачивать и отображать обложки проигрываемых треков. Да, конечно, не всех, но умеет. Музыка играла, обложки частично показывались и на какое-то время внутренний перфекционист успокоился, но как оказалось — ненадолго.

    Через некоторое время всплыл крайне неприятный баг приложения связанный с неверной обработкой юникода — если и название трека и исполнителя было не в латинице — обложка альбома показывалась неверно. Мало того — всегда одна и та же. И я вам даже больше скажу — это почему-то всегда была Нюша. Вот этого я уже вытерпеть не смог.

    image
    Скриншот иллюстрирующий как XiiaLive покусился на святое.

    Можно было бы подождать, пока разработчики пофиксят этот баг, но, здраво рассудив, что вряд ли у них найдутся обложки для всего, что находится в ротации именно на моей станции (у них точно не будет обложек для Ishome, Interior Disposition, tmtnsft и тем более MΣ$†ΛMN ΣKCПØNΛ†), показалось правильнее написать своё api для обложек. Которое будет уметь работать именно по локальной базе файлов с музыкой и, по возможности, без привязки к конкретному серверу вещания.

    Исследуем вопрос


    Найти описание стандартного протокола для отдачи обложек не удалось (предполагаю, что единого стандарта вообще нет), поэтому решил пойти от обратного — посмотреть как это реализовано у больших дядек, в частности у того же XiiaLive. Вооружаемся Packet Capture на Android, ловим пакеты и смотрим куда приложение ходит и зачем:

    GET /songart.php?partner_token=7144969234&title=Umbrella&artist=The+Baseballs&res=hi HTTP/1.1
    User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.4.2; QuadCore-R16 Build/KVT49L)
    Host: api.dar.fm
    Connection: Keep-Alive
    Accept-Encoding: gzip
    
    HTTP/1.1 200 OK
    Server: Apache/2.2.15 (CentOS)
    X-Powered-By: PHP/5.3.3
    Set-Cookie: PHPSESSID=u5sgs13h1315k9184nvvutaf33; expires=Fri, 03-Aug-2018 18:39:08 GMT; path=/; domain=.dar.fm
    Expires: Thu, 19 Nov 1981 08:52:00 GMT
    Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
    Pragma: no-cache
    X-Train: wreck="mofas16"
    Content-Type: application/xml; charset=UTF-8
    Content-Length: 57
    Accept-Ranges: bytes
    Date: Thu, 03 Aug 2017 18:39:08 GMT
    Via: 1.1 varnish
    Age: 0
    Connection: keep-alive
    X-Served-By: cache-ams4143-AMS
    X-Cache: MISS
    X-Cache-Hits: 0
    X-Timer: S1501785548.973935,VS0,VE390
    

    Оказалось, что посылается обычный GET запрос с четырьмя переменными:

    • partner_token — токен авторизации, при запросе без него, или с неправильным токеном — возвращается 403.
    • title — заголовок трека
    • artist — имя исполнителя
    • res — желаемое разрешение картинки. Несложный перебор дал следующий набор выдаваемых разрешений (обложки квадратные, так что разрешение описывается одним числом):
      * hi — 1080 px
      * low — 250 px
      * во всех остальных случаях — 400 px

    В ответ на запрос приложение ожидает в ответ xml такого вида:

    <?xml version="1.0" encoding="UTF-8" ?>
    <songs>
    	<song>
    <arturl>http://coverartarchive.org/release/c8b16143-e87e-440d-bbb2-5c96615bed2b/2098621288-500.jpg
    	</arturl>
    	<artist>The Baseballs</artist>
    	<title>Tik Tok</title>
    	<album></album>
    	<size>1080</size>
    </song>
    </songs>
    

    И следующим запросом приложение ожидаемо идёт на сервер статики за картинкой. Если по сочетанию “Исполнитель” + “Название трека” ничего не найдено, то возвращается пустой xml:

    <?xml version="1.0" encoding="UTF-8" ?>
    <songs>
    </songs>
    

    Проектирование


    Окей, входные и выходные параметры чёрного ящика определены, осталось выстроить логику его работы. И самое главное — решить откуда мы будем брать обложку для запрошенного трека.

    Делать отдельную базу картинок обложек, как-то связывать её с треками, поддерживать в актуальном состоянии — мне лень. Да и не стоит умножать сущностей, заводить какие-то лишние базы и связи, ибо формат mp3 тегов ID3v2 поддерживает хранение обложек в самих mp3 файлах уже много лет, вот и будем ходить внутрь файла за обложкой (если она там, конечно, есть). А если файл не найден (или обложки в нём нет), то вместо пустого xml мы лучше будем отдавать одну из дефолтных обложек для радиостанции, чтобы пользователь не смотрел в пустой квадрат.

    Вообще я предпочитаю всё скриптовать и поменьше работы делать руками. Например, как сейчас выглядит добавление файла в ротацию: закинул файл по ftp/scp в inbox каталог и забыл. Через минуту пришёл скрипт обслуживания, нашёл файл, переименовал его как нужно и переложил в каталог радиостанции. А раз в 10 минут LiquidSoap перечитает каталог, обнаружит новый файл и добавит его в плейлист. Придёт запрос на обложку — скрипт найдёт файл и извлечёт обложку из него.

    У хорошего системного администратора даже Sysadmin Day отмечается автоматически.
    По cron-у.

    Правда в процессе реализации и тестирования логика несколько усложнилась. Ведь зачастую есть ещё cover.jpg в каталоге альбома (для исполнителей, которые в ротации присутствуют целыми альбомами). А есть ещё многочисленные исполнители из SoundCloud / PromoDJ да и просто из vk которые редко собирают треки в альбомы, или вообще заботятся вопросом обложки для трека. Для этих исполнителей (их не так уж много), заведём на сервере статики отдельный каталог с дефолтными обложками по имени исполнителя.

    Последний вопрос: как найти соответствующий запрошенным тэгам файл на диске, учитывая что на момент начала поиска у нас есть только имя исполнителя и название трека? Можно хранить информацию где-нибудь в БД по ключам “исполнитель, трек -> файл на диске”, можно ходить по файлам, смотреть в них mp3-теги сравнивая с запросом (но это долго), а можно, следуя принципу не умножения сущностей просто хранить файлы на диске с именами вида "%artist% — %title%.mp3". У меня сделано именно так. Когда-то для этого я пользовался лучшей, на мой взгляд, для этих целей, программой TagScanner от Сергея Серкова, а потом перешёл на python-скрипт, который автоматически переименовывает файлы в нужный формат.

    Окончательная логика работы получилась такая:

    • Приняли GET запрос.
    • Если запрос пустой (не содержит GET параметров) — возвращает пустой XML
    • Если включена авторизация по токенам (не нулевой список tokens в файле конфигурации) — проверяется пришедший токен. Если токен неверен — 401 Unauthorized.
    • Если в запросе присутствуют переменные artist и title происходит поиск в локальном каталоге mp3 файлов:
    • Если файл не найден — возвращается пустой XML
    • Если файл найден — последовательность следующая:
    • Проверяем — нет ли уже готовой обложки для этого файла в каталоге обложек? Если есть — отдаём ссылку на неё.
    • Если в файле есть обложка — извлекаем её в каталог с обложками, отдаём ссылку.
    • Если в в каталоге с .mp3 файлом есть обложка альбома (файл cover.jpg) — переносим его в каталог обложек альбомов, отдаём ссылку на него.
    • Если в каталоге исполнителей есть обложка с именем `artist` — отдаём ссылку на него.
    • Если совсем ничего не найдено — отдаём случайную картинку из каталога дефолтных обложек радиостанции.

    Ну а теперь, когда логика работы определена, осталось только оформить её в виде функций.

    Код


    Для извлечения обложек из mp3-файлов воспользуемся модулем mutagen. Функция, которая извлекает обложки из mp3 файлов и пишет их в .jpg:

    import mutagen.mp3
    
    def extract_cover(local_file, covers_dir, cover_name):
       """
       Extracts cover art from mp3 file
       :param local_file: file name (with path)
       :param covers_dir: path to store cover art files
       :param cover_name: name for extracted cover art file
       :rtype: bool
       :return:
           False - file not found or contains no cover art
           True - all ok, cover extracted
       """
       try:
           tags = mutagen.mp3.Open(local_file)
           data = ""
           for i in tags:
               if i.startswith("APIC"):
                   data = tags[i].data
                   break
           if not data:
               return False
           else:
               with open(covers_dir + cover_name, "w") as cover:
                   cover.write(data)
                   return True
       except:
           logging.error('extract_cover: File \"%s\" not found in %s', local_file, covers_dir)
           return False

    Если в файле есть обложка и мы её успешно извлекли — делаем ресайз под нужные размеры с сохранением пропорций картинки (ибо не всегда в файле лежат стандартные квадратные обложки). С этим отлично справляется Python Imaging Library (PIL), который ещё и умеет в antialias:

    from PIL import Image
    
    def resize_image(image_file, new_size):
       """
       Resizes image keeping aspect ratio
       :param image_file: file name (with full path)
       :param new_size: new file max size
       :rtype bool
       :return:
           False - resize unsuccessful or file not found
           True - otherwise
       """
       try:
           img = Image.open(image_file)
       except:
           return False
       if max(img.size) != new_size:
           k = float(new_size) / float(max(img.size))
           new_img = img.resize(tuple([int(k * x) for x in img.size]), Image.ANTIALIAS)
           img.close()
           new_img.save(image_file)
       return True

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

    В моей практике был случай, когда из 15-и мегабайтного .mp3 файла половину (7.62 мб), занимала обложка размерами 3508x3508, к тому же с нестандартным цветовым профилем. Этот файл наглухо вешал программу TagScanner, которой я пользуюсь для редактирования тегов. Не знаю, сколько бы отправлялся этот файл по 3G связи, и что стало бы с Андроидом при попытке подогнать его под размер экрана.

    Так как XiiaLive не имеет настроек для выбора сервера обложек, пришлось подменить адрес api.dar.fm, к которому он обращается, на свой. На рутованном Android это просто:

    /etc/hosts
    <my_api_ip>		api.dar.fm

    И объясняем Nginx, что все приходящие запросы, вне зависимости от того, куда они пришли и чего хотят — обслуживает наш скрипт. Заодно поднимаем виртуальный хост для статики, откуда будут отдаваться картинки. Конечно, можно всё делать в рамках одного хоста, но всё-таки лучше мухи api отдельно, а котлеты статика — отдельно.

    upstream fcgiwrap_factory {
       server                        unix:/run/fcgiwrap.socket;
       keepalive                     32;
    }
    
    server {
       listen                        80;
       server_name                   api.<yourserver> api.dar.fm;
    
       root                          /var/wwws/<yourserver>/api;
       access_log                    /var/log/nginx/api.access.log main;
       error_log                     /var/log/nginx/api.error.log;
    
       location / {
           try_files                 $uri    /api.py?$args;
       }
    
       location ~ api.py {
           fastcgi_pass              fcgiwrap_factory;
           include                   /etc/nginx/fastcgi_params;
           fastcgi_param             SCRIPT_FILENAME   $document_root$fastcgi_script_name;
       }
    }
    
    server {
        listen                        80;
        server_name                   static.<yourserver>
    
        root                          /var/wwws/<yourserver>/static;
        access_log                    /var/log/nginx/static.access.log main;
        error_log                     /var/log/nginx/static.error.log;
        index                         index.html;
    
        location / {
        }
    }
    

    После исправления багов и допиливания тонких мест — всё заработало. Музыка играет, картинки извлекаются из mp3 файлов и складываются в каталог хоста со статикой для отдачи через веб. По идее, через некоторое время все обложки перекочуют из недр mp3 файлов в static каталог, но, во-первых процесс извлечения обложки занимает в среднем 100 мс, а во-вторых — место на хостинге всё-таки не резиновое, поэтому картинки через какое-то время удаляются простейшим однострочником на баше, который висит себе в кроне и удаляет файлы к которым обращались больше недели назад:

    find /var/wwws/<yourserver>/static/covers/ -maxdepth 1 -type f -iname '*.jpg' -atime +7 -exec rm {} \;

    Разумеется, чтобы это работало, на разделе с музыкой не должен быть установлен noatime.

    image
    Ну вот всё и заработало, как должно работать.

    Доработка


    Через неделю я проанализировал логи сервера и обнаружил интересное: сразу после запуска приложение посылает запрос вида:

    GET /songart.php?partner_token=7144969234&res=hi HTTP/1.1" 200 334 "-" "Dalvik/1.6.0 (Linux; U; Android 4.4.2; QuadCore-R16 Build/KVT49L)

    И только некоторое время спустя:

    GET /songart.php?partner_token=7144969234&title=Summer+Nights&artist=John+Travolta+and+Olivia+Newton-John&res=hi HTTP/1.1" 200 334 "-" "Dalvik/1.6.0 (Linux; U; Android 4.4.2; QuadCore-R16 Build/KVT49L)

    Соответственно между этими двумя запросами на экране никакой обложки нет, темнота и уныние.

    Непорядок.

    Причина ясна: приложение при запуске ещё не успело извлечь из потока тэги и не знает что играет, почему бы ему не помочь? Добавим первым пунктом ещё одно условие в логику работы программы:

    • Если пришел GET запрос с токеном авторизации, но без указания исполнителя и названия трека — отдать картинку для текущего проигрываемого трека. Если есть переменная stream — из запрошенного потока вещания, иначе — из того, который мы считаем основным.

    Но откуда брать название текущего трека? Не грепать же логи сервера. Очень удачно, что Icecast умеет отдавать состояние примонтированных точек в XML или JSON формате. JSON для Python более нативен, поэтому будем использовать его. Т.к. в Icecast-KH такой статистики “из коробки” нет, воспользуемся xsl файлом из статьи уважаемого namikiri, нечувствительно доработанным мной:

    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
       <xsl:output omit-xml-declaration="yes" method="text" doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN"
                   doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" indent="no" encoding="UTF-8"
                   media-type="application/json; charset=utf-8"/>
       <xsl:strip-space elements="*"/>
       <xsl:template match="/icestats">
           {
           <xsl:for-each select="source">
               "<xsl:value-of select="@mount"/>":
               {
               "name" : "<xsl:value-of select="server_name"/>",
               "listeners" : "<xsl:value-of select="listeners"/>",
               "listener_peak" : "<xsl:value-of select="listener_peak"/>",
               "description" : "<xsl:value-of select="server_description"/>",
               "title" : "<xsl:value-of select="title"/>",
               "genre" : "<xsl:value-of select="genre"/>",
               "url" : "<xsl:value-of select="server_url"/>"
               }
               <xsl:if test="position() != last()">
                   <xsl:text>,</xsl:text>
               </xsl:if>
           </xsl:for-each>
           }
       </xsl:template>
    </xsl:stylesheet>

    Файл кладём в web каталог Icecast-kh (на Ubuntu по умолчанию это /usr/local/share/icecast/web/), и при обращении через http получаем в ответ что-то типа такого:

    {
                "/256":
                {
                "name" : "Radio /256kbps",
                "listeners" : "2",
                "listener_peak" : "5",
                "description" : "mp3, 265kbit",
                "title" : "The Kelly Family - Fell In Love With An Alien",
                "genre" : "Various",
                "url" : ""
                },
    
                "/128":
                {
                "name" : "Radio /128kbps",
                "listeners" : "0",
                "listener_peak" : "1",
                "description" : "mp3, 128kbit",
                "title" : "The Kelly Family - Fell In Love With An Alien",
                "genre" : "Various",
                "url" : ""
                },
    
                "/64":
                {
                "name" : "Radio /64kbps",
                "listeners" : "0",
                "listener_peak" : "2",
                "description" : "mp3, 64kbit",
                "title" : "The Kelly Family - Fell In Love With An Alien",
                "genre" : "Various",
                "url" : ""
                }
    }

    Как видно — радио имеет три точки монтирования (на самом деле несколько больше), вещающих один и тот же поток, но с разным качеством. Ну а дальше всё совсем просто:

    import urllib2
    import json
    
    def get_now_playing(stats_url, stats_stream):
       """
       Retruns current playing song - artist and title
       :param stats_url: url points to icecast stats url (JSON format)
       :param stats_stream: main stream to get info
       :return: string "Artist - Title"
       """
       try:
           stats = json.loads(urllib2.urlopen(stats_url).read())
       except:
           logging.error('get_current_song: Can not open stats url \"%s\"', stats_url)
           return False
    
       if stats_stream not in stats:
           logging.error('get_current_song: Can not find stream \"%s\" in stats data', stats_stream)
           return False
       return stats[stats_stream]['title'].encode("utf-8")

    Функция ходит по указанному адресу статистики, и возвращает исполнителя и заголовок текущей композиции из нужного потока. Поток приходит либо в запросе, либо берётся дефолтный (из настроек).

    Web


    Теперь пришла пора заняться сайтом. Для онлайн воспроизведения я давным-давно использую бесплатный flash-плеер от uppod в минималистичных настройках, который смотрит в /flv поток и при воспроизведении отображает проигрываемый трек. Выглядит это так:

    image

    А для отображения текущего трека, когда плеер свёрнут или неактивн, я, как и многие другие столкнувшиеся с этой проблемой, до недавнего времени пользовался прокладкой в виде .php скрипта на сервере, который ходил к Icecast за статистикой и возвращал строку с именем проигрываемого трека. Пора избавляться от промежуточных шагов, да и обложки на сайте во время онлайн-воспроизведения показывать бы хотелось, раз уж я теперь умею их отдавать.

    Задача решается в два шага:

    Добавляем в конфигурацию Nginx для api кастомный header разрешающий обращаться к нему через jQuery с другого хоста:

    add_header                      Access-Control-Allow-Origin *;

    И помещаем в тело веб-страницы радиостанции такой скрипт:

    var now_playing = '';
    
    setInterval(function () {
       jQuery.ajax(
           {
               type: "GET",
               url: "http://api.<yoursite>/?partner_token=<token>&stream=/<stream>",
               dataType: "xml",
               success: xmlParser
           })
    }, 5000);
    
    function xmlParser(xml) {
       var parsedXml = jQuery(xml);
       var title = parsedXml.find('title').text();
       var artist = parsedXml.find('artist').text();
       var arturl = parsedXml.find('arturl').text();
       var song = artist.concat(" — ").concat(title);
    
       if (now_playing !== song) {
           jQuery('div.now_playing').html(song);
           jQuery('div.cover_art').html(arturl);
           now_playing = song;
       }
    };

    Как видим, раз в пять секунд скрипт ходит туда же, куда и приложение, авторизуется там, получает .xml файл и забирает из него проигрываемый трек и ссылку на обложку. И если с момента прошлой проверки они изменились — то пишет их в нужные div-ы веб-страницы радиостанции для отображения. Сразу прошу господ фронтенд-разработчиков не ругаться на возможную корявость скрипта — jQuery я вижу первый (ну ладно — второй), раз в жизни. Скрипт может и неказист, но прекрасно работает.

    image
    Под плеером добавлен ещё один div в котором динамически меняются обложки.

    Заключение


    На этом все намеченные задачи решены. Радио вещает как и много лет до этого, но теперь ещё и отображает обложки проигрываемых треков и делает это правильно. Маленький перфекционист внутри моей головы спит, удовлетворённо посапывая, и не отвлекает от работы.

    Я понимаю, что описанная тема достаточно узкоспецифичная, и может быть интересна небольшому кругу людей, но думаю, что мой опыт кому-нибудь всё-таки пригодится. Так что полные тексты всего описанного выше кода, плюс примеры настроек Nginx и описание установки, доступны на GitHub.

    Всем музыки!
    Support the author
    Share post

    Similar posts

    Comments 10

      0
      Очень интересная статья, возможно однажды я сделаю также, чтобы не хранить по 10Гб музыки на каждом устройстве. А есть ли возможность переключения треков? Или слушаем то что есть?
        0
        Радио, по самому определению, вещает то что задано плейлистом. Плейлист может генерироваться по разному (у меня это делает liquidsoap по правилам описанным в его конфигурации). Штатной возможности переключать треки у Icecast нет, и её иногда не хватает, да. Я сейчас пишу телеграм-бота для Icecast, думаю с помощью него реализовать переключение треков. Других вариантов пока не вижу.
          0
          Онлайн радио имеет недостаток, возможно из за которого и нет штатного переключения треков.
          Это кеширование на клиенте. После события переключить трек — вам придется ожидать 1-3 секунды(в зависимости от плеера) пока кеш закончится и трек переключится.
          0

          А чем не устраивает, допустим, Google Music? В него можно загрузить 10 000 своих треков.

            0
            50 000 своих. 10к очень маленькое кол-во треков для музыки.
          0
          Здорово, спасибо! Воозьму на заметку
            0
            Очень круто! Тоже о таком задумывался, но решил, что даже прослушивание MP3 в хорошем качестве обойдётся достаточно дорого в плане стоимости 3G-связи. В итоге просто купил SSD на 250 гиг в качестве большой флэшки и закинул туда все любимые вещи в lossless, сколько влезло. Но всё равно периодически задумываюсь о том, что круто было бы сделать всю свою музыку доступной везде, а не только там, куда я её физически принёс (тем более, что коллекция весит уже около 800 ГБ).

            А возможно ли стримить лосслесс, а ещё лучше — читать лосслесс, а стримить с опциональным сжатием на выбор клиента?
            И правильно ли я понимаю, что раз уж клиент всё равно подключен к сети и связан с сервером, то можно реализовать полноценное управление проигрыванием — пропуск трека, запрос списка треков, переход к указанному?
              0
              Если вам нужно просто иметь доступ к коллекции музыки отовсюду — то, наверное, проще воспользоваться чем-то типа Google Music, который советовали выше, или чем-то подобным (тут посоветовать не могу, увы).
              А я делал именно онлайн-радиостанцию (такую же как soma.fm, PSYCHDELICK, тысячи их!) — с изменением ротации в зависимости от времени суток и дня недели, джинглами, часовыми отбивками, но играющую только то, что нравится мне.

              Про стрим: принцип работы источника потокового аудио — любой сжатый/несжатый формат сначала разворачивается в PCM, а потом уже кодируется в нужные форматы вещания (т.к. радио обычно вещает параллельно один и тот же поток, но с разным качеством). Вещать в loseless, конечно можно, но это будет ад по траффику. Лучше, наверное, по соотношению объём/качество, вещать в aac.
                0
                Да, я понимаю, что такое радиостанция.
                Лучший из известных мне lossy-форматов — Vorbis (OGG). Сейчас загуглил — его тоже можно стримить. Идеальный вариант.
              0
              Прочитал, заинтересовался, зарегистрировал в их каталог свою станцию, офигел от ценника, испугался, сходил в гитхабы, нагуглил оттуда пяток токенов, прикрутил к своему радио и имею сказать следующее.

              Система рекомендаций отвратительная — лучше даже не пробовать. Картинки возвращаются далеко не всегда те, которые к треку подходят или даже к альбому откуда трек. Запрашиваешь один тайтл — в ответе приходит другой. Если проверять совпадает ли резалт с запросом (в лоб, банально), то никаких проблем не будет.

              Так же опытным путём было установлено, что если добавить аргумент callback=json, то ответ будет в ём самом.

              И уже потом основательно перерыв их старомодный сайт подвисающий на виражах я обнаружил, что в триальном аккаунте запросто регистрируется бесплатно своя станция и к ней получается бесплатный токен. И наверное даже не один — не стал пробовать.

              Хорошая статья, респект и уважение коллеге по „сам себе сделай радио”. :)

              Only users with full accounts can post comments. Log in, please.