using System.Drawing;
using Dpz.Core.Shard.Service;
using Dpz.Core.Web.Models.Video;

namespace Dpz.Core.Web.Controllers;

public class VideoController(
    IBarrageService barrageService,
    ILogger<VideoController> logger,
    IVideoShardService videoShardService,
    IConfiguration configuration
) : Controller
{
    private readonly Lazy<string> _lazyCdnHost = new(() =>
        configuration["S3:CdnHost"]
        ?? throw new InvalidConfigurationException("S3:CdnHost is empty or null")
    );

    public async Task<IActionResult> Index()
    {
        this.SetTitle("视频");
        var videos = await videoShardService.GetVideosAsync();
        return View(videos);
    }

    [Route("video/watch/{id}.html")]
    public async Task<IActionResult> Watch(string id)
    {
        var video = await videoShardService.GetVideoAsync(id);
        if (video == null)
        {
            return NotFound();
        }

        var title = string.IsNullOrWhiteSpace(video.VideoTitle)
            ? video.Title ?? "未命名视频"
            : video.VideoTitle;
        var subtitle = string.IsNullOrWhiteSpace(video.SubTitle) ? video.Name : video.SubTitle;
        var description = video.Description?.Trim();
        var coverUrl = string.IsNullOrWhiteSpace(video.Cover)
            ? $"{_lazyCdnHost.Value}/Video/{video.Title}/video.webp"
            : video.Cover;
        var videoUrl = string.IsNullOrWhiteSpace(video.M3u8)
            ? $"{_lazyCdnHost.Value}/Video/{video.Title}/1080p.m3u8"
            : video.M3u8;
        var tags = video.Tags.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();

        var model = new VideoWatchModel(
            subtitle ?? "",
            description ?? "",
            video.PlayCount,
            video.DanmakuCount,
            video.CommentCount,
            tags,
            new VideoConfig(video.Id ?? "", videoUrl, coverUrl, title)
        );

        this.SetTitle(video.VideoTitle ?? "");
        return View(model);
    }

    [HttpPatch("play/{id}")]
    public async Task<IActionResult> Play(string id)
    {
        await videoShardService.AddPlayAsync(id);
        return NoContent();
    }

    [Route("history/chat/v3/"), HttpGet]
    public async Task<IActionResult> HistoryChat(string id)
    {
        var dbDanmaku = await barrageService.GetGroupBarragesAsync(id);
        var allDanmaku = dbDanmaku.Select(x =>
        {
            var result = int.TryParse(x.Color, out var color);
            return new object[]
            {
                x.Time,
                x.Position,
                result ? color : x.Color ?? "#FFF",
                "阿胖",
                x.Text ?? "",
            };
        });
        return Json(new { code = 0, data = allDanmaku });
    }

    [Route("history/danmaku/{id}"), HttpGet]
    public async Task<IActionResult> HistoryDanmaku(string id)
    {
        var dbDanmaku = await barrageService.GetGroupBarragesAsync(id);
        var allDanmaku = dbDanmaku
            .OrderBy(x => x.Time)
            .Select(x =>
            {
                var result = int.TryParse(x.Color, out var color);
                var colorRgb = Color.FromArgb(color);
                return new
                {
                    color = result ? $"rgb({colorRgb.R},{colorRgb.G},{colorRgb.B})" : x.Color,
                    text = x.Text,
                    time = x.Time,
                    type = x.Position switch
                    {
                        0 => "scroll",
                        1 => "top",
                        2 => "bottom",
                        _ => "",
                    },
                };
            })
            .ToList();
        return Json(allDanmaku);
    }

    [Route("history/danmaku"), HttpPost]
    public async Task<IActionResult> HistoryDanmaku([FromBody] VideoDanmaku2 danmaku)
    {
        if (string.IsNullOrWhiteSpace(danmaku.Text))
        {
            return Json(new ResultInfo(true));
        }
        var color = ColorTranslator.FromHtml("#FF0000");
        var vmBarrage = new VmBarrage
        {
            Color = color.ToArgb().ToString(),
            Group = danmaku.Id,
            Position = danmaku.Type switch
            {
                "scroll" => 0,
                "top" => 1,
                "bottom" => 2,
                _ => 0,
            },
            SendTime = DateTime.Now,
            Size = 0,
            Text = danmaku.Text,
            Time = danmaku.Time,
        };
        await barrageService.AddBarrageAsync(vmBarrage);
        return Json(new ResultInfo(true));
    }

    [Route("history/chat/v3/"), HttpPost]
    public async Task<IActionResult> HistoryChat([FromBody] VideoDanmaku danmaku)
    {
        if (string.IsNullOrWhiteSpace(danmaku.Text))
        {
            return Json(new { code = 0, data = danmaku });
        }
        var vmBarrage = new VmBarrage
        {
            Color = danmaku.Color.ToString(),
            Group = danmaku.Id,
            Position = (int)danmaku.Type,
            SendTime = DateTime.Now,
            Size = 0,
            Text = danmaku.Text,
            Time = danmaku.Time,
        };
        await barrageService.AddBarrageAsync(vmBarrage);
        return Json(new { code = 0, data = danmaku });
    }

    [Route("history/danmaku/v2/{id}"), HttpGet]
    public async Task<IActionResult> HistoryDanmakuV2(string id)
    {
        var dbDanmaku = await barrageService.GetGroupBarragesAsync(id);
        var data = dbDanmaku
            .Select(x =>
            {
                var result = int.TryParse(x.Color, out var color);
                var colorRgb = Color.FromArgb(color);
                return new
                {
                    text = x.Text,
                    time = x.Time,
                    color = result ? $"rgb({colorRgb.R},{colorRgb.G},{colorRgb.B})" : x.Color,
                    border = false,
                    mode = x.Position switch
                    {
                        0 => 0,
                        1 => 1,
                        2 => 1,
                        _ => 0,
                    },
                };
            })
            .ToList();
        return Json(data);
    }

    [Route("send/danmaku/v2"), HttpPost]
    public async Task<IActionResult> SendDanmaku([FromBody] SendDanmaku danmaku)
    {
        if (!ModelState.IsValid)
        {
            return Json(new ResultInfo(false, ModelState));
        }
        Color color;
        try
        {
            color = ColorTranslator.FromHtml(danmaku.Color);
        }
        catch (Exception e)
        {
            color = ColorTranslator.FromHtml("#FFFFFF");
            logger.LogError(e, "color translator fail,in color:{Color}", danmaku.Color);
        }

        var vmBarrage = new VmBarrage
        {
            Color = color.ToArgb().ToString(),
            Group = danmaku.Id,
            Position = danmaku.Mode,
            SendTime = DateTime.Now,
            Size = 0,
            Text = danmaku.Text,
            Time = danmaku.Time,
        };
        await barrageService.AddBarrageAsync(vmBarrage);
        return Json(new ResultInfo(true));
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

VideoController.cs 解析报告

总体定位

VideoController.cs 是「叫我阿胖」网站中负责视频模块的 ASP.NET Core MVC 控制器。它同时承担两类职责:

  1. 页面渲染:视频列表页、视频播放页(MVC View)。
  2. 弹幕(danmaku/barrage)数据接口:为页面上的播放器提供历史弹幕拉取与发送的多种版本 JSON API(兼容不同前端/播放器实现)。

控制器使用 C# 12 的主构造函数注入依赖,继承 Controller,整体没有类级路由前缀,各 Action 通过特性自定义路由。

依赖与字段

通过主构造函数注入(来自 using 与外部命名空间,均已通过工具确认):

依赖命名空间/文件用途
IBarrageServiceIBarrageService.cs弹幕的读取(GetGroupBarragesAsync)与写入(AddBarrageAsync
ILogger<VideoController>框架颜色解析异常时记日志
IVideoShardServiceIVideoShardService.cs视频元数据读取(列表/单个/播放数自增)
IConfiguration框架读取 CDN 配置

成员字段 _lazyCdnHostLazy<string>,首次访问时才读取配置键 S3:CdnHost;缺失或为空时抛出 InvalidConfigurationException.cs(该类由我通过搜索确认存在,具体行为以文件为准)。CDN 主机用于拼接视频默认封面、m3u8 播放地址。

各 Action 职责

页面类

  • Index():调用 videoShardService.GetVideosAsync() 取视频列表,经扩展方法 SetTitle(定义在 WebToolsExtensions.cs,向 ViewData["Title"] 写入 "{标题} - 叫我阿胖")设置标题「视频」,返回列表 View。
  • Watch(string id)(路由 video/watch/{id}.html):
    • 按 id 取视频,不存在返回 404。
    • 对字段做降级处理:主标题取 VideoTitleTitle→「未命名视频」;副标题取 SubTitleName;封面缺省时拼接 {CDN}/Video/{视频目录名}/video.webp;m3u8 缺省时拼接 .../1080p.m3u8(说明默认存储目录约定为视频 Title 下的固定文件名,且视频存放在 S3 兼容的对象存储上,播放经 CDN 分发)。
    • 过滤掉空白标签后,组装成 record VideoWatchModel(见 VideoWatchModel.cs,内含 VideoConfig,并提供序列化好的 ConfigJson 供页面 JS 直接使用),返回播放页 View。
  • Play(string id)HttpPatch play/{id}):videoShardService.AddPlayAsync(id) 自增播放数,返回 204。

弹幕读取接口(GET)

三条 GET 接口都从 barrageService.GetGroupBarragesAsync(id) 拉取同一组弹幕,但序列化格式完全不同,以兼容不同版本的播放器协议:

  • HistoryChathistory/chat/v3/):返回 {code:0, data:[[time, position, color, "阿胖", text]…]}。其中每行是一个匿名 object 数组(DPlayer 聊天弹幕风格);发送者固定为「阿胖」;颜色若为可解析整数则原样给出 ARGB 整数值,否则用原始十六进制字符串兜底。
  • HistoryDanmakuhistory/danmaku/{id}):按 Time 升序,颜色统一转成 rgb(r,g,b) 字符串;Position(0=滚动/1=顶部/2=底部,见 VmBarrage.cs 注释)映射为 scroll/top/bottom
  • HistoryDanmakuV2history/danmaku/v2/{id}):颜色同样转 rgb(),新增 border:false 字段,并把位置归一成 mode:0→滚动,1/2→1(顶部、底部都按静止处理,符合 SendDanmaku.cs 中「mode 0-滚动,1-静止」的定义)。

三处都用 System.Drawing.Color.FromArgb(color) 把数据库里存的 ARGB 整数还原成颜色再取 R/G/B。

弹幕发送接口(POST)

  • HistoryDanmakuhistory/danmaku,请求体 VideoDanmaku2.cs):文本为空直接返回成功占位。注意一个可疑点:虽然模型携带 Color 字段,但实现并未使用它,而是硬编码 ColorTranslator.FromHtml("#FF0000")(红色)再转 ARGB 存储;Type 字符串 scroll/top/bottom 被映射为 Position 0/1/2。
  • HistoryChathistory/chat/v3/,请求体 VideoDanmaku.cs):Colorint,直接 .ToString() 存入;Position(int)danmaku.Type(对应枚举 DanmakuType.cs:Rolling=0/Top=1/Bottom=2)。
  • SendDanmakusend/danmaku/v2,请求体 SendDanmaku.cs):最完整的入口——先做 ModelState 校验(含 MaxLength),失败返回 ResultInfo.cs 包装的错误;再用 ColorTranslator.FromHtml 解析前端传来的十六进制颜色,解析异常时降级为白色 #FFFFFFLogErrorPosition = danmaku.Mode

所有 POST 最终统一构建 VmBarrageColor 为 ARGB 整数串、Group=视频 id、SendTime=当前时间、Size=0Time=播放器时间点)交给 barrageService.AddBarrageAsync 持久化。

与相关文件的协作关系

  • 视频元数据模型 VmVideo.csWatch 中读取的 Title/Name/VideoTitle/SubTitle/Description/Tags/PlayCount/DanmakuCount/CommentCount/Cover/M3u8 都来自它。
  • 弹幕实体 VmBarrage.cs:三条发送接口统一写入、三条 GET 接口统一读取的对象。
  • 请求/响应模型:VideoDanmakuVideoDanmaku2SendDanmakuDanmakuType 位于 Models/Video;统一响应包装 ResultInfo;页面模型 VideoWatchModel
  • 扩展方法 SetTitle:位于 WebToolsExtensions.cs,供所有页面 Action 设置浏览器标题(拼上站点名「叫我阿胖」)。

关键流程示意

flowchart TD
    subgraph 页面类
        A1[GET /] --> A2[GetVideosAsync 列表] --> A3[SetTitle 视频] --> A4[View 列表页]
        B1[GET video/watch/{id}.html] --> B2[GetVideoAsync]
        B2 -- null --> B3[404]
        B2 -- 有数据 --> B4[字段降级: 标题/封面/m3u8 兜底拼接 CDN] --> B5[构造 VideoWatchModel] --> B6[View 播放页]
        C1[PATCH play/{id}] --> C2[AddPlayAsync 播放数+1] --> C3[204]
    end

    subgraph 弹幕 GET(读取同组 VmBarrage)
        D1[GET history/chat/v3/] --> D2[DPlayer chat 数组格式 code:0]
        E1[GET history/danmaku/{id}] --> E2[排序+rgb颜色+scroll/top/bottom]
        F1[GET history/danmaku/v2/{id}] --> F2[rgb+border:false+mode 0/1]
    end

    subgraph 弹幕 POST(写 VmBarrage)
        G1[POST history/danmaku] --> G2[颜色硬编码红/ Type串映射 Position]
        H1[POST history/chat/v3/] --> H2[Color int 直接存/ Type枚举转 int]
        I1[POST send/danmaku/v2] --> I2[ModelState校验] --> I3[FromHtml 解析颜色 异常兜底白并记日志] --> I4[Position=Mode]
        G2 & H2 & I4 --> J[barrageService.AddBarrageAsync]
    end

    D2 & E2 & F2 --> K[barrageService.GetGroupBarragesAsync]

值得注意的实现细节

  1. 多套弹幕协议并存(chat v3、danmaku v1、danmaku v2),读取与写入各对应一套,说明站点需要兼容多种播放器/历史数据,这是"以兼容为目的"的遗留式设计。
  2. POST history/danmaku 忽略请求体中的 Color 字段、固定存红色,与其它入口不一致,疑似历史遗留或前端固定红色弹幕;前端 VideoDanmaku2 的 Color 在此完全未使用。
  3. 数据库里 VmBarrage.Color 实际存的是 ARGB 整数的字符串ToArgb().ToString()),但读取端同时兼容「整数串→Color.FromArgb→rgb()」和「原始十六进制字符串」两种形态,说明库中存在新老两种颜色数据。
  4. HistoryChat 返回数组中出现固定作者「阿胖」以及 Position、颜色整数值直接透传,属于面向特定播放器(DPlayer 风格 chat 弹幕)的协议格式。
  5. 所有弹幕接口都没有做身份校验/发言频率限制(仅 SendDanmaku 做了 ModelState 校验),权限控制不在本控制器职责范围内。

(说明:InvalidConfigurationExceptionSetTitle 行为已经由源码确认;VideoShardService 的内部实现细节本次未读取,视频元数据的具体持久化来源不展开。)

评论加载中...