]>
Commit | Line | Data |
---|---|---|
3bc2ddcc JMF |
1 | import os |
2 | import re | |
3 | import subprocess | |
4 | import sys | |
5 | import time | |
6 | ||
7 | from .common import FileDownloader | |
8 | from ..utils import ( | |
9 | encodeFilename, | |
10 | format_bytes, | |
11 | ) | |
12 | ||
13 | ||
14 | class RtmpFD(FileDownloader): | |
15 | def real_download(self, filename, info_dict): | |
16 | def run_rtmpdump(args): | |
17 | start = time.time() | |
18 | resume_percent = None | |
19 | resume_downloaded_data_len = None | |
20 | proc = subprocess.Popen(args, stderr=subprocess.PIPE) | |
21 | cursor_in_new_line = True | |
22 | proc_stderr_closed = False | |
23 | while not proc_stderr_closed: | |
24 | # read line from stderr | |
25 | line = u'' | |
26 | while True: | |
27 | char = proc.stderr.read(1) | |
28 | if not char: | |
29 | proc_stderr_closed = True | |
30 | break | |
31 | if char in [b'\r', b'\n']: | |
32 | break | |
33 | line += char.decode('ascii', 'replace') | |
34 | if not line: | |
35 | # proc_stderr_closed is True | |
36 | continue | |
37 | mobj = re.search(r'([0-9]+\.[0-9]{3}) kB / [0-9]+\.[0-9]{2} sec \(([0-9]{1,2}\.[0-9])%\)', line) | |
38 | if mobj: | |
39 | downloaded_data_len = int(float(mobj.group(1))*1024) | |
40 | percent = float(mobj.group(2)) | |
41 | if not resume_percent: | |
42 | resume_percent = percent | |
43 | resume_downloaded_data_len = downloaded_data_len | |
44 | eta = self.calc_eta(start, time.time(), 100-resume_percent, percent-resume_percent) | |
45 | speed = self.calc_speed(start, time.time(), downloaded_data_len-resume_downloaded_data_len) | |
46 | data_len = None | |
47 | if percent > 0: | |
48 | data_len = int(downloaded_data_len * 100 / percent) | |
49 | data_len_str = u'~' + format_bytes(data_len) | |
50 | self.report_progress(percent, data_len_str, speed, eta) | |
51 | cursor_in_new_line = False | |
52 | self._hook_progress({ | |
53 | 'downloaded_bytes': downloaded_data_len, | |
54 | 'total_bytes': data_len, | |
55 | 'tmpfilename': tmpfilename, | |
56 | 'filename': filename, | |
57 | 'status': 'downloading', | |
58 | 'eta': eta, | |
59 | 'speed': speed, | |
60 | }) | |
61 | else: | |
62 | # no percent for live streams | |
63 | mobj = re.search(r'([0-9]+\.[0-9]{3}) kB / [0-9]+\.[0-9]{2} sec', line) | |
64 | if mobj: | |
65 | downloaded_data_len = int(float(mobj.group(1))*1024) | |
66 | time_now = time.time() | |
67 | speed = self.calc_speed(start, time_now, downloaded_data_len) | |
68 | self.report_progress_live_stream(downloaded_data_len, speed, time_now - start) | |
69 | cursor_in_new_line = False | |
70 | self._hook_progress({ | |
71 | 'downloaded_bytes': downloaded_data_len, | |
72 | 'tmpfilename': tmpfilename, | |
73 | 'filename': filename, | |
74 | 'status': 'downloading', | |
75 | 'speed': speed, | |
76 | }) | |
77 | elif self.params.get('verbose', False): | |
78 | if not cursor_in_new_line: | |
79 | self.to_screen(u'') | |
80 | cursor_in_new_line = True | |
81 | self.to_screen(u'[rtmpdump] '+line) | |
82 | proc.wait() | |
83 | if not cursor_in_new_line: | |
84 | self.to_screen(u'') | |
85 | return proc.returncode | |
86 | ||
87 | url = info_dict['url'] | |
88 | player_url = info_dict.get('player_url', None) | |
89 | page_url = info_dict.get('page_url', None) | |
90 | play_path = info_dict.get('play_path', None) | |
91 | tc_url = info_dict.get('tc_url', None) | |
92 | live = info_dict.get('rtmp_live', False) | |
93 | conn = info_dict.get('rtmp_conn', None) | |
94 | ||
95 | self.report_destination(filename) | |
96 | tmpfilename = self.temp_name(filename) | |
97 | test = self.params.get('test', False) | |
98 | ||
99 | # Check for rtmpdump first | |
100 | try: | |
101 | subprocess.call(['rtmpdump', '-h'], stdout=(open(os.path.devnull, 'w')), stderr=subprocess.STDOUT) | |
102 | except (OSError, IOError): | |
103 | self.report_error(u'RTMP download detected but "rtmpdump" could not be run') | |
104 | return False | |
105 | ||
106 | # Download using rtmpdump. rtmpdump returns exit code 2 when | |
107 | # the connection was interrumpted and resuming appears to be | |
108 | # possible. This is part of rtmpdump's normal usage, AFAIK. | |
109 | basic_args = ['rtmpdump', '--verbose', '-r', url, '-o', tmpfilename] | |
110 | if player_url is not None: | |
111 | basic_args += ['--swfVfy', player_url] | |
112 | if page_url is not None: | |
113 | basic_args += ['--pageUrl', page_url] | |
114 | if play_path is not None: | |
115 | basic_args += ['--playpath', play_path] | |
116 | if tc_url is not None: | |
117 | basic_args += ['--tcUrl', url] | |
118 | if test: | |
119 | basic_args += ['--stop', '1'] | |
120 | if live: | |
121 | basic_args += ['--live'] | |
122 | if conn: | |
123 | basic_args += ['--conn', conn] | |
124 | args = basic_args + [[], ['--resume', '--skip', '1']][self.params.get('continuedl', False)] | |
125 | ||
126 | if sys.platform == 'win32' and sys.version_info < (3, 0): | |
127 | # Windows subprocess module does not actually support Unicode | |
128 | # on Python 2.x | |
129 | # See http://stackoverflow.com/a/9951851/35070 | |
130 | subprocess_encoding = sys.getfilesystemencoding() | |
131 | args = [a.encode(subprocess_encoding, 'ignore') for a in args] | |
132 | else: | |
133 | subprocess_encoding = None | |
134 | ||
135 | if self.params.get('verbose', False): | |
136 | if subprocess_encoding: | |
137 | str_args = [ | |
138 | a.decode(subprocess_encoding) if isinstance(a, bytes) else a | |
139 | for a in args] | |
140 | else: | |
141 | str_args = args | |
142 | try: | |
143 | import pipes | |
144 | shell_quote = lambda args: ' '.join(map(pipes.quote, str_args)) | |
145 | except ImportError: | |
146 | shell_quote = repr | |
147 | self.to_screen(u'[debug] rtmpdump command line: ' + shell_quote(str_args)) | |
148 | ||
149 | retval = run_rtmpdump(args) | |
150 | ||
151 | while (retval == 2 or retval == 1) and not test: | |
152 | prevsize = os.path.getsize(encodeFilename(tmpfilename)) | |
153 | self.to_screen(u'[rtmpdump] %s bytes' % prevsize) | |
154 | time.sleep(5.0) # This seems to be needed | |
155 | retval = run_rtmpdump(basic_args + ['-e'] + [[], ['-k', '1']][retval == 1]) | |
156 | cursize = os.path.getsize(encodeFilename(tmpfilename)) | |
157 | if prevsize == cursize and retval == 1: | |
158 | break | |
159 | # Some rtmp streams seem abort after ~ 99.8%. Don't complain for those | |
160 | if prevsize == cursize and retval == 2 and cursize > 1024: | |
161 | self.to_screen(u'[rtmpdump] Could not download the whole video. This can happen for some advertisements.') | |
162 | retval = 0 | |
163 | break | |
164 | if retval == 0 or (test and retval == 2): | |
165 | fsize = os.path.getsize(encodeFilename(tmpfilename)) | |
166 | self.to_screen(u'[rtmpdump] %s bytes' % fsize) | |
167 | self.try_rename(tmpfilename, filename) | |
168 | self._hook_progress({ | |
169 | 'downloaded_bytes': fsize, | |
170 | 'total_bytes': fsize, | |
171 | 'filename': filename, | |
172 | 'status': 'finished', | |
173 | }) | |
174 | return True | |
175 | else: | |
176 | self.to_stderr(u"\n") | |
177 | self.report_error(u'rtmpdump exited with code %d' % retval) | |
178 | return False |