# -*- coding:utf-8 -*- from chat_downloader import ChatDownloader import sys import math import urllib.request import re import argparse # 目前仅支持开启了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 def chat2ass(link, name, delay, cookies): pattern = re.compile(r"(?:.*?)youtube\.com/(?:v/|live/|watch\?(?:.*&)?v=)(?P[\w-]{11})") vid_match = pattern.split(link) vid = [x for x in vid_match if x][0] url = f"https://www.youtube.com/watch?v={vid}" html = urllib.request.urlopen(url).read().decode('utf-8') names = [name] title = re.findall("(.+?)", html)[0].replace(' - YouTube', '') names += re.findall('link itemprop="name" content="(.+?)">', html) chat = ChatDownloader(cookies=cookies).get_chat(url, message_groups=['messages', 'superchat']) # 默认普通评论和sc count = 0 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 = f'''[Script Info] ; Script generated by Aegisub 3.2.2 ; http://www.aegisub.org/ ScriptType: v4.00+ PlayResX: {videoWidth} PlayResY: {videoHeight} [V4+ Styles] Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, marginL, marginR, marginV, Encoding Style: Default,微软雅黑,54,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,0,0,0,0 Style: Alternate,微软雅黑,36,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,0,0,0,0 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 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 [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text Comment: 0,00:00:00.0,00:00:00.0,Danmaku,标题,0,0,0,,{title} ''' f = open(vid+'.ass', 'w', encoding='utf-8-sig') f.write(head) for message in chat: vpos = message['time_in_seconds'] - float(delay) if vpos > 0: 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 names 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 text: if len(text) == 0: continue else: continue if message['author']['name'] in names: # 特定账号的弹幕放上面并加上背景 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') count += 1 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') count += 1 f.close() print(f'{title}的弹幕已经存为{vid}.ass,共{count}条') def main(): if len(sys.argv) == 1: sys.argv.append('--help') parser = argparse.ArgumentParser() parser.add_argument('-n', '--name', metavar='str', help='除主播外,需将弹幕显示在上方的账号') parser.add_argument('-d', '--delay', metavar='str', help='弹幕延迟,一般适用于首播') parser.add_argument('-c', '--cookie', metavar='str', help='cookie文件的地址') parser.add_argument('link', metavar='str', help='视频链接或视频id') args = parser.parse_args() if args.link: if not args.name: args.name = '' if not args.delay: args.delay = 0 if not args.cookie: args.cookie = None chat2ass(args.link, args.name, args.delay , args.cookie) if __name__ == '__main__': main()