AzuraCast:自托管一体化网络电台管理套件

AzuraCast:自托管一体化网络电台管理套件

项目描述

AzuraCast是一个自托管、一体化的网络电台管理套件。通过其简单的安装程序和强大直观的Web界面,您可以在几分钟内启动一个完全可用的网络电台。AzuraCast适用于各种类型和规模的网络电台,旨在运行在最经济的VPS虚拟主机上。

功能特性

  • 完整的电台管理:提供从流媒体广播到节目编排的完整解决方案
  • 多格式支持:支持Icecast、HLS等多种流媒体协议和格式
  • 智能自动DJ:支持播放列表调度、歌曲请求和实时流媒体
  • 共享编码器:可选功能,可在具有相同比特率和格式的多个流之间共享编码器,显著降低CPU消耗
  • Liquidsoap 2.4.0集成:更新的流媒体后端,包含关键错误修复和改进
  • 用户权限管理:细粒度的权限控制系统,支持全局和电台级别权限
  • API接口:完整的RESTful API,支持第三方集成
  • 多语言支持:支持多种语言界面
  • 主题定制:可定制的用户界面主题
  • 备份恢复:完整的数据库和媒体备份恢复功能

安装指南

系统要求

  • PHP 8.1或更高版本
  • MariaDB/MySQL数据库
  • Redis缓存(可选但推荐)
  • 足够的磁盘空间用于媒体存储

安装步骤

  1. 下载项目

    git clone https://github.com/AzuraCast/AzuraCast.git
    cd AzuraCast
    
  2. 运行安装程序

    ./docker.sh setup
    
  3. 或使用传统安装方式

    php bin/console azuracast:setup
    
  4. 访问管理界面
    安装完成后,通过浏览器访问您的服务器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

使用说明

基本配置

  1. 创建第一个电台

    • 登录管理后台
    • 点击"添加电台"
    • 配置基本信息、流媒体设置和自动DJ
  2. 上传媒体文件

    • 通过Web界面上传音乐文件
    • 或使用SFTP直接上传到媒体目录
  3. 配置播放列表

    • 创建定时播放列表
    • 设置播放规则和顺序
    • 配置交叉淡入淡出效果

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智能小助手)
对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)

公众号二维码

公众号二维码

posted @ 2025-11-15 18:06  qife  阅读(0)  评论(0)    收藏  举报