Created
April 15, 2024 14:16
-
-
Save devgianlu/9a40b05364f69959e7f9f96734cf7322 to your computer and use it in GitHub Desktop.
Some scripts to download videos from Microsoft Stream Classic
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"github.com/elazarl/goproxy" | |
"net/http" | |
"regexp" | |
) | |
const Cookie = "" // TODO | |
func main() { | |
proxy := goproxy.NewProxyHttpServer() | |
proxy.Verbose = true | |
proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("^.*:443$"))).HandleConnect(goproxy.AlwaysMitm) | |
proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("^.*$"))).DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { | |
req.Header.Set("Cookie", Cookie) | |
resp, err := ctx.RoundTrip(req) | |
if err != nil { | |
ctx.Warnf("error: %s", err) | |
return req, nil | |
} | |
return req, resp | |
}) | |
panic(http.ListenAndServe("0.0.0.0:7777", proxy)) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import json | |
import os.path | |
import re | |
import subprocess | |
import requests | |
msft = requests.Session() | |
msft.headers['Cookie'] = '' # TODO | |
def parse_duration(val): | |
if 'H' not in val: | |
m = re.match(r'PT(\d+)M([\d.]+)S', val) | |
assert m is not None | |
return int(m[1]) * 60 + float(m[2]) | |
else: | |
m = re.match(r'PT(\d+)H(\d+)M([\d.]+)S', val) | |
assert m is not None | |
return int(m[1]) * 3600 + int(m[2]) * 60 + float(m[3]) | |
VIDEO_IDS = [] | |
def main(): | |
total_duration = 0 | |
names_map = {} | |
download_tasks = [] | |
for video in VIDEO_IDS: | |
r = msft.get(f'https://euwe-1.api.microsoftstream.com/api/videos/{video}?api-version=1.0-private') | |
r.raise_for_status() | |
r = r.json() | |
names_map[video] = {'name': r['name']} | |
remote_duration = parse_duration(r['media']['duration']) | |
total_duration += remote_duration | |
if os.path.exists(f'videos/{video}.mkv'): | |
local_duration = float(subprocess.check_output( | |
['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', | |
'default=noprint_wrappers=1:nokey=1', f'videos/{video}.mkv']).decode().strip()) | |
if abs(remote_duration - local_duration) < 15: | |
names_map[video]['ok'] = True | |
continue | |
assert False, f'Video {video} downloaded partially, remote: {remote_duration}, local: {local_duration}' | |
playback_urls = r['playbackUrls'] | |
try: | |
m3u8 = next(filter(lambda x: x['mimeType'] == 'application/vnd.apple.mpegurl', playback_urls)) | |
except StopIteration: | |
assert False, f'Unsupported video: {r}' | |
download_tasks.append((video, m3u8['playbackUrl'])) | |
names_map[video]['ok'] = False | |
with open('videos/names.json', 'w') as f: | |
json.dump(names_map, f, indent=2) | |
print(f'Total duration is {total_duration} seconds') | |
for uid, url in download_tasks: | |
ffmpeg_command = f"ffmpeg -i '{url}' -map 0:v:3 -map 0:a -c copy videos/{uid}.mkv" | |
subprocess.check_output(['tmux', 'new-session', '-d', '-s', f'download_{uid}', ffmpeg_command], | |
env={'http_proxy': 'http://127.0.0.1:7777'}) | |
print(f'Started download for {uid}') | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment