/')
1996 | def video(id, cid, ispgc, audio_only, title):
1997 | ispgc = ispgc == 'true'
1998 | audio_only = audio_only == 'true'
1999 | video_url = ''
2000 | enable_dash = getSetting('enable_dash') == 'true'
2001 | if cid == '0':
2002 | res = get_api_data('/x/web-interface/view', {'bvid': id})
2003 |
2004 | data = res['data']
2005 | if res['code'] != 0:
2006 | return
2007 |
2008 | cid = data['pages'][0]['cid']
2009 | if 'redirect_url' in data and 'bangumi/play/ep' in data['redirect_url']:
2010 | ispgc = True
2011 | else:
2012 | ispgc = False
2013 |
2014 | if ispgc:
2015 | url = '/pgc/player/web/playurl'
2016 | else:
2017 | url = '/x/player/playurl'
2018 |
2019 | qn = getSetting('video_resolution')
2020 |
2021 | if enable_dash or audio_only:
2022 | params = {
2023 | 'bvid': id,
2024 | 'cid': cid,
2025 | 'qn': qn,
2026 | 'fnval': 4048,
2027 | 'fourk': 1
2028 | }
2029 | else:
2030 | params = {
2031 | 'bvid': id,
2032 | 'cid': cid,
2033 | 'qn': qn,
2034 | 'fnval': 128,
2035 | 'fourk': 1
2036 | }
2037 |
2038 | res = get_api_data(url, data=params)
2039 |
2040 | if res['code'] != 0:
2041 | return
2042 | if ispgc:
2043 | data = res['result']
2044 | else:
2045 | data = res['data']
2046 |
2047 | if 'dash' in data:
2048 | if audio_only:
2049 | video_url = data['dash']['audio'][0]['baseUrl'] + '|Referer=https://www.bilibili.com'
2050 | video_url = {
2051 | 'label': title,
2052 | 'path': video_url
2053 | }
2054 | plugin.set_resolved_url(video_url)
2055 | return
2056 | else:
2057 | mpd = generate_mpd(data['dash'])
2058 | success = None
2059 | basepath = 'special://temp/plugin.video.bili/'
2060 | if not make_dirs(basepath):
2061 | return
2062 | filepath = '{}{}.mpd'.format(basepath, cid)
2063 | with xbmcvfs.File(filepath, 'w') as mpd_file:
2064 | success = mpd_file.write(mpd)
2065 | if not success:
2066 | return
2067 | ip_address = '127.0.0.1'
2068 | port = getSetting('server_port')
2069 | video_url = {
2070 | 'path': 'http://{}:{}/{}.mpd'.format(ip_address, port, cid),
2071 | 'properties': {
2072 | 'inputstream': 'inputstream.adaptive',
2073 | 'inputstream.adaptive.manifest_type': 'mpd',
2074 | 'inputstream.adaptive.manifest_headers': 'Referer=https://www.bilibili.com',
2075 | 'inputstream.adaptive.stream_headers': 'Referer=https://www.bilibili.com'
2076 | }
2077 | }
2078 | elif 'durl' in data:
2079 | video_url = data['durl'][0]['url']
2080 | if video_url:
2081 | video_url += '|Referer=https://www.bilibili.com'
2082 | else:
2083 | video_url = ''
2084 |
2085 | if video_url and getSetting('enable_danmaku') == 'true':
2086 | ass = generate_ass(cid)
2087 | if ass:
2088 | player = xbmc.Player()
2089 | if player.isPlaying():
2090 | player.stop()
2091 | if video_url and (getSetting('report_history') == 'true'):
2092 | report_history(id, cid)
2093 | plugin.set_resolved_url(video_url, ass)
2094 | return
2095 | if video_url and (getSetting('report_history') == 'true'):
2096 | report_history(id, cid)
2097 | plugin.set_resolved_url(video_url)
2098 |
2099 |
2100 | if __name__ == '__main__':
2101 | plugin.run()
--------------------------------------------------------------------------------
/addon.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | video
11 |
12 |
13 |
14 | all
15 | https://www.bilibili.com
16 | https://github.com/chen310/plugin.video.bili
17 | Plugin for Bilibili
18 | 哔哩哔哩插件
19 | Bilibili
20 | 哔哩哔哩
21 |
22 | icon.jpg
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/danmaku2ass.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # The original author of this program, Danmaku2ASS, is StarBrilliant.
4 | # This file is released under General Public License version 3.
5 | # You should have received a copy of General Public License text alongside with
6 | # this program. If not, you can obtain it at http://gnu.org/copyleft/gpl.html .
7 | # This program comes with no warranty, the author will not be resopnsible for
8 | # any damage or problems caused by this program.
9 |
10 | # You can obtain a latest copy of Danmaku2ASS at:
11 | # https://github.com/m13253/danmaku2ass
12 | # Please update to the latest version before complaining.
13 |
14 | import argparse
15 | import calendar
16 | import gettext
17 | import io
18 | import json
19 | import logging
20 | import math
21 | import os
22 | import random
23 | import re
24 | import sys
25 | import time
26 | import xml.dom.minidom
27 |
28 |
29 | if sys.version_info < (3,):
30 | raise RuntimeError('at least Python 3.0 is required')
31 |
32 | gettext.install('danmaku2ass', os.path.join(os.path.dirname(os.path.abspath(os.path.realpath(sys.argv[0] or 'locale'))), 'locale'))
33 |
34 |
35 | def SeekZero(function):
36 | def decorated_function(file_):
37 | file_.seek(0)
38 | try:
39 | return function(file_)
40 | finally:
41 | file_.seek(0)
42 | return decorated_function
43 |
44 |
45 | def EOFAsNone(function):
46 | def decorated_function(*args, **kwargs):
47 | try:
48 | return function(*args, **kwargs)
49 | except EOFError:
50 | return None
51 | return decorated_function
52 |
53 |
54 | @SeekZero
55 | @EOFAsNone
56 | def ProbeCommentFormat(f):
57 | tmp = f.read(1)
58 | if tmp == '[':
59 | return 'Acfun'
60 | # It is unwise to wrap a JSON object in an array!
61 | # See this: http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx/
62 | # Do never follow what Acfun developers did!
63 | elif tmp == '{':
64 | tmp = f.read(14)
65 | if tmp == '"status_code":':
66 | return 'Tudou'
67 | elif tmp.strip().startswith('"result'):
68 | return 'Tudou2'
69 | elif tmp == '<':
70 | tmp = f.read(1)
71 | if tmp == '?':
72 | tmp = f.read(38)
73 | if tmp == 'xml version="1.0" encoding="UTF-8"?>\n<':
82 | return 'Bilibili' # Komica, with the same file format as Bilibili
83 | elif tmp == 'xml version="1.0" encoding="UTF-8"?>\n<':
84 | tmp = f.read(20)
85 | if tmp == '!-- BoonSutazioData=':
86 | return 'Niconico' # Niconico videos downloaded with NicoFox
87 | else:
88 | return 'MioMio'
89 | elif tmp == 'p':
90 | return 'Niconico' # Himawari Douga, with the same file format as Niconico Douga
91 |
92 |
93 | #
94 | # ReadComments**** protocol
95 | #
96 | # Input:
97 | # f: Input file
98 | # fontsize: Default font size
99 | #
100 | # Output:
101 | # yield a tuple:
102 | # (timeline, timestamp, no, comment, pos, color, size, height, width)
103 | # timeline: The position when the comment is replayed
104 | # timestamp: The UNIX timestamp when the comment is submitted
105 | # no: A sequence of 1, 2, 3, ..., used for sorting
106 | # comment: The content of the comment
107 | # pos: 0 for regular moving comment,
108 | # 1 for bottom centered comment,
109 | # 2 for top centered comment,
110 | # 3 for reversed moving comment
111 | # color: Font color represented in 0xRRGGBB,
112 | # e.g. 0xffffff for white
113 | # size: Font size
114 | # height: The estimated height in pixels
115 | # i.e. (comment.count('\n')+1)*size
116 | # width: The estimated width in pixels
117 | # i.e. CalculateLength(comment)*size
118 | #
119 | # After implementing ReadComments****, make sure to update ProbeCommentFormat
120 | # and CommentFormatMap.
121 | #
122 |
123 |
124 | def ReadCommentsNiconico(f, fontsize):
125 | NiconicoColorMap = {'red': 0xff0000, 'pink': 0xff8080, 'orange': 0xffcc00, 'yellow': 0xffff00, 'green': 0x00ff00, 'cyan': 0x00ffff, 'blue': 0x0000ff, 'purple': 0xc000ff, 'black': 0x000000, 'niconicowhite': 0xcccc99, 'white2': 0xcccc99, 'truered': 0xcc0033, 'red2': 0xcc0033, 'passionorange': 0xff6600, 'orange2': 0xff6600, 'madyellow': 0x999900, 'yellow2': 0x999900, 'elementalgreen': 0x00cc66, 'green2': 0x00cc66, 'marineblue': 0x33ffcc, 'blue2': 0x33ffcc, 'nobleviolet': 0x6633cc, 'purple2': 0x6633cc}
126 | dom = xml.dom.minidom.parse(f)
127 | comment_element = dom.getElementsByTagName('chat')
128 | for comment in comment_element:
129 | try:
130 | c = str(comment.childNodes[0].wholeText)
131 | if c.startswith('/'):
132 | continue # ignore advanced comments
133 | pos = 0
134 | color = 0xffffff
135 | size = fontsize
136 | for mailstyle in str(comment.getAttribute('mail')).split():
137 | if mailstyle == 'ue':
138 | pos = 1
139 | elif mailstyle == 'shita':
140 | pos = 2
141 | elif mailstyle == 'big':
142 | size = fontsize * 1.44
143 | elif mailstyle == 'small':
144 | size = fontsize * 0.64
145 | elif mailstyle in NiconicoColorMap:
146 | color = NiconicoColorMap[mailstyle]
147 | yield (max(int(comment.getAttribute('vpos')), 0) * 0.01, int(comment.getAttribute('date')), int(comment.getAttribute('no')), c, pos, color, size, (c.count('\n') + 1) * size, CalculateLength(c) * size)
148 | except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
149 | logging.warning(_('Invalid comment: %s') % comment.toxml())
150 | continue
151 |
152 |
153 | def ReadCommentsAcfun(f, fontsize):
154 | #comment_element = json.load(f)
155 | # after load acfun comment json file as python list, flatten the list
156 | #comment_element = [c for sublist in comment_element for c in sublist]
157 | comment_elements = json.load(f)
158 | comment_element = comment_elements[2]
159 | for i, comment in enumerate(comment_element):
160 | try:
161 | p = str(comment['c']).split(',')
162 | assert len(p) >= 6
163 | assert p[2] in ('1', '2', '4', '5', '7')
164 | size = int(p[3]) * fontsize / 25.0
165 | if p[2] != '7':
166 | c = str(comment['m']).replace('\\r', '\n').replace('\r', '\n')
167 | yield (float(p[0]), int(p[5]), i, c, {'1': 0, '2': 0, '4': 2, '5': 1}[p[2]], int(p[1]), size, (c.count('\n') + 1) * size, CalculateLength(c) * size)
168 | else:
169 | c = dict(json.loads(comment['m']))
170 | yield (float(p[0]), int(p[5]), i, c, 'acfunpos', int(p[1]), size, 0, 0)
171 | except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
172 | logging.warning(_('Invalid comment: %r') % comment)
173 | continue
174 |
175 |
176 | def ReadCommentsBilibili(f, fontsize):
177 | dom = xml.dom.minidom.parse(f)
178 | comment_element = dom.getElementsByTagName('d')
179 | for i, comment in enumerate(comment_element):
180 | try:
181 | p = str(comment.getAttribute('p')).split(',')
182 | assert len(p) >= 5
183 | assert p[1] in ('1', '4', '5', '6', '7', '8')
184 | if comment.childNodes.length > 0:
185 | if p[1] in ('1', '4', '5', '6'):
186 | c = str(comment.childNodes[0].wholeText).replace('/n', '\n')
187 | size = int(p[2]) * fontsize / 25.0
188 | yield (float(p[0]), int(p[4]), i, c, {'1': 0, '4': 2, '5': 1, '6': 3}[p[1]], int(p[3]), size, (c.count('\n') + 1) * size, CalculateLength(c) * size)
189 | elif p[1] == '7': # positioned comment
190 | c = str(comment.childNodes[0].wholeText)
191 | yield (float(p[0]), int(p[4]), i, c, 'bilipos', int(p[3]), int(p[2]), 0, 0)
192 | elif p[1] == '8':
193 | pass # ignore scripted comment
194 | except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
195 | logging.warning(_('Invalid comment: %s') % comment.toxml())
196 | continue
197 |
198 |
199 | def ReadCommentsBilibili2(f, fontsize):
200 | dom = xml.dom.minidom.parse(f)
201 | comment_element = dom.getElementsByTagName('d')
202 | for i, comment in enumerate(comment_element):
203 | try:
204 | p = str(comment.getAttribute('p')).split(',')
205 | assert len(p) >= 7
206 | assert p[3] in ('1', '4', '5', '6', '7', '8')
207 | if comment.childNodes.length > 0:
208 | time = float(p[2]) / 1000.0
209 | if p[3] in ('1', '4', '5', '6'):
210 | c = str(comment.childNodes[0].wholeText).replace('/n', '\n')
211 | size = int(p[4]) * fontsize / 25.0
212 | yield (time, int(p[6]), i, c, {'1': 0, '4': 2, '5': 1, '6': 3}[p[3]], int(p[5]), size, (c.count('\n') + 1) * size, CalculateLength(c) * size)
213 | elif p[3] == '7': # positioned comment
214 | c = str(comment.childNodes[0].wholeText)
215 | yield (time, int(p[6]), i, c, 'bilipos', int(p[5]), int(p[4]), 0, 0)
216 | elif p[3] == '8':
217 | pass # ignore scripted comment
218 | except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
219 | logging.warning(_('Invalid comment: %s') % comment.toxml())
220 | continue
221 |
222 |
223 | def ReadCommentsTudou(f, fontsize):
224 | comment_element = json.load(f)
225 | for i, comment in enumerate(comment_element['comment_list']):
226 | try:
227 | assert comment['pos'] in (3, 4, 6)
228 | c = str(comment['data'])
229 | assert comment['size'] in (0, 1, 2)
230 | size = {0: 0.64, 1: 1, 2: 1.44}[comment['size']] * fontsize
231 | yield (int(comment['replay_time'] * 0.001), int(comment['commit_time']), i, c, {3: 0, 4: 2, 6: 1}[comment['pos']], int(comment['color']), size, (c.count('\n') + 1) * size, CalculateLength(c) * size)
232 | except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
233 | logging.warning(_('Invalid comment: %r') % comment)
234 | continue
235 |
236 |
237 | def ReadCommentsTudou2(f, fontsize):
238 | comment_element = json.load(f)
239 | for i, comment in enumerate(comment_element['result']):
240 | try:
241 | c = str(comment['content'])
242 | prop = json.loads(str(comment['propertis']) or '{}')
243 | size = int(prop.get('size', 1))
244 | assert size in (0, 1, 2)
245 | size = {0: 0.64, 1: 1, 2: 1.44}[size] * fontsize
246 | pos = int(prop.get('pos', 3))
247 | assert pos in (0, 3, 4, 6)
248 | yield (
249 | int(comment['playat'] * 0.001), int(comment['createtime'] * 0.001), i, c,
250 | {0: 0, 3: 0, 4: 2, 6: 1}[pos],
251 | int(prop.get('color', 0xffffff)), size, (c.count('\n') + 1) * size, CalculateLength(c) * size)
252 | except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
253 | logging.warning(_('Invalid comment: %r') % comment)
254 | continue
255 |
256 |
257 | def ReadCommentsMioMio(f, fontsize):
258 | NiconicoColorMap = {'red': 0xff0000, 'pink': 0xff8080, 'orange': 0xffc000, 'yellow': 0xffff00, 'green': 0x00ff00, 'cyan': 0x00ffff, 'blue': 0x0000ff, 'purple': 0xc000ff, 'black': 0x000000}
259 | dom = xml.dom.minidom.parse(f)
260 | comment_element = dom.getElementsByTagName('data')
261 | for i, comment in enumerate(comment_element):
262 | try:
263 | message = comment.getElementsByTagName('message')[0]
264 | c = str(message.childNodes[0].wholeText)
265 | pos = 0
266 | size = int(message.getAttribute('fontsize')) * fontsize / 25.0
267 | yield (float(comment.getElementsByTagName('playTime')[0].childNodes[0].wholeText), int(calendar.timegm(time.strptime(comment.getElementsByTagName('times')[0].childNodes[0].wholeText, '%Y-%m-%d %H:%M:%S'))) - 28800, i, c, {'1': 0, '4': 2, '5': 1}[message.getAttribute('mode')], int(message.getAttribute('color')), size, (c.count('\n') + 1) * size, CalculateLength(c) * size)
268 | except (AssertionError, AttributeError, IndexError, TypeError, ValueError):
269 | logging.warning(_('Invalid comment: %s') % comment.toxml())
270 | continue
271 |
272 |
273 | CommentFormatMap = {'Niconico': ReadCommentsNiconico, 'Acfun': ReadCommentsAcfun, 'Bilibili': ReadCommentsBilibili, 'Bilibili2': ReadCommentsBilibili2, 'Tudou': ReadCommentsTudou, 'Tudou2': ReadCommentsTudou2, 'MioMio': ReadCommentsMioMio}
274 |
275 |
276 | def WriteCommentBilibiliPositioned(f, c, width, height, styleid):
277 | # BiliPlayerSize = (512, 384) # Bilibili player version 2010
278 | # BiliPlayerSize = (540, 384) # Bilibili player version 2012
279 | BiliPlayerSize = (672, 438) # Bilibili player version 2014
280 | ZoomFactor = GetZoomFactor(BiliPlayerSize, (width, height))
281 |
282 | def GetPosition(InputPos, isHeight):
283 | isHeight = int(isHeight) # True -> 1
284 | if isinstance(InputPos, int):
285 | return ZoomFactor[0] * InputPos + ZoomFactor[isHeight + 1]
286 | elif isinstance(InputPos, float):
287 | if InputPos > 1:
288 | return ZoomFactor[0] * InputPos + ZoomFactor[isHeight + 1]
289 | else:
290 | return BiliPlayerSize[isHeight] * ZoomFactor[0] * InputPos + ZoomFactor[isHeight + 1]
291 | else:
292 | try:
293 | InputPos = int(InputPos)
294 | except ValueError:
295 | InputPos = float(InputPos)
296 | return GetPosition(InputPos, isHeight)
297 |
298 | try:
299 | comment_args = safe_list(json.loads(c[3]))
300 | text = ASSEscape(str(comment_args[4]).replace('/n', '\n'))
301 | from_x = comment_args.get(0, 0)
302 | from_y = comment_args.get(1, 0)
303 | to_x = comment_args.get(7, from_x)
304 | to_y = comment_args.get(8, from_y)
305 | from_x = GetPosition(from_x, False)
306 | from_y = GetPosition(from_y, True)
307 | to_x = GetPosition(to_x, False)
308 | to_y = GetPosition(to_y, True)
309 | alpha = safe_list(str(comment_args.get(2, '1')).split('-'))
310 | from_alpha = float(alpha.get(0, 1))
311 | to_alpha = float(alpha.get(1, from_alpha))
312 | from_alpha = 255 - round(from_alpha * 255)
313 | to_alpha = 255 - round(to_alpha * 255)
314 | rotate_z = int(comment_args.get(5, 0))
315 | rotate_y = int(comment_args.get(6, 0))
316 | lifetime = float(comment_args.get(3, 4500))
317 | duration = int(comment_args.get(9, lifetime * 1000))
318 | delay = int(comment_args.get(10, 0))
319 | fontface = comment_args.get(12)
320 | isborder = comment_args.get(11, 'true')
321 | from_rotarg = ConvertFlashRotation(rotate_y, rotate_z, from_x, from_y, width, height)
322 | to_rotarg = ConvertFlashRotation(rotate_y, rotate_z, to_x, to_y, width, height)
323 | styles = ['\\org(%d, %d)' % (width / 2, height / 2)]
324 | if from_rotarg[0:2] == to_rotarg[0:2]:
325 | styles.append('\\pos(%.0f, %.0f)' % (from_rotarg[0:2]))
326 | else:
327 | styles.append('\\move(%.0f, %.0f, %.0f, %.0f, %.0f, %.0f)' % (from_rotarg[0:2] + to_rotarg[0:2] + (delay, delay + duration)))
328 | styles.append('\\frx%.0f\\fry%.0f\\frz%.0f\\fscx%.0f\\fscy%.0f' % (from_rotarg[2:7]))
329 | if (from_x, from_y) != (to_x, to_y):
330 | styles.append('\\t(%d, %d, ' % (delay, delay + duration))
331 | styles.append('\\frx%.0f\\fry%.0f\\frz%.0f\\fscx%.0f\\fscy%.0f' % (to_rotarg[2:7]))
332 | styles.append(')')
333 | if fontface:
334 | styles.append('\\fn%s' % ASSEscape(fontface))
335 | styles.append('\\fs%.0f' % (c[6] * ZoomFactor[0]))
336 | if c[5] != 0xffffff:
337 | styles.append('\\c&H%s&' % ConvertColor(c[5]))
338 | if c[5] == 0x000000:
339 | styles.append('\\3c&HFFFFFF&')
340 | if from_alpha == to_alpha:
341 | styles.append('\\alpha&H%02X' % from_alpha)
342 | elif (from_alpha, to_alpha) == (255, 0):
343 | styles.append('\\fad(%.0f,0)' % (lifetime * 1000))
344 | elif (from_alpha, to_alpha) == (0, 255):
345 | styles.append('\\fad(0, %.0f)' % (lifetime * 1000))
346 | else:
347 | styles.append('\\fade(%(from_alpha)d, %(to_alpha)d, %(to_alpha)d, 0, %(end_time).0f, %(end_time).0f, %(end_time).0f)' % {'from_alpha': from_alpha, 'to_alpha': to_alpha, 'end_time': lifetime * 1000})
348 | if isborder == 'false':
349 | styles.append('\\bord0')
350 | f.write('Dialogue: -1,%(start)s,%(end)s,%(styleid)s,,0,0,0,,{%(styles)s}%(text)s\n' % {'start': ConvertTimestamp(c[0]), 'end': ConvertTimestamp(c[0] + lifetime), 'styles': ''.join(styles), 'text': text, 'styleid': styleid})
351 | except (IndexError, ValueError) as e:
352 | try:
353 | logging.warning(_('Invalid comment: %r') % c[3])
354 | except IndexError:
355 | logging.warning(_('Invalid comment: %r') % c)
356 |
357 |
358 | def WriteCommentAcfunPositioned(f, c, width, height, styleid):
359 | AcfunPlayerSize = (560, 400)
360 | ZoomFactor = GetZoomFactor(AcfunPlayerSize, (width, height))
361 |
362 | def GetPosition(InputPos, isHeight):
363 | isHeight = int(isHeight) # True -> 1
364 | return AcfunPlayerSize[isHeight] * ZoomFactor[0] * InputPos * 0.001 + ZoomFactor[isHeight + 1]
365 |
366 | def GetTransformStyles(x=None, y=None, scale_x=None, scale_y=None, rotate_z=None, rotate_y=None, color=None, alpha=None):
367 | styles = []
368 | out_x, out_y = x, y
369 | if rotate_z is not None and rotate_y is not None:
370 | assert x is not None
371 | assert y is not None
372 | rotarg = ConvertFlashRotation(rotate_y, rotate_z, x, y, width, height)
373 | out_x, out_y = rotarg[0:2]
374 | if scale_x is None:
375 | scale_x = 1
376 | if scale_y is None:
377 | scale_y = 1
378 | styles.append('\\frx%.0f\\fry%.0f\\frz%.0f\\fscx%.0f\\fscy%.0f' % (rotarg[2:5] + (rotarg[5] * scale_x, rotarg[6] * scale_y)))
379 | else:
380 | if scale_x is not None:
381 | styles.append('\\fscx%.0f' % (scale_x * 100))
382 | if scale_y is not None:
383 | styles.append('\\fscy%.0f' % (scale_y * 100))
384 | if color is not None:
385 | styles.append('\\c&H%s&' % ConvertColor(color))
386 | if color == 0x000000:
387 | styles.append('\\3c&HFFFFFF&')
388 | if alpha is not None:
389 | alpha = 255 - round(alpha * 255)
390 | styles.append('\\alpha&H%02X' % alpha)
391 | return out_x, out_y, styles
392 |
393 | def FlushCommentLine(f, text, styles, start_time, end_time, styleid):
394 | if end_time > start_time:
395 | f.write('Dialogue: -1,%(start)s,%(end)s,%(styleid)s,,0,0,0,,{%(styles)s}%(text)s\n' % {'start': ConvertTimestamp(start_time), 'end': ConvertTimestamp(end_time), 'styles': ''.join(styles), 'text': text, 'styleid': styleid})
396 |
397 | try:
398 | comment_args = c[3]
399 | text = ASSEscape(str(comment_args['n']).replace('\r', '\n'))
400 | common_styles = ['\org(%d, %d)' % (width / 2, height / 2)]
401 | anchor = {0: 7, 1: 8, 2: 9, 3: 4, 4: 5, 5: 6, 6: 1, 7: 2, 8: 3}.get(comment_args.get('c', 0), 7)
402 | if anchor != 7:
403 | common_styles.append('\\an%s' % anchor)
404 | font = comment_args.get('w')
405 | if font:
406 | font = dict(font)
407 | fontface = font.get('f')
408 | if fontface:
409 | common_styles.append('\\fn%s' % ASSEscape(str(fontface)))
410 | fontbold = bool(font.get('b'))
411 | if fontbold:
412 | common_styles.append('\\b1')
413 | common_styles.append('\\fs%.0f' % (c[6] * ZoomFactor[0]))
414 | isborder = bool(comment_args.get('b', True))
415 | if not isborder:
416 | common_styles.append('\\bord0')
417 | to_pos = dict(comment_args.get('p', {'x': 0, 'y': 0}))
418 | to_x = round(GetPosition(int(to_pos.get('x', 0)), False))
419 | to_y = round(GetPosition(int(to_pos.get('y', 0)), True))
420 | to_scale_x = float(comment_args.get('e', 1.0))
421 | to_scale_y = float(comment_args.get('f', 1.0))
422 | to_rotate_z = float(comment_args.get('r', 0.0))
423 | to_rotate_y = float(comment_args.get('k', 0.0))
424 | to_color = c[5]
425 | to_alpha = float(comment_args.get('a', 1.0))
426 | from_time = float(comment_args.get('t', 0.0))
427 | action_time = float(comment_args.get('l', 3.0))
428 | actions = list(comment_args.get('z', []))
429 | to_out_x, to_out_y, transform_styles = GetTransformStyles(to_x, to_y, to_scale_x, to_scale_y, to_rotate_z, to_rotate_y, to_color, to_alpha)
430 | FlushCommentLine(f, text, common_styles + ['\\pos(%.0f, %.0f)' % (to_out_x, to_out_y)] + transform_styles, c[0] + from_time, c[0] + from_time + action_time, styleid)
431 | action_styles = transform_styles
432 | for action in actions:
433 | action = dict(action)
434 | from_x, from_y = to_x, to_y
435 | from_out_x, from_out_y = to_out_x, to_out_y
436 | from_scale_x, from_scale_y = to_scale_x, to_scale_y
437 | from_rotate_z, from_rotate_y = to_rotate_z, to_rotate_y
438 | from_color, from_alpha = to_color, to_alpha
439 | transform_styles, action_styles = action_styles, []
440 | from_time += action_time
441 | action_time = float(action.get('l', 0.0))
442 | if 'x' in action:
443 | to_x = round(GetPosition(int(action['x']), False))
444 | if 'y' in action:
445 | to_y = round(GetPosition(int(action['y']), True))
446 | if 'f' in action:
447 | to_scale_x = float(action['f'])
448 | if 'g' in action:
449 | to_scale_y = float(action['g'])
450 | if 'c' in action:
451 | to_color = int(action['c'])
452 | if 't' in action:
453 | to_alpha = float(action['t'])
454 | if 'd' in action:
455 | to_rotate_z = float(action['d'])
456 | if 'e' in action:
457 | to_rotate_y = float(action['e'])
458 | to_out_x, to_out_y, action_styles = GetTransformStyles(to_x, to_y, from_scale_x, from_scale_y, to_rotate_z, to_rotate_y, from_color, from_alpha)
459 | if (from_out_x, from_out_y) == (to_out_x, to_out_y):
460 | pos_style = '\\pos(%.0f, %.0f)' % (to_out_x, to_out_y)
461 | else:
462 | pos_style = '\\move(%.0f, %.0f, %.0f, %.0f)' % (from_out_x, from_out_y, to_out_x, to_out_y)
463 | styles = common_styles + transform_styles
464 | styles.append(pos_style)
465 | if action_styles:
466 | styles.append('\\t(%s)' % (''.join(action_styles)))
467 | FlushCommentLine(f, text, styles, c[0] + from_time, c[0] + from_time + action_time, styleid)
468 | except (IndexError, ValueError) as e:
469 | logging.warning(_('Invalid comment: %r') % c[3])
470 |
471 |
472 | # Result: (f, dx, dy)
473 | # To convert: NewX = f*x+dx, NewY = f*y+dy
474 | def GetZoomFactor(SourceSize, TargetSize):
475 | try:
476 | if (SourceSize, TargetSize) == GetZoomFactor.Cached_Size:
477 | return GetZoomFactor.Cached_Result
478 | except AttributeError:
479 | pass
480 | GetZoomFactor.Cached_Size = (SourceSize, TargetSize)
481 | try:
482 | SourceAspect = SourceSize[0] / SourceSize[1]
483 | TargetAspect = TargetSize[0] / TargetSize[1]
484 | if TargetAspect < SourceAspect: # narrower
485 | ScaleFactor = TargetSize[0] / SourceSize[0]
486 | GetZoomFactor.Cached_Result = (ScaleFactor, 0, (TargetSize[1] - TargetSize[0] / SourceAspect) / 2)
487 | elif TargetAspect > SourceAspect: # wider
488 | ScaleFactor = TargetSize[1] / SourceSize[1]
489 | GetZoomFactor.Cached_Result = (ScaleFactor, (TargetSize[0] - TargetSize[1] * SourceAspect) / 2, 0)
490 | else:
491 | GetZoomFactor.Cached_Result = (TargetSize[0] / SourceSize[0], 0, 0)
492 | return GetZoomFactor.Cached_Result
493 | except ZeroDivisionError:
494 | GetZoomFactor.Cached_Result = (1, 0, 0)
495 | return GetZoomFactor.Cached_Result
496 |
497 |
498 | # Calculation is based on https://github.com/jabbany/CommentCoreLibrary/issues/5#issuecomment-40087282
499 | # and https://github.com/m13253/danmaku2ass/issues/7#issuecomment-41489422
500 | # ASS FOV = width*4/3.0
501 | # But Flash FOV = width/math.tan(100*math.pi/360.0)/2 will be used instead
502 | # Result: (transX, transY, rotX, rotY, rotZ, scaleX, scaleY)
503 | def ConvertFlashRotation(rotY, rotZ, X, Y, width, height):
504 | def WrapAngle(deg):
505 | return 180 - ((180 - deg) % 360)
506 | rotY = WrapAngle(rotY)
507 | rotZ = WrapAngle(rotZ)
508 | if rotY in (90, -90):
509 | rotY -= 1
510 | if rotY == 0 or rotZ == 0:
511 | outX = 0
512 | outY = -rotY # Positive value means clockwise in Flash
513 | outZ = -rotZ
514 | rotY *= math.pi / 180.0
515 | rotZ *= math.pi / 180.0
516 | else:
517 | rotY *= math.pi / 180.0
518 | rotZ *= math.pi / 180.0
519 | outY = math.atan2(-math.sin(rotY) * math.cos(rotZ), math.cos(rotY)) * 180 / math.pi
520 | outZ = math.atan2(-math.cos(rotY) * math.sin(rotZ), math.cos(rotZ)) * 180 / math.pi
521 | outX = math.asin(math.sin(rotY) * math.sin(rotZ)) * 180 / math.pi
522 | trX = (X * math.cos(rotZ) + Y * math.sin(rotZ)) / math.cos(rotY) + (1 - math.cos(rotZ) / math.cos(rotY)) * width / 2 - math.sin(rotZ) / math.cos(rotY) * height / 2
523 | trY = Y * math.cos(rotZ) - X * math.sin(rotZ) + math.sin(rotZ) * width / 2 + (1 - math.cos(rotZ)) * height / 2
524 | trZ = (trX - width / 2) * math.sin(rotY)
525 | FOV = width * math.tan(2 * math.pi / 9.0) / 2
526 | try:
527 | scaleXY = FOV / (FOV + trZ)
528 | except ZeroDivisionError:
529 | logging.error('Rotation makes object behind the camera: trZ == %.0f' % trZ)
530 | scaleXY = 1
531 | trX = (trX - width / 2) * scaleXY + width / 2
532 | trY = (trY - height / 2) * scaleXY + height / 2
533 | if scaleXY < 0:
534 | scaleXY = -scaleXY
535 | outX += 180
536 | outY += 180
537 | logging.error('Rotation makes object behind the camera: trZ == %.0f < %.0f' % (trZ, FOV))
538 | return (trX, trY, WrapAngle(outX), WrapAngle(outY), WrapAngle(outZ), scaleXY * 100, scaleXY * 100)
539 |
540 |
541 | def ProcessComments(comments, f, width, height, bottomReserved, fontface, fontsize, alpha, duration_marquee, duration_still, filters_regex, reduced, progress_callback):
542 | styleid = 'Danmaku2ASS_%04x' % random.randint(0, 0xffff)
543 | WriteASSHead(f, width, height, fontface, fontsize, alpha, styleid)
544 | rows = [[None] * (height - bottomReserved + 1) for i in range(4)]
545 | for idx, i in enumerate(comments):
546 | if progress_callback and idx % 1000 == 0:
547 | progress_callback(idx, len(comments))
548 | if isinstance(i[4], int):
549 | skip = False
550 | for filter_regex in filters_regex:
551 | if filter_regex and filter_regex.search(i[3]):
552 | skip = True
553 | break
554 | if skip:
555 | continue
556 | row = 0
557 | rowmax = height - bottomReserved - i[7]
558 | while row <= rowmax:
559 | freerows = TestFreeRows(rows, i, row, width, height, bottomReserved, duration_marquee, duration_still)
560 | if freerows >= i[7]:
561 | MarkCommentRow(rows, i, row)
562 | WriteComment(f, i, row, width, height, bottomReserved, fontsize, duration_marquee, duration_still, styleid)
563 | break
564 | else:
565 | row += freerows or 1
566 | else:
567 | if not reduced:
568 | row = FindAlternativeRow(rows, i, height, bottomReserved)
569 | MarkCommentRow(rows, i, row)
570 | WriteComment(f, i, row, width, height, bottomReserved, fontsize, duration_marquee, duration_still, styleid)
571 | elif i[4] == 'bilipos':
572 | WriteCommentBilibiliPositioned(f, i, width, height, styleid)
573 | elif i[4] == 'acfunpos':
574 | WriteCommentAcfunPositioned(f, i, width, height, styleid)
575 | else:
576 | logging.warning(_('Invalid comment: %r') % i[3])
577 | if progress_callback:
578 | progress_callback(len(comments), len(comments))
579 |
580 |
581 | def TestFreeRows(rows, c, row, width, height, bottomReserved, duration_marquee, duration_still):
582 | res = 0
583 | rowmax = height - bottomReserved
584 | targetRow = None
585 | if c[4] in (1, 2):
586 | while row < rowmax and res < c[7]:
587 | if targetRow != rows[c[4]][row]:
588 | targetRow = rows[c[4]][row]
589 | if targetRow and targetRow[0] + duration_still > c[0]:
590 | break
591 | row += 1
592 | res += 1
593 | else:
594 | try:
595 | thresholdTime = c[0] - duration_marquee * (1 - width / (c[8] + width))
596 | except ZeroDivisionError:
597 | thresholdTime = c[0] - duration_marquee
598 | while row < rowmax and res < c[7]:
599 | if targetRow != rows[c[4]][row]:
600 | targetRow = rows[c[4]][row]
601 | try:
602 | if targetRow and (targetRow[0] > thresholdTime or targetRow[0] + targetRow[8] * duration_marquee / (targetRow[8] + width) > c[0]):
603 | break
604 | except ZeroDivisionError:
605 | pass
606 | row += 1
607 | res += 1
608 | return res
609 |
610 |
611 | def FindAlternativeRow(rows, c, height, bottomReserved):
612 | res = 0
613 | for row in range(height - bottomReserved - math.ceil(c[7])):
614 | if not rows[c[4]][row]:
615 | return row
616 | elif rows[c[4]][row][0] < rows[c[4]][res][0]:
617 | res = row
618 | return res
619 |
620 |
621 | def MarkCommentRow(rows, c, row):
622 | try:
623 | for i in range(row, row + math.ceil(c[7])):
624 | rows[c[4]][i] = c
625 | except IndexError:
626 | pass
627 |
628 |
629 | def WriteASSHead(f, width, height, fontface, fontsize, alpha, styleid):
630 | f.write(
631 | '''[Script Info]
632 | ; Script generated by Danmaku2ASS
633 | ; https://github.com/m13253/danmaku2ass
634 | Script Updated By: Danmaku2ASS (https://github.com/m13253/danmaku2ass)
635 | ScriptType: v4.00+
636 | PlayResX: %(width)d
637 | PlayResY: %(height)d
638 | Aspect Ratio: %(width)d:%(height)d
639 | Collisions: Normal
640 | WrapStyle: 2
641 | ScaledBorderAndShadow: yes
642 | YCbCr Matrix: TV.601
643 |
644 | [V4+ Styles]
645 | Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
646 | Style: %(styleid)s, %(fontface)s, %(fontsize).0f, &H%(alpha)02XFFFFFF, &H%(alpha)02XFFFFFF, &H%(alpha)02X000000, &H%(alpha)02X000000, 0, 0, 0, 0, 100, 100, 0.00, 0.00, 1, %(outline).0f, 0, 7, 0, 0, 0, 0
647 |
648 | [Events]
649 | Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
650 | ''' % {'width': width, 'height': height, 'fontface': fontface, 'fontsize': fontsize, 'alpha': 255 - round(alpha * 255), 'outline': max(fontsize / 25.0, 1), 'styleid': styleid}
651 | )
652 |
653 |
654 | def WriteComment(f, c, row, width, height, bottomReserved, fontsize, duration_marquee, duration_still, styleid):
655 | text = ASSEscape(c[3])
656 | styles = []
657 | if c[4] == 1:
658 | styles.append('\\an8\\pos(%(halfwidth)d, %(row)d)' % {'halfwidth': width / 2, 'row': row})
659 | duration = duration_still
660 | elif c[4] == 2:
661 | styles.append('\\an2\\pos(%(halfwidth)d, %(row)d)' % {'halfwidth': width / 2, 'row': ConvertType2(row, height, bottomReserved)})
662 | duration = duration_still
663 | elif c[4] == 3:
664 | styles.append('\\move(%(neglen)d, %(row)d, %(width)d, %(row)d)' % {'width': width, 'row': row, 'neglen': -math.ceil(c[8])})
665 | duration = duration_marquee
666 | else:
667 | styles.append('\\move(%(width)d, %(row)d, %(neglen)d, %(row)d)' % {'width': width, 'row': row, 'neglen': -math.ceil(c[8])})
668 | duration = duration_marquee
669 | if not (-1 < c[6] - fontsize < 1):
670 | styles.append('\\fs%.0f' % c[6])
671 | if c[5] != 0xffffff:
672 | styles.append('\\c&H%s&' % ConvertColor(c[5]))
673 | if c[5] == 0x000000:
674 | styles.append('\\3c&HFFFFFF&')
675 | f.write('Dialogue: 2,%(start)s,%(end)s,%(styleid)s,,0000,0000,0000,,{%(styles)s}%(text)s\n' % {'start': ConvertTimestamp(c[0]), 'end': ConvertTimestamp(c[0] + duration), 'styles': ''.join(styles), 'text': text, 'styleid': styleid})
676 |
677 |
678 | def ASSEscape(s):
679 | def ReplaceLeadingSpace(s):
680 | if len(s) == 0:
681 | return s
682 | if s[0] in (' ', '\t'):
683 | s = '\u200b' + s
684 | if s[-1] in (' ', '\t'):
685 | s = s + '\u200b'
686 | return s
687 | return '\\N'.join((ReplaceLeadingSpace(i) or ' ' for i in str(s).replace('\\', '\\\u200b').replace('{', '\\{').replace('}', '\\}').split('\n')))
688 |
689 |
690 | def CalculateLength(s):
691 | return max(map(len, s.split('\n'))) # May not be accurate
692 |
693 |
694 | def ConvertTimestamp(timestamp):
695 | timestamp = round(timestamp * 100.0)
696 | hour, minute = divmod(timestamp, 360000)
697 | minute, second = divmod(minute, 6000)
698 | second, centsecond = divmod(second, 100)
699 | return '%d:%02d:%02d.%02d' % (int(hour), int(minute), int(second), int(centsecond))
700 |
701 |
702 | def ConvertColor(RGB, width=1280, height=576):
703 | if RGB == 0x000000:
704 | return '000000'
705 | elif RGB == 0xffffff:
706 | return 'FFFFFF'
707 | R = (RGB >> 16) & 0xff
708 | G = (RGB >> 8) & 0xff
709 | B = RGB & 0xff
710 | if width < 1280 and height < 576:
711 | return '%02X%02X%02X' % (B, G, R)
712 | else: # VobSub always uses BT.601 colorspace, convert to BT.709
713 | ClipByte = lambda x: 255 if x > 255 else 0 if x < 0 else round(x)
714 | return '%02X%02X%02X' % (
715 | ClipByte(R * 0.00956384088080656 + G * 0.03217254540203729 + B * 0.95826361371715607),
716 | ClipByte(R * -0.10493933142075390 + G * 1.17231478191855154 + B * -0.06737545049779757),
717 | ClipByte(R * 0.91348912373987645 + G * 0.07858536372532510 + B * 0.00792551253479842)
718 | )
719 |
720 |
721 | def ConvertType2(row, height, bottomReserved):
722 | return height - bottomReserved - row
723 |
724 |
725 | def ConvertToFile(filename_or_file, *args, **kwargs):
726 | if isinstance(filename_or_file, bytes):
727 | filename_or_file = str(bytes(filename_or_file).decode('utf-8', 'replace'))
728 | if isinstance(filename_or_file, str):
729 | return open(filename_or_file, *args, **kwargs)
730 | else:
731 | return filename_or_file
732 |
733 |
734 | def FilterBadChars(f):
735 | s = f.read()
736 | s = re.sub('[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f]', '\ufffd', s)
737 | return io.StringIO(s)
738 |
739 |
740 | class safe_list(list):
741 |
742 | def get(self, index, default=None):
743 | try:
744 | return self[index]
745 | except IndexError:
746 | return default
747 |
748 |
749 | def export(func):
750 | global __all__
751 | try:
752 | __all__.append(func.__name__)
753 | except NameError:
754 | __all__ = [func.__name__]
755 | return func
756 |
757 |
758 | @export
759 | def Danmaku2ASS(input_files, input_format, output_file, stage_width, stage_height, reserve_blank=0, font_face=_('(FONT) sans-serif')[7:], font_size=25.0, text_opacity=1.0, duration_marquee=5.0, duration_still=5.0, comment_filter=None, comment_filters_file=None, is_reduce_comments=False, progress_callback=None):
760 | comment_filters = [comment_filter]
761 | if comment_filters_file:
762 | with open(comment_filters_file, 'r') as f:
763 | d = f.readlines()
764 | comment_filters.extend([i.strip() for i in d])
765 | filters_regex = []
766 | for comment_filter in comment_filters:
767 | try:
768 | if comment_filter:
769 | filters_regex.append(re.compile(comment_filter))
770 | except:
771 | raise ValueError(_('Invalid regular expression: %s') % comment_filter)
772 | fo = None
773 | comments = ReadComments(input_files, input_format, font_size)
774 | try:
775 | if output_file:
776 | fo = ConvertToFile(output_file, 'w', encoding='utf-8-sig', errors='replace', newline='\r\n')
777 | else:
778 | fo = sys.stdout
779 | ProcessComments(comments, fo, stage_width, stage_height, reserve_blank, font_face, font_size, text_opacity, duration_marquee, duration_still, filters_regex, is_reduce_comments, progress_callback)
780 | finally:
781 | if output_file and fo != output_file:
782 | fo.close()
783 |
784 |
785 | @export
786 | def ReadComments(input_files, input_format, font_size=25.0, progress_callback=None):
787 | if isinstance(input_files, bytes):
788 | input_files = str(bytes(input_files).decode('utf-8', 'replace'))
789 | if isinstance(input_files, str):
790 | input_files = [input_files]
791 | else:
792 | input_files = list(input_files)
793 | comments = []
794 | for idx, i in enumerate(input_files):
795 | if progress_callback:
796 | progress_callback(idx, len(input_files))
797 | with ConvertToFile(i, 'r', encoding='utf-8', errors='replace') as f:
798 | s = f.read()
799 | str_io = io.StringIO(s)
800 | if input_format == 'autodetect':
801 | CommentProcessor = GetCommentProcessor(str_io)
802 | if not CommentProcessor:
803 | raise ValueError(
804 | _('Failed to detect comment file format: %s') % i
805 | )
806 | else:
807 | CommentProcessor = CommentFormatMap.get(input_format)
808 | if not CommentProcessor:
809 | raise ValueError(
810 | _('Unknown comment file format: %s') % input_format
811 | )
812 | comments.extend(CommentProcessor(FilterBadChars(str_io), font_size))
813 | if progress_callback:
814 | progress_callback(len(input_files), len(input_files))
815 | comments.sort()
816 | return comments
817 |
818 |
819 | @export
820 | def GetCommentProcessor(input_file):
821 | return CommentFormatMap.get(ProbeCommentFormat(input_file))
822 |
823 |
824 | def main():
825 | logging.basicConfig(format='%(levelname)s: %(message)s')
826 | if len(sys.argv) == 1:
827 | sys.argv.append('--help')
828 | parser = argparse.ArgumentParser()
829 | parser.add_argument('-f', '--format', metavar=_('FORMAT'), help=_('Format of input file (autodetect|%s) [default: autodetect]') % '|'.join(i for i in CommentFormatMap), default='autodetect')
830 | parser.add_argument('-o', '--output', metavar=_('OUTPUT'), help=_('Output file'))
831 | parser.add_argument('-s', '--size', metavar=_('WIDTHxHEIGHT'), required=True, help=_('Stage size in pixels'))
832 | parser.add_argument('-fn', '--font', metavar=_('FONT'), help=_('Specify font face [default: %s]') % _('(FONT) sans-serif')[7:], default=_('(FONT) sans-serif')[7:])
833 | parser.add_argument('-fs', '--fontsize', metavar=_('SIZE'), help=(_('Default font size [default: %s]') % 25), type=float, default=25.0)
834 | parser.add_argument('-a', '--alpha', metavar=_('ALPHA'), help=_('Text opacity'), type=float, default=1.0)
835 | parser.add_argument('-dm', '--duration-marquee', metavar=_('SECONDS'), help=_('Duration of scrolling comment display [default: %s]') % 5, type=float, default=5.0)
836 | parser.add_argument('-ds', '--duration-still', metavar=_('SECONDS'), help=_('Duration of still comment display [default: %s]') % 5, type=float, default=5.0)
837 | parser.add_argument('-fl', '--filter', help=_('Regular expression to filter comments'))
838 | parser.add_argument('-flf', '--filter-file', help=_('Regular expressions from file (one line one regex) to filter comments'))
839 | parser.add_argument('-p', '--protect', metavar=_('HEIGHT'), help=_('Reserve blank on the bottom of the stage'), type=int, default=0)
840 | parser.add_argument('-r', '--reduce', action='store_true', help=_('Reduce the amount of comments if stage is full'))
841 | parser.add_argument('file', metavar=_('FILE'), nargs='+', help=_('Comment file to be processed'))
842 | args = parser.parse_args()
843 | try:
844 | width, height = str(args.size).split('x', 1)
845 | width = int(width)
846 | height = int(height)
847 | except ValueError:
848 | raise ValueError(_('Invalid stage size: %r') % args.size)
849 | Danmaku2ASS(args.file, args.format, args.output, width, height, args.protect, args.font, args.fontsize, args.alpha, args.duration_marquee, args.duration_still, args.filter, args.filter_file, args.reduce)
850 |
851 |
852 | if __name__ == '__main__':
853 | main()
--------------------------------------------------------------------------------
/http_server.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from http import server as BaseHTTPServer
3 | import re
4 | import socket
5 | import os
6 | import xbmc
7 | import xbmcvfs
8 |
9 | try:
10 | xbmc.translatePath = xbmcvfs.translatePath
11 | except AttributeError:
12 | pass
13 |
14 | class BilibiliRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
15 | def __init__(self, request, client_address, server):
16 | self.addon_id = 'plugin.video.bili'
17 | self.chunk_size = 1024 * 64
18 | try:
19 | self.base_path = xbmc.translatePath('special://temp/%s' % self.addon_id).decode('utf-8')
20 | except AttributeError:
21 | self.base_path = xbmc.translatePath('special://temp/%s' % self.addon_id)
22 | BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request, client_address, server)
23 |
24 | def do_GET(self):
25 | stripped_path = self.path.rstrip('/')
26 | if self.path.endswith('.mpd'):
27 | file_path = os.path.join(self.base_path, self.path.strip('/').strip('\\'))
28 | file_chunk = True
29 | try:
30 | with open(file_path, 'rb') as f:
31 | self.send_response(200)
32 | self.send_header('Content-Type', 'application/xml+dash')
33 | self.send_header('Content-Length', os.path.getsize(file_path))
34 | self.end_headers()
35 | while file_chunk:
36 | file_chunk = f.read(self.chunk_size)
37 | if file_chunk:
38 | self.wfile.write(file_chunk)
39 | except IOError:
40 | response = 'File Not Found: |{proxy_path}| -> |{file_path}|'.format(proxy_path=self.path, file_path=file_path.encode('utf-8'))
41 | self.send_error(404, response)
42 |
43 |
44 | def do_HEAD(self):
45 | if self.path.endswith('.mpd'):
46 | file_path = os.path.join(self.base_path, self.path.strip('/').strip('\\'))
47 | if not os.path.isfile(file_path):
48 | response = 'File Not Found: |{proxy_path}| -> |{file_path}|'.format(proxy_path=self.path, file_path=file_path.encode('utf-8'))
49 | self.send_error(404, response)
50 | else:
51 | self.send_response(200)
52 | self.send_header('Content-Type', 'application/xml+dash')
53 | self.send_header('Content-Length', os.path.getsize(file_path))
54 | self.end_headers()
55 | else:
56 | self.send_error(501)
57 |
58 |
59 | def get_http_server(address=None, port=None):
60 | address = address if re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', address) else '0.0.0.0'
61 | port = int(port) if port else 54321
62 | try:
63 | server = BaseHTTPServer.HTTPServer((address, port), BilibiliRequestHandler)
64 | return server
65 | except socket.error as e:
66 | return None
67 |
--------------------------------------------------------------------------------
/icon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codecook-start/plugin/c441c69f785bab752208f12fa6347b434d55adb7/icon.jpg
--------------------------------------------------------------------------------
/monitor.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 | import shutil
4 | import threading
5 | import xbmc
6 | import xbmcvfs
7 | import xbmcaddon
8 |
9 | from http_server import get_http_server
10 |
11 | try:
12 | xbmc.translatePath = xbmcvfs.translatePath
13 | except AttributeError:
14 | pass
15 |
16 |
17 | class BilibiliMonitor(xbmc.Monitor):
18 | def __init__(self, *args, **kwargs):
19 | self.addon_id = 'plugin.video.bili'
20 | self._httpd_port = int(xbmcaddon.Addon(self.addon_id).getSetting('server_port'))
21 | self._httpd_address = '0.0.0.0'
22 | self.httpd = None
23 | self.httpd_thread = None
24 |
25 | self.start_httpd()
26 |
27 | def start_httpd(self):
28 | if not self.httpd:
29 | self.httpd = get_http_server(address=self._httpd_address, port=self._httpd_port)
30 | if self.httpd:
31 | self.httpd_thread = threading.Thread(target=self.httpd.serve_forever)
32 | self.httpd_thread.daemon = True
33 | self.httpd_thread.start()
34 |
35 | def shutdown_httpd(self):
36 | if self.httpd:
37 | self.httpd.shutdown()
38 | self.httpd.socket.close()
39 | self.httpd_thread.join()
40 | self.httpd_thread = None
41 | self.httpd = None
42 |
43 | def restart_httpd(self):
44 | self.shutdown_httpd()
45 | self.start_httpd()
46 |
47 | def remove_temp_dir(self):
48 | try:
49 | path = xbmc.translatePath('special://temp/%s' % self.addon_id).decode('utf-8')
50 | except AttributeError:
51 | path = xbmc.translatePath('special://temp/%s' % self.addon_id)
52 |
53 | if os.path.isdir(path):
54 | try:
55 | xbmcvfs.rmdir(path, force=True)
56 | except:
57 | pass
58 | if os.path.isdir(path):
59 | try:
60 | shutil.rmtree(path)
61 | except:
62 | pass
63 |
64 | if os.path.isdir(path):
65 | return False
66 | else:
67 | return True
--------------------------------------------------------------------------------
/public/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codecook-start/plugin/c441c69f785bab752208f12fa6347b434d55adb7/public/home.png
--------------------------------------------------------------------------------
/public/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codecook-start/plugin/c441c69f785bab752208f12fa6347b434d55adb7/public/settings.png
--------------------------------------------------------------------------------
/public/video.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codecook-start/plugin/c441c69f785bab752208f12fa6347b434d55adb7/public/video.png
--------------------------------------------------------------------------------
/resources/language/resource.language.en_gb/strings.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 |
4 | ################################
5 |
6 | msgctxt "#30001"
7 | msgid "General Settings"
8 | msgstr ""
9 |
10 | msgctxt "#30002"
11 | msgid "Login"
12 | msgstr ""
13 |
14 | msgctxt "#30003"
15 | msgid "Cookie"
16 | msgstr ""
17 |
18 | msgctxt "#30004"
19 | msgid "Danmaku Settings"
20 | msgstr ""
21 |
22 | msgctxt "#30006"
23 | msgid "QR Code Login"
24 | msgstr ""
25 |
26 | msgctxt "#30007"
27 | msgid "Cookie Login"
28 | msgstr ""
29 |
30 | msgctxt "#30008"
31 | msgid "Check Login Status"
32 | msgstr ""
33 |
34 | msgctxt "#30009"
35 | msgid "Logout"
36 | msgstr ""
37 |
38 | msgctxt "#30005"
39 | msgid "Enable Danmaku"
40 | msgstr ""
41 |
42 | msgctxt "#30010"
43 | msgid "Dash Settings"
44 | msgstr ""
45 |
46 | msgctxt "#30011"
47 | msgid "Enable Dash"
48 | msgstr ""
49 |
50 | msgctxt "#30012"
51 | msgid "Server Port"
52 | msgstr ""
53 |
54 | msgctxt "#30016"
55 | msgid "Font Size"
56 | msgstr ""
57 |
58 | msgctxt "#30017"
59 | msgid "The danmaku style will only take effect after clearing the cache files or restarting Kodi for videos that have already been played."
60 | msgstr ""
61 |
62 | msgctxt "#30018"
63 | msgid "Opacity"
64 | msgstr ""
65 |
66 | msgctxt "#30019"
67 | msgid "Danmaku Stay Time"
68 | msgstr ""
69 |
70 | msgctxt "#30020"
71 | msgid "Video Resolution"
72 | msgstr ""
73 |
74 | msgctxt "#30021"
75 | msgid "Video Resolution"
76 | msgstr ""
77 |
78 | msgctxt "#30022"
79 | msgid "8K Ultra HD"
80 | msgstr ""
81 |
82 | msgctxt "#30023"
83 | msgid "Dolby Vision"
84 | msgstr ""
85 |
86 | msgctxt "#30024"
87 | msgid "HDR True Color"
88 | msgstr ""
89 |
90 | msgctxt "#30025"
91 | msgid "4K Ultra HD"
92 | msgstr ""
93 |
94 | msgctxt "#30026"
95 | msgid "1080P60 High Frame Rate"
96 | msgstr ""
97 |
98 | msgctxt "#30027"
99 | msgid "1080P+ High Bitrate"
100 | msgstr ""
101 |
102 | msgctxt "#30028"
103 | msgid "1080P HD"
104 | msgstr ""
105 |
106 | msgctxt "#30029"
107 | msgid "720P60 High Frame Rate"
108 | msgstr ""
109 |
110 | msgctxt "#30030"
111 | msgid "720P"
112 | msgstr ""
113 |
114 | msgctxt "#30031"
115 | msgid "480P"
116 | msgstr ""
117 |
118 | msgctxt "#30032"
119 | msgid "360P"
120 | msgstr ""
121 |
122 | msgctxt "#30033"
123 | msgid "240P"
124 | msgstr ""
125 |
126 | msgctxt "#30035"
127 | msgid "Video Encoding"
128 | msgstr ""
129 |
130 | msgctxt "#30036"
131 | msgid "AV1"
132 | msgstr ""
133 |
134 | msgctxt "#30037"
135 | msgid "HEVC"
136 | msgstr ""
137 |
138 | msgctxt "#30038"
139 | msgid "AVC"
140 | msgstr ""
141 |
142 | msgctxt "#30040"
143 | msgid "Live Resolution"
144 | msgstr ""
145 |
146 | msgctxt "#30041"
147 | msgid "Live Resolution"
148 | msgstr ""
149 |
150 | msgctxt "#30042"
151 | msgid "Original "
152 | msgstr ""
153 |
154 | msgctxt "#30043"
155 | msgid "Bluray"
156 | msgstr ""
157 |
158 | msgctxt "#30044"
159 | msgid "Ultra HD"
160 | msgstr ""
161 |
162 | msgctxt "#30045"
163 | msgid "HD"
164 | msgstr ""
165 |
166 | msgctxt "#30046"
167 | msgid "Smooth"
168 | msgstr ""
169 |
170 | msgctxt "#30047"
171 | msgid "Live Video Encoding"
172 | msgstr ""
173 |
174 | msgctxt "#30048"
175 | msgid "HEVC"
176 | msgstr ""
177 |
178 | msgctxt "#30049"
179 | msgid "AVC"
180 | msgstr ""
181 |
182 | msgctxt "#30050"
183 | msgid "Other"
184 | msgstr ""
185 |
186 | msgctxt "#30051"
187 | msgid "Upload playback history"
188 | msgstr ""
189 |
190 | msgctxt "#30052"
191 | msgid "When enabled, you can see Kodi's playback history in the browsing history on Bilibili."
192 | msgstr ""
193 |
194 | msgctxt "#30053"
195 | msgid "Remove Cache Files"
196 | msgstr ""
197 |
198 | msgctxt "#30054"
199 | msgid "Remove danmaku files, QR code images and MPD format DASH files."
200 | msgstr ""
201 |
202 | msgctxt "#30055"
203 | msgid "Network Request Cache"
204 | msgstr ""
205 |
206 | msgctxt "#30056"
207 | msgid " When enabled, the software will cache network requests for a duration of 1 minute."
208 | msgstr ""
209 |
210 | msgctxt "#30100"
211 | msgid "Function Settings"
212 | msgstr ""
213 |
214 | msgctxt "#30101"
215 | msgid "Homepage"
216 | msgstr ""
217 |
218 | msgctxt "#30102"
219 | msgid "Video Categories"
220 | msgstr ""
221 |
222 | msgctxt "#30103"
223 | msgid "Rankings"
224 | msgstr ""
225 |
226 | msgctxt "#30104"
227 | msgid "Live Streaming Categories"
228 | msgstr ""
229 |
230 | msgctxt "#30105"
231 | msgid "Live Streams I Follow"
232 | msgstr ""
233 |
234 | msgctxt "#30106"
235 | msgid "My Collections"
236 | msgstr ""
237 |
238 | msgctxt "#30107"
239 | msgid "Updates"
240 | msgstr ""
241 |
242 | msgctxt "#30108"
243 | msgid "Following List"
244 | msgstr ""
245 |
246 | msgctxt "#30109"
247 | msgid "Followers List"
248 | msgstr ""
249 |
250 | msgctxt "#30110"
251 | msgid "Watch Later"
252 | msgstr ""
253 |
254 | msgctxt "#30111"
255 | msgid "History"
256 | msgstr ""
257 |
258 | msgctxt "#30112"
259 | msgid "Uploaded Videos"
260 | msgstr ""
261 |
262 | msgctxt "#30113"
263 | msgid "Search"
264 | msgstr ""
265 |
266 | msgctxt "#30114"
267 | msgid "Must-Watch Weekly"
268 | msgstr ""
269 |
270 | msgctxt "#30115"
271 | msgid "Must-Watch"
272 | msgstr ""
273 |
274 | msgctxt "#30116"
275 | msgid "Open Settings"
276 | msgstr ""
277 |
278 | msgctxt "#30117"
279 | msgid "My"
280 | msgstr ""
281 |
282 | msgctxt "#30130"
283 | msgid "Danmaku Display Area"
284 | msgstr ""
285 |
286 | msgctxt "#30131"
287 | msgid "Install the inputstream.adaptive plugin"
288 | msgstr ""
289 |
290 | msgctxt "#30132"
291 | msgid "This feature requires the inputstream.adaptive plugin. When disabled, the maximum video resolution is limited to 720P."
292 | msgstr ""
293 |
--------------------------------------------------------------------------------
/resources/language/resource.language.zh_cn/strings.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 |
4 | ################################
5 |
6 | msgctxt "#30001"
7 | msgid "General Settings"
8 | msgstr "常规设置"
9 |
10 | msgctxt "#30002"
11 | msgid "Login"
12 | msgstr "登录"
13 |
14 | msgctxt "#30003"
15 | msgid "Cookie"
16 | msgstr "Cookie"
17 |
18 | msgctxt "#30004"
19 | msgid "Danmaku Settings"
20 | msgstr "弹幕设置"
21 |
22 | msgctxt "#30006"
23 | msgid "QR Code Login"
24 | msgstr "二维码登录"
25 |
26 | msgctxt "#30007"
27 | msgid "Cookie Login"
28 | msgstr "Cookie 登录"
29 |
30 | msgctxt "#30008"
31 | msgid "Check Login Status"
32 | msgstr "检查登录状态"
33 |
34 | msgctxt "#30009"
35 | msgid "Logout"
36 | msgstr "退出登录"
37 |
38 | msgctxt "#30005"
39 | msgid "Enable Danmaku"
40 | msgstr "开启弹幕"
41 |
42 | msgctxt "#30010"
43 | msgid "Dash Settings"
44 | msgstr "Dash 设置"
45 |
46 | msgctxt "#30011"
47 | msgid "Enable Dash"
48 | msgstr "使用 Dash"
49 |
50 | msgctxt "#30012"
51 | msgid "Server Port"
52 | msgstr "服务器端口"
53 |
54 | msgctxt "#30016"
55 | msgid "Font Size"
56 | msgstr "字体大小"
57 |
58 | msgctxt "#30017"
59 | msgid "The danmaku style will only take effect after clearing the cache files or restarting Kodi for videos that have already been played."
60 | msgstr "已经播放过的视频需要清除缓存文件或重启 Kodi 后弹幕样式才能生效"
61 |
62 | msgctxt "#30018"
63 | msgid "Opacity"
64 | msgstr "不透明度"
65 |
66 | msgctxt "#30019"
67 | msgid "Danmaku Stay Time"
68 | msgstr "弹幕停留时间"
69 |
70 | msgctxt "#30020"
71 | msgid "Video Resolution"
72 | msgstr "视频分辨率"
73 |
74 | msgctxt "#30021"
75 | msgid "Video Resolution"
76 | msgstr "视频分辨率"
77 |
78 | msgctxt "#30022"
79 | msgid "8K Ultra HD"
80 | msgstr "8K 超高清"
81 |
82 | msgctxt "#30023"
83 | msgid "Dolby Vision"
84 | msgstr "杜比视界"
85 |
86 | msgctxt "#30024"
87 | msgid "HDR True Color"
88 | msgstr "HDR 真彩色"
89 |
90 | msgctxt "#30025"
91 | msgid "4K Ultra HD"
92 | msgstr "4K 超清"
93 |
94 | msgctxt "#30026"
95 | msgid "1080P60 High Frame Rate"
96 | msgstr "1080P60 高帧率"
97 |
98 | msgctxt "#30027"
99 | msgid "1080P+ High Bitrate"
100 | msgstr "1080P+ 高码率"
101 |
102 | msgctxt "#30028"
103 | msgid "1080P HD"
104 | msgstr "1080P 高清"
105 |
106 | msgctxt "#30029"
107 | msgid "720P60 High Frame Rate"
108 | msgstr "720P60 高帧率"
109 |
110 | msgctxt "#30030"
111 | msgid "720P HD"
112 | msgstr "720P 高清"
113 |
114 | msgctxt "#30031"
115 | msgid "480P"
116 | msgstr "480P 清晰"
117 |
118 | msgctxt "#30032"
119 | msgid "360P"
120 | msgstr "360P 流畅"
121 |
122 | msgctxt "#30033"
123 | msgid "240P"
124 | msgstr "240P 极速"
125 |
126 | msgctxt "#30035"
127 | msgid "Video Encoding"
128 | msgstr "视频编码"
129 |
130 | msgctxt "#30036"
131 | msgid "AV1"
132 | msgstr "AV1"
133 |
134 | msgctxt "#30037"
135 | msgid "HEVC"
136 | msgstr "HEVC"
137 |
138 | msgctxt "#30038"
139 | msgid "AVC"
140 | msgstr "AVC"
141 |
142 | msgctxt "#30040"
143 | msgid "Live Resolution"
144 | msgstr "直播清晰度"
145 |
146 | msgctxt "#30041"
147 | msgid "Live Resolution"
148 | msgstr "直播清晰度"
149 |
150 | msgctxt "#30042"
151 | msgid "Original "
152 | msgstr "原画"
153 |
154 | msgctxt "#30043"
155 | msgid "Bluray"
156 | msgstr "蓝光"
157 |
158 | msgctxt "#30044"
159 | msgid "Ultra HD"
160 | msgstr "超清"
161 |
162 | msgctxt "#30045"
163 | msgid "HD"
164 | msgstr "高清"
165 |
166 | msgctxt "#30046"
167 | msgid "Smooth"
168 | msgstr "流畅"
169 |
170 | msgctxt "#30047"
171 | msgid "Live Video Encoding"
172 | msgstr "直播视频编码"
173 |
174 | msgctxt "#30048"
175 | msgid "HEVC"
176 | msgstr "HEVC"
177 |
178 | msgctxt "#30049"
179 | msgid "AVC"
180 | msgstr "AVC"
181 |
182 | msgctxt "#30050"
183 | msgid "Other"
184 | msgstr "其他"
185 |
186 | msgctxt "#30051"
187 | msgid "Upload playback history"
188 | msgstr "上报播放记录"
189 |
190 | msgctxt "#30052"
191 | msgid "When enabled, you can see Kodi's playback history in the browsing history on Bilibili."
192 | msgstr "开启后,将能够在哔哩哔哩的历史记录中看到 Kodi 的播放记录"
193 |
194 | msgctxt "#30053"
195 | msgid "Remove Cache Files"
196 | msgstr "清除缓存文件"
197 |
198 | msgctxt "#30054"
199 | msgid "Remove danmaku files, QR code images and MPD format DASH files."
200 | msgstr "清除弹幕文件、二维码图片、MPD 格式的 DASH 文件"
201 |
202 | msgctxt "#30055"
203 | msgid "Network Request Cache"
204 | msgstr "网络请求缓存"
205 |
206 | msgctxt "#30056"
207 | msgid " When enabled, the software will cache network requests for a duration of 1 minute."
208 | msgstr "开启后,软件将对网络请求进行缓存,缓存时间为1分钟"
209 |
210 | msgctxt "#30100"
211 | msgid "Function Settings"
212 | msgstr "功能设置"
213 |
214 | msgctxt "#30101"
215 | msgid "Homepage"
216 | msgstr "首页推荐"
217 |
218 | msgctxt "#30102"
219 | msgid "Video Categories"
220 | msgstr "视频分区"
221 |
222 | msgctxt "#30103"
223 | msgid "Rankings"
224 | msgstr "排行榜"
225 |
226 | msgctxt "#30104"
227 | msgid "Live Streaming Categories"
228 | msgstr "直播分区"
229 |
230 | msgctxt "#30105"
231 | msgid "Live Streams I Follow"
232 | msgstr "我关注的直播"
233 |
234 | msgctxt "#30106"
235 | msgid "My Collections"
236 | msgstr "我的收藏"
237 |
238 | msgctxt "#30107"
239 | msgid "Updates"
240 | msgstr "动态"
241 |
242 | msgctxt "#30108"
243 | msgid "Following List"
244 | msgstr "关注列表"
245 |
246 | msgctxt "#30109"
247 | msgid "Followers List"
248 | msgstr "粉丝列表"
249 |
250 | msgctxt "#30110"
251 | msgid "Watch Later"
252 | msgstr "稍后再看"
253 |
254 | msgctxt "#30111"
255 | msgid "History"
256 | msgstr "历史记录"
257 |
258 | msgctxt "#30112"
259 | msgid "Uploaded Videos"
260 | msgstr "投稿的视频"
261 |
262 | msgctxt "#30113"
263 | msgid "Search"
264 | msgstr "搜索"
265 |
266 | msgctxt "#30114"
267 | msgid "Must-Watch Weekly"
268 | msgstr "每周必看"
269 |
270 | msgctxt "#30115"
271 | msgid "Must-Watch"
272 | msgstr "入站必刷"
273 |
274 | msgctxt "#30116"
275 | msgid "Open Settings"
276 | msgstr "打开设置"
277 |
278 | msgctxt "#30117"
279 | msgid "My"
280 | msgstr "我的"
281 |
282 | msgctxt "#30130"
283 | msgid "Danmaku Display Area"
284 | msgstr "弹幕显示区域"
285 |
286 | msgctxt "#30131"
287 | msgid "Install the inputstream.adaptive plugin"
288 | msgstr "安装 inputstream.adaptive 插件"
289 |
290 | msgctxt "#30132"
291 | msgid "This feature requires the inputstream.adaptive plugin. When disabled, the maximum video resolution is limited to 720P."
292 | msgstr "此功能需要安装 inputstream.adaptive 插件。禁用后,视频最高分辨率将限制为 720P"
293 |
--------------------------------------------------------------------------------
/resources/settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 0
8 |
9 |
10 | true
11 |
12 |
13 | RunPlugin(plugin://plugin.video.bili/qrcode_login/)
14 |
15 |
16 |
17 | 0
18 |
19 |
20 | true
21 |
22 |
23 | RunPlugin(plugin://plugin.video.bili/cookie_login/)
24 |
25 |
26 |
27 | 0
28 |
29 |
30 | true
31 |
32 |
33 | RunPlugin(plugin://plugin.video.bili/check_login/)
34 |
35 |
36 |
37 | 0
38 |
39 |
40 | true
41 |
42 |
43 | RunPlugin(plugin://plugin.video.bili/logout/)
44 |
45 |
46 |
47 |
48 |
49 | 0
50 | true
51 |
52 |
53 |
54 | 0
55 | 25
56 |
57 | 5
58 | 1
59 | 100
60 |
61 |
62 | false
63 |
64 |
65 |
66 | 0
67 | 1
68 |
69 | 0.0
70 | 0.01
71 | 1.0
72 |
73 |
74 | false
75 |
76 |
77 |
78 | 0
79 | 5
80 |
81 | 1
82 | 1
83 | 20
84 |
85 |
86 | false
87 |
88 |
89 |
90 | 0
91 | 1
92 |
93 | 0.0
94 | 0.01
95 | 1.0
96 |
97 |
98 | false
99 |
100 |
101 |
102 |
103 |
104 | 0
105 | true
106 |
107 |
108 |
109 | 0
110 |
111 |
112 | true
113 |
114 |
115 | InstallAddon(inputstream.adaptive)
116 |
117 |
118 |
119 | !System.HasAddon(inputstream.adaptive)
120 |
121 |
122 |
123 |
124 | 0
125 | 54321
126 |
127 | 30012
128 |
129 |
130 |
131 |
132 |
133 | 0
134 | 116
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 | 30021
153 |
154 |
155 |
156 | 0
157 | 7
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | 30035
167 |
168 |
169 |
170 | true
171 |
172 |
173 |
174 |
175 |
176 |
177 | 0
178 | 10000
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 | 30041
190 |
191 |
192 |
193 | 0
194 | 7
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 | 30047
203 |
204 |
205 |
206 |
207 |
208 | 0
209 | true
210 |
211 |
212 |
213 | 0
214 |
215 |
216 | true
217 |
218 |
219 | RunPlugin(plugin://plugin.video.bili/remove_cache_files/)
220 |
221 |
222 |
223 | 0
224 | true
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 | 0
233 | true
234 |
235 |
236 |
237 | 0
238 | true
239 |
240 |
241 |
242 | 0
243 | true
244 |
245 |
246 |
247 | 0
248 | true
249 |
250 |
251 |
252 | 0
253 | true
254 |
255 |
256 |
257 | 0
258 | true
259 |
260 |
261 |
262 | 0
263 | true
264 |
265 |
266 |
267 | 0
268 | true
269 |
270 |
271 |
272 | 0
273 | false
274 |
275 |
276 |
277 | 0
278 | true
279 |
280 |
281 |
282 | 0
283 | true
284 |
285 |
286 |
287 | 0
288 | true
289 |
290 |
291 |
292 | 0
293 | true
294 |
295 |
296 |
297 | 0
298 | true
299 |
300 |
301 |
302 | 0
303 | true
304 |
305 |
306 |
307 | 0
308 | true
309 |
310 |
311 |
312 | 0
313 | true
314 |
315 |
316 |
317 |
318 |
319 |
320 |
--------------------------------------------------------------------------------
/service.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from monitor import BilibiliMonitor
3 |
4 |
5 | def run():
6 | sleep_time = 10
7 | monitor = BilibiliMonitor()
8 |
9 | monitor.remove_temp_dir()
10 |
11 | while not monitor.abortRequested():
12 | if monitor.waitForAbort(sleep_time):
13 | break
14 |
15 | if monitor.httpd:
16 | monitor.shutdown_httpd()
17 |
18 |
19 | if __name__ == '__main__':
20 | run()
21 |
--------------------------------------------------------------------------------