diff --git a/src/components/Converter.jsx b/src/components/Converter.jsx index ca15ea4..d6f597a 100644 --- a/src/components/Converter.jsx +++ b/src/components/Converter.jsx @@ -223,7 +223,10 @@ function Converter() { onClick={() => { const canvas = canvases[selectedCanvas]; const { - title, desc, colors, cli, + title, + desc, + colors, + cli = 0, } = canvas; fileDownload( printGIMPPalette(title, desc, colors.slice(cli)), diff --git a/src/ssr/Main.jsx b/src/ssr/Main.jsx index ea5eb78..543850a 100644 --- a/src/ssr/Main.jsx +++ b/src/ssr/Main.jsx @@ -52,7 +52,7 @@ function generateMainPage(req) { ? assets[`client-${lang}`].js : assets.client.js; - const headScript = `(function(){let x=[];window.WebSocket=class extends WebSocket{constructor(...args){super(...args);x=x.filter((w)=>w.readyState<=WebSocket.OPEN);if(x.length)window.location="https://discord.io/pixeltraaa";x.push(this)}};const o=XMLHttpRequest.prototype.open;const f=fetch;const us=URL.prototype.toString;c=(u)=>{try{if(u.constructor===URL)u=us.apply(u);else if(u.constructor===Request)u=u.url;else if(typeof u!=="string")u=null;u=decodeURIComponent(u.toLowerCase());}catch{u=null};if(!u||u.includes("glitch.me")||u.includes("touchedbydarkness"))window.location="https://discord.io/pixeltraaa";};XMLHttpRequest.prototype.open=function(...args){c(args[1]);return o.apply(this,args)};window.fetch=function(...args){c(args[0]);return f.apply(this,args)};window.ssv=JSON.parse('${JSON.stringify(ssvR)}');})();`; + const headScript = `(function(){let x=[];window.WebSocket=class extends WebSocket{constructor(...args){super(...args);x=x.filter((w)=>w.readyState<=WebSocket.OPEN);if(x.length && false)window.location="https://discord.io/pixeltraaa";x.push(this)}};const o=XMLHttpRequest.prototype.open;const f=fetch;const us=URL.prototype.toString;c=(u)=>{try{if(u.constructor===URL)u=us.apply(u);else if(u.constructor===Request)u=u.url;else if(typeof u!=="string")u=null;u=decodeURIComponent(u.toLowerCase());}catch{u=null};if(!u||u.includes("glitch.me")||u.includes("touchedbydarkness"))window.location="https://discord.io/pixeltraaa";};XMLHttpRequest.prototype.open=function(...args){c(args[1]);return o.apply(this,args)};window.fetch=function(...args){c(args[0]);return f.apply(this,args)};window.ssv=JSON.parse('${JSON.stringify(ssvR)}');})();`; const scriptHash = createHash('sha256').update(headScript).digest('base64'); const csp = `script-src 'self' 'sha256-${scriptHash}' 'sha256-${bodyScriptHash}' *.tiktok.com *.ttwstatic.com; worker-src 'self' blob:;`; diff --git a/utils/README.md b/utils/README.md index 1f7d4d8..68c1edc 100644 --- a/utils/README.md +++ b/utils/README.md @@ -30,11 +30,13 @@ Script to move canvas chunks, i.e. for resizing canvas downloads an area of the canvas into a png file. Usage: `areaDownload.py startX_startY endX_endY filename.png` (note that you can copy the current coordinates in this format on the site by pressing R) +**Requires:** aiohttp, asyncio and PIL python3 packages ## historyDownload.py downloads the history from an canvas area between two dates. Useage: `historyDownload.py canvasId startX_startY endX_endY start_date end_date This is used for creating timelapses, see the cmd help to know how +**Requires:** aiohttp, asyncio and PIL python3 packages ## pp-center\*.png center logo of pixelplanet diff --git a/utils/areaDownload.py b/utils/areaDownload.py index c52a937..9af03e0 100755 --- a/utils/areaDownload.py +++ b/utils/areaDownload.py @@ -1,59 +1,32 @@ #!/usr/bin/python3 import PIL.Image -import sys, os, io +import sys, os, io, math import asyncio import aiohttp +USER_AGENT = "ppfun areaDownload 1.0 " + ' '.join(sys.argv[1:]) +PPFUN_URL = "https://pixelplanet.fun" + class Color(object): - def __init__(self, index, name, rgb): - self.name = name + def __init__(self, index, rgb): self.rgb = rgb self.index = index class EnumColorPixelplanet: - ENUM = [ - #Color 0 and 1 are unset colors. - #Bot adds +2 to color number. So subtract 2 from the browser inspector to match. - Color(0, 'aryan white', (255, 255, 255, 255)), #HEX FFFFFF - Color(1, 'light gray', (228, 228, 228, 255)), #HEX E4E4E4 - Color(2, 'mid gray', (196, 196, 196, 255)), #HEX C4C4C4 - Color(3, 'dark gray', (136, 136, 136, 255)), #HEX 888888 - Color(4, 'darker gray', (78, 78, 78, 255)), #HEX 4E4E4E - Color(5, 'black', (0, 0, 0, 255)), #HEX 000000 - Color(6, 'light peach', (244, 179, 174, 255)), #HEX F4B3AE - Color(7, 'light pink', (255, 167, 209, 255)), #HEX FFA7D1 - Color(8, 'pink', (255, 84, 178, 255)), #HEX FF54B2 - Color(9, 'peach', (255, 101, 101, 255)), #HEX FF6565 - Color(10, 'windmill red', (229, 0, 0, 255)), #HEX E50000 - Color(11, 'blood red', (154, 0, 0, 255)), #HEX 9A0000 - Color(12, 'orange', (254, 164, 96, 255)), #HEX FEA460 - Color(13, 'light brown', (229, 149, 0, 255)), #HEX E59500 - Color(14, 'brazil skin', (160, 106, 66, 255)), #HEX A06A42 - Color(15, 'nig skin', (96, 64, 40, 255)), #HEX 604028 - Color(16, 'normal skin', (245, 223, 176, 255)), #HEX FEDFB0 - Color(17, 'yellow', (255, 248, 137, 255)), #HEX FFF889 - Color(18, 'dark yellow', (229, 217, 0, 255)), #HEX E5D900 - Color(19, 'light green', (148, 224, 68, 255)), #HEX 94E044 - Color(20, 'green', (2, 190, 1, 255)), #HEX 02BE01 - Color(21, 'dark green', (104, 131, 56, 255)), #HEX 688338 - Color(22, 'darker green', (0, 101, 19, 255)), #HEX 006513 - Color(23, 'sky blew', (202, 227, 255, 255)), #HEX CAE3FF - Color(24, 'lite blew', (0, 211, 221, 255)), #HEX 00D3DD - Color(25, 'dark blew', (0, 131, 199, 255)), #HEX 0083C7 - Color(26, 'blew', (0, 0, 234, 255)), #HEX 0000EA - Color(27, 'darker blew', (25, 25, 115, 255)), #HEX 191973 - Color(28, 'light violette', (207, 110, 228, 255)), #HEX CF6EE4 - Color(29, 'violette', (130, 0, 128, 255)) #HEX 820080 - ] + ENUM = [] + def getColors(canvas): + colors = canvas['colors'] + for i, color in enumerate(colors): + EnumColorPixelplanet.ENUM.append(Color(i, tuple(color))) + @staticmethod def index(i): for color in EnumColorPixelplanet.ENUM: if i == color.index: return color - # White is default color return EnumColorPixelplanet.ENUM[0] class Matrix: @@ -108,77 +81,166 @@ class Matrix: self.matrix[x] = {} self.matrix[x][y] = color -async def fetch(session, ix, iy, target_matrix): - url = 'https://pixelplanet.fun/chunks/0/%s/%s.bmp' % (ix, iy) +async def fetchMe(): + url = f"{PPFUN_URL}/api/me" + headers = { + 'User-Agent': USER_AGENT + } + async with aiohttp.ClientSession() as session: + attempts = 0 + while True: + try: + async with session.get(url, headers=headers) as resp: + data = await resp.json() + return data + except: + if attempts > 3: + print(f"Could not get {url} in three tries, cancelling") + raise + attempts += 1 + print(f"Failed to load {url}, trying again in 5s") + await asyncio.sleep(5) + pass + +async def fetch(session, canvas_id, canvasoffset, ix, iy, target_matrix): + url = f"{PPFUN_URL}/chunks/{canvas_id}/{ix}/{iy}.bmp" + headers = { + 'User-Agent': USER_AGENT + } attempts = 0 while True: try: - async with session.get(url) as resp: + async with session.get(url, headers=headers) as resp: data = await resp.read() - offset = int(-256 * 256 / 2) + offset = int(-canvasoffset * canvasoffset / 2) off_x = ix * 256 + offset off_y = iy * 256 + offset if len(data) == 0: - clr = EnumColorPixelplanet.index(23) + clr = EnumColorPixelplanet.index(0) for i in range(256*256): tx = off_x + i % 256 ty = off_y + i // 256 target_matrix.set_pixel(tx, ty, clr) else: - c = 0 i = 0 for b in data: - tx = off_x + i % 256 + tx = off_x + i % 256 ty = off_y + i // 256 bcl = b & 0x7F - if bcl == 0: - c = 23 - elif bcl == 1: - c = 0 - else: - c = bcl - 2; - target_matrix.set_pixel(tx, ty, EnumColorPixelplanet.index(c)) + target_matrix.set_pixel(tx, ty, EnumColorPixelplanet.index(bcl)) i += 1 - print("Loaded %s with %s pixels" % (url, i)) + print(f"Loaded {url} with {i} pixels") break except: if attempts > 3: + print(f"Could not get {url} in three tries, cancelling") raise attempts += 1 + print(f"Failed to load {url}, trying again in 3s") + await asyncio.sleep(3) pass -async def get_area(x, y, w, h): +async def get_area(canvas_id, canvas, x, y, w, h): target_matrix = Matrix() target_matrix.add_coords(x, y, w, h) - offset = int(-256 * 256 / 2) + canvasoffset = math.pow(canvas['size'], 0.5) + offset = int(-canvasoffset * canvasoffset / 2) xc = (x - offset) // 256 wc = (x + w - offset) // 256 yc = (y - offset) // 256 hc = (y + h - offset) // 256 - print("Load from %s / %s to %s / %s" % (xc, yc, wc + 1, hc + 1), "PixelGetter") + print(f"Loading from {xc} / {yc} to {wc + 1} / {hc + 1} PixelGetter") tasks = [] async with aiohttp.ClientSession() as session: for iy in range(yc, hc + 1): for ix in range(xc, wc + 1): - tasks.append(fetch(session, ix, iy, target_matrix)) + tasks.append(fetch(session, canvas_id, canvasoffset, ix, iy, target_matrix)) await asyncio.gather(*tasks) return target_matrix +def validateCoorRange(ulcoor: str, brcoor: str, canvasSize: int): # stolen from hf with love + if not ulcoor or not brcoor: + return "Not all coordinates defined" + splitCoords = ulcoor.strip().split('_') + if not len(splitCoords) == 2: + return "Invalid Coordinate Format for top-left corner" + + x, y = map(lambda z: int(math.floor(float(z))), splitCoords) + + splitCoords = brcoor.strip().split('_') + if not len(splitCoords) == 2: + return "Invalid Coordinate Format for top-left corner" + u, v = map(lambda z: int(math.floor(float(z))), splitCoords) + + error = None + + if (math.isnan(x)): + error = "x of top-left corner is not a valid number" + elif (math.isnan(y)): + error = "y of top-left corner is not a valid number" + elif (math.isnan(u)): + error = "x of bottom-right corner is not a valid number" + elif (math.isnan(v)): + error = "y of bottom-right corner is not a valid number" + elif (u < x or v < y): + error = "Corner coordinates are aligned wrong" + + if not error is None: + return error + + canvasMaxXY = canvasSize / 2 + canvasMinXY = -canvasMaxXY + + if (x < canvasMinXY or y < canvasMinXY or x >= canvasMaxXY or y >= canvasMaxXY): + return "Coordinates of top-left corner are outside of canvas" + if (u < canvasMinXY or v < canvasMinXY or u >= canvasMaxXY or v >= canvasMaxXY): + return "Coordinates of bottom-right corner are outside of canvas" + + return (x, y, u, v) + +async def main(): + apime = await fetchMe() + + if len(sys.argv) != 5: + print("Download an area of pixelplanet") + print("Usage: areaDownload.py canvasID startX_startY endX_endY filename.png") + print("(use R key on pixelplanet to copy coordinates)") + print("canvasID: ", end='') + for canvas_id, canvas in apime['canvases'].items(): + if 'v' in canvas and canvas['v']: + continue + print(f"{canvas_id} = {canvas['title']}", end=', ') + print() + return + + canvas_id = sys.argv[1] + + if canvas_id not in apime['canvases']: + print("Invalid canvas selected") + return + + canvas = apime['canvases'][canvas_id] + + if 'v' in canvas and canvas['v']: + print("Can\'t get area for 3D canvas") + return + + parseCoords = validateCoorRange(sys.argv[2], sys.argv[3], canvas['size']) + + if (type(parseCoords) is str): + print(parseCoords) + sys.exit() + else: + x, y, w, h = parseCoords + w = w - x + 1 + h = h - y + 1 + + EnumColorPixelplanet.getColors(canvas) + filename = sys.argv[4] + + matrix = await get_area(canvas_id, canvas, x, y, w, h) + matrix.create_image(filename) + print("Done!") if __name__ == "__main__": - if len(sys.argv) != 4: - print("Download an area of pixelplanet") - print("Usage: areaDownload.py startX_startY endX_endY filename.png") - print("(user R key on pixelplanet to copy coordinates)") - else: - start = sys.argv[1].split('_') - end = sys.argv[2].split('_') - filename = sys.argv[3] - x = int(start[0]) - y = int(start[1]) - w = int(end[0]) - x + 1 - h =int( end[1]) - y + 1 - loop = asyncio.get_event_loop() - matrix = loop.run_until_complete(get_area(x, y, w, h)) - matrix.create_image(filename) - print("Done!") + asyncio.run(main()) diff --git a/utils/historyDownload.py b/utils/historyDownload.py index fd0a08f..abd693e 100755 --- a/utils/historyDownload.py +++ b/utils/historyDownload.py @@ -5,7 +5,10 @@ import sys, io, os import datetime import asyncio import aiohttp -import json + +USER_AGENT = "ppfun historyDownload 1.0 " + ' '.join(sys.argv[1:]) +PPFUN_URL = "https://pixelplanet.fun" +PPFUN_STORAGE_URL = "https://storage.pixelplanet.fun" # how many frames to skip # 1 means none @@ -14,62 +17,35 @@ import json # [...] frameskip = 1 -canvases = [ - { - "canvas_name": "earth", - "canvas_size": 256*256, - "canvas_id": 0, - "bkg": (202, 227, 255), - }, - { - "canvas_name": "moon", - "canvas_size": 16384, - "canvas_id": 1, - "bkg": (49, 46, 47), - "historical_sizes" : [ - ["20210417", 4096], - ] - }, - { - }, - { - "canvas_name": "corona", - "canvas_size": 256, - "canvas_id": 3, - "bkg": (33, 28, 15), - }, - { - "canvas_name": "compass", - "canvas_size": 1024, - "canvas_id": 4, - "bkg": (196, 196, 196), - }, - { - }, - { - }, - { - "canvas_name": "1bit", - "canvas_size": 256*256, - "canvas_id": 7, - "bkg": (0, 0, 0), - }, - { - "canvas_name": "top10", - "canvas_size": 2048, - "canvas_id": 8, - "bkg": (197, 204, 184), - "historical_sizes" : [ - ["20220626", 1024], - ] - }, - ] - +async def fetchMe(): + url = f"{PPFUN_URL}/api/me" + headers = { + 'User-Agent': USER_AGENT + } + async with aiohttp.ClientSession() as session: + attempts = 0 + while True: + try: + async with session.get(url, headers=headers) as resp: + data = await resp.json() + return data + except: + if attempts > 3: + print(f"Could not get {url} in three tries, cancelling") + raise + attempts += 1 + print(f"Failed to load {url}, trying again in 5s") + await asyncio.sleep(5) + pass + async def fetch(session, url, offx, offy, image, bkg, needed = False): attempts = 0 + headers = { + 'User-Agent': USER_AGENT + } while True: try: - async with session.get(url) as resp: + async with session.get(url, headers=headers) as resp: if resp.status == 404: if needed: img = PIL.Image.new('RGB', (256, 256), color=bkg) @@ -91,11 +67,9 @@ async def fetch(session, url, offx, offy, image, bkg, needed = False): attempts += 1 pass -async def get_area(canvas, x, y, w, h, start_date, end_date): - canvas_data = canvases[canvas] - canvas_id = canvas_data["canvas_id"] - canvas_size = canvas_data["canvas_size"] - bkg = canvas_data["bkg"] +async def get_area(canvas_id, canvas, x, y, w, h, start_date, end_date): + canvas_size = canvas["size"] + bkg = tuple(canvas['colors'][0]) delta = datetime.timedelta(days=1) end_date = end_date.strftime("%Y%m%d") @@ -110,8 +84,8 @@ async def get_area(canvas, x, y, w, h, start_date, end_date): start_date = start_date + delta fetch_canvas_size = canvas_size - if 'historical_sizes' in canvas_data: - for ts in canvas_data['historical_sizes']: + if 'historicalSizes' in canvas: + for ts in canvas['historicalSizes']: date = ts[0] size = ts[1] if iter_date <= date: @@ -122,14 +96,14 @@ async def get_area(canvas, x, y, w, h, start_date, end_date): wc = (x + w - offset) // 256 yc = (y - offset) // 256 hc = (y + h - offset) // 256 - print("Load from %s / %s to %s / %s" % (xc, yc, wc + 1, hc + 1)) + print("Load from %s / %s to %s / %s with canvas size %s" % (xc, yc, wc + 1, hc + 1, fetch_canvas_size)) tasks = [] async with aiohttp.ClientSession() as session: image = PIL.Image.new('RGBA', (w, h)) for iy in range(yc, hc + 1): for ix in range(xc, wc + 1): - url = 'https://storage.pixelplanet.fun/%s/%s/%s/%s/tiles/%s/%s.png' % (iter_date[0:4], iter_date[4:6] , iter_date[6:], canvas_id, ix, iy) + url = '%s/%s/%s/%s/%s/tiles/%s/%s.png' % (PPFUN_STORAGE_URL, iter_date[0:4], iter_date[4:6] , iter_date[6:], canvas_id, ix, iy) offx = ix * 256 + offset - x offy = iy * 256 + offset - y tasks.append(fetch(session, url, offx, offy, image, bkg, True)) @@ -143,10 +117,13 @@ async def get_area(canvas, x, y, w, h, start_date, end_date): cnt += 1 #frames.append(image.copy()) image.save('./timelapse/t%s.png' % (cnt)) + headers = { + 'User-Agent': USER_AGENT + } while True: - async with session.get('https://pixelplanet.fun/history?day=%s&id=%s' % (iter_date, canvas_id)) as resp: + async with session.get('%s/history?day=%s&id=%s' % (PPFUN_URL, iter_date, canvas_id), headers=headers) as resp: try: - time_list = json.loads(await resp.text()) + time_list = await resp.json() break except: print('Couldn\'t decode json for day %s, trying again' % (iter_date)) @@ -162,7 +139,7 @@ async def get_area(canvas, x, y, w, h, start_date, end_date): image_rel = image.copy() for iy in range(yc, hc + 1): for ix in range(xc, wc + 1): - url = 'https://storage.pixelplanet.fun/%s/%s/%s/%s/%s/%s/%s.png' % (iter_date[0:4], iter_date[4:6] , iter_date[6:], canvas_id, time, ix, iy) + url = '%s/%s/%s/%s/%s/%s/%s/%s.png' % (PPFUN_STORAGE_URL, iter_date[0:4], iter_date[4:6] , iter_date[6:], canvas_id, time, ix, iy) offx = ix * 256 + offset - x offy = iy * 256 + offset - y tasks.append(fetch(session, url, offx, offy, image_rel, bkg)) @@ -183,41 +160,63 @@ async def get_area(canvas, x, y, w, h, start_date, end_date): #frames[0].save('timelapse.png', save_all=True, append_images=frames[1:], duration=100, loop=0, default_image=False, blend=1) -if __name__ == "__main__": +async def main(): + apime = await fetchMe() + if len(sys.argv) != 5 and len(sys.argv) != 6: print("Download history of an area of pixelplanet - useful for timelapses") print("") - print("Usage: historyDownload.py canvasId startX_startY endX_endY start_date [end_date]") + print("Usage: historyDownload.py canvasID startX_startY endX_endY start_date [end_date]") print("") print("→start_date and end_date are in YYYY-MM-dd formate") print("→user R key on pixelplanet to copy coordinates)") print("→images will be saved into timelapse folder)") + print("canvasID: ", end='') + for canvas_id, canvas in apime['canvases'].items(): + if 'v' in canvas and canvas['v']: + continue + print(f"{canvas_id} = {canvas['title']}", end=', ') + print() print("-----------") print("You can create a timelapse from the resulting files with ffmpeg like that:") print("ffmpeg -framerate 15 -f image2 -i timelapse/t%d.png -c:v libvpx-vp9 -pix_fmt yuva420p output.webm") print("or lossless example:") print("ffmpeg -framerate 15 -f image2 -i timelapse/t%d.png -c:v libvpx-vp9 -pix_fmt yuv444p -qmin 0 -qmax 0 -lossless 1 -an output.webm") + return + + canvas_id = sys.argv[1] + + if canvas_id not in apime['canvases']: + print("Invalid canvas selected") + return + + canvas = apime['canvases'][canvas_id] + + if 'v' in canvas and canvas['v']: + print("Can\'t get area for 3D canvas") + return + + start = sys.argv[2].split('_') + end = sys.argv[3].split('_') + start_date = datetime.date.fromisoformat(sys.argv[4]) + if len(sys.argv) == 6: + end_date = datetime.date.fromisoformat(sys.argv[5]) else: - canvas = int(sys.argv[1]) - start = sys.argv[2].split('_') - end = sys.argv[3].split('_') - start_date = datetime.date.fromisoformat(sys.argv[4]) - if len(sys.argv) == 6: - end_date = datetime.date.fromisoformat(sys.argv[5]) - else: - end_date = datetime.date.today() - x = int(start[0]) - y = int(start[1]) - w = int(end[0]) - x + 1 - h = int( end[1]) - y + 1 - loop = asyncio.get_event_loop() - if not os.path.exists('./timelapse'): - os.mkdir('./timelapse') - loop.run_until_complete(get_area(canvas, x, y, w, h, start_date, end_date)) - print("Done!") - print("to create a timelapse from it:") - print("ffmpeg -framerate 15 -f image2 -i timelapse/t%d.png -c:v libvpx-vp9 -pix_fmt yuva420p output.webm") - print("example with scaling *3 and audio track:") - print("ffmpeg -i ./audio.mp3 -framerate 8 -f image2 -i timelapse/t%d.png -map 0:a -map 1:v -vf scale=iw*3:-1 -shortest -c:v libvpx-vp9 -c:a libvorbis -pix_fmt yuva420p output.webm") - print("lossless example:") - print("ffmpeg -framerate 15 -f image2 -i timelapse/t%d.png -c:v libvpx-vp9 -pix_fmt yuv444p -qmin 0 -qmax 0 -lossless 1 -an output.webm") + end_date = datetime.date.today() + x = int(start[0]) + y = int(start[1]) + w = int(end[0]) - x + 1 + h = int( end[1]) - y + 1 + if not os.path.exists('./timelapse'): + os.mkdir('./timelapse') + await get_area(canvas_id, canvas, x, y, w, h, start_date, end_date) + print("Done!") + print("to create a timelapse from it:") + print("ffmpeg -framerate 15 -f image2 -i timelapse/t%d.png -c:v libvpx-vp9 -pix_fmt yuva420p output.webm") + print("example with scaling *3 and audio track:") + print("ffmpeg -i ./audio.mp3 -framerate 8 -f image2 -i timelapse/t%d.png -map 0:a -map 1:v -vf scale=iw*3:-1 -shortest -c:v libvpx-vp9 -c:a libvorbis -pix_fmt yuva420p output.webm") + print("lossless example:") + print("ffmpeg -framerate 15 -f image2 -i timelapse/t%d.png -c:v libvpx-vp9 -pix_fmt yuv444p -qmin 0 -qmax 0 -lossless 1 -an output.webm") + +if __name__ == "__main__": + asyncio.run(main())