AzuraCast:自托管一体化网络电台管理套件
AzuraCast:自托管一体化网络电台管理套件
项目描述
AzuraCast是一个自托管、一体化的网络电台管理套件。通过其简单的安装程序和强大直观的Web界面,您可以在几分钟内启动一个完全可用的网络电台。AzuraCast适用于各种类型和规模的网络电台,旨在运行在最经济的VPS虚拟主机上。
功能特性
- 完整的电台管理:提供从流媒体广播到节目编排的完整解决方案
- 多格式支持:支持Icecast、HLS等多种流媒体协议和格式
- 智能自动DJ:支持播放列表调度、歌曲请求和实时流媒体
- 共享编码器:可选功能,可在具有相同比特率和格式的多个流之间共享编码器,显著降低CPU消耗
- Liquidsoap 2.4.0集成:更新的流媒体后端,包含关键错误修复和改进
- 用户权限管理:细粒度的权限控制系统,支持全局和电台级别权限
- API接口:完整的RESTful API,支持第三方集成
- 多语言支持:支持多种语言界面
- 主题定制:可定制的用户界面主题
- 备份恢复:完整的数据库和媒体备份恢复功能
安装指南
系统要求
- PHP 8.1或更高版本
- MariaDB/MySQL数据库
- Redis缓存(可选但推荐)
- 足够的磁盘空间用于媒体存储
安装步骤
-
下载项目
git clone https://github.com/AzuraCast/AzuraCast.git cd AzuraCast -
运行安装程序
./docker.sh setup -
或使用传统安装方式
php bin/console azuracast:setup -
访问管理界面
安装完成后,通过浏览器访问您的服务器IP地址即可开始配置。
Docker安装(推荐)
AzuraCast提供完整的Docker支持,包含所有必要的依赖项:
curl -fsSL https://raw.githubusercontent.com/AzuraCast/AzuraCast/main/docker.sh > docker.sh
chmod a+x docker.sh
./docker.sh setup
使用说明
基本配置
-
创建第一个电台
- 登录管理后台
- 点击"添加电台"
- 配置基本信息、流媒体设置和自动DJ
-
上传媒体文件
- 通过Web界面上传音乐文件
- 或使用SFTP直接上传到媒体目录
-
配置播放列表
- 创建定时播放列表
- 设置播放规则和顺序
- 配置交叉淡入淡出效果
API使用示例
获取当前播放信息:
curl https://your-azuracast-instance.com/api/nowplaying
获取电台列表:
curl https://your-azuracast-instance.com/api/stations
命令行工具
AzuraCast提供丰富的命令行工具:
# 同步当前播放信息
php bin/console azuracast:sync:nowplaying
# 备份系统
php bin/console azuracast:backup /path/to/backup.zip
# 恢复备份
php bin/console azuracast:restore /path/to/backup.zip
# 清除缓存
php bin/console azuracast:cache:clear
核心代码
权限控制系统
<?php
declare(strict_types=1);
namespace App;
final class Acl
{
public function __construct(
private readonly ReloadableEntityManagerInterface $em,
private readonly EventDispatcherInterface $dispatcher
) {
$this->reload();
}
/**
* 检查用户是否具有特定权限
*/
public function userAllowed(User $user, string $permission, ?Station $station = null): bool
{
$userRoles = $user->roles;
foreach ($userRoles as $role) {
if ($this->roleHasPermission($role, $permission, $station)) {
return true;
}
}
return false;
}
/**
* 重新加载ACL缓存
*/
public function reload(): void
{
// 从数据库加载权限配置
$sql = $this->em->createQuery(
<<<'DQL'
SELECT IDENTITY(rp.station) AS station_id,
IDENTITY(rp.role) AS role_id,
rp.action_name
FROM App\Entity\RolePermission rp
DQL
);
$this->actions = [];
foreach ($sql->toIterable() as $row) {
if ($row['station_id']) {
$this->actions[$row['role_id']]['stations'][$row['station_id']][] = $row['action_name'];
} else {
$this->actions[$row['role_id']]['global'][] = $row['action_name'];
}
}
}
}
当前播放缓存系统
<?php
declare(strict_types=1);
namespace App\Cache;
final class NowPlayingCache
{
private const int NOWPLAYING_CACHE_TTL = 180;
/**
* 设置电台的当前播放信息
*/
public function setForStation(
Station $station,
?NowPlaying $nowPlaying
): void {
$this->populateLookupCache($station);
$stationCacheItem = $this->getStationCache($station->short_name);
$stationCacheItem->set($nowPlaying);
$stationCacheItem->expiresAfter(self::NOWPLAYING_CACHE_TTL);
$this->cache->saveDeferred($stationCacheItem);
$this->cache->commit();
}
/**
* 获取所有电台的当前播放信息
*/
public function getForAllStations(bool $publicOnly = false): array
{
$lookupCacheItem = $this->getLookupCache();
if (!$lookupCacheItem->isHit()) {
return [];
}
$np = [];
$lookupCache = (array)$lookupCacheItem->get();
foreach ($lookupCache as $stationInfo) {
if ($publicOnly && !$stationInfo['is_public']) {
continue;
}
$npRowItem = $this->getStationCache($stationInfo['short_name']);
$npRow = $npRowItem->isHit() ? $npRowItem->get() : null;
if ($npRow instanceof NowPlaying) {
$np[] = $npRow;
}
}
return $np;
}
}
自定义资源管理系统
<?php
declare(strict_types=1);
namespace App\Assets;
abstract class AbstractCustomAsset implements CustomAssetInterface
{
/**
* 获取资源路径
*/
public function getPath(): string
{
$pattern = sprintf($this->getPattern(), '');
return $this->getBasePath() . '/' . $pattern;
}
/**
* 获取资源URL
*/
public function getUrl(): string
{
$path = $this->getPath();
if (is_file($path)) {
$pattern = $this->getPattern();
$mtime = filemtime($path);
return $this->getBaseUrl() . '/' . sprintf(
$pattern,
'.' . $mtime
);
}
return $this->getDefaultUrl();
}
/**
* 上传资源
*/
public function upload(ImageInterface $image, string $mimeType): void
{
$newImage = clone $image;
$newImage->resizeDown(1500, 1500);
$this->delete();
$patterns = $this->getPatterns();
[$pattern, $encoder] = $patterns[$mimeType] ?? $patterns['default'];
$destPath = $this->getPathForPattern($pattern);
$this->ensureDirectoryExists(dirname($destPath));
$newImage->encode($encoder)->save($destPath);
}
}
消息队列处理系统
<?php
declare(strict_types=1);
namespace App\Console\Command\MessageQueue;
final class ProcessCommand extends AbstractSyncCommand
{
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->logToExtraFile('app_worker.log');
$runtime = Types::int($input->getArgument('runtime'));
$workerName = Types::stringOrNull($input->getOption('worker-name'), true);
$this->logger->notice('Starting new Message Queue worker process.', [
'runtime' => $runtime,
'workerName' => $workerName,
]);
// 配置消息队列接收器
$receivers = $this->queueManager->getTransports();
// 添加事件订阅者
$this->eventDispatcher->addServiceSubscriber(ClearEntityManagerSubscriber::class);
$this->eventDispatcher->addServiceSubscriber(LogWorkerExceptionSubscriber::class);
// 设置工作器超时
$this->eventDispatcher->addSubscriber(
new StopWorkerOnTimeLimitListener($runtime, $busLogger)
);
try {
$worker = new Worker($receivers, $this->messageBus, $this->eventDispatcher, $busLogger);
$worker->run();
} catch (Throwable $e) {
$this->logger->error('Message queue error: ' . $e->getMessage(), [
'workerName' => $workerName,
'exception' => $e,
]);
return 1;
}
return 0;
}
}
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)
公众号二维码

公众号二维码


浙公网安备 33010602011771号