├── README.md ├── DanmuTemplate.ass └── Danmu.py /README.md: -------------------------------------------------------------------------------- 1 | # aniGamerDanmu 動畫瘋彈幕下載 lib 2 | 將動畫瘋彈幕轉換成 .ass 字幕 3 | 4 | # Dependency 5 | ```shell 6 | pip3 install requests beautifulsoup4 7 | ``` 8 | 9 | # Usage 10 | 可以直接跑 11 | ```shell 12 | # 看 help 13 | python3 Danmu.py -h 14 | 15 | # 下載 sn=12345 的彈幕 16 | python3 Danmu.py -s 12345 17 | 18 | # 下載 sn=12345 作品的所有集數彈幕 19 | python3 Danmu.py -s 12345 --all 20 | ``` 21 | 也可以 ```import Danmu``` 有兩個函數 22 | ```python 23 | def download(sn, full_filename) 24 | # sn: 動畫的 sn 25 | # full_filename: 輸出的 path + filename 26 | 27 | def downlaod_all(sn, bangumi_path, format_str='{anime_name}[{episode}].ass') 28 | # sn: 動畫的 sn 29 | # bangumi_path: 彈幕會下載在 bangumi_path//*.ass 30 | # format_str: 自訂字幕的檔名 可省略 31 | ``` 32 | 33 | 若想要變更字體等可以至 ```DanmuTemplate.ass``` 修改,程式會將此檔案作為字幕開頭 34 | # Warning 35 | 本專案僅供學習程式設計以及字幕設計所用,請勿用於不法用途 -------------------------------------------------------------------------------- /DanmuTemplate.ass: -------------------------------------------------------------------------------- 1 | [Script Info] 2 | ; Script generated by IanSung 3 | Title: bahaDanmu 4 | ScriptType: v4.00+ 5 | WrapStyle: 0 6 | PlayResX: 1920 7 | PlayResY: 1080 8 | ScaledBorderAndShadow: yes 9 | YCbCr Matrix: TV.709 10 | 11 | 12 | [V4+ Styles] 13 | Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding 14 | Style: Roll,Microsoft JhengHei,50,&H4CFFFFFF,&H000000FF,&H4C000000,&HFF000000,-1,0,0,0,100,100,0,0,1,2,1,7,0,0,0,1 15 | Style: Top,Microsoft JhengHei,50,&H4CFFFFFF,&H000000FF,&H4C000000,&HFF000000,-1,0,0,0,100,100,0,0,1,2,1,8,0,0,0,1 16 | Style: Bottom,Microsoft JhengHei,50,&H4CFFFFFF,&H000000FF,&H4C000000,&HFF000000,-1,0,0,0,100,100,0,0,1,2,1,2,0,0,200,1 17 | 18 | [Events] 19 | Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text 20 | -------------------------------------------------------------------------------- /Danmu.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import random 4 | from bs4 import BeautifulSoup 5 | import re 6 | import os 7 | import argparse 8 | 9 | 10 | def download(sn, full_filename): 11 | h = get_header() 12 | data = {'sn': str(sn)} 13 | r = requests.post( 14 | 'https://ani.gamer.com.tw/ajax/danmuGet.php', data=data, headers=h) 15 | 16 | if r.status_code != 200: 17 | print('sn=' + str(sn) + ' 彈幕下載失敗, status_code=' + str(status_code)) 18 | return 19 | 20 | output = open(full_filename, 'w', encoding='utf8') 21 | with open('DanmuTemplate.ass', 'r', encoding='utf8') as temp: 22 | for line in temp.readlines(): 23 | output.write(line) 24 | 25 | j = json.loads(r.text) 26 | 27 | height = 50 28 | 29 | roll_channel = list() 30 | roll_time = list() 31 | 32 | for danmu in j: 33 | output.write('Dialogue: ') 34 | output.write('0,') 35 | 36 | start_time = int(danmu['time'] / 10) 37 | hundred_ms = danmu['time'] % 10 38 | m, s = divmod(start_time, 60) 39 | h, m = divmod(m, 60) 40 | output.write(f'{h:d}:{m:02d}:{s:02d}.{hundred_ms:d}0,') 41 | 42 | if danmu['position'] == 0: # Roll danmu 43 | height = 0 44 | end_time = 0 45 | for i in range(len(roll_channel)): 46 | if roll_channel[i] <= danmu['time']: 47 | height = i * 54 + 27 48 | roll_channel[i] = danmu['time'] + (len(danmu['text']) * roll_time[i]) / 8 + 1 49 | end_time = start_time + roll_time[i] 50 | break 51 | if height == 0: 52 | roll_channel.append(0) 53 | roll_time.append(random.randint(10, 14)) 54 | roll_channel[-1] = danmu['time'] + (len(danmu['text']) * roll_time[-1]) / 8 + 1 55 | height = len(roll_channel) * 54 - 27 56 | end_time = start_time + roll_time[-1] 57 | 58 | m, s = divmod(end_time, 60) 59 | h, m = divmod(m, 60) 60 | output.write(f'{h:d}:{m:02d}:{s:02d}.{hundred_ms:d}0,') 61 | 62 | output.write( 63 | 'Roll,,0,0,0,,{\\move(1920,' + str(height) + ',-1000,' + str(height) + ')\\1c&H4C' + danmu['color'][1:] + '}') 64 | elif danmu['position'] == 1: # Top danmu 65 | end_time = start_time + 5 66 | m, s = divmod(end_time, 60) 67 | h, m = divmod(m, 60) 68 | output.write(f'{h:d}:{m:02d}:{s:02d}.{hundred_ms:d}0,') 69 | output.write( 70 | 'Top,,0,0,0,,{\\1c&H4C' + danmu['color'][1:] + '}') 71 | else: # Bottom danmu 72 | end_time = start_time + 5 73 | m, s = divmod(end_time, 60) 74 | h, m = divmod(m, 60) 75 | output.write(f'{h:d}:{m:02d}:{s:02d}.{hundred_ms:d}0,') 76 | output.write( 77 | 'Bottom,,0,0,0,,{\\1c&H4C' + danmu['color'][1:] + '}') 78 | 79 | output.write(danmu['text']) 80 | output.write('\n') 81 | 82 | print('彈幕下載完成 file: ' + full_filename) 83 | 84 | 85 | def downlaod_all(sn, bangumi_path, format_str='{anime_name}[{episode}].ass'): 86 | h = h = get_header() 87 | r = requests.get( 88 | 'https://ani.gamer.com.tw/animeVideo.php?sn=' + str(sn), headers=h) 89 | 90 | if r.status_code != 200: 91 | print(str(sn) + '彈幕下載失敗, status_code=' + str(status_code)) 92 | return 93 | 94 | soup = BeautifulSoup(r.text, 'lxml') 95 | anime_name = re.match(r'(^.+)\s\[.+\]$', soup.find_all('div', 96 | class_='anime_name')[0].h1.string).group(1) 97 | print(anime_name) 98 | 99 | # This may fail 抓不到其他集數 可能為劇場版 套用單集下載策略 100 | sn_list = soup.find_all('section', class_='season')[0].find_all('a') 101 | 102 | os.makedirs(os.path.join(bangumi_path, anime_name), exist_ok=True) 103 | 104 | for s in sn_list: 105 | episode = s.string 106 | download(s['href'][4:], os.path.join(bangumi_path, anime_name, 107 | format_str.format(anime_name=anime_name, episode=episode))) 108 | 109 | 110 | def get_info(sn): 111 | h = get_header() 112 | r = requests.get('https://ani.gamer.com.tw/animeVideo.php?sn=' + str(sn), headers=h) 113 | 114 | if r.status_code != 200: 115 | print(str(sn) + '獲取資訊失敗, status_code=' + str(status_code)) 116 | return 117 | 118 | soup = BeautifulSoup(r.text, 'lxml') 119 | anime_name = re.match(r'(^.+)\s\[.+\]$', soup.find_all('div', 120 | class_='anime_name')[0].h1.string).group(1) 121 | episode = re.match(r'^.+\s\[(.+)\]$', soup.find_all('div', 122 | class_='anime_name')[0].h1.string).group(1) 123 | return anime_name, episode 124 | 125 | 126 | def get_header(): 127 | return { 128 | 'Content-Type': 129 | 'application/x-www-form-urlencoded;charset=utf-8', 130 | 'origin': 131 | 'https://ani.gamer.com.tw', 132 | 'authority': 133 | 'ani.gamer.com.tw', 134 | 'user-agent': 135 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36' 136 | } 137 | 138 | 139 | if __name__ == '__main__': 140 | parser = argparse.ArgumentParser() 141 | parser.add_argument('--sn', '-s', type=int, help='動畫 SN 碼(數字)') 142 | parser.add_argument('--all', '-a', action='store_true', help='下載所有彈幕') 143 | parser.add_argument('--format', '-f', type=str, 144 | default='{anime_name}[{episode}].ass', help='字幕檔名 format 預設為 \'{anime_name}[{episode}].ass\'') 145 | # parser.add_argument('--episode', '-e', type=str, help='根據集數下載,以逗號分開') 146 | parser.add_argument('--path', '-p', type=str, 147 | help='下載的資料夾位置,預設為 ./bangumi//*.ass', default='bangumi') 148 | 149 | arg = parser.parse_args() 150 | if arg.sn == None: 151 | print('請輸入 SN') 152 | exit(0) 153 | if arg.all == True: 154 | try: 155 | downlaod_all(arg.sn, arg.path, arg.format) 156 | except: 157 | print('抓不到其他集數 可能為劇場版 套用單集下載策略') 158 | anime_name, episode = get_info(arg.sn) 159 | os.makedirs(os.path.join(arg.path, anime_name), exist_ok=True) 160 | download(arg.sn, os.path.join(arg.path, anime_name, 161 | arg.format.format(anime_name=anime_name, episode=episode))) 162 | else: 163 | anime_name, episode = get_info(arg.sn) 164 | os.makedirs(os.path.join(arg.path, anime_name), exist_ok=True) 165 | download(arg.sn, os.path.join(arg.path, anime_name, 166 | arg.format.format(anime_name=anime_name, episode=episode))) 167 | --------------------------------------------------------------------------------