Skip to content

Instantly share code, notes, and snippets.

@devgianlu
Created April 15, 2024 14:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save devgianlu/9a40b05364f69959e7f9f96734cf7322 to your computer and use it in GitHub Desktop.
Save devgianlu/9a40b05364f69959e7f9f96734cf7322 to your computer and use it in GitHub Desktop.
Some scripts to download videos from Microsoft Stream Classic
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))
}
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