Skip to content

Instantly share code, notes, and snippets.

@WitherOrNot
Last active May 10, 2023 20:38
Show Gist options
  • Save WitherOrNot/0531a879b55ffeb0d36145936b5f80e2 to your computer and use it in GitHub Desktop.
Save WitherOrNot/0531a879b55ffeb0d36145936b5f80e2 to your computer and use it in GitHub Desktop.
office click-to-run (c2r) downloader
#!/usr/bin/env python3
from bs4 import BeautifulSoup
from requests import head, get
from os import remove
from subprocess import run
from argparse import ArgumentParser
from platform import architecture
from shutil import copy
ROOT = "https://officecdn.microsoft.com/pr/{ffn}"
IMG_URLS = [
'/media/{lang}/Access2019Retail.img',
'/media/{lang}/Access2021Retail.img',
'/media/{lang}/AccessRuntime2019Retail.img',
'/media/{lang}/Excel2019Retail.img',
'/media/{lang}/Excel2021Retail.img',
'/media/{lang}/HomeBusiness2019Retail.img',
'/media/{lang}/HomeBusiness2021Retail.img',
'/media/{lang}/HomeStudent2019Retail.img',
'/media/{lang}/HomeStudent2021Retail.img',
'/media/{lang}/O365BusinessRetail.img',
'/media/{lang}/O365HomePremRetail.img',
'/media/{lang}/O365ProPlusRetail.img',
'/media/{lang}/OneNote2021Retail.img',
'/media/{lang}/Outlook2019Retail.img',
'/media/{lang}/Outlook2021Retail.img',
'/media/{lang}/Personal2019Retail.img',
'/media/{lang}/Personal2021Retail.img',
'/media/{lang}/PowerPoint2019Retail.img',
'/media/{lang}/PowerPoint2021Retail.img',
'/media/{lang}/Professional2019Retail.img',
'/media/{lang}/Professional2021Retail.img',
'/media/{lang}/ProjectPro2019Retail.img',
'/media/{lang}/ProjectPro2021Retail.img',
'/media/{lang}/ProjectStd2019Retail.img',
'/media/{lang}/ProjectStd2021Retail.img',
'/media/{lang}/ProPlus2019Retail.img',
'/media/{lang}/ProPlus2021Retail.img',
'/media/{lang}/Publisher2019Retail.img',
'/media/{lang}/Publisher2021Retail.img',
'/media/{lang}/SkypeforBusiness2019Retail.img',
'/media/{lang}/SkypeforBusiness2021Retail.img',
'/media/{lang}/SkypeforBusinessEntry2019Retail.img',
'/media/{lang}/Standard2019Retail.img',
'/media/{lang}/Standard2021Retail.img',
'/media/{lang}/VisioPro2019Retail.img',
'/media/{lang}/VisioPro2021Retail.img',
'/media/{lang}/VisioStd2019Retail.img',
'/media/{lang}/VisioStd2021Retail.img',
'/media/{lang}/Word2019Retail.img',
'/media/{lang}/Word2021Retail.img'
]
def gen_url(ffn, arch, lang, path, version=""):
return (ROOT + path).format(ffn=ffn, arch=arch, lang=lang, version=version)
def check_file(ffn, arch, lang, path, version=""):
return head(gen_url(ffn, arch, lang, path, version)).status_code == 200
def api_get(url):
return get(url, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"})
def get_channels():
data = api_get("https://config.office.com/api/filelist/channels").json()
channels = {}
for channel in data:
ffn = channel["baseUrl"].split("/")[-1]
version = api_get(f"https://mrodevicemgr.officeapps.live.com/mrodevicemgrsvc/api/v2/C2RReleaseData/{ffn}").json()["AvailableBuild"]
channels[channel["name"]] = (ffn, version)
return channels
def scan_imgs(ffn, arch, lang):
for path in IMG_URLS:
if check_file(ffn, arch, lang, path):
print(gen_url(ffn, arch, lang, path))
def download_c2r(channel, version, arch, langs, root_dir):
url = f"https://config.office.com/api/filelist?Channel={channel}&Arch={arch}&Version={version}&lid=x-none&" + "&".join(["lid=" + lang for lang in langs])
filedata = api_get(url).json()
with open("c2r.tmp", "w") as f:
for fentry in filedata["files"]:
url = fentry["url"]
dir = root_dir + fentry["relativePath"]
name = fentry["name"]
f.write(f"{url}\n\tout={name}\n\tdir={dir}\n")
url = gen_url("wsus", "", "", "/setup.exe")
f.write(f"{url}\n\tdir={root_dir}\n")
run(["aria2c", "--console-log-level=error", "--summary-interval=0", "--download-result=hide", "-x", "16", "-j", "16", "-i", "c2r.tmp"])
print()
remove("c2r.tmp")
def parse_config_xml(xml):
soup = BeautifulSoup(xml, "lxml")
langs = list(set(x.attrs["id"] for x in soup.find_all("language")))
channel = soup.find("add").attrs["channel"]
arch = soup.find("add").attrs["officeclientedition"]
return channel, arch, langs
if __name__ == "__main__":
parser = ArgumentParser()
parser.add_argument("mode", help="config (download from configuration xml), img (get img installer urls), or channels (list available channels)")
parser.add_argument("--configuration", "-f", help="configuration xml file for download, default Configuration.xml", default="Configuration.xml")
parser.add_argument("--directory", "-d", help="directory to download to in config mode", default=None)
parser.add_argument("--channel", "-c", help="channel for img mode, default is Current", default="Current")
parser.add_argument("--arch", "-a", help="architecture for img mode, default is system architecture", default=architecture()[0][:2])
parser.add_argument("--language", "-l", help="language for img mode, default is en-us", default="en-us")
args = parser.parse_args()
channels = get_channels()
if args.mode == "img":
print(f"Scanning IMG files for {args.arch}-bit {args.channel} channel in {args.language} locale:")
ffn, version = channels[args.channel]
scan_imgs(ffn, args.arch, args.language)
elif args.mode == "config":
with open(args.configuration, "r") as f:
channel, arch, langs = parse_config_xml(f.read())
ffn, version = channels[channel]
print(f"Downloading C2R setup files for {arch}-bit {channel} channel in locale(s) {', '.join(langs)}:")
if args.directory is None:
directory = f"{channel}_{version}_{'_'.join(langs)}_x{arch}"
else:
directory = args.directory
download_c2r(channel, version, arch, langs, directory)
copy(args.configuration, directory)
elif args.mode == "channels":
print("Available channels:")
for channel in channels:
ffn, version = channels[channel]
print(f"{channel} (FFN {ffn}): version {version}")
else:
parser.print_help()
exit(1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment