├── LICENSE ├── README.md └── rbxdl.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Modnark 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rbxdl 2 | ROBLOX Asset downloader 3 | 4 | There are many things you can do with this little tool 5 | 6 | Visit the [wiki](https://github.com/Modnark/rbxdl/wiki) for more information on getting started 7 | -------------------------------------------------------------------------------- /rbxdl.py: -------------------------------------------------------------------------------- 1 | import requests, argparse, time, os, random 2 | from ast import literal_eval 3 | #Still have yet to find a way to reduce this... 4 | astTypes = { 5 | 0:['Unknown', ''], 6 | 1:['Image', '.png'], 7 | 2:['TeeShirt', '.xml'], 8 | 3:['Audio', '.xml'], 9 | 4:['Mesh', ''], 10 | 5:['Lua', ''], 11 | 8:['Hat', '.xml'], 12 | 9:['Place', '.rbxl'], 13 | 10:['Model', '.rbxm'], 14 | 11:['Shirt', '.xml'], 15 | 12:['Pants', '.xml'], 16 | 13:['Decal', '.xml'], 17 | 17:['Head', '.xml'], 18 | 18:['Face', '.xml'], 19 | 19:['Gear', '.xml'], 20 | 21:['Badge', ''], 21 | 24:['Animation', '.xml'], 22 | 27:['Torso', '.xml'], 23 | 28:['RightArm', '.xml'], 24 | 29:['LeftArm', '.xml'], 25 | 30:['LeftLeg', '.xml'], 26 | 31:['RightLeg', '.xml'], 27 | 32:['Package', '.xml'], 28 | 33:['YouTubeVideo', ''], 29 | 34:['GamePass', '.xml'], 30 | 38:['Plugin', '.xml'], 31 | 39:['SolidModel', '.xml'], 32 | 40:['MeshPart', '.rbxm'], 33 | 41:['HairAccessory', '.xml'], 34 | 42:['FaceAccessory', '.xml'], 35 | 43:['NeckAccessory', '.xml'], 36 | 44:['ShoulderAccessory', '.xml'], 37 | 45:['FrontAccessory', '.xml'], 38 | 46:['BackAccessory', '.xml'], 39 | 47:['WaistAccessory', '.xml'], 40 | 48:['ClimbAnimation', ''], 41 | 49:['DeathAnimation', ''], 42 | 50:['FallAnimation', ''], 43 | 51:['IdleAnimation', ''], 44 | 52:['JumpAnimation', ''], 45 | 53:['RunAnimation', ''], 46 | 54:['SwimAnimation', ''], 47 | 55:['WalkAnimation', ''], 48 | 56:['PoseAnimation', ''], 49 | 57:['EarAccessory', '.xml'], 50 | 58:['EyeAccessory', '.xml'], 51 | 61:['EmoteAnimation', ''], 52 | 62:['Video', ''] 53 | } 54 | #urls used in program 55 | astUrl = 'https://assetdelivery.roblox.com/v1/asset?id=' 56 | apiUrl = 'https://api.roblox.com/marketplace/productinfo?assetId=' 57 | #Creates web requests and handles most errors and status codes 58 | def makeWebReq(url): 59 | try: 60 | resp = requests.get(url) 61 | resp.close() 62 | return [resp.status_code, resp] 63 | except requests.RequestException as e: 64 | print("Exception occured whilst making request. This has been logged.") 65 | writeLogs(e) 66 | #used for getting metadata of an asset 67 | def getMeta(astId, specific = None): 68 | resp = makeWebReq(f'{apiUrl}{astId}') 69 | if resp[0] == 200: 70 | return resp[1].json().get(specific) or resp[1].json() 71 | else: 72 | return 0 73 | #Reduces amount of code I need to write 74 | def createDirectory(dirName): 75 | if not os.path.isdir(str(dirName)): 76 | os.mkdir(str(dirName)) 77 | return(str(dirName)) 78 | #Save asset to file 79 | def saveAsset(astId, astTypeStr, cDir, sDirName, astData, astVer): 80 | try: 81 | createDirectory(cDir) 82 | createDirectory(f'{cDir}\\{astTypeStr}') 83 | saveLocation = createDirectory(f'{cDir}\\{astTypeStr}\\{astId}') if sDirName is True else f'{cDir}\\{astTypeStr}' 84 | fileName = f'{saveLocation}\\{astId}-version{astVer}' if astVer is not None else f'{saveLocation}\\{astId}' 85 | assetSave = open(f'{fileName}{astTypes[getMeta(astId, "AssetTypeId")][1]}','wb+') 86 | assetSave.write(astData) 87 | assetSave.close() 88 | jsonMeta = getMeta(astId) 89 | if jsonMeta != 0: 90 | metaSaveLoc = f'{fileName}-META.txt' 91 | if not os.path.isfile(metaSaveLoc): 92 | metaFile = open(f'{metaSaveLoc}', 'a', encoding = 'utf-8') 93 | for i in jsonMeta: 94 | if i == 'Creator': 95 | metaFile.write('Creator: \n') 96 | for e in jsonMeta[i]: 97 | metaFile.write(f'\t{e} : {jsonMeta[i][e]}\n') 98 | else: 99 | metaFile.write(f'{i}: {jsonMeta[i]}\n') 100 | metaFile.close() 101 | return 1 102 | except OSError as e: 103 | writeLogs(e) 104 | return e 105 | #Download asset 106 | def download(astId, astVer, args): 107 | cDir = args.dir if args.dir is not None else 'Downloaded' 108 | sDir = args.sdirs 109 | url = f'{astUrl}{astId}&version={astVer}' if astVer is not None else f'{astUrl}{astId}' 110 | print(f'Downloading: {url}...') 111 | resp = makeWebReq(url) 112 | if resp[0] == 200: 113 | print(f'Saving: {url}...') 114 | save = saveAsset(astId, astTypes[getMeta(astId, 'AssetTypeId')][0], cDir, sDir, resp[1].content, astVer) 115 | if save == 1: 116 | print(f'Saved asset sucessfully!') 117 | else: 118 | print(f'Save failed, Check logs for more info...') 119 | return 1 120 | elif resp[0] == 404: 121 | print('Could not download because asset was not found') 122 | elif resp[0] == 403: 123 | print('Could not download because asset is copylocked') 124 | else: 125 | print(f'Could not download due to {resp[0]}') 126 | return 0 127 | #attempt to get every version of the asset 128 | def allVer(astId, args): 129 | aVer = 1 130 | while True: 131 | if download(astId, aVer, args) == 0: 132 | break 133 | aVer += 1 134 | def writeLogs(msg): 135 | logFile = open('rbxdl.log', 'a') 136 | logFile.write(f'{msg}\n\n') 137 | logFile.close() 138 | #Reduces code 139 | def startDL(astId, astVer, args, getAll=False): 140 | if getAll: 141 | allVer(astId, args) 142 | else: 143 | return download(astId, astVer, args) 144 | #Handle the user input 145 | def handleArgs(args): 146 | astId = literal_eval(args.assetid) 147 | dlm = args.downlmode 148 | astVer = args.ver 149 | getAll = args.allVer 150 | if dlm == 'single': 151 | startDL(astId, astVer, args, getAll) 152 | elif dlm == 'bulk': 153 | if isinstance(astId, list): 154 | for i in astId: 155 | startDL(i, astVer, args, getAll) 156 | else: 157 | raise ValueError('Incorrect format for bulk downloading. Should be laid out as [id1,id2,id3,etc..]') 158 | elif dlm == 'range': 159 | if isinstance(astId, list) and len(astId) == 2: 160 | for i in range(astId[0], astId[1]+1): 161 | startDL(i, astVer, args, getAll) 162 | else: 163 | raise ValueError('Incorrect format for range downloading. Should be laid out as [minId, maxId]') 164 | elif dlm == 'roulette': 165 | rlAmn = args.rltAmnt if args.rltAmnt is not None else 1 166 | rlType = args.rltType 167 | for i in range(1,rlAmn+1): 168 | while True: 169 | canDl = True 170 | randomId = random.randint(1000, 5000000000) 171 | if rlType is not None: 172 | if getMeta(randomId, 'AssetTypeId') != rlType: 173 | canDl = False 174 | if canDl: 175 | if startDL(randomId, None, args) == 1: 176 | break 177 | #argparse 178 | cmdParse = argparse.ArgumentParser(description='Download assets from ROBLOX.') 179 | cmdParse.add_argument('downlmode', choices=['single', 'bulk', 'range', 'roulette'], help='mode for asset downloading', type=str) 180 | cmdParse.add_argument('assetid', help='id(s) of asset', type=str) 181 | cmdParse.add_argument('--dir', help='save assets into your own directory', type=str) 182 | cmdParse.add_argument('--ver', help='version(s) of the asset(s)', type=str) 183 | cmdParse.add_argument('--sdirs', help='save assets in their own directories', action='store_true') 184 | cmdParse.add_argument('--allVer', help='Download all of the versions of an asset (slow)', action='store_true') 185 | cmdParse.add_argument('--rltAmnt', help='How many times should the roulette download something?', type=int) 186 | cmdParse.add_argument('--rltType', help='What specific asset type should the roulette look for?', type=int) 187 | args = cmdParse.parse_args() 188 | handleArgs(args) 189 | --------------------------------------------------------------------------------