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