ytbchat2ass.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. # -*- coding:utf-8 -*-
  2. from chat_downloader import ChatDownloader
  3. import sys
  4. import math
  5. import urllib.request
  6. import re
  7. import argparse
  8. # 目前仅支持开启了chat的回放,看不懂chat-downloader的源代码,甚至不想把直播安排进todo
  9. def sec2hms(sec): # 时间转换
  10. hms = str(int(sec//3600)).zfill(2)+':' + str(int((sec % 3600)//60)).zfill(2)+':'+str(round(sec % 60, 2))
  11. return hms
  12. def chat2ass(link, name, delay, cookies):
  13. pattern = re.compile(r"(?:.*?)youtube\.com/(?:v/|live/|watch\?(?:.*&)?v=)(?P<video_id>[\w-]{11})")
  14. vid_match = pattern.split(link)
  15. vid = [x for x in vid_match if x][0]
  16. url = f"https://www.youtube.com/watch?v={vid}"
  17. html = urllib.request.urlopen(url).read().decode('utf-8')
  18. names = [name]
  19. title = re.findall("<title>(.+?)</title>", html)[0].replace(' - YouTube', '')
  20. names += re.findall('link itemprop="name" content="(.+?)">', html)
  21. chat = ChatDownloader(cookies=cookies).get_chat(url, message_groups=['messages', 'superchat']) # 默认普通评论和sc
  22. count = 0
  23. limitLineAmount = 12 # 屏上弹幕行数限制
  24. danmakuPassageway = [] # 塞弹幕用,记录每行上一条弹幕的消失时间
  25. for i in range(limitLineAmount):
  26. danmakuPassageway.append(0)
  27. fontName = 'Source Han Sans JP' # 字体自己换
  28. videoWidth = 1280 # 视频宽度,按720P处理了后面的内容,不建议改
  29. videoHeight = 720 # 视频高度
  30. OfficeBgHeight = 72
  31. OfficeSize = 36
  32. fontSize = 58
  33. head = f'''[Script Info]
  34. ; Script generated by Aegisub 3.2.2
  35. ; http://www.aegisub.org/
  36. ScriptType: v4.00+
  37. PlayResX: {videoWidth}
  38. PlayResY: {videoHeight}
  39. [V4+ Styles]
  40. Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, marginL, marginR, marginV, Encoding
  41. Style: Default,微软雅黑,54,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,0,0,0,0
  42. Style: Alternate,微软雅黑,36,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,0,0,0,0
  43. Style: Office,{fontName},{OfficeSize},&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,-1,0,0,0,100,100,2,0,1,1.5,0,2,0,0,10,0
  44. Style: Danmaku,{fontName},{fontSize},&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,-1,0,0,0,100,100,2,0,1,1.5,0,2,0,0,10,0
  45. [Events]
  46. Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
  47. Comment: 0,00:00:00.0,00:00:00.0,Danmaku,标题,0,0,0,,{title}
  48. '''
  49. f = open(vid+'.ass', 'w', encoding='utf-8-sig')
  50. f.write(head)
  51. for message in chat:
  52. vpos = message['time_in_seconds'] - float(delay)
  53. if vpos > 0:
  54. vpos_end = vpos+8 # 普通弹幕的时长,默认8秒
  55. else:
  56. continue
  57. if 'name' not in message['author'].keys():
  58. continue
  59. if 'money' in message.keys():
  60. text = '('+str(message['money']['amount']) + message['money']['currency']+')' # 打钱的标上数额
  61. if 'message' in message.keys():
  62. if message['message']:
  63. text += message['message'] # 打钱有留言的加上
  64. vpos_end += 2 # 打钱的多给2秒
  65. else:
  66. # 没打钱的直接记录弹幕,设置了的号加上账号名字
  67. text = message['author']['name']+': ' + message['message'] if message['author']['name'] in names else message['message']
  68. if 'emotes' in message.keys():
  69. for i in message['emotes']:
  70. if i['is_custom_emoji']:
  71. text = text.replace(i['name'], '')
  72. else:
  73. text = text.replace(i['name'], i['id'])
  74. if text:
  75. if len(text) == 0:
  76. continue
  77. else:
  78. continue
  79. if message['author']['name'] in names: # 特定账号的弹幕放上面并加上背景
  80. 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')
  81. 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')
  82. count += 1
  83. else: # 其他人的弹幕放滚动
  84. vpos_next_min = float('inf')
  85. vpos_next = vpos+1280/(len(text)*60+1280) * 8
  86. for i in range(limitLineAmount):
  87. if vpos_next >= danmakuPassageway[i]:
  88. passageway_index = i
  89. danmakuPassageway[i] = vpos+8
  90. break
  91. elif danmakuPassageway[i] < vpos_next_min:
  92. vpos_next_min = danmakuPassageway[i]
  93. Passageway_min = i
  94. if i == limitLineAmount-1 and vpos_next < vpos_next_min:
  95. passageway_index = Passageway_min
  96. danmakuPassageway[Passageway_min] = vpos+8
  97. # 计算弹幕位置
  98. sx = videoWidth
  99. sy = fontSize*(passageway_index)
  100. ex = 0
  101. for i in text:
  102. if re.search("[A-Za-z 0-9',.]", i):
  103. ex = ex-30
  104. else:
  105. ex = ex-60
  106. ey = fontSize*(passageway_index)
  107. 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')
  108. count += 1
  109. f.close()
  110. print(f'{title}的弹幕已经存为{vid}.ass,共{count}条')
  111. def main():
  112. if len(sys.argv) == 1:
  113. sys.argv.append('--help')
  114. parser = argparse.ArgumentParser()
  115. parser.add_argument('-n', '--name', metavar='str', help='除主播外,需将弹幕显示在上方的账号')
  116. parser.add_argument('-d', '--delay', metavar='str', help='弹幕延迟,一般适用于首播')
  117. parser.add_argument('-c', '--cookie', metavar='str', help='cookie文件的地址')
  118. parser.add_argument('link', metavar='str', help='视频链接或视频id')
  119. args = parser.parse_args()
  120. if args.link:
  121. if not args.name:
  122. args.name = ''
  123. if not args.delay:
  124. args.delay = 0
  125. if not args.cookie:
  126. args.cookie = None
  127. chat2ass(args.link, args.name, args.delay , args.cookie)
  128. if __name__ == '__main__':
  129. main()