mixch2ass.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. # -*- coding:utf-8 -*-
  2. import sys
  3. import re
  4. import json
  5. import websockets
  6. import asyncio
  7. import warnings
  8. warnings.filterwarnings('ignore', '"@coroutine"', category=DeprecationWarning)
  9. def sec2hms(sec):# 时间转换
  10. hms = str(int(sec//3600)).zfill(2)+':' + \
  11. str(int((sec % 3600)//60)).zfill(2)+':'+str(round(sec % 60, 2)).zfill(2)
  12. return hms
  13. timeDanmaku = 8 # 普通弹幕持续时间,默认8秒
  14. limitLineAmount = 12 # 屏上弹幕行数限制
  15. danmakuPassageway = [] # 塞弹幕用,记录每行上一条弹幕的消失时间
  16. title = path = f = None
  17. vposOffset = 0 # 起始时间与epoch的时间差
  18. for i in range(limitLineAmount):
  19. danmakuPassageway.append(0)
  20. fontName = 'Source Han Sans JP' # 字体自己换
  21. videoWidth = 1280 # 视频宽度,按720P处理了后面的内容,不建议改
  22. videoHeight = 720 # 视频高度
  23. fontSize = 58
  24. head = '[Script Info]\n\
  25. ; Script generated by Aegisub 3.2.2\n\
  26. ; http://www.aegisub.org/\n\
  27. ScriptType: v4.00+\n\
  28. PlayResX: 1280\n\
  29. PlayResY: 720\n\
  30. \n\
  31. [V4+ Styles]\n\
  32. 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\
  33. Style: Default,微软雅黑,54,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,0,0,0,0\n\
  34. Style: Alternate,微软雅黑,36,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,0,0,0,0\n\
  35. 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\
  36. [Events]\n\
  37. Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n'
  38. def start_writing():
  39. global f, path
  40. f = open(path,'w',encoding='utf-8-sig')
  41. f.write(head)
  42. def process_line(json_line, live=False):
  43. global title, vposOffset
  44. try:
  45. message = json.loads(json_line)
  46. except json.JSONDecodeError:
  47. print('error: '+json_line)
  48. return
  49. if message['kind'] == 10 and 'live_session_id' in message:
  50. if not title:
  51. title = message['title']
  52. vposOffset = message['last_item_updated']
  53. start_writing()
  54. if live:
  55. print('已开播 '+sec2hms(message['elapsed']))
  56. return
  57. elif message['kind'] == 0 and 'user_id' in message:
  58. vpos = message['created']-vposOffset
  59. vpos_end = vpos+timeDanmaku
  60. else:
  61. return
  62. text=message['name'] +'[Lv.{}]: '.format(message['level'])+message['body']
  63. vpos_next_min = float('inf')
  64. vpos_next = vpos+1280/(len(text)*60+1280) * 8
  65. for i in range(limitLineAmount):
  66. if vpos_next >= danmakuPassageway[i]:
  67. passageway_index = i
  68. danmakuPassageway[i] = vpos+8
  69. break
  70. elif danmakuPassageway[i] < vpos_next_min:
  71. vpos_next_min = danmakuPassageway[i]
  72. Passageway_min = i
  73. if i == limitLineAmount-1 and vpos_next < vpos_next_min:
  74. passageway_index = Passageway_min
  75. danmakuPassageway[Passageway_min] = vpos+8
  76. # 计算弹幕位置
  77. sx = videoWidth
  78. sy = fontSize*(passageway_index)
  79. ex = 0
  80. for i in text:
  81. if re.search("[A-Za-z 0-9',.]",i):
  82. ex = ex-30
  83. else:
  84. ex = ex-60
  85. ey = fontSize*(passageway_index)
  86. 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')
  87. @asyncio.coroutine
  88. def shutdown():
  89. if f:
  90. f.close()
  91. print(title+'的弹幕已经存为'+path)
  92. yield
  93. loop = asyncio.get_event_loop()
  94. match = re.match(r'^(?:https://)?mixch.tv/u/(\d+)(?:/.*)?$', sys.argv[1])
  95. if not match:
  96. match = re.match(r'^wss://chat.mixch.tv/.*/(\d+)$', sys.argv[1])
  97. if match:
  98. path = 'torte_'+match[1]+'.ass'
  99. url = 'wss://chat.mixch.tv/torte/room/'+match[1]
  100. websocket = loop.run_until_complete(websockets.connect(url))
  101. @asyncio.coroutine
  102. def receive():
  103. try:
  104. line = yield from websocket.recv()
  105. yield process_line(line, live=True)
  106. asyncio.ensure_future(receive())
  107. except websockets.ConnectionClosedOK:
  108. print()
  109. asyncio.ensure_future(shutdown())
  110. try:
  111. print('抓取实时评论中,按Ctrl+C终止并保存')
  112. asyncio.ensure_future(receive())
  113. loop.run_forever()
  114. except KeyboardInterrupt:
  115. loop.run_until_complete(websocket.close())
  116. else:
  117. path = sys.argv[1]+'.ass'
  118. with open(sys.argv[1], 'r', encoding='utf-8') as file:
  119. for line in file:
  120. process_line(line)
  121. loop.run_until_complete(shutdown())