Browse Source

init mixch

Mike L 3 years ago
parent
commit
ee37caaebc
4 changed files with 136 additions and 112 deletions
  1. 1 0
      LICENSE
  2. 7 7
      README.md
  3. 128 0
      mixch2ass.py
  4. 0 105
      ytbchat2ass.py

+ 1 - 0
LICENSE

@@ -1,6 +1,7 @@
 MIT License
 
 Copyright (c) 2021 水瀬kumaねる
+Copyright (c) 2022 Mike L.
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal

+ 7 - 7
README.md

@@ -2,12 +2,12 @@
 ## 环境
 Python 3.x  
 ## 用例
-For youtu.be/nP-9Nuvi9rw :
+For [https://mixch.tv/u/15545360/live](https://mixch.tv/u/15545360/live):
 ```sh
-python ytbchat2ass.py nP-9Nuvi9rw
+python3 mixch2ass.py https://mixch.tv/u/15545360/live
+```
+Or from archive:
+```
+websocat wss://chat.mixch.tv/torte/room/15545360 | tee -a torte_15545360.log
+python3 mixch2ass.py torte_15545360.log
 ```
-## 其它
-第一次使用前请pip install chat-downloader  
-基本上就是把评论拉出来套上nico弹幕的样式,体现的就是一个懒(x
-## 致谢
-[chat-downloader@xenova](https://github.com/xenova/chat-downloader)

+ 128 - 0
mixch2ass.py

@@ -0,0 +1,128 @@
+# -*- coding:utf-8 -*-
+import sys
+import re
+import json
+import websockets
+import asyncio
+import warnings
+
+warnings.filterwarnings('ignore', '"@coroutine"', category=DeprecationWarning)
+
+def sec2hms(sec):# 时间转换
+    hms = str(int(sec//3600)).zfill(2)+':' + \
+        str(int((sec % 3600)//60)).zfill(2)+':'+str(round(sec % 60, 2)).zfill(2)
+    return hms
+
+timeDanmaku = 8  # 普通弹幕持续时间,默认8秒
+limitLineAmount = 12  # 屏上弹幕行数限制
+danmakuPassageway = []  # 塞弹幕用,记录每行上一条弹幕的消失时间
+title = path = f = None
+vposOffset = 0 # 起始时间与epoch的时间差
+for i in range(limitLineAmount):
+    danmakuPassageway.append(0)
+fontName = 'Source Han Sans JP'  # 字体自己换
+videoWidth = 1280  # 视频宽度,按720P处理了后面的内容,不建议改
+videoHeight = 720  # 视频高度
+fontSize = 58
+head = '[Script Info]\n\
+; Script generated by Aegisub 3.2.2\n\
+; http://www.aegisub.org/\n\
+ScriptType: v4.00+\n\
+PlayResX: 1280\n\
+PlayResY: 720\n\
+\n\
+[V4+ Styles]\n\
+Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, marginL, marginR, marginV, Encoding\n\
+Style: Default,微软雅黑,54,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,0,0,0,0\n\
+Style: Alternate,微软雅黑,36,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,0,0,0,0\n\
+Style: Danmaku,'+fontName+','+str(fontSize)+',&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,-1,0,0,0,100,100,2,0,1,1.5,0,2,0,0,10,0\n\n\
+[Events]\n\
+Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n'
+
+def start_writing():
+    global f, path
+    f = open(path,'w',encoding='utf-8-sig')
+    f.write(head)
+
+def process_line(json_line, live=False):
+    global title, vposOffset
+    try:
+        message = json.loads(json_line)
+    except json.JSONDecodeError:
+        print('error: '+json_line)
+        return
+    if message['kind'] == 10 and 'live_session_id' in message:
+        if not title:
+            title = message['title']
+            vposOffset = message['last_item_updated']
+            start_writing()
+        if live:
+            print('已开播 '+sec2hms(message['elapsed']))
+        return
+    elif message['kind'] == 0 and 'user_id' in message:
+        vpos = message['created']-vposOffset
+        vpos_end = vpos+timeDanmaku
+    else:
+        return
+    text=message['name'] +'[Lv.{}]: '.format(message['level'])+message['body']
+    vpos_next_min = float('inf')
+    vpos_next = vpos+1280/(len(text)*60+1280) * 8
+    for i in range(limitLineAmount):
+        if vpos_next >= danmakuPassageway[i]:
+            passageway_index = i
+            danmakuPassageway[i] = vpos+8
+            break
+        elif danmakuPassageway[i] < vpos_next_min:
+            vpos_next_min = danmakuPassageway[i]
+            Passageway_min = i
+        if i == limitLineAmount-1 and vpos_next < vpos_next_min:
+            passageway_index = Passageway_min
+            danmakuPassageway[Passageway_min] = vpos+8
+    # 计算弹幕位置
+    sx = videoWidth
+    sy = fontSize*(passageway_index)
+    ex = 0
+    for i in text:
+        if re.search("[A-Za-z 0-9',.]",i):
+            ex = ex-30
+        else:
+            ex = ex-60
+    ey = fontSize*(passageway_index)
+    f.write('Dialogue: 0,'+sec2hms(vpos)+','+ sec2hms(vpos_end) + ',Danmaku,'+message['name'].replace(',','')+',0,0,0,,{\\an7\\move('+str(sx)+','+str(sy)+','+str(ex)+','+str(ey)+')}'+text+'\n')
+
+@asyncio.coroutine
+def shutdown():
+    if f:
+        f.close()
+        print(title+'的弹幕已经存为'+path)
+    yield
+
+loop = asyncio.get_event_loop()
+match = re.match(r'^(?:https://)?mixch.tv/u/(\d+)(?:/.*)?$', sys.argv[1])
+if not match:
+    match = re.match(r'^wss://chat.mixch.tv/.*/(\d+)$', sys.argv[1])
+if match:
+    path = 'torte_'+match[1]+'.ass'
+    url = 'wss://chat.mixch.tv/torte/room/'+match[1]
+    websocket = loop.run_until_complete(websockets.connect(url))
+    @asyncio.coroutine
+    def receive():
+        try:
+            line = yield from websocket.recv()
+            yield process_line(line, live=True)
+            asyncio.ensure_future(receive())
+        except websockets.ConnectionClosedOK:
+            print()
+            asyncio.ensure_future(shutdown())
+    try:
+        print('抓取实时评论中,按Ctrl+C终止并保存')
+        asyncio.ensure_future(receive())
+        loop.run_forever()
+    except KeyboardInterrupt:
+        loop.run_until_complete(websocket.close())
+else:
+    path = sys.argv[1]+'.ass'
+    with open(sys.argv[1], 'r', encoding='utf-8') as file:
+        for line in file:
+            process_line(line)
+    loop.run_until_complete(shutdown())

+ 0 - 105
ytbchat2ass.py

@@ -1,105 +0,0 @@
-# -*- coding:utf-8 -*-
-from chat_downloader import ChatDownloader
-import sys
-import math
-import urllib.request
-import re
-
-# 目前仅支持开启了chat的回放,看不懂chat-downloader的源代码,甚至不想把直播安排进todo
-
-def sec2hms(sec):# 时间转换
-    hms = str(int(sec//3600)).zfill(2)+':' + \
-        str(int((sec % 3600)//60)).zfill(2)+':'+str(round(sec % 60, 2))
-    return hms
-
-url = 'youtu.be/'+sys.argv[1]
-html = urllib.request.urlopen("https://www.youtube.com/watch?v="+sys.argv[1]).read().decode('utf-8')
-name = [] #预留加人用
-title = re.findall("<title>(.+?)</title>",html)[0].replace(' - YouTube','')
-name += re.findall('itemprop="name" content="(.+?)">',html)
-chat = ChatDownloader().get_chat(url,message_groups=['messages','superchat']) #默认普通评论和sc
-
-
-limitLineAmount = 12  # 屏上弹幕行数限制
-danmakuPassageway = []  # 塞弹幕用,记录每行上一条弹幕的消失时间
-for i in range(limitLineAmount):
-    danmakuPassageway.append(0)
-fontName = 'Source Han Sans JP'  # 字体自己换
-videoWidth = 1280  # 视频宽度,按720P处理了后面的内容,不建议改
-videoHeight = 720  # 视频高度
-OfficeBgHeight = 72
-OfficeSize = 36
-fontSize = 58
-head = '[Script Info]\n\
-; Script generated by Aegisub 3.2.2\n\
-; http://www.aegisub.org/\n\
-ScriptType: v4.00+\n\
-PlayResX: 1280\n\
-PlayResY: 720\n\
-\n\
-[V4+ Styles]\n\
-Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, marginL, marginR, marginV, Encoding\n\
-Style: Default,微软雅黑,54,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,0,0,0,0\n\
-Style: Alternate,微软雅黑,36,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,0,0,0,0\n\
-Style: Office,'+fontName+','+str(OfficeSize)+',&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,-1,0,0,0,100,100,2,0,1,1.5,0,2,0,0,10,0\n\
-Style: Danmaku,'+fontName+','+str(fontSize)+',&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,-1,0,0,0,100,100,2,0,1,1.5,0,2,0,0,10,0\n\n\
-[Events]\n\
-Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n'
-
-f = open(sys.argv[1]+'.ass','w',encoding='utf-8-sig')
-f.write(head)
-for message in chat:
-    if message['time_in_seconds'] > 0:
-        vpos = message['time_in_seconds']
-        vpos_end = vpos+8 # 普通弹幕的时长,默认8秒
-    else:
-        continue
-    if 'name' not in message['author'].keys():
-        continue
-    if 'money' in message.keys():
-        text = '('+str(message['money']['amount'])+message['money']['currency']+')' # 打钱的标上数额
-        if 'message' in message.keys():
-            if message['message']:
-                text += message['message'] # 打钱有留言的加上
-        vpos_end += 2 # 打钱的多给2秒
-    else:
-        text=message['author']['name']+': '+message['message'] if message['author']['name'] in name else message['message'] # 没打钱的直接记录弹幕,设置了的号加上账号名字
-    if 'emotes' in message.keys():
-        for i in message['emotes']:
-            if i['is_custom_emoji']:
-                text = text.replace(i['name'],'')
-            else:
-                text = text.replace(i['name'],i['id'])
-    if len(text) == 0:
-        continue
-    
-    if message['author']['name'] in name: # 特定账号的弹幕放上面并加上背景
-        f.write('Dialogue: 4,'+sec2hms(vpos)+','+sec2hms(vpos_end)+',Office,,0,0,0,,{\\an5\\p1\\pos('+str(videoWidth/2)+','+str(math.floor(OfficeBgHeight/2))+')\\bord0\\1c&H000000&\\1a&H78&}'+'m 0 0 l '+str(videoWidth)+' 0 l '+str(videoWidth) + ' '+str(OfficeBgHeight)+' l 0 '+str(OfficeBgHeight)+'\n')
-        f.write('Dialogue: 5,'+sec2hms(vpos)+','+sec2hms(vpos_end)+',Office,,0,0,0,,{\\an5\\pos('+str(videoWidth/2)+','+str(math.floor(OfficeBgHeight/2))+')\\bord0\\fsp0}'+text+'\n')
-    else: # 其他人的弹幕放滚动
-        vpos_next_min = float('inf')
-        vpos_next = vpos+1280/(len(text)*60+1280) * 8
-        for i in range(limitLineAmount):
-                if vpos_next >= danmakuPassageway[i]:
-                    passageway_index = i
-                    danmakuPassageway[i] = vpos+8
-                    break
-                elif danmakuPassageway[i] < vpos_next_min:
-                    vpos_next_min = danmakuPassageway[i]
-                    Passageway_min = i
-                if i == limitLineAmount-1 and vpos_next < vpos_next_min:
-                    passageway_index = Passageway_min
-                    danmakuPassageway[Passageway_min] = vpos+8
-        # 计算弹幕位置
-        sx = videoWidth
-        sy = fontSize*(passageway_index)
-        ex = 0
-        for i in text:
-            if re.search("[A-Za-z 0-9',.]",i):
-                ex = ex-30
-            else:
-                ex = ex-60
-        ey = fontSize*(passageway_index)
-        f.write('Dialogue: 0,'+sec2hms(vpos)+','+ sec2hms(vpos_end) + ',Danmaku,'+message['author']['name'].replace(',','')+',0,0,0,,{\\an7\\move('+str(sx)+','+str(sy)+','+str(ex)+','+str(ey)+')}'+text+'\n')
-f.close()
-print(title+'的弹幕已经存为'+sys.argv[1]+'.ass')