Добрый день, %username%!
Как-то мы с компанией друзей решили сделать интернет радио, но как оказалось, выделяемого места на VPS недостаточно для большого архива музыки, более того покупка дополнительных гигабайтов — настоящий грабеж.
Я долго искал решение, как вдруг наткнулся на прекрасную статью ableev «Яндекс.Диск как файловая система». Меня посетила идея, почему бы не хранить музыку на Яндекс диске? Опустим здесь проблемы лицензирования и авторских прав — это совсем другая история, меня же интересует техническая часть. Как оказалось не всегда IceCast успевает подгружать музыку с Яндекс диска, что приводит к запинаниям и прерываниям в вещании, а это совсем не хорошо. Эта проблема меня зацепила, и я нашел решение — определять что играет в текущий момент на радио и заранее подгружать следующие треки, а проигранные треки с сервера удалять. Это порождает трафик, согласен, но на текущий момент VPS с безлимитным трафиком полно, а с безлимитным местом на дисках нет.
Так как из языков я худо-бедно владею C#, пришлось прибегнуть к mono, а также написать несколько вспомогательных скриптов на Python, PHP, bash.
Вспомогательные скрипты в студию!
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 с информацией о текущем треке и выдает название играющего трека.
<?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 проверяет запущен ли скрипт радио и информацию о статусе также пишет в текстовый файл.
#!/bin/bash rm isOnline.txt ps ax | grep -v grep | grep radio.sh > isOnline.txt
Скрипт радио liquidsoap 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 плейлист.
#!/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
Теперь осталось написать управляющий скрипт, который будет следить, не упал ли скрипт радио, запускать его в случае падения, следить за плейлистом, подгружать и удалять треки.
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/
Добавить комментарий