Добрый день, %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. Все. Дальше он сам запустит радио, сгенерирует плейлист, подгрузит музыку и при этом будет писать в терминал, что он делает.
Радио к сожалению мы закрыли, но мне очень хотелось, чтобы мои труды не пропали напрасно, надеюсь, кому-нибудь помог.
Комментарии (15)
eZyatev
09.11.2015 08:20Почему все же закрыли радио?
UrbanRider
09.11.2015 08:46+1Утрата интереса и разлад в команде.
Опыт радио был очень интересен, но наши ожидания не совпали с реальностью за счет не знания всего процесса, когда мы это начинали.
В итоге мы набрали много людей, так как справиться со всеми идеями в 3-4 человека нереально. Команда разрослась до 20+ человек.
Радио было не коммерческим, а работать надо было много (Нам не интересно было просто крутить музыку — мы выходили в эфиры, делали аудио спектакли и т.д.), что естественно у многих людей быстро вызвало желание все бросить.
Для дилетантов мы вполне не плохо справлялись:
Но удержать команду на одном энтузиазме тяжело, а на уровень получения доходов с рекламы мы не вышли, посему увы и ах.
Nadoedalo
09.11.2015 16:42О, интересно. А как решили проблему с вещанием сразу в 2 форматах(ogg и mp3) с разным битрейтом? И что бы все эти дорожки во времени были синхронизированы? Просто если радио — это веб-радио, то нужно и разный битрейт и разный формат поддерживать, иначе у некоторых пользователей будут проблемы.
UrbanRider
09.11.2015 19:17в radio.sh который liquidsoap:
Вот такой вот вариант# и, наконец, запускаем вещалки с разным качеством out( %vorbis.abr(samplerate = 44100, channels = 2, bitrate = 128, max_bitrate = 192, min_bitrate = 96), description = "Average vorbis 96-128-192 Kbps", mount = "HabraRadio_vorbis_avg_128", mksafe(radio) ) out( %mp3(bitrate = 320, id3v2 = true), description = "MP3 320 Kbps", mount = "HabraRadio_320", mksafe(radio) ) out( %mp3(bitrate = 192, id3v2 = true), description = "MP3 192 Kbps", mount = "HabraRadio_192", mksafe(radio) )
Nadoedalo
09.11.2015 19:37О, спасибо. Всегда хотел собственную интернет-радиостанцию со своим плейлистом и собственный интернет-кинотеатр что бы смотреть всякую шнягу прямо в браузере.
Чувствую что таки потрачу кучу времени и таки подниму всё это =), а потом интегрирую в «умный дом»… ну и так далее. Вот ведь круто было бы — со смартфона управлять всем и иметь персональное облако со всякими ништяками ) Причём только твоё )
Methos
Мастерхост недавно сделал безлимитное место.
UrbanRider
Интересная информация, посмотрю, спасибо.
Это для шаред хостинга. Для VPS безлимита не нашел.
Firz
Так используйте шаред хостинг только как хранилище =)
UrbanRider
Ну тогда все равно метод, описанный в статье будет актуален :)
kAIST
Что то не нашёл звёздочек в тарифе, это настораживает )
Ищу сейчас себе облако подешевле на 1тб, и это получается самый дешевый вариант
MrFrizzy
Я как-то наткнулся на такой вариант, но не тестировал.
Хотя любой амазон\гугол\яндекс будет дешевле на не коммерческом тарифе.
Тоже интересно насчет шаред хостинга — халявы не бывает…
skobkin
Есть HubiC, но там не всё так просто с монтированием, например. Хотя с виндой проще.
Зато какая цена за гигабайт.
Alexufo
Ой да ладно, проходили сто раз. Что они говорят насчет использования как файлохранилище и какие лимиты исходящего канала? Вот там то и ответ. В свое время понаспрашивался, сейчас «пока хостер не вкурсе, работает»