Решение проблемы хранения музыки для Интернет-радио

от автора


Добрый день, %username%!
Как-то мы с компанией друзей решили сделать интернет радио, но как оказалось, выделяемого места на VPS недостаточно для большого архива музыки, более того покупка дополнительных гигабайтов — настоящий грабеж.

Я долго искал решение, как вдруг наткнулся на прекрасную статью ableev «Яндекс.Диск как файловая система». Меня посетила идея, почему бы не хранить музыку на Яндекс диске? Опустим здесь проблемы лицензирования и авторских прав — это совсем другая история, меня же интересует техническая часть. Как оказалось не всегда IceCast успевает подгружать музыку с Яндекс диска, что приводит к запинаниям и прерываниям в вещании, а это совсем не хорошо. Эта проблема меня зацепила, и я нашел решение — определять что играет в текущий момент на радио и заранее подгружать следующие треки, а проигранные треки с сервера удалять. Это порождает трафик, согласен, но на текущий момент VPS с безлимитным трафиком полно, а с безлимитным местом на дисках нет.

Так как из языков я худо-бедно владею C#, пришлось прибегнуть к mono, а также написать несколько вспомогательных скриптов на Python, PHP, bash.

Вспомогательные скрипты в студию!

id3.py получает при вызове аргументом трек, из которого берет теги и записывает их в текстовый файл:

id3.py

#!/usr/bin/env python  class Unbuffered(object):    def __init__(self, stream):        self.stream = stream    def write(self, data):        self.stream.write(data)        self.stream.flush()    def __getattr__(self, attr):        return getattr(self.stream, attr)  import eyeD3 import sys import argparse  def createParser (): 	parser = argparse.ArgumentParser() 	parser.add_argument('-f', '--file') 	 	return parser  if __name__ == '__main__': 	parser = createParser() 	namespace = parser.parse_args(sys.argv[1:])   tag = eyeD3.Tag() tag.link(namespace.file) sys.stdout = Unbuffered(sys.stdout) s = tag.getArtist()+" - "+tag.getTitle() f = open('tag.txt','w') f.write(s) f.close() 

getCurrent.php парсит страничку IceCast с информацией о текущем треке и выдает название играющего трека.

getCurrent.php

<?php error_reporting(0); header("Content-Type: text/html; charset=UTF-8"); $file_name="http://localhost:8000/status.xsl?mount=/stream"; $r=fopen($file_name,'r'); $text=fread($r,10000); fclose($r); $mas=explode('<tr>', $text); $name = explode(':', $mas[3]); $q = explode ('</td>',$name[1]); $q2 = explode ('<td class="streamdata">',$q[1]); $rj = $q2[1];  if($rj == "0" or $rj == ""){ echo " Nonstop";  }else { $fl = file_get_contents('http://localhost:8000/status.xsl?mount=/stream'); function antara($string, $start, $end){ $string = " ".$string; $ini = strpos($string,$start); if ($ini == 0) return ""; $ini += strlen($start); $len = strpos($string,$end,$ini) - $ini; return substr($string,$ini,$len); } $stream = antara($fl,"<td>Stream Title:</td>\n<td class=\"streamdata\">","</td>");  $description = antara($fl, "<td>Stream Description:</td>\n<td class=\"streamdata\">", "</td>");  $listeners = antara($fl, "<td>Current Listeners:</td>\n<td class=\"streamdata\">", "</td>");  $max = antara($fl, "<td>Peak Listeners:</td>\n<td class=\"streamdata\">", "</td>");  $song = antara($fl, "<td>Current Song:</td>\n<td class=\"streamdata\">", "</td>");  echo $song; ?> 

Маленький скрипт на bash isOnline.sh проверяет запущен ли скрипт радио и информацию о статусе также пишет в текстовый файл.

isOnline.sh

#!/bin/bash rm isOnline.txt ps ax | grep -v grep | grep radio.sh > isOnline.txt 

Скрипт радио liquidsoap radio.sh — собственно само радио.

radio.sh

#!/usr/bin/liquidsoap  # создаём переменные быстрого исправления в одном месте по необходимости # базовая информация о выводимом потоке out = output.icecast( # хост с icecast host = "127.0.0.1",  # его порт port = 8000,  # логин user = "source",  # и пароль password = "password",  # название name = "Radio Name",  # жанр genre = "Various",  # ссылка на сайт url = "http://www.host.local"  # кодировка encoding = "UTF-8" )  # включаем telnet-сервер #set("server.telnet.bind_addr","127.0.0.1") #set("server.telnet",true)  # _____________________________________ # Описание файловой структуры нашего радиосервера.  # Переменные можно не использовать, а писать сразу полные пути к плейлистам, но при изменении названия одной из папок, придётся править довольно много строк в конфигурации. Как показала практика, такой подход удобнее.  # абсолютный путь к рабочей директории #wd = "/home/admin/radio"  # путь к папке с аудиофайлами #pl = "#{wd}/collection"  # техническая папка #tech = "#{wd}/technical"  # логи set("log.file.append",true) set("log.file",true) set("log.file.path","/var/data/liquidsoap.log") # путь к файлу лога set("log.level", 3) # уровень логирования #set("buffering.kind","disk_manyfiles") #set("decoding.buffer_length",30.) #set("buffering.path","/tmp/radio") # папка с информационными вставками #promo_dir = "#{pl}/promo"  # папка с программами #progr_dir = "#{pl}/programs"  # папка с изменяющимся эфиром #ef = "#{pl}/efir"  # папки соответствующих эфиров #ni = "#{ef}/night"  # папки с музыкой mus_ni_dir = "loc.playlist.txt"  # папки с джинглами jin_ni_dir = "/mnt/username.yadisk/RT/jingles"  #promo promo_dir = "/mnt/username.yadisk/RT/promo"  mus_ni = playlist (reload = 86400, "#{mus_ni_dir}", mode = "normal") jin_ni = playlist (reload = 86400, "#{jin_ni_dir}", mode = "normal") promo    = playlist (reload = 86400,  "#{promo_dir}") ni = rotate (weights = [1, 10, 1, 5], [jin_ni, mus_ni, promo, mus_ni])  radio = switch (track_sensitive = true, [   #({ (2w16h10m - 2w16h20m) or (3w14h - 3w14h5m) or (4w16h - 4w16h5m)}, spekt), ({ (3w23h - 3w23h5m) or (4w10h - 4w10h5m)}, pozdr), ({ (2w18h - 2w18h5m) or #(3w18h - 3w18h5m)}, xmas_trad), ({ (6w14h - 6w14h10m) or (6w18h - 6w18h10m)}, prog1), ({ (6w0h - 7w18h)}, xmas_mus_prog),   ({ 1w0h - 7w23h59m }, ni) ])  radio = mksafe(radio) radio = crossfade(start_next=2., fade_out=2., fade_in=2., radio)  out(  %mp3(bitrate = 128, id3v2 = true),  description = "Radio Name 128kbps",  mount = "stream",  mksafe(radio) )  

Генератор списка воспроизведения generator.sh создает список файлов на смонтированном диске, перемешивает список и записывает в текстовый файл. Это скрипт замечателен тем, что здесь можно добавить много дисков и собрать все в 1 плейлист.

generator.sh

#!/bin/sh find "/mnt/username.yadisk/RT/music" -name '*.mp3' -print > "disk.playlist.txt" shuf -n 500 "disk.playlist.txt" -o "disk.playlist.txt" sed 's/\/mnt\/username.yadisk\/RT\/music/\/home\/admin\/rt\/tmp/g' disk.playlist.txt > loc.playlist.txt 

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

radio_control.cs

using System; using System.Collections.Generic; using System.IO; using System.Collections; using System.Diagnostics;   namespace radio_control {     class song     {         string filename;         string tagname;         string prepared;          System.Diagnostics.Process getTag;           public song()         {             getTag = new System.Diagnostics.Process();             getTag.EnableRaisingEvents = false;             getTag.StartInfo.FileName = "./id3.py";          }          public string FileName         {             get { return filename; }             set { filename = value; }         }         public string Tag         {             get { return tagname; }             set { tagname = value; }         }         public string Prepared         {             get { return prepared; }             set { prepared = value; }         }          /// <summary>         /// Загружает файл во временную папку, и записывает путь к файлу в поле Prepared         /// </summary>         /// <param name="tmpDir"></param>         public void Prepare(string tmpDir)         {             File.Copy(FileName, tmpDir+Path.GetFileName(FileName), true);             Prepared = tmpDir + Path.GetFileName(FileName);             Tag = _getTag().Replace(Environment.NewLine,"");         }          //удаляет файл из временной директории         public void Destroy()         {             File.Delete(Prepared);         }          //Получаем тэги подготовленного файла         string _getTag()         {             getTag.StartInfo.Arguments = "-f " + this.Prepared;             getTag.Start();             getTag.WaitForExit();             StreamReader str = new StreamReader("tag.txt");             string tag = str.ReadToEnd();             str.Close();             return tag;         }      }      class Program     {           static void Main(string[] args)         {             //счетчик, чтобы определить какой элемент списка подгружать             int count = 0;              string pl_file = "/home/admin/rt/disk.playlist.txt"; //расположение плейлиста             string tmpDir = "/home/admin/rt/tmp/"; //расположение директории для временных файлов             string newplTime = "23:30";             //готовим список объектов песен             List<song> songs = new List<song>();                          //Процесс получения текущего играющего трека с icecast             System.Diagnostics.Process getCurrent = new System.Diagnostics.Process();             getCurrent.EnableRaisingEvents = false;             getCurrent.StartInfo.RedirectStandardOutput = true;             getCurrent.StartInfo.FileName = "/usr/bin/php";             getCurrent.StartInfo.UseShellExecute = false;             getCurrent.StartInfo.Arguments = "getCurrent.php";              //Процесс проверки, запущен ли скрипт радио             System.Diagnostics.Process isRadio = new System.Diagnostics.Process();             isRadio.EnableRaisingEvents = false;             getCurrent.StartInfo.UseShellExecute = false;             getCurrent.StartInfo.RedirectStandardOutput = true;             isRadio.StartInfo.FileName = "isOnline.sh";              //Процесс запуска радио             System.Diagnostics.Process radio = new System.Diagnostics.Process();             radio.EnableRaisingEvents = false;             getCurrent.StartInfo.UseShellExecute = false;             getCurrent.StartInfo.RedirectStandardOutput = true;             radio.StartInfo.FileName = "screen";             radio.StartInfo.Arguments = "-dmS radio liquidsoap --verbose radiotera.sh";              //Процесс прибития скрина radio              System.Diagnostics.Process killRadio = new System.Diagnostics.Process();             killRadio.EnableRaisingEvents = false;             getCurrent.StartInfo.UseShellExecute = false;             getCurrent.StartInfo.RedirectStandardOutput = true;             killRadio.StartInfo.FileName = "screen";             killRadio.StartInfo.Arguments = "-X -S radio quit";              //Процесс запуска генератора плейлиста             Process genPl = new Process();             genPl.EnableRaisingEvents = false;             genPl.StartInfo.UseShellExecute = false;             genPl.StartInfo.FileName = "generator.sh";              //создаем очередь из треков, очередь - это стек типа first in - first out             Queue queue = new Queue(3);                  //загружаем в плейлист список файлов                 string[] playlist = (System.IO.File.ReadAllLines(pl_file));                 log("Loading playlist");                 //определяем ID3 теги файлов и заполняем настоящий плейлист                 song sng;                 foreach (string value in playlist)                 {                     sng = new song();                     sng.FileName = value;                     songs.Add(sng);                 }                 log("Playlist loaded.", ConsoleColor.Green);                 //подготавливаем первые 3 трека и увеличиваем счетчик                 songs[0].Prepare(tmpDir);                 count++;                 songs[1].Prepare(tmpDir);                 count++;                 songs[2].Prepare(tmpDir);                 count++;                                   //добавляем их в очередь                 queue.Enqueue(songs[0]);                 queue.Enqueue(songs[1]);                 queue.Enqueue(songs[2]);                 log("First 3 tracks prepared:\n"+songs[0].Tag+"\n"+songs[1].Tag+"\n"+songs[2].Tag, ConsoleColor.Green);                    //основной цикл программы                 song tmp = new song();                 StreamReader rdr;             string online = "";                              while (true)                 {                     log("Check time to change playlist"); //проверяем время изменения плейлиста                     if (DateTime.Now.ToString("HH:mm") == newplTime)                     {                         log("Its time to change playlist!", ConsoleColor.Red);                         genPl.Start();  //генерируем плейлист                         genPl.WaitForExit();                         log("Playlist generated", ConsoleColor.Green);                         log("Loading playlist");                         playlist = (System.IO.File.ReadAllLines(pl_file));                         songs.Clear();                         foreach (string value in playlist) //загружаем плейлист                         {                             sng = new song();                             sng.FileName = value;                             songs.Add(sng);                         }                         log("Playlist loaded", ConsoleColor.Green);                      }                      log("Check the availability of radio");                      isRadio.Start();                     isRadio.WaitForExit();                     rdr = new StreamReader("isOnline.txt");                     online=rdr.ReadToEnd();                     rdr.Close();                     //Включено ли радио?                     if (online == "")                     {                         log("Radio is offline!\nClearing screen", ConsoleColor.Red);                         killRadio.Start();                         killRadio.WaitForExit();                         log("Starting the radio");                         radio.Start();                         log("Waiting for 10 seconds");                         System.Threading.Thread.Sleep(10000);                     }                     log("Get current song"); //получаем текущий трек                     getCurrent.Start();                     getCurrent.WaitForExit();                     string curr = getCurrent.StandardOutput.ReadToEnd();                     if (curr == "") continue;                     //получаем первый трек очереди                     tmp = new song();                     tmp = (song)queue.Peek();                      //проверяем, этот ли трек сейчас играет                     log("Now playing: " + curr, ConsoleColor.Cyan);                     log("Checking queue tag: "+tmp.Tag, ConsoleColor.Cyan);                     if (tmp.Tag != curr)                     {                         //если играет не он, но играет джингл — ничего не делаем.                         //Задавайте своим джинглам одинаковые тэги и впишите их в проверку здесь                         if ((curr == "Radio TERA - Radio TERA") || (curr=="Unknown") || (curr=="NonstopS"))                          {                             log("Now playing radio jingle, move on to the next iteration");                             continue;                         }                         log("The current track is different from the queue!!!\nMove the queue", ConsoleColor.Red);                         //если все таки трек уже закончился, то убираем трек из очереди                         queue.Dequeue();                         log("Dequene track with tag "+tmp.Tag);                          //удаляем трек из временной папки                         log("Remove the track ended");                         tmp.Destroy();                          //копируем следующий трек во временную папку                         log("Prepare next track");                         songs[count].Prepare(tmpDir);                         //добавляем его в очередь                         log("Adding it to queue "+songs[count].Tag);                         queue.Enqueue(songs[count]);                                                                           //увеличиваем счетчик                         count++;                         log("Count now: " + count.ToString());                         //если счетчик превышает количество песен, значит плейлист закончился и пора играть все сначала                         if (count > songs.Count - 1)                         {                             count = 0;                         }                     }                      //висим 30 секунд                     System.Threading.Thread.Sleep(30000);                 }          }         static void log(string str)         {             Console.WriteLine(str);         }         static void log(string str, ConsoleColor frcolor)         {             Console.ForegroundColor = frcolor;             Console.WriteLine(str);             Console.ForegroundColor=ConsoleColor.White;         }     } } 

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

Для нормальной работы вам понадобится:

  • mono
  • liquidsoap
  • screen
  • IceCast
  • davfs2
  • php
  • eyeD3 для Python

Запускается система через radio_control.cs. Все. Дальше он сам запустит радио, сгенерирует плейлист, подгрузит музыку и при этом будет писать в терминал, что он делает.

Радио к сожалению мы закрыли, но мне очень хотелось, чтобы мои труды не пропали напрасно, надеюсь, кому-нибудь помог.

ссылка на оригинал статьи http://habrahabr.ru/post/270415/