Created
April 20, 2024 02:57
-
-
Save 0187773933/bb2968438c14f359b5ec603984e55408 to your computer and use it in GitHub Desktop.
Downloads Miro Board Backup
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
#!/usr/bin/env python3 | |
import sys | |
import time | |
import json | |
import requests | |
from pprint import pprint | |
import mimetypes | |
from tqdm import tqdm | |
from concurrent.futures import ThreadPoolExecutor | |
from box import Box | |
from pathlib import Path | |
import concurrent.futures | |
import time | |
from ratelimit import rate_limited | |
# https://developers.miro.com/docs/rest-api-build-your-first-hello-world-app#step-1-create-your-app-in-miro | |
# https://miro.com/app/settings/user-profile/apps/#asdf | |
TOKEN = "asdf" | |
def write_json( file_path , python_object ): | |
with open( file_path , 'w', encoding='utf-8' ) as f: | |
json.dump( python_object , f , ensure_ascii=False , indent=4 ) | |
def read_json( file_path ): | |
with open( file_path ) as f: | |
return json.load( f ) | |
def get_file_extension( content_type ): | |
extension = mimetypes.guess_extension( content_type ) | |
if extension: | |
return extension | |
# return ".bin" | |
return ".pdf" | |
def get_boards(): | |
global TOKEN | |
headers = { | |
'accept': 'application/json, text/plain, */*', | |
'Authorization': f"Bearer {TOKEN}" | |
} | |
base_url = "https://api.miro.com/v2/boards" | |
boards_data = [] | |
def fetch_page(url): | |
response = requests.get(url, headers=headers) | |
response.raise_for_status() | |
return response.json() | |
# Start with the first page | |
url = base_url | |
total = 0 | |
while url: | |
data = fetch_page(url) | |
total = data["total"] | |
print( f"Downloaded [ {len(boards_data)} ] of {total}" ) | |
for board in data['data']: | |
board_info = { | |
'name': board['name'], | |
'id': board['id'] | |
} | |
boards_data.append(board_info) | |
url = data['links'].get('next', None) | |
print( len( boards_data ) ) | |
time.sleep( 1 ) | |
return boards_data | |
def get_item_details( board_id , item_id ): | |
global TOKEN | |
headers = { | |
'accept': 'application/json, text/plain, */*', | |
'Authorization': f"Bearer {TOKEN}" | |
} | |
url = f"https://api.miro.com/v2/boards/{board_id}/items/{item_id}" | |
response = requests.get( url , headers=headers ) | |
response.raise_for_status() | |
json_result = response.json() | |
return json_result | |
def get_items_on_board( board_id ): | |
global TOKEN | |
headers = { | |
'accept': 'application/json, text/plain, */*', | |
'Authorization': f"Bearer {TOKEN}" | |
} | |
base_url = f"https://api.miro.com/v2/boards/{board_id}/items" | |
boards_data = [] | |
cursor = False | |
def fetch_page(url): | |
if cursor: | |
response = requests.get( url , headers=headers , params={ "cursor" : cursor } ) | |
else: | |
response = requests.get( url , headers=headers ) | |
print( response.text ) | |
response.raise_for_status() | |
return response.json() | |
# Start with the first page | |
url = base_url | |
total = 0 | |
while url: | |
data = fetch_page(base_url) | |
total = data["total"] | |
print( f"Downloaded [ {len(boards_data)} ] of {total}" ) | |
boards_data.extend( data['data'] ) | |
# write_json( BoardName , boards_data ) | |
url = data['links'].get('next', None) | |
if "cursor" in data: | |
cursor = data['cursor'] | |
print( len( boards_data ) ) | |
time.sleep( 6 ) | |
return boards_data | |
@rate_limited( calls=1900 , period=60 ) | |
def download_resource_with_redirect( url , id , save_path ): | |
global TOKEN | |
headers = {'Authorization': f"Bearer {TOKEN}"} | |
with requests.get( url , stream=True , headers=headers ) as r: | |
if r.status_code == 429: | |
print( "Rate Limited ...." ) | |
print( r.text ) | |
return | |
if r.status_code == 200: | |
content_type = r.headers.get( "content-type" ) | |
file_extension = get_file_extension( content_type ) | |
file_name = f"{id}.{file_extension}" | |
output_file_with_extension = save_path.joinpath( id + file_extension ) | |
if Path(output_file_with_extension).is_file(): | |
print(f"File already exists: {output_file_with_extension}") | |
return | |
total_size = int(r.headers.get('content-length', 0)) | |
block_size = 1024 | |
t = tqdm(total=total_size, unit='iB', unit_scale=True) | |
with open(str(output_file_with_extension), 'wb') as f: | |
for data in r.iter_content(block_size): | |
t.update(len(data)) | |
f.write(data) | |
t.close() | |
# if total_size != 0 and t.n != total_size: | |
if total_size != 0 and t.n != total_size: | |
print("ERROR, something went wrong") | |
print( t.n , total_size ) | |
# else: | |
# print(f"File downloaded and saved as: {output_file_with_extension}") | |
elif r.status_code == 307: | |
redirect_url = r.headers.get('location') | |
print(f"Received 307 Temporary Redirect. Following the redirect to: {redirect_url}") | |
download_resource_with_redirect(redirect_url, id , save_path) | |
else: | |
print(f"Unexpected status code: {r.status_code}, response content: {r.content}") | |
def find_frame_for_image( image_item , frames ): | |
image_x, image_y = image_item['position']['x'], image_item['position']['y'] | |
image_width, image_height = image_item['geometry']['width'], image_item['geometry']['height'] | |
image_origin = image_item['position']['origin'] | |
for frame in frames: | |
frame_x, frame_y = frame['position']['x'], frame['position']['y'] | |
frame_width, frame_height = frame['geometry']['width'], frame['geometry']['height'] | |
frame_origin = frame['position']['origin'] | |
# Adjust the image position based on its origin | |
if image_origin == 'center': | |
image_x -= image_width / 2 | |
image_y -= image_height / 2 | |
# Adjust the frame position based on its origin | |
if frame_origin == 'center': | |
frame_x -= frame_width / 2 | |
frame_y -= frame_height / 2 | |
# Check if the image is within the frame | |
if ( | |
frame_x <= image_x <= frame_x + frame_width and | |
frame_y <= image_y <= frame_y + frame_height and | |
image_width <= frame_width and | |
image_height <= frame_height | |
): | |
return frame | |
return None | |
if __name__ == "__main__": | |
# 1.) Get All Boards | |
# boards = get_boards() | |
# write_json( "miro_boards.json" , boards ) | |
# 2.) Get all items on a Board | |
board_name = "TMP" | |
board_id = "asdf=" # you can get the id from the embed url | |
board_data = get_items_on_board( board_id ) | |
write_json( f"{board_name}.json" , board_data ) | |
# 3.) Download actual Board Data ( images , documents ) | |
board = read_json( f"{board_name}.json" ) | |
frames = [] | |
images = [] | |
documents = [] | |
embeds = [] | |
# // cards , shapes , sticky_notes , text ( we don't care about these for now ) | |
for x in board: | |
if x[ "type" ] == "frame": | |
frames.append( x ) | |
elif x[ "type" ] == "image": | |
images.append( x ) | |
elif x[ "type" ] == "document": | |
documents.append( x ) | |
elif x[ "type" ] == "embed": | |
embeds.append( x ) | |
print( f"Frames = {len( frames )}" ) | |
print( f"Images = {len( images )}" ) | |
print( f"Documents = {len( documents )}" ) | |
print( f"Embeds = {len( embeds )}" ) | |
save_path = Path.cwd().joinpath( f"{board_name}-attachments" ) | |
save_path.mkdir( parents=True , exist_ok=True ) | |
total_images = len( images ) | |
for i , item in enumerate( images ): | |
url = item[ "data" ][ "imageUrl" ].split( "?format" )[ 0 ] + "?format=original&redirect=true" | |
print( f"Downloading Image [ {i+1} ] of {total_images} === {item[ 'id' ]} === {url}" ) | |
download_resource_with_redirect( url , item[ "id" ] , save_path ) | |
total_documents = len( documents ) | |
for i , item in enumerate( documents ): | |
url = item[ "data" ][ "documentUrl" ].split( "?" )[ 0 ] + "?redirect=true" | |
print( f"Downloading Document [ {i+1} ] of {total_documents} === {item[ 'id' ]} === {url}" ) | |
download_resource_with_redirect( url , item[ "id" ] , save_path ) | |
# 4.) Reconstruct into HTML Canvas |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment