From 474b97a40a03e72b03b87fc2a044a39d2a26804f Mon Sep 17 00:00:00 2001
From: Jerry Yan <792602257@qq.com>
Date: Tue, 3 Jan 2023 13:07:45 +0800
Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=B8=BB=E5=8A=A8=E5=AF=BC?=
 =?UTF-8?q?=E5=85=A5=E5=BC=B9=E5=B9=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../DanmakuConstructController.php            | 71 +++++++++++++++++++
 app/Models/VideoDanmakus.php                  |  6 ++
 app/Util/DanmakuUtil.php                      | 32 +++++++++
 composer.json                                 |  1 +
 resources/views/common/header.blade.php       |  4 +-
 .../danmaku/construct/batch_import.blade.php  | 35 +++++++++
 resources/views/video/index.blade.php         |  3 +
 routes/web.php                                | 66 +++++++++--------
 8 files changed, 187 insertions(+), 31 deletions(-)
 create mode 100644 app/Http/Controllers/DanmakuConstructController.php
 create mode 100644 app/Util/DanmakuUtil.php
 create mode 100644 resources/views/danmaku/construct/batch_import.blade.php

diff --git a/app/Http/Controllers/DanmakuConstructController.php b/app/Http/Controllers/DanmakuConstructController.php
new file mode 100644
index 0000000..6852eef
--- /dev/null
+++ b/app/Http/Controllers/DanmakuConstructController.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\VideoDanmakus;
+use App\Models\Videos;
+use App\Util\DanmakuUtil;
+use Illuminate\Http\Request;
+use Illuminate\Routing\Controller as BaseController;
+use Illuminate\Support\Facades\DB;
+
+class DanmakuConstructController extends BaseController
+{
+    public function page(Request $request)
+    {
+        $view = view("danmaku.construct.batch_import");
+        if ($request->has("video_bvid")) {
+            $bvid = $request->get("video_bvid");
+            $video = Videos::query()->where("bvid", "=", $bvid)->first();
+            if ($video == null) {
+                $view->withErrors([
+                    "video_bvid" => "系统无此对应视频",
+                ]);
+            } else {
+                $request->session()->flashInput([
+                    "video_bvid" => $bvid
+                ]);
+            }
+        }
+        return $view;
+    }
+
+    public function do_import(Request $request)
+    {
+        $request->validate([
+            'video_bvid' => ['required'],
+            'platform_id' => ['required', 'int'],
+            'file' => ['required']
+        ]);
+        $payload = $request->only(["video_bvid", "platform_id"]);
+        $files = $request->file("file");
+        if (!is_array($files)) {
+            $files = [$files];
+        }
+        $video = Videos::query()->where("bvid", "=", $payload["video_bvid"])->first();
+        if ($video == null) {
+            return back()->withInput()->withErrors([
+                "video_bvid" => "系统无此对应视频",
+            ]);
+        }
+        foreach ($files as $file) {
+            $danmakus = DanmakuUtil::parse_danmaku($file->getFileInfo());
+            DB::beginTransaction();
+            try {
+                foreach ($danmakus as &$danmaku) {
+                    $danmaku['video_bvid'] = $video->bvid;
+                    $danmaku['platform_id'] = $payload["platform_id"];
+                    unset($danmaku);
+                }
+                VideoDanmakus::insert($danmakus);
+                DB::commit();
+            } catch (\Exception $e) {
+                DB::rollBack();
+                return back()->withInput()->withErrors([
+                    "file" => "文件导入异常:" . $e->getMessage(),
+                ]);
+            }
+        }
+        return redirect("/danmakus/" . $payload["video_bvid"]);
+    }
+}
diff --git a/app/Models/VideoDanmakus.php b/app/Models/VideoDanmakus.php
index 41af5ad..851fb77 100644
--- a/app/Models/VideoDanmakus.php
+++ b/app/Models/VideoDanmakus.php
@@ -6,8 +6,14 @@ use Illuminate\Database\Eloquent\Model;
 
 class VideoDanmakus extends Model
 {
+    protected $guarded = [];
     protected $table = "video_danmakus";
     protected $dateFormat = 'U';
+    public $timestamps = false;
+    protected $casts = [
+        'created_at' => 'datetime:Y-m-d H:i:s',
+    ];
+    protected $fillable = ["from", "from_mid", "content"];
     public function video(): \Illuminate\Database\Eloquent\Relations\BelongsTo
     {
         return $this->belongsTo(Videos::class, "video_bvid", "bvid");
diff --git a/app/Util/DanmakuUtil.php b/app/Util/DanmakuUtil.php
new file mode 100644
index 0000000..5b59ac2
--- /dev/null
+++ b/app/Util/DanmakuUtil.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Util;
+
+use SplFileInfo;
+
+class DanmakuUtil
+{
+    public static function parse_danmaku(SplFileInfo $file): array
+    {
+        $document = new \DOMDocument();
+        $document->load($file->getRealPath());
+        $danmaku_items = $document->getElementsByTagName("d");
+        $result = [];
+        /** @var \DOMNode $item */
+        foreach ($danmaku_items as $item) {
+            $paramsNode = $item->attributes->getNamedItem("p");
+            $param_list = mb_split(",", $paramsNode->value);
+            if (sizeof($param_list) < 7) {
+                throw new \Exception("弹幕格式异常");
+            }
+            $userNode = $item->attributes->getNamedItem("user");
+            $result[] = [
+                "from" => $userNode->value,
+                "from_mid" => $param_list[6],
+                "content" => $item->textContent,
+                "created_at" => intval($param_list[4])/1000,
+            ];
+        }
+        return $result;
+    }
+}
diff --git a/composer.json b/composer.json
index afcb5ef..a7c93d6 100644
--- a/composer.json
+++ b/composer.json
@@ -6,6 +6,7 @@
     "license": "MIT",
     "require": {
         "php": "^7.3|^8.0",
+        "ext-dom": "*",
         "ext-json": "*",
         "ext-mbstring": "*",
         "fruitcake/laravel-cors": "^2.0",
diff --git a/resources/views/common/header.blade.php b/resources/views/common/header.blade.php
index 534d931..16c71ca 100644
--- a/resources/views/common/header.blade.php
+++ b/resources/views/common/header.blade.php
@@ -21,7 +21,7 @@
                         <a class="{{ (request()->is('danmakus', 'danmakus/*')) ? 'bg-gray-900 text-white' : 'text-gray-700 hover:bg-gray-700 hover:text-white' }} px-3 py-2 rounded-md text-sm font-medium" href="/danmakus">稿件查询</a>
                         <a class="{{ (request()->is('programs', '/', 'programs/*/video')) ? 'bg-gray-900 text-white' : 'text-gray-700 hover:bg-gray-700 hover:text-white' }} px-3 py-2 rounded-md text-sm font-medium" href="/programs" title="数据不全,待补充">节目查询</a>
                         @auth("web")
-                            <a class="{{ (request()->is('programs/construct', 'programs/construct/*')) ? 'bg-gray-900 text-white' : 'text-gray-700 hover:bg-gray-700 hover:text-white' }} px-3 py-2 rounded-md text-sm font-medium" href="/programs/construct">节目建设</a>
+                            <a class="{{ (request()->is('construct/*')) ? 'bg-gray-900 text-white' : 'text-gray-700 hover:bg-gray-700 hover:text-white' }} px-3 py-2 rounded-md text-sm font-medium" href="/construct/programs">节目建设</a>
                         @endauth
                     </div>
                 </div>
@@ -50,7 +50,7 @@
             <a class="{{ (request()->is('danmakus', 'danmakus/*')) ? 'bg-gray-900 text-white' : 'text-gray-700 hover:bg-gray-700 hover:text-white' }} block px-3 py-2 rounded-md text-base font-medium" href="/danmakus">稿件查询</a>
             <a class="{{ (request()->is('programs', '/', 'programs/*/video')) ? 'bg-gray-900 text-white' : 'text-gray-700 hover:bg-gray-700 hover:text-white' }} block px-3 py-2 rounded-md text-base font-medium" href="/programs" title="数据不全,待补充">节目查询</a>
             @auth("web")
-                <a class="{{ (request()->is('programs/construct', 'programs/construct/*')) ? 'bg-gray-900 text-white' : 'text-gray-700 hover:bg-gray-700 hover:text-white' }} block px-3 py-2 rounded-md text-base font-medium" href="/programs/construct">节目建设</a>
+                <a class="{{ (request()->is('construct/*')) ? 'bg-gray-900 text-white' : 'text-gray-700 hover:bg-gray-700 hover:text-white' }} block px-3 py-2 rounded-md text-base font-medium" href="/construct/programs">节目建设</a>
             @endauth
         </div>
     </div>
diff --git a/resources/views/danmaku/construct/batch_import.blade.php b/resources/views/danmaku/construct/batch_import.blade.php
new file mode 100644
index 0000000..b03a2ea
--- /dev/null
+++ b/resources/views/danmaku/construct/batch_import.blade.php
@@ -0,0 +1,35 @@
+<html lang="zh">
+<head>
+    <meta charset="UTF-8">
+    <title>弹幕导入</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <link href="{{ mix('/css/app.css') }}" rel="stylesheet"/>
+</head>
+<body>
+    @include("common.header")
+    <form class="w-full lg:w-1/2 lg:ml-6 border-2" action="" method="post" enctype="multipart/form-data">
+        @csrf
+        <label class="block my-2">
+            BVID
+            <input class="form-input border-0 border-b-2 w-full" type="text" name="video_bvid" required value="{{ old('video_bvid') }}">
+        </label>
+        <label class="block my-2">
+            弹幕类型
+            <span class="block form-input border-0 border-b-2 w-full">
+                <input type="radio" value="1" name="platform_id" checked>B站
+                <input type="radio" value="2" name="platform_id" disabled>西瓜
+                <input type="radio" value="3" name="platform_id" disabled>抖音
+            </span>
+        </label>
+        <label class="block my-2">
+            弹幕文件
+            <input class="form-input border-0 border-b-2 w-full" multiple type="file" name="file[]" accept="text/xml">
+        </label>
+        @include("common.form_error")
+        <div class="block my-2 text-center">
+            <input class="px-6 py-2 inline-block rounded-full bg-cyan-600 text-white" type="submit">
+        </div>
+    </form>
+    @include("common.footer")
+</body>
+</html>
diff --git a/resources/views/video/index.blade.php b/resources/views/video/index.blade.php
index 0ca521d..2e6531f 100644
--- a/resources/views/video/index.blade.php
+++ b/resources/views/video/index.blade.php
@@ -68,6 +68,9 @@
 @if(sizeof($video_pivots) === 0 && $comment)
     <a href="{{ url(route("program.construct.from_comment", ["comment"=>$comment->id])) }}" class="px-6 py-2 inline-block rounded-full bg-cyan-600 text-white">一键导入评论中的节目单</a>
 @endif
+@if($video->danmakus->count() === 0)
+    <a href="{{ url(route("danmaku.construct.batch_import", ["video_bvid"=>$video->bvid])) }}" class="px-6 py-2 inline-block rounded-full bg-cyan-600 text-white">导入直播弹幕</a>
+@endif
 @endauth
 @include("common.footer")
 </body>
diff --git a/routes/web.php b/routes/web.php
index a88b052..ab61b96 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -31,36 +31,44 @@ Route::post("/login/webauthn/", ["\\App\\Http\\Controllers\\UserWebAuthnControll
 Route::get('/register', ["\\App\\Http\\Controllers\\UserController", "register_page"])->name("register");
 Route::post('/register', ["\\App\\Http\\Controllers\\UserController", "register"])->name("register.submit");
 Route::get('/logout', ["\\App\\Http\\Controllers\\UserController", "logout"])->name("logout");
+// 弹幕建设
 // 建设部分
-Route::prefix("/programs/construct")->middleware("auth:web")->group(function (Router $router) {
-    // 节目建设
-    $router->get('/', ["\\App\\Http\\Controllers\\ProgramConstructController","index"])->name("program.construct.list");
-    $router->get('/add', ["\\App\\Http\\Controllers\\ProgramConstructController","add"])->name("program.construct.add");
-    $router->post('/add', ["\\App\\Http\\Controllers\\ProgramConstructController","create"])->name("program.construct.create");
-    $router->get('/from_comment/{comment}', ["\\App\\Http\\Controllers\\ProgramConstructController","from_comment"])->name("program.construct.from_comment");
-    $router->get('/batch', ["\\App\\Http\\Controllers\\ProgramConstructController","batch_add"])->name("program.construct.batch_add");
-    $router->post('/batch', ["\\App\\Http\\Controllers\\ProgramConstructController","batch_create"])->name("program.construct.batch_create");
-    $router->get('/{program}', ["\\App\\Http\\Controllers\\ProgramConstructController","edit"])->name("program.construct.edit");
-    $router->post('/{program}', ["\\App\\Http\\Controllers\\ProgramConstructController", "submit"])->name("program.construct.submit");
-    // 节目关联视频建设
-    $router->get("/{program}/video", ["\\App\\Http\\Controllers\\ProgramVideoConstructController","index"])->name("program.construct.video.list");
-    $router->get("/{program}/video/add", ["\\App\\Http\\Controllers\\ProgramVideoConstructController","add"])->name("program.construct.video.add");
-    $router->post("/{program}/video/add", ["\\App\\Http\\Controllers\\ProgramVideoConstructController","create"])->name("program.construct.video.create");
-    $router->get("/video/{program_video}", ["\\App\\Http\\Controllers\\ProgramVideoConstructController","edit"])->name("program.construct.video.edit");
-    $router->post("/video/{program_video}", ["\\App\\Http\\Controllers\\ProgramVideoConstructController","submit"])->name("program.construct.video.submit");
-    $router->get("/video/{program_video}/manual_fix", ["\\App\\Http\\Controllers\\ProgramVideoConstructController","to_fix_created_at"])->name("program.construct.video.manual_fix_created_at.view");
-    $router->post("/video/{program_video}/manual_fix", ["\\App\\Http\\Controllers\\ProgramVideoConstructController","fix_created_at_base_on"])->name("program.construct.video.manual_fix_created_at");
-    $router->get("/video/fix/{bvid}", ["\\App\\Http\\Controllers\\ProgramVideoConstructController","auto_fix_created_at"])->name("program.construct.video.auto_fix_created_at");
-    // 节目关联点播建设
-    $router->get('/{program}/append', ["\\App\\Http\\Controllers\\ProgramAppendConstructController","index"])->name("program.construct.append.list");
-    $router->get('/{program}/append/add', ["\\App\\Http\\Controllers\\ProgramAppendConstructController","add"])->name("program.construct.append.add");
-    $router->post('/{program}/append/add', ["\\App\\Http\\Controllers\\ProgramAppendConstructController","create"])->name("program.construct.append.create");
-    $router->get('/append/from_list', ["\\App\\Http\\Controllers\\ProgramAppendConstructController", "from_list"])->name("program.construct.append.from_list");
-    $router->get('/{program}/append/copy', ["\\App\\Http\\Controllers\\ProgramAppendConstructController","copy_view"])->name("program.construct.append.copy");
-    $router->post('/{program}/append/copy', ["\\App\\Http\\Controllers\\ProgramAppendConstructController","copy_append"])->name("program.construct.append.copy.submit");
-    $router->get('/append/broadcast_list', ["\\App\\Http\\Controllers\\ProgramAppendConstructController", "broadcast_list"])->name("program.construct.append.broadcast_list");
-    $router->get('/append/{append}', ["\\App\\Http\\Controllers\\ProgramAppendConstructController","edit"])->name("program.construct.append.edit");
-    $router->post('/append/{append}', ["\\App\\Http\\Controllers\\ProgramAppendConstructController","submit"])->name("program.construct.append.submit");
+Route::prefix("/construct")->middleware("auth:web")->group(function (Router $router) {
+    Route::prefix("/programs")->group(function (Router $router) {
+        // 节目建设
+        $router->get('/', ["\\App\\Http\\Controllers\\ProgramConstructController","index"])->name("program.construct.list");
+        $router->get('/add', ["\\App\\Http\\Controllers\\ProgramConstructController","add"])->name("program.construct.add");
+        $router->post('/add', ["\\App\\Http\\Controllers\\ProgramConstructController","create"])->name("program.construct.create");
+        $router->get('/from_comment/{comment}', ["\\App\\Http\\Controllers\\ProgramConstructController","from_comment"])->name("program.construct.from_comment");
+        $router->get('/batch', ["\\App\\Http\\Controllers\\ProgramConstructController","batch_add"])->name("program.construct.batch_add");
+        $router->post('/batch', ["\\App\\Http\\Controllers\\ProgramConstructController","batch_create"])->name("program.construct.batch_create");
+        $router->get('/{program}', ["\\App\\Http\\Controllers\\ProgramConstructController","edit"])->name("program.construct.edit");
+        $router->post('/{program}', ["\\App\\Http\\Controllers\\ProgramConstructController", "submit"])->name("program.construct.submit");
+        // 节目关联视频建设
+        $router->get("/{program}/video", ["\\App\\Http\\Controllers\\ProgramVideoConstructController","index"])->name("program.construct.video.list");
+        $router->get("/{program}/video/add", ["\\App\\Http\\Controllers\\ProgramVideoConstructController","add"])->name("program.construct.video.add");
+        $router->post("/{program}/video/add", ["\\App\\Http\\Controllers\\ProgramVideoConstructController","create"])->name("program.construct.video.create");
+        $router->get("/video/{program_video}", ["\\App\\Http\\Controllers\\ProgramVideoConstructController","edit"])->name("program.construct.video.edit");
+        $router->post("/video/{program_video}", ["\\App\\Http\\Controllers\\ProgramVideoConstructController","submit"])->name("program.construct.video.submit");
+        $router->get("/video/{program_video}/manual_fix", ["\\App\\Http\\Controllers\\ProgramVideoConstructController","to_fix_created_at"])->name("program.construct.video.manual_fix_created_at.view");
+        $router->post("/video/{program_video}/manual_fix", ["\\App\\Http\\Controllers\\ProgramVideoConstructController","fix_created_at_base_on"])->name("program.construct.video.manual_fix_created_at");
+        $router->get("/video/fix/{bvid}", ["\\App\\Http\\Controllers\\ProgramVideoConstructController","auto_fix_created_at"])->name("program.construct.video.auto_fix_created_at");
+        // 节目关联点播建设
+        $router->get('/{program}/append', ["\\App\\Http\\Controllers\\ProgramAppendConstructController","index"])->name("program.construct.append.list");
+        $router->get('/{program}/append/add', ["\\App\\Http\\Controllers\\ProgramAppendConstructController","add"])->name("program.construct.append.add");
+        $router->post('/{program}/append/add', ["\\App\\Http\\Controllers\\ProgramAppendConstructController","create"])->name("program.construct.append.create");
+        $router->get('/append/from_list', ["\\App\\Http\\Controllers\\ProgramAppendConstructController", "from_list"])->name("program.construct.append.from_list");
+        $router->get('/{program}/append/copy', ["\\App\\Http\\Controllers\\ProgramAppendConstructController","copy_view"])->name("program.construct.append.copy");
+        $router->post('/{program}/append/copy', ["\\App\\Http\\Controllers\\ProgramAppendConstructController","copy_append"])->name("program.construct.append.copy.submit");
+        $router->get('/append/broadcast_list', ["\\App\\Http\\Controllers\\ProgramAppendConstructController", "broadcast_list"])->name("program.construct.append.broadcast_list");
+        $router->get('/append/{append}', ["\\App\\Http\\Controllers\\ProgramAppendConstructController","edit"])->name("program.construct.append.edit");
+        $router->post('/append/{append}', ["\\App\\Http\\Controllers\\ProgramAppendConstructController","submit"])->name("program.construct.append.submit");
+    });
+    // 弹幕维护
+    Route::prefix("/danmaku")->group(function (Router $router) {
+        $router->get("/batch_import", ["\\App\\Http\\Controllers\\DanmakuConstructController", "page"])->name("danmaku.construct.batch_import.page");
+        $router->post("/batch_import", ["\\App\\Http\\Controllers\\DanmakuConstructController", "do_import"])->name("danmaku.construct.batch_import");
+    });
 });
 Route::prefix("/user")->middleware("auth:web")->group(function (Router $router) {
     $router->post("/webauthn/options", ["\\App\\Http\\Controllers\\UserWebAuthnController", "register_options"])->name("user.webauthn.bind.options");