X-Git-Url: https://git.ao2.it/smooth-dl.git/blobdiff_plain/00716d449533774cb4ab83bdae6d980848160484..ec7067e85004c582a0e77c4c55fd9ce24c4bfdfb:/smooth-dl.py diff --git a/smooth-dl.py b/smooth-dl.py index aa57c6e..39785a4 100755 --- a/smooth-dl.py +++ b/smooth-dl.py @@ -40,9 +40,9 @@ import tempfile from optparse import OptionParser from urlparse import urlparse, urlunparse -__description = "Download videos served using Smooth Streaming technology" -__version = "0.x" -__author_info = "Written by Antonio Ospite http://ao2.it" +__description__ = "Download videos served using Smooth Streaming technology" +__version__ = "0.x" +__author_info__ = "Written by Antonio Ospite http://ao2.it" def get_chunk_data(data): @@ -56,7 +56,7 @@ def get_chunk_data(data): # print len(data[data_start:]), \ # len(data[data_start:data_start + data_size]), data_size - assert(len(data[data_start:]) == data_size) + assert len(data[data_start:]) == data_size return data[data_start:data_start + data_size] @@ -64,7 +64,7 @@ def get_chunk_data(data): def hexstring_to_bytes(hex_string): res = "" for i in range(0, len(hex_string), 2): - res += chr(int(hex_string[i:i + 2], 16)) + res += chr(int(hex_string[i:i + 2], 16)) return res @@ -97,7 +97,7 @@ def download_file(src_url, dest_file, mode): try: response = urllib2.urlopen(src_url) data = response.read() - except urllib2.HTTPError as e: + except urllib2.HTTPError: sys.stderr.write("Error while dowloading URL: %s" % src_url) raise @@ -109,11 +109,8 @@ def download_file(src_url, dest_file, mode): return data -def get_manifest(url, dest_dir=tempfile.gettempdir()): - """Returns the manifest and the new URL if this is changed""" - - if os.path.exists(dest_dir) == False: - os.mkdir(dest_dir, 0755) +def get_manifest(url, dest_dir): + """Returns the manifest element and the base content URL""" # Remove the querystring if present manifest_url = urlunparse(urlparse(url)._replace(query='')) @@ -121,11 +118,11 @@ def get_manifest(url, dest_dir=tempfile.gettempdir()): if not manifest_url.lower().endswith(('/manifest', '.ismc', '.csm')): manifest_url += '/Manifest' - if manifest_url.startswith('http://'): + if os.path.exists(url): + local_manifest_path = url + else: local_manifest_path = os.path.join(dest_dir, 'Manifest') download_file(manifest_url, local_manifest_path, "w") - else: - local_manifest_path = url manifest = etree.parse(local_manifest_path) @@ -153,7 +150,6 @@ def print_manifest_info(manifest): for i, s in enumerate(streams): stream_type = s.attrib["Type"] - url = s.attrib["Url"] print "Stream: %s Type: %s" % (i, stream_type) @@ -200,25 +196,20 @@ def get_chunk_quality_string(stream, quality_level): return chunks_quality -def get_chunk_name_string(stream, chunk): - t = chunk.attrib["t"] +def get_chunk_name_string(stream, chunk_time): url = stream.attrib["Url"] - chunk_name = url.split('/')[1].replace("{start time}", t) + chunk_name = url.split('/')[1].replace("{start time}", str(chunk_time)) return chunk_name def download_chunks(base_url, manifest, stream_index, quality_level, dest_dir): - - if os.path.exists(dest_dir) == False: - os.mkdir(dest_dir, 0755) - stream = manifest.findall('.//StreamIndex')[stream_index] chunks_quality = get_chunk_quality_string(stream, quality_level) chunks_dest_dir = os.path.join(dest_dir, chunks_quality) - if os.path.exists(chunks_dest_dir) == False: + if not os.path.exists(chunks_dest_dir): os.mkdir(chunks_dest_dir, 0755) chunks = stream.findall("c") @@ -226,12 +217,20 @@ def download_chunks(base_url, manifest, stream_index, quality_level, dest_dir): print "\nDownloading Stream %d" % stream_index print "\tChunks %10d/%-10d" % (0, len(chunks)), "\r", sys.stdout.flush() - for i, c in enumerate(chunks): - chunk_name = get_chunk_name_string(stream, c) - chunk_file = os.path.join(dest_dir, chunks_quality, chunk_name) + stream_duration = 0 + for i, chunk in enumerate(chunks): + + if "t" in chunk.attrib: + chunk_time = chunk.attrib["t"] + elif "d" in chunk.attrib: + chunk_time = stream_duration + stream_duration = chunk_time + int(chunk.attrib["d"]) - if os.path.exists(chunk_file) == False: + chunk_name = get_chunk_name_string(stream, chunk_time) + chunk_file = os.path.join(dest_dir, chunks_quality, chunk_name) + + if not os.path.exists(chunk_file): chunk_url = base_url + '/' + chunks_quality + '/' + chunk_name data = download_file(chunk_url, chunk_file, "wb") else: @@ -264,9 +263,17 @@ def rebuild_stream(manifest, stream_index, quality_level, src_dir, print "\nRebuilding Stream %d" % stream_index print "\tChunks %10d/%-10d" % (0, len(chunks)), "\r", sys.stdout.flush() - for i, c in enumerate(chunks): - chunk_name = get_chunk_name_string(stream, c) + stream_duration = 0 + for i, chunk in enumerate(chunks): + + if "t" in chunk.attrib: + chunk_time = chunk.attrib["t"] + elif "d" in chunk.attrib: + chunk_time = stream_duration + stream_duration = chunk_time + int(chunk.attrib["d"]) + + chunk_name = get_chunk_name_string(stream, chunk_time) chunk_file = os.path.join(chunks_src_dir, chunk_name) f = open(chunk_file, "rb") @@ -309,9 +316,16 @@ def calc_tracks_delay(manifest, stream1_index, stream2_index): s1 = streams[stream1_index] s2 = streams[stream2_index] + if "TimeScale" not in s1 or "TimeScale" not in s2: + return 0 + s1_start_chunk = s1.find("c") s2_start_chunk = s2.find("c") + if "t" not in s1_start_chunk.attrib \ + or "t" not in s2_start_chunk.attrib: + return 0 + s1_start_time = int(s1_start_chunk.attrib['t']) s2_start_time = int(s2_start_chunk.attrib['t']) @@ -332,49 +346,49 @@ def get_clip_duration(manifest): return float(duration) / 10000000 # here is the default timescale -def smooth_download(url, manifest, dest_dir=tempfile.gettempdir(), +def smooth_download(url, manifest, dest_dir, video_stream_index=0, audio_stream_index=1, video_quality_level=0, audio_quality_level=0, chunks_dir=None, download=True, out_video_file='_video.vc1', out_audio_file='_audio.raw'): - if chunks_dir is None: - chunks_dir = dest_dir + if chunks_dir is None: + chunks_dir = dest_dir - if download: - download_chunks(url, manifest, video_stream_index, - video_quality_level, chunks_dir) - download_chunks(url, manifest, audio_stream_index, - audio_quality_level, chunks_dir) + if download: + download_chunks(url, manifest, video_stream_index, + video_quality_level, chunks_dir) + download_chunks(url, manifest, audio_stream_index, + audio_quality_level, chunks_dir) - dest_video = os.path.join(dest_dir, out_video_file) - dest_audio = os.path.join(dest_dir, out_audio_file) + dest_video = os.path.join(dest_dir, out_video_file) + dest_audio = os.path.join(dest_dir, out_audio_file) - rebuild_stream(manifest, video_stream_index, video_quality_level, - chunks_dir, dest_video) - rebuild_stream(manifest, audio_stream_index, audio_quality_level, - chunks_dir, dest_audio, dest_audio + '.wav') + rebuild_stream(manifest, video_stream_index, video_quality_level, + chunks_dir, dest_video) + rebuild_stream(manifest, audio_stream_index, audio_quality_level, + chunks_dir, dest_audio, dest_audio + '.wav') - # duration = get_clip_duration(manifest) + # duration = get_clip_duration(manifest) - delay = calc_tracks_delay(manifest, video_stream_index, - audio_stream_index) + delay = calc_tracks_delay(manifest, video_stream_index, + audio_stream_index) - # optionally encode audio to vorbis: - # ffmpeg -i _audio.raw.wav -acodec libvorbis -aq 60 audio.ogg - mux_command = ("ffmpeg -i %s \\\n" + - " -itsoffset %f -async 1 -i %s \\\n" + - " -vcodec copy -acodec copy ffout.mkv") % \ - (dest_video, delay, dest_audio + '.wav') + # optionally encode audio to vorbis: + # ffmpeg -i _audio.raw.wav -acodec libvorbis -aq 60 audio.ogg + mux_command = ("ffmpeg -i %s \\\n" + + " -itsoffset %f -async 1 -i %s \\\n" + + " -vcodec copy -acodec copy ffout.mkv") % \ + (dest_video, delay, dest_audio + '.wav') - print mux_command + print mux_command def options_parser(): - version = "%%prog %s" % __version + version = "%%prog %s" % __version__ usage = "usage: %prog [options] " parser = OptionParser(usage=usage, version=version, - description=__description, epilog=__author_info) + description=__description__, epilog=__author_info__) parser.add_option("-i", "--info", action="store_true", dest="info_only", default=False, help="print Manifest info and exit") @@ -393,7 +407,7 @@ def options_parser(): parser.add_option("-c", "--chunks-dir", metavar="", dest="chunks_dir", default=None, help="directory containing chunks, if different from destination dir") - parser.add_option("-v", "--video-stream", metavar="", + parser.add_option("-v", "--video-stream", metavar="", type="int", dest="video_stream_index", default=0, help="index of the video stream") parser.add_option("-a", "--audio-stream", metavar="", @@ -409,8 +423,7 @@ def options_parser(): return parser -if __name__ == "__main__": - +def main(): parser = options_parser() (options, args) = parser.parse_args() @@ -418,6 +431,9 @@ if __name__ == "__main__": parser.print_help() parser.exit(1) + if not os.path.exists(options.dest_dir): + os.mkdir(options.dest_dir, 0755) + url = args[0] manifest, url = get_manifest(url, options.dest_dir) @@ -440,3 +456,7 @@ if __name__ == "__main__": options.video_stream_index, options.audio_stream_index, options.video_quality_level, options.audio_quality_level, options.chunks_dir, options.download) + + +if __name__ == "__main__": + main()