]>
Commit | Line | Data |
---|---|---|
3bc2ddcc JMF |
1 | import os |
2 | import re | |
3 | import subprocess | |
3bc2ddcc JMF |
4 | import time |
5 | ||
6 | from .common import FileDownloader | |
1cc79574 | 7 | from ..compat import compat_str |
3bc2ddcc | 8 | from ..utils import ( |
f8271158 | 9 | Popen, |
7798fad5 | 10 | check_executable, |
9e105a85 | 11 | encodeArgument, |
f8271158 | 12 | encodeFilename, |
4c83c967 | 13 | get_exe_version, |
3bc2ddcc JMF |
14 | ) |
15 | ||
16 | ||
4c83c967 PH |
17 | def rtmpdump_version(): |
18 | return get_exe_version( | |
19 | 'rtmpdump', ['--help'], r'(?i)RTMPDump\s*v?([0-9a-zA-Z._-]+)') | |
20 | ||
21 | ||
3bc2ddcc JMF |
22 | class RtmpFD(FileDownloader): |
23 | def real_download(self, filename, info_dict): | |
24 | def run_rtmpdump(args): | |
25 | start = time.time() | |
9b0b6275 S |
26 | resume_percent = None |
27 | resume_downloaded_data_len = None | |
d3c93ec2 | 28 | proc = Popen(args, stderr=subprocess.PIPE) |
3bc2ddcc | 29 | cursor_in_new_line = True |
9b0b6275 S |
30 | proc_stderr_closed = False |
31 | try: | |
ddd8486a S |
32 | while not proc_stderr_closed: |
33 | # read line from stderr | |
34 | line = '' | |
35 | while True: | |
36 | char = proc.stderr.read(1) | |
37 | if not char: | |
38 | proc_stderr_closed = True | |
39 | break | |
40 | if char in [b'\r', b'\n']: | |
41 | break | |
42 | line += char.decode('ascii', 'replace') | |
43 | if not line: | |
44 | # proc_stderr_closed is True | |
45 | continue | |
46 | mobj = re.search(r'([0-9]+\.[0-9]{3}) kB / [0-9]+\.[0-9]{2} sec \(([0-9]{1,2}\.[0-9])%\)', line) | |
3bc2ddcc | 47 | if mobj: |
2514d263 | 48 | downloaded_data_len = int(float(mobj.group(1)) * 1024) |
ddd8486a S |
49 | percent = float(mobj.group(2)) |
50 | if not resume_percent: | |
51 | resume_percent = percent | |
52 | resume_downloaded_data_len = downloaded_data_len | |
3bc2ddcc | 53 | time_now = time.time() |
ddd8486a S |
54 | eta = self.calc_eta(start, time_now, 100 - resume_percent, percent - resume_percent) |
55 | speed = self.calc_speed(start, time_now, downloaded_data_len - resume_downloaded_data_len) | |
56 | data_len = None | |
57 | if percent > 0: | |
58 | data_len = int(downloaded_data_len * 100 / percent) | |
3bc2ddcc | 59 | self._hook_progress({ |
ddd8486a | 60 | 'status': 'downloading', |
3bc2ddcc | 61 | 'downloaded_bytes': downloaded_data_len, |
ddd8486a | 62 | 'total_bytes_estimate': data_len, |
3bc2ddcc JMF |
63 | 'tmpfilename': tmpfilename, |
64 | 'filename': filename, | |
ddd8486a | 65 | 'eta': eta, |
5cda4eda | 66 | 'elapsed': time_now - start, |
3bc2ddcc | 67 | 'speed': speed, |
3ba7740d | 68 | }, info_dict) |
5cda4eda | 69 | cursor_in_new_line = False |
ddd8486a S |
70 | else: |
71 | # no percent for live streams | |
72 | mobj = re.search(r'([0-9]+\.[0-9]{3}) kB / [0-9]+\.[0-9]{2} sec', line) | |
73 | if mobj: | |
74 | downloaded_data_len = int(float(mobj.group(1)) * 1024) | |
75 | time_now = time.time() | |
76 | speed = self.calc_speed(start, time_now, downloaded_data_len) | |
77 | self._hook_progress({ | |
78 | 'downloaded_bytes': downloaded_data_len, | |
79 | 'tmpfilename': tmpfilename, | |
80 | 'filename': filename, | |
81 | 'status': 'downloading', | |
82 | 'elapsed': time_now - start, | |
83 | 'speed': speed, | |
3ba7740d | 84 | }, info_dict) |
ddd8486a S |
85 | cursor_in_new_line = False |
86 | elif self.params.get('verbose', False): | |
87 | if not cursor_in_new_line: | |
88 | self.to_screen('') | |
89 | cursor_in_new_line = True | |
90 | self.to_screen('[rtmpdump] ' + line) | |
f5b1bca9 | 91 | if not cursor_in_new_line: |
92 | self.to_screen('') | |
93 | return proc.wait() | |
94 | except BaseException: # Including KeyboardInterrupt | |
95 | proc.kill() | |
ddd8486a | 96 | proc.wait() |
f5b1bca9 | 97 | raise |
3bc2ddcc JMF |
98 | |
99 | url = info_dict['url'] | |
d800609c S |
100 | player_url = info_dict.get('player_url') |
101 | page_url = info_dict.get('page_url') | |
102 | app = info_dict.get('app') | |
103 | play_path = info_dict.get('play_path') | |
104 | tc_url = info_dict.get('tc_url') | |
105 | flash_version = info_dict.get('flash_version') | |
3bc2ddcc | 106 | live = info_dict.get('rtmp_live', False) |
d800609c S |
107 | conn = info_dict.get('rtmp_conn') |
108 | protocol = info_dict.get('rtmp_protocol') | |
7bb3ceb4 | 109 | real_time = info_dict.get('rtmp_real_time', False) |
7906d199 | 110 | no_resume = info_dict.get('no_resume', False) |
f101079a | 111 | continue_dl = self.params.get('continuedl', True) |
3dee7826 | 112 | |
3bc2ddcc JMF |
113 | self.report_destination(filename) |
114 | tmpfilename = self.temp_name(filename) | |
115 | test = self.params.get('test', False) | |
116 | ||
117 | # Check for rtmpdump first | |
7798fad5 | 118 | if not check_executable('rtmpdump', ['-h']): |
beb4b92a | 119 | self.report_error('RTMP download detected but "rtmpdump" could not be run. Please install') |
3bc2ddcc JMF |
120 | return False |
121 | ||
122 | # Download using rtmpdump. rtmpdump returns exit code 2 when | |
17cc1534 | 123 | # the connection was interrupted and resuming appears to be |
3bc2ddcc | 124 | # possible. This is part of rtmpdump's normal usage, AFAIK. |
2a15a98a PH |
125 | basic_args = [ |
126 | 'rtmpdump', '--verbose', '-r', url, | |
9e105a85 | 127 | '-o', tmpfilename] |
3bc2ddcc JMF |
128 | if player_url is not None: |
129 | basic_args += ['--swfVfy', player_url] | |
130 | if page_url is not None: | |
131 | basic_args += ['--pageUrl', page_url] | |
082c6c86 S |
132 | if app is not None: |
133 | basic_args += ['--app', app] | |
3bc2ddcc JMF |
134 | if play_path is not None: |
135 | basic_args += ['--playpath', play_path] | |
136 | if tc_url is not None: | |
156fc83a | 137 | basic_args += ['--tcUrl', tc_url] |
3bc2ddcc JMF |
138 | if test: |
139 | basic_args += ['--stop', '1'] | |
082c6c86 S |
140 | if flash_version is not None: |
141 | basic_args += ['--flashVer', flash_version] | |
3bc2ddcc JMF |
142 | if live: |
143 | basic_args += ['--live'] | |
eb451334 S |
144 | if isinstance(conn, list): |
145 | for entry in conn: | |
146 | basic_args += ['--conn', entry] | |
147 | elif isinstance(conn, compat_str): | |
3bc2ddcc | 148 | basic_args += ['--conn', conn] |
087ca2cb JMF |
149 | if protocol is not None: |
150 | basic_args += ['--protocol', protocol] | |
0865f397 PH |
151 | if real_time: |
152 | basic_args += ['--realtime'] | |
3dee7826 PH |
153 | |
154 | args = basic_args | |
155 | if not no_resume and continue_dl and not live: | |
156 | args += ['--resume'] | |
157 | if not live and continue_dl: | |
158 | args += ['--skip', '1'] | |
3bc2ddcc | 159 | |
9e105a85 | 160 | args = [encodeArgument(a) for a in args] |
3bc2ddcc | 161 | |
9e105a85 | 162 | self._debug_cmd(args, exe='rtmpdump') |
3bc2ddcc | 163 | |
35241756 S |
164 | RD_SUCCESS = 0 |
165 | RD_FAILED = 1 | |
166 | RD_INCOMPLETE = 2 | |
52d6a9a6 | 167 | RD_NO_CONNECT = 3 |
35241756 | 168 | |
f16f4877 S |
169 | started = time.time() |
170 | ||
ddd8486a S |
171 | try: |
172 | retval = run_rtmpdump(args) | |
173 | except KeyboardInterrupt: | |
174 | if not info_dict.get('is_live'): | |
175 | raise | |
176 | retval = RD_SUCCESS | |
177 | self.to_screen('\n[rtmpdump] Interrupted by user') | |
3bc2ddcc | 178 | |
52d6a9a6 | 179 | if retval == RD_NO_CONNECT: |
8dec03ec | 180 | self.report_error('[rtmpdump] Could not connect to RTMP server.') |
52d6a9a6 S |
181 | return False |
182 | ||
40fcba5e | 183 | while retval in (RD_INCOMPLETE, RD_FAILED) and not test and not live: |
3bc2ddcc | 184 | prevsize = os.path.getsize(encodeFilename(tmpfilename)) |
f16f4877 | 185 | self.to_screen('[rtmpdump] Downloaded %s bytes' % prevsize) |
5f6a1245 | 186 | time.sleep(5.0) # This seems to be needed |
9e105a85 S |
187 | args = basic_args + ['--resume'] |
188 | if retval == RD_FAILED: | |
189 | args += ['--skip', '1'] | |
190 | args = [encodeArgument(a) for a in args] | |
191 | retval = run_rtmpdump(args) | |
3bc2ddcc | 192 | cursize = os.path.getsize(encodeFilename(tmpfilename)) |
35241756 | 193 | if prevsize == cursize and retval == RD_FAILED: |
3bc2ddcc | 194 | break |
7af808a5 | 195 | # Some rtmp streams seem abort after ~ 99.8%. Don't complain for those |
35241756 | 196 | if prevsize == cursize and retval == RD_INCOMPLETE and cursize > 1024: |
8dec03ec | 197 | self.to_screen('[rtmpdump] Could not download the whole video. This can happen for some advertisements.') |
35241756 | 198 | retval = RD_SUCCESS |
3bc2ddcc | 199 | break |
35241756 | 200 | if retval == RD_SUCCESS or (test and retval == RD_INCOMPLETE): |
3bc2ddcc | 201 | fsize = os.path.getsize(encodeFilename(tmpfilename)) |
f16f4877 | 202 | self.to_screen('[rtmpdump] Downloaded %s bytes' % fsize) |
3bc2ddcc JMF |
203 | self.try_rename(tmpfilename, filename) |
204 | self._hook_progress({ | |
205 | 'downloaded_bytes': fsize, | |
206 | 'total_bytes': fsize, | |
207 | 'filename': filename, | |
208 | 'status': 'finished', | |
f16f4877 | 209 | 'elapsed': time.time() - started, |
3ba7740d | 210 | }, info_dict) |
3bc2ddcc JMF |
211 | return True |
212 | else: | |
8dec03ec S |
213 | self.to_stderr('\n') |
214 | self.report_error('rtmpdump exited with code %d' % retval) | |
3bc2ddcc | 215 | return False |