├── .gitignore ├── Api ├── api.py ├── constants.py ├── data.py ├── extras.json ├── extras.py ├── extras.txt ├── osu_wiki.sh ├── readme.md ├── requirements.txt └── update.py ├── api.py ├── assets └── osu.ico ├── autopy2exe.json ├── buen.py ├── buffer.py ├── build.sh ├── constants.py ├── data.py ├── database.py ├── download.py ├── entity.py ├── gui.py ├── gui_extra.py ├── main.py ├── misc.py ├── oauth.py ├── readme.md ├── requirements.txt ├── scraper.py ├── setup.py └── strings.py /.gitignore: -------------------------------------------------------------------------------- 1 | sensitive_data.py 2 | __pycache__/ 3 | build/ 4 | osu!.db 5 | collection.db 6 | tournament.json 7 | mappack.json 8 | osu-assistant.data 9 | /Api/osu-wiki/ 10 | /output/ -------------------------------------------------------------------------------- /Api/api.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_restful import Resource, Api, reqparse 3 | import data 4 | import time 5 | from threading import Thread 6 | 7 | app = Flask(__name__) 8 | api = Api(app) 9 | 10 | def start(): 11 | app.run(debug=False) 12 | 13 | def update_data(): 14 | while True: 15 | data.update_tournaments() 16 | data.update_mappacks() 17 | time.sleep(60) 18 | 19 | class Tournaments(Resource): 20 | def get(self): 21 | # example: localhost:80?gamemode=m 22 | # parser = reqparse.RequestParser() 23 | # type=parser.add_argument("type", type=int, location='args') 24 | # gamemode=parser.add_argument("gamemode", type=int, location='args') 25 | return data.tournament_json 26 | 27 | class Mappack(Resource): 28 | def get(self): 29 | # example: localhost:80?status=r&gamemode=m 30 | # parser = reqparse.RequestParser() 31 | # type=parser.add_argument("status", type=int, location='args') 32 | # gamemode=parser.add_argument("gamemode", type=int, location='args') 33 | return data.mappack_json 34 | 35 | api.add_resource(Tournaments, '/tournament', endpoint='tournament') 36 | api.add_resource(Mappack, '/mappack', endpoint='mappack') 37 | 38 | if __name__ == '__main__': 39 | p1=Thread(target=update_data) 40 | p1.daemon=True 41 | p1.start() 42 | start() -------------------------------------------------------------------------------- /Api/constants.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobermilk/osu-assistant/0b10aa6e8a75824b9e3138e4acaa4c15731f7047/Api/constants.py -------------------------------------------------------------------------------- /Api/data.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import time 4 | import requests 5 | import subprocess 6 | from bs4 import BeautifulSoup, SoupStrainer 7 | from urlextract import URLExtract 8 | 9 | # Data 10 | tournament_json={} 11 | mappack_json={} 12 | mappack_data={} 13 | 14 | def update_tournaments(): 15 | global tournament_json 16 | urlextractor=URLExtract() 17 | proc=subprocess.run([os.path.join(os.getcwd(), 'osu_wiki.sh')], check=True, capture_output=True, text=True) 18 | should_update=proc.stdout 19 | if should_update == "1": 20 | # How to parse markdown 101 21 | # list of (tournament_name, beatmaps) 22 | tournaments={} 23 | tournament_dir=os.path.join(os.getcwd(),"osu-wiki", "wiki", "Tournaments") 24 | with open(os.path.join(tournament_dir,"en.md"), "r") as f: 25 | items=[x for x in f.read().split("\n") if len(x)>3 and x[:3]=='| ['] 26 | for item in items: 27 | item=item.split("](") 28 | beatmaps=[] 29 | tournament_name=item[0][3:] 30 | tournament_tag=item[1][:item[1].find(")")] 31 | tournament_source_key=tournament_tag.replace('/', '-') 32 | dir=os.path.join(tournament_dir, tournament_tag) 33 | fcontent=open(os.path.join(dir, "en.md")).read() 34 | urls=urlextractor.find_urls(fcontent) 35 | osu_urls=[x for x in urls if "beatmapsets" in x] 36 | beatconnect_urls=[x for x in urls if "beatconnect" in x] 37 | for url in osu_urls: 38 | url=url.replace('#', '/').split("/") 39 | if url[-3]!="osu.ppy.sh": 40 | beatmaps.append((url[-3], url[-1], url[-2])) # beatmapset_id, beatmap_id, gamemode 41 | else: 42 | beatmaps.append((url[-1], None, None)) 43 | for url in beatconnect_urls: 44 | beatmaps.append((url.split("/")[-2], None, None)) 45 | tournaments[tournament_source_key]=[tournament_name, beatmaps] 46 | tournament_json=json.dumps(tournaments) 47 | 48 | with open("tournament.json", "w") as f: 49 | f.write(json.dumps(tournament_json)) 50 | 51 | def pausechamp(r): 52 | if r.status_code != 200: 53 | raise Exception() 54 | time.sleep(5) 55 | 56 | def update_mappacks(): 57 | global mappack_json 58 | mappacks={} 59 | sites = [ 60 | "https://osu.ppy.sh/beatmaps/packs?type=standard&page={}", 61 | "https://osu.ppy.sh/beatmaps/packs?type=chart&page={}", 62 | "https://osu.ppy.sh/beatmaps/packs?type=theme&page={}", 63 | "https://osu.ppy.sh/beatmaps/packs?type=artist&page={}", 64 | ] 65 | 66 | for i, site in enumerate(sites): 67 | r = requests.get(site.format(1)) 68 | pausechamp(r) 69 | soup = BeautifulSoup(r.text, "html.parser") 70 | page_cnt = int(soup.find_all("a", {"class": "pagination-v2__link"})[-2].text) 71 | pack_ids=[] 72 | for page_num in range(1, page_cnt + 1): 73 | # get pack titles 74 | r=requests.get(site.format(page_num)) 75 | pausechamp(r) 76 | soup = BeautifulSoup(r.text, "html.parser") 77 | packs = soup.find_all("a", {"class", "beatmap-pack__header js-accordion__item-header"}) 78 | beatmaps=[] 79 | for pack in packs: 80 | try: 81 | soup=BeautifulSoup(r.text, "lxml", parse_only=SoupStrainer('a')) 82 | mappack_id=pack['href'].split("/")[-1] 83 | r=requests.get("https://osu.ppy.sh/beatmaps/packs/{}/raw".format(mappack_id)) 84 | pausechamp(r) 85 | soup=BeautifulSoup(r.text, "lxml", parse_only=SoupStrainer('a')) 86 | urls=[x['href'] for x in soup if x.has_attr('href')] 87 | urls.pop(0) # remove mediafire link 88 | for url in urls: 89 | url=url.replace('#', '/').split("/") 90 | if url[-3]!="osu.ppy.sh": 91 | beatmaps.append((url[-3], url[-1], url[-2])) # beatmapset_id, beatmap_id, gamemode 92 | else: 93 | beatmaps.append((url[-1], None, None)) 94 | except: 95 | pass 96 | beatmaps.append([pack.find("div", {"class", "beatmap-pack__name"}).getText(), beatmaps]) 97 | 98 | pack_ids.append(beatmaps) 99 | mappacks[i]=pack_ids 100 | mappack_json=json.dumps(mappacks) 101 | with open("mappack.json", "w") as f: 102 | f.write(json.dumps(mappacks)) -------------------------------------------------------------------------------- /Api/extras.json: -------------------------------------------------------------------------------- 1 | {"SOFT-6": ["Springtime Osu!mania Free-for-all Tournament 6", [["1456779", "2997896", "mania"], ["1395676", "2880761", "mania"], ["1554627", "3552261", "mania"], ["1473890", "3061470", "mania"], ["1445730", "3439520", "mania"], ["1726407", "3555245", "mania"], ["1210416", "2519885", "mania"], ["1738667", "3553580", "mania"], ["1711751", "3497681", "mania"], ["1043983", "2182529", "mania"], ["1691684", "3456772", "mania"], ["1738003", "3552265", "mania"], ["1738595", "3553471", "mania"], ["1332427", "2971707", "mania"], ["1716737", "3566313", "mania"], ["1433708", "3401102", "mania"], ["1742468", "3562095", "mania"], ["1742466", "3562090", "mania"], ["1716026", "3506434", "mania"], ["1732317", "3555235", "mania"], ["1374143", "2840694", "mania"], ["1742617", "3562410", "mania"], ["1742494", "3562153", "mania"], ["1592955", "3253451", "mania"], ["1743589", "3564434", "mania"], ["1561114", "3271022", "mania"], ["1665853", "3400906", "mania"], ["1743586", "3564430", "mania"], ["1538061", "3144683", "mania"], ["1747908", "3575284", "mania"], ["1599384", "3266499", "mania"], ["1684011", "3440885", "mania"], ["1748681", "3576941", "mania"], ["1716930", "3513551", "mania"], ["1438897", "3114627", "mania"], ["1748968", "3577582", "mania"], ["1645612", "3359076", "mania"], ["1749442", "3578805", "mania"], ["1698324", "3470046", "mania"], ["1601476", "3575285", "mania"], ["1747998", "3575492", "mania"], ["1684765", "3442316", "mania"], ["1644115", "3355979", "mania"], ["1478749", "3033721", "mania"], ["1753753", "3589150", "mania"], ["1446657", "2979763", "mania"], ["1691775", "3456955", "mania"], ["1543786", "3155498", "mania"], ["1754534", "3590552", "mania"], ["1753543", "3602083", "mania"], ["1654112", "3376274", "mania"], ["1753466", "3588370", "mania"], ["1338625", "2772922", "mania"], ["1663547", "3588596", "mania"], ["1551057", "3589487", "mania"], ["1549666", "3181476", "mania"], ["1693630", "3460738", "mania"], ["1753464", "3588365", "mania"], ["1687858", "3590282", "mania"], ["1754350", "3590176", "mania"], ["1759864", "3601901", "mania"], ["1759474", "3600991", "mania"], ["1613480", "3294142", "mania"], ["1757210", "3595938", "mania"], ["1758267", "3598252", "mania"], ["1400999", "3599142", "mania"], ["1632108", "3331565", "mania"], ["860461", "1800326", "mania"], ["1757208", "3595933", "mania"], ["1759394", "3600842", "mania"], ["1458667", "3601962", "mania"], ["1264250", "3601608", "mania"], ["1759390", "3600837", "mania"], ["1758063", "3597792", "mania"], ["1755294", "3645191", "mania"], ["796368", "1672273", "mania"], ["1752950", "3587304", "mania"], ["1714022", "3502314", "mania"], ["1607175", "3281799", "mania"], ["1661338", "3392120", "mania"], ["1570575", "3407304", "mania"], ["1567335", "3202329", "mania"], ["1013400", "2121101", "mania"], ["1144083", "2388838", "mania"], ["1672422", "3415961", "mania"], ["1714023", "3502315", "mania"], ["1713404", "3501159", "mania"], ["1714020", "3502311", "mania"], ["1722701", "3520822", "mania"], ["1630846", "3330710", "mania"], ["1563146", "3192075", "mania"], ["1397098", "2883272", "mania"], ["1545890", "3520829", "mania"], ["1666577", "3402883", "mania"], ["1722744", "3520919", "mania"], ["1722578", "3571372", "mania"], ["1627077", "3321847", "mania"], ["1665688", "3405712", "mania"], ["1723186", "3521751", "mania"], ["1717148", "3509031", "mania"], ["1417394", "2920589", "mania"], ["1457545", "3005312", "mania"], ["1685404", "3444426", "mania"], ["1626036", "3543458", "mania"], ["1619954", "3542995", "mania"], ["1576670", "3218998", "mania"], ["1497081", "3143239", "mania"], ["1728049", "3531267", "mania"], ["1288004", "3543523", "mania"], ["1733418", "3543019", "mania"], ["1667776", "3405870", "mania"], ["1130027", "2429180", "mania"], ["1708945", "3491985", "mania"], ["1708946", "3491986", "mania"], ["1709088", "3492350", "mania"], ["1708943", "3491980", "mania"]]]} -------------------------------------------------------------------------------- /Api/extras.py: -------------------------------------------------------------------------------- 1 | import json 2 | maps={} 3 | maps_entry=["Springtime Osu!mania Free-for-all Tournament 6"] 4 | mapplist=[] 5 | with open("extras.txt", "r") as f: 6 | links=[x.split()[0] for x in f.readlines()] 7 | for link in links: 8 | l=link.replace("#", "/").split("/") 9 | mapplist.append([l[-3], l[-1], l[-2]]) 10 | maps_entry.append(mapplist) 11 | maps["SOFT-6"]=maps_entry 12 | with open("extras.json", "w") as f: 13 | json.dump(maps,f) 14 | 15 | -------------------------------------------------------------------------------- /Api/extras.txt: -------------------------------------------------------------------------------- 1 | https://osu.ppy.sh/beatmapsets/1456779#mania/2997896 | E-Type - Set The World On Fire (Sped Up Ver.) · beatmap info | osu! 2 | https://osu.ppy.sh/beatmapsets/1395676#mania/2880761 | goreshit - thinking of you · beatmap info | osu! 3 | https://osu.ppy.sh/beatmapsets/1554627#mania/3552261 | Falcom Sound Team jdk - Inevitable Struggle · beatmap info | osu! 4 | https://osu.ppy.sh/beatmapsets/1473890#mania/3061470 | onumi - REGRET PART TWO · beatmap info | osu! 5 | https://osu.ppy.sh/beatmapsets/1445730#mania/3439520 | Billain - Specialist · beatmap info | osu! 6 | https://osu.ppy.sh/beatmapsets/1726407#mania/3555245 | Nightmare - Epileptic Crisis · beatmap info | osu! 7 | https://osu.ppy.sh/beatmapsets/1210416#mania/2519885 | Camellia - Maboroshi · beatmap info | osu! 8 | https://osu.ppy.sh/beatmapsets/1738667#mania/3553580 | MisoilePunch ~pan soe~ - MeTear'n TruX · beatmap info | osu! 9 | https://osu.ppy.sh/beatmapsets/1711751#mania/3497681 | s-don vs. Hino Isuka - Trrricksters!! · beatmap info | osu! 10 | https://osu.ppy.sh/beatmapsets/1043983#mania/2182529 | Kagetora. - Reimei · beatmap info | osu! 11 | https://osu.ppy.sh/beatmapsets/1691684#mania/3456772 | xelloscope - Apollo · beatmap info | osu! 12 | https://osu.ppy.sh/beatmapsets/1738003#mania/3552265 | IOSYS - Marisa wa Taihen na Mono wo Nusunde Ikimashita · beatmap info | osu! 13 | https://osu.ppy.sh/beatmapsets/1738595#mania/3553471 | Camellia - Dyscontrolled Galaxy ("Apoptosis" Long ver.) · beatmap info | osu! 14 | https://osu.ppy.sh/beatmapsets/1332427#mania/2971707 | SHIKI - Angelic Layer · beatmap info | osu! 15 | https://osu.ppy.sh/beatmapsets/1716737#mania/3566313 | Yuaru - Streaming Heart · beatmap info | osu! 16 | https://osu.ppy.sh/beatmapsets/1433708#mania/3401102 | DJ SHARPNEL - Black Chocolates · beatmap info | osu! 17 | https://osu.ppy.sh/beatmapsets/1742468#mania/3562095 | Tess - Justify My Love · beatmap info | osu! 18 | https://osu.ppy.sh/beatmapsets/1742466#mania/3562090 | Blacklolita - Sound Alchemist · beatmap info | osu! 19 | https://osu.ppy.sh/beatmapsets/1716026#mania/3506434 | Frums - Great Fury of Heaven · beatmap info | osu! 20 | https://osu.ppy.sh/beatmapsets/1732317#mania/3555235 | Nightmare - Boulafacet · beatmap info | osu! 21 | https://osu.ppy.sh/beatmapsets/1374143#mania/2840694 | Apo11o"TSUKUYOMI INOCHI"program vs. NeLiME OVERDRIVE - Re:VeLATiON Koudou to Hakai no Soushiro Tsubasa · beatmap info | osu! 22 | https://osu.ppy.sh/beatmapsets/1742617#mania/3562410 | MisomyL - Mukishitsu Sekai ni Irudori o (Cut Ver.) · beatmap info | osu! 23 | https://osu.ppy.sh/beatmapsets/1742494#mania/3562153 | xi - Angel's Ladder · beatmap info | osu! 24 | https://osu.ppy.sh/beatmapsets/1592955#mania/3253451 | Moon Symphony - Regulus · beatmap info | osu! 25 | https://osu.ppy.sh/beatmapsets/1743589#mania/3564434 | penoreri - Desperado Waltz · beatmap info | osu! 26 | https://osu.ppy.sh/beatmapsets/1561114#mania/3271022 | Frums - VIS::CRACKED · beatmap info | osu! 27 | https://osu.ppy.sh/beatmapsets/1665853#mania/3400906 | D-Cee - CITRUS MONSTER · beatmap info | osu! 28 | https://osu.ppy.sh/beatmapsets/1743586#mania/3564430 | ReeK - Galaxy Collision · beatmap info | osu! 29 | https://osu.ppy.sh/beatmapsets/1538061#mania/3144683 | E-Type - Russian Lullaby · beatmap info | osu! 30 | https://osu.ppy.sh/beatmapsets/1747908#mania/3575284 | Smiley - Destiny GAMMA · beatmap info | osu! 31 | https://osu.ppy.sh/beatmapsets/1599384#mania/3266499 | Falcom Sound Team jdk - Desert After Tears · beatmap info | osu! 32 | https://osu.ppy.sh/beatmapsets/1684011#mania/3440885 | Camellia - Bring Our Ignition Back · beatmap info | osu! 33 | https://osu.ppy.sh/beatmapsets/1748681#mania/3576941 | Dirtyphonics & Sullivan King - Vantablack · beatmap info | osu! 34 | https://osu.ppy.sh/beatmapsets/1716930#mania/3513551 | Camellia - Xeroa · beatmap info | osu! 35 | https://osu.ppy.sh/beatmapsets/1438897#mania/3114627 | Aquellex - Obligatory (Kurorak's Obligatory Destruction rmx) · beatmap info | osu! 36 | https://osu.ppy.sh/beatmapsets/1748968#mania/3577582 | technoplanet - Intuition · beatmap info | osu! 37 | https://osu.ppy.sh/beatmapsets/1645612#mania/3359076 | Kaneko Chiharu - Re:miniscence · beatmap info | osu! 38 | https://osu.ppy.sh/beatmapsets/1749442#mania/3578805 | seatrus - ILLEGAL LEGACY · beatmap info | osu! 39 | https://osu.ppy.sh/beatmapsets/1698324#mania/3470046 | Function Phantom - Integral Cube · beatmap info | osu! 40 | https://osu.ppy.sh/beatmapsets/1601476#mania/3575285 | Kaneko Chiharu - Lachryma · beatmap info | osu! 41 | https://osu.ppy.sh/beatmapsets/1747998#mania/3575492 | JAKAZiD - Slaughta · beatmap info | osu! 42 | https://osu.ppy.sh/beatmapsets/1684765#mania/3442316 | MEMODEMO - Extragalactic · beatmap info | osu! 43 | https://osu.ppy.sh/beatmapsets/1644115#mania/3355979 | Sot-C - Cursed Metamorph · beatmap info | osu! 44 | https://osu.ppy.sh/beatmapsets/1478749#mania/3033721 | The Lonely Island - Spring Break Anthem · beatmap info | osu! 45 | https://osu.ppy.sh/beatmapsets/1753753#mania/3589150 | Lime - Chronomia · beatmap info | osu! 46 | https://osu.ppy.sh/beatmapsets/1446657#mania/2979763 | DJ SHARPNEL - SHINE!! · beatmap info | osu! 47 | https://osu.ppy.sh/beatmapsets/1691775#mania/3456955 | Ototsugi Kanade - Shutai · beatmap info | osu! 48 | https://osu.ppy.sh/beatmapsets/1543786#mania/3155498 | dev-null - Goblin · beatmap info | osu! 49 | https://osu.ppy.sh/beatmapsets/1754534#mania/3590552 | missing | osu! 50 | https://osu.ppy.sh/beatmapsets/1753543#mania/3602083 | Pendulum - The Island - Pt. II (Dusk) · beatmap info | osu! 51 | https://osu.ppy.sh/beatmapsets/1654112#mania/3376274 | Renard - Terminal · beatmap info | osu! 52 | https://osu.ppy.sh/beatmapsets/1753466#mania/3588370 | Kaneko Chiharu - iLLness LiLin · beatmap info | osu! 53 | https://osu.ppy.sh/beatmapsets/1338625#mania/2772922 | Camellia - +ERABY+E CONNEC+10N · beatmap info | osu! 54 | https://osu.ppy.sh/beatmapsets/1663547#mania/3588596 | TJ.hangneil - Apollo · beatmap info | osu! 55 | https://osu.ppy.sh/beatmapsets/1551057#mania/3589487 | onumi - Spider Tank · beatmap info | osu! 56 | https://osu.ppy.sh/beatmapsets/1549666#mania/3181476 | paraoka feat. haru*nya - Floating Away · beatmap info | osu! 57 | https://osu.ppy.sh/beatmapsets/1693630#mania/3460738 | BlackY - LAMIA · beatmap info | osu! 58 | https://osu.ppy.sh/beatmapsets/1753464#mania/3588365 | xi - Elemental Creation -xiRemix- · beatmap info | osu! 59 | https://osu.ppy.sh/beatmapsets/1687858#mania/3590282 | LeaF - Kyouki Ranbu (extended ver.) · beatmap info | osu! 60 | https://osu.ppy.sh/beatmapsets/1754350#mania/3590176 | Gram vs. Camellia - Ragnarok · beatmap info | osu! 61 | https://osu.ppy.sh/beatmapsets/1759864#mania/3601901 | Freestylers - Cracks (Flux Pavilion Remix) ft. Belle Humble · beatmap info | osu! 62 | https://osu.ppy.sh/beatmapsets/1759474#mania/3600991 | katagiri - STRONG 280 · beatmap info | osu! 63 | https://osu.ppy.sh/beatmapsets/1613480#mania/3294142 | Falcom Sound Team jdk - Hard Desperation · beatmap info | osu! 64 | https://osu.ppy.sh/beatmapsets/1757210#mania/3595938 | sun3 - Messier 333 · beatmap info | osu! 65 | https://osu.ppy.sh/beatmapsets/1758267#mania/3598252 | Terminal 11 - Cold Heart · beatmap info | osu! 66 | https://osu.ppy.sh/beatmapsets/1400999#mania/3599142 | Script - Ideal Ratio 4-1-2.5 (Viticz Remix) · beatmap info | osu! 67 | https://osu.ppy.sh/beatmapsets/1632108#mania/3331565 | Terminal 11 - Fury · beatmap info | osu! 68 | https://osu.ppy.sh/beatmapsets/860461#mania/1800326 | DJ Myosuke - HARDCORE NO KOKOROE (Original Mix) · beatmap info | osu! 69 | https://osu.ppy.sh/beatmapsets/1757208#mania/3595933 | katagiri - Angel's Salad · beatmap info | osu! 70 | https://osu.ppy.sh/beatmapsets/1759394#mania/3600842 | technoplanet - Inscape [Extended Mix] · beatmap info | osu! 71 | https://osu.ppy.sh/beatmapsets/1458667#mania/3601962 | Silentroom vs Frums - Aegleseeker · beatmap info | osu! 72 | https://osu.ppy.sh/beatmapsets/1264250#mania/3601608 | Camellia - welcome to wobbling field · beatmap info | osu! 73 | https://osu.ppy.sh/beatmapsets/1759390#mania/3600837 | B-ko (Cv:Touyama Nao) - Nisemono Chuuihou · beatmap info | osu! 74 | https://osu.ppy.sh/beatmapsets/1758063#mania/3597792 | 69 de 74 - LWVIIX · beatmap info | osu! 75 | https://osu.ppy.sh/beatmapsets/1755294#mania/3645191 | Camellia - Blackmagik Blazing · beatmap info | osu! 76 | https://osu.ppy.sh/beatmapsets/796368#mania/1672273 | TCY Force - Pantscada · beatmap info | osu! 77 | https://osu.ppy.sh/beatmapsets/1752950#mania/3587304 | Aquellex - Starflight · beatmap info | osu! 78 | https://osu.ppy.sh/beatmapsets/1714022#mania/3502314 | The J. Arthur Keenes Band - Oprah Shmup · beatmap info | osu! 79 | https://osu.ppy.sh/beatmapsets/1607175#mania/3281799 | Hato - Alastor · beatmap info | osu! 80 | https://osu.ppy.sh/beatmapsets/1661338#mania/3392120 | ARM (IOSYS) - CHAOS Terror-Tech Mix · beatmap info | osu! 81 | https://osu.ppy.sh/beatmapsets/1570575#mania/3407304 | sylcmyk - sylcmyk's theme · beatmap info | osu! 82 | https://osu.ppy.sh/beatmapsets/1567335#mania/3202329 | Venetian Snares - Szamar Madar · beatmap info | osu! 83 | https://osu.ppy.sh/beatmapsets/1013400#mania/2121101 | Nekomata Gekidan - AsiaN distractive · beatmap info | osu! 84 | https://osu.ppy.sh/beatmapsets/1144083#mania/2388838 | kors k - Insane Techniques · beatmap info | osu! 85 | https://osu.ppy.sh/beatmapsets/1672422#mania/3415961 | Red Velvet - Russian Roulette · beatmap info | osu! 86 | https://osu.ppy.sh/beatmapsets/1714023#mania/3502315 | dj TAKA with NAOKI - Kakumei · beatmap info | osu! 87 | https://osu.ppy.sh/beatmapsets/1713404#mania/3501159 | lapix - Foolish Hero · beatmap info | osu! 88 | https://osu.ppy.sh/beatmapsets/1714020#mania/3502311 | Mameyudoufu - Quality Control · beatmap info | osu! 89 | https://osu.ppy.sh/beatmapsets/1722701#mania/3520822 | Falcom Sound Team jdk - GENS D'ARMES · beatmap info | osu! 90 | https://osu.ppy.sh/beatmapsets/1630846#mania/3330710 | LV.4 - -273.15 · beatmap info | osu! 91 | https://osu.ppy.sh/beatmapsets/1563146#mania/3192075 | Koloto - Fox Tales · beatmap info | osu! 92 | https://osu.ppy.sh/beatmapsets/1397098#mania/2883272 | Shirobon - Take Me to Pleasure Island · beatmap info | osu! 93 | https://osu.ppy.sh/beatmapsets/1545890#mania/3520829 | BEMANI Sound Team "Yvya" - Vitrum · beatmap info | osu! 94 | https://osu.ppy.sh/beatmapsets/1666577#mania/3402883 | HALLPBE.S - Bang Bang · beatmap info | osu! 95 | https://osu.ppy.sh/beatmapsets/1722744#mania/3520919 | Pegboard Nerds - Badboi · beatmap info | osu! 96 | https://osu.ppy.sh/beatmapsets/1722578#mania/3571372 | Dareharu - Flowering · beatmap info | osu! 97 | https://osu.ppy.sh/beatmapsets/1627077#mania/3321847 | IOSYS - Cirno no Perfect Sansuu Kyoushitsu · beatmap info | osu! 98 | https://osu.ppy.sh/beatmapsets/1665688#mania/3405712 | rejection - Signal (feat. Such) - Mameyudoufu Remix (osu! edit) · beatmap info | osu! 99 | https://osu.ppy.sh/beatmapsets/1723186#mania/3521751 | BT (feat. JC Chasez) - Simply Being Loved (Somnambulist) · beatmap info | osu! 100 | https://osu.ppy.sh/beatmapsets/1717148#mania/3509031 | Ametsuchi Enikki - Reimei Sketchbook · beatmap info | osu! 101 | https://osu.ppy.sh/beatmapsets/1417394#mania/2920589 | Lime - Replica · beatmap info | osu! 102 | https://osu.ppy.sh/beatmapsets/1457545#mania/3005312 | DJ SHARPNEL - Flower Forever · beatmap info | osu! 103 | https://osu.ppy.sh/beatmapsets/1685404#mania/3444426 | Kei Toriki - Utopia · beatmap info | osu! 104 | https://osu.ppy.sh/beatmapsets/1626036#mania/3543458 | leroy - ricky bobby · beatmap info | osu! 105 | https://osu.ppy.sh/beatmapsets/1619954#mania/3542995 | Falcom Sound Team jdk - Animal Minimal · beatmap info | osu! 106 | https://osu.ppy.sh/beatmapsets/1576670#mania/3218998 | Feryquitous - Friction[!]Function · beatmap info | osu! 107 | https://osu.ppy.sh/beatmapsets/1497081#mania/3143239 | cosMo@BousouP - Deux Saint-Co Odyssey!! · beatmap info | osu! 108 | https://osu.ppy.sh/beatmapsets/1728049#mania/3531267 | Official HIGE DANdism - Bad for me (Dz'Xa's Amenpunk) · beatmap info | osu! 109 | https://osu.ppy.sh/beatmapsets/1288004#mania/3543523 | Kuroneko Dungeon - Ryoushi no Umi no Lindwurm · beatmap info | osu! 110 | https://osu.ppy.sh/beatmapsets/1733418#mania/3543019 | Memme - Force of RA · beatmap info | osu! 111 | https://osu.ppy.sh/beatmapsets/1667776#mania/3405870 | ZeRo-BaSs - Atomic Trigger 3 · beatmap info | osu! 112 | https://osu.ppy.sh/beatmapsets/1130027#mania/2429180 | Camellia feat. Mayumi Morinaga - re:||BIRTH · beatmap info | osu! 113 | https://osu.ppy.sh/beatmapsets/1708945#mania/3491985 | Culprate & Dictate - Pencilina · beatmap info | osu! 114 | https://osu.ppy.sh/beatmapsets/1708946#mania/3491986 | zabutom - Level X · beatmap info | osu! 115 | https://osu.ppy.sh/beatmapsets/1709088#mania/3492350 | Ashrount vs polysha - ZENITH · beatmap info | osu! 116 | https://osu.ppy.sh/beatmapsets/1708943#mania/3491980 | sasakure.UK VS Anayama Daisuke - ANU · beatmap info | osu! 117 | -------------------------------------------------------------------------------- /Api/osu_wiki.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Updates the osu wiki used to extract all official tournament data 3 | if [ ! -d "osu-wiki" ]; then 4 | git clone https://github.com/ppy/osu-wiki 5 | cd osu-wiki 6 | echo -n 1 7 | else 8 | cd osu-wiki 9 | if [ "$(git pull)" == "Already up to date." ]; then 10 | echo -n 0 11 | else 12 | echo -n 1 13 | fi 14 | fi -------------------------------------------------------------------------------- /Api/readme.md: -------------------------------------------------------------------------------- 1 | # About 2 | This is the api that osu! assistant uses for getting data for tournaments and and mappacks 3 | -------------------------------------------------------------------------------- /Api/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | beautifulsoup4 3 | requests 4 | urlextract 5 | flask_restful 6 | lxml -------------------------------------------------------------------------------- /Api/update.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup, SoupStrainer 2 | import requests 3 | import time 4 | import json 5 | import subprocess 6 | import os 7 | from bs4 import BeautifulSoup, SoupStrainer 8 | from urlextract import URLExtract 9 | 10 | def pausechamp(r): 11 | if r.status_code != 200: 12 | print("missing pack but what the hell lol") 13 | time.sleep(10) 14 | 15 | urlextractor=URLExtract() 16 | sync_cnt=0 17 | try: 18 | while True: 19 | sync_cnt+=1 20 | print("ouscollection sync #{}".format(sync_cnt)) 21 | # server 22 | # proc=subprocess.run(['bash', 'osu_wiki.sh'], check=True, capture_output=True, text=True) 23 | # should_update=proc.stdout 24 | should_update="1" 25 | if should_update == "1": 26 | # How to parse markdown 101 27 | # list of (tournament_name, beatmaps) 28 | with open("extras.json", "r") as f: 29 | tournaments=json.load(f) 30 | tournament_dir=os.path.join(os.getcwd(),"osu-wiki", "wiki", "Tournaments") 31 | with open(os.path.join(tournament_dir,"en.md"), "r") as f: 32 | items=[x for x in f.read().split("\n") if len(x)>3 and x[:3]=='| ['] 33 | # Peppy didnt update the wiki :sadge: 34 | items.append("| [Springtime osu!mania Free-for-all Tournament 4](SOFT/4) |") 35 | items.append("| [Springtime osu!mania Free-for-all Tournament 5](SOFT/5) |") 36 | for item in items: 37 | item=item.split("](") 38 | beatmaps=[] 39 | tournament_name=item[0][3:] 40 | tournament_tag=item[1][:item[1].find(")")] 41 | tournament_source_key=tournament_tag.replace('/', '-') 42 | dir=os.path.join(tournament_dir, tournament_tag) 43 | fcontent=open(os.path.join(dir, "en.md")).read() 44 | urls=urlextractor.find_urls(fcontent) 45 | osu_urls=[x for x in urls if "beatmapsets" in x] 46 | beatconnect_urls=[x for x in urls if "beatconnect" in x] 47 | if len(osu_urls)>0: 48 | for url in osu_urls: 49 | url=url.replace('#', '/').split("/") 50 | if url[-3]!="osu.ppy.sh": 51 | beatmaps.append((url[-3], url[-1], url[-2])) # beatmapset_id, beatmap_id, gamemode 52 | else: 53 | beatmaps.append((url[-1], None, None)) 54 | for url in beatconnect_urls: 55 | beatmaps.append((url.split("/")[-2], None, None)) 56 | tournaments[tournament_source_key]=[tournament_name, beatmaps] 57 | try: 58 | with open("tournament.json", "w") as f: 59 | json.dump(tournaments, f) 60 | except: 61 | pass 62 | print("updated tournament.json") 63 | 64 | # server 65 | # with open("mappack.json", "r") as f: 66 | # mappacks=json.load(f) 67 | mappacks=dict() 68 | sites = [ 69 | "https://osu.ppy.sh/beatmaps/packs?type=standard&page={}", 70 | "https://osu.ppy.sh/beatmaps/packs?type=chart&page={}", 71 | "https://osu.ppy.sh/beatmaps/packs?type=theme&page={}", 72 | "https://osu.ppy.sh/beatmaps/packs?type=artist&page={}", 73 | ] 74 | 75 | for i, site in enumerate(sites): 76 | r = requests.get(site.format(1)) 77 | pausechamp(r) 78 | soup = BeautifulSoup(r.text, "html.parser") 79 | page_cnt = int(soup.find_all("a", {"class": "pagination-v2__link"})[-2].text) 80 | # server 81 | # pack_ids=mappacks[str(i)] 82 | pack_ids=dict() 83 | stop=False 84 | for page_num in range(1, page_cnt + 1): 85 | if stop: 86 | break 87 | # get pack titles 88 | r=requests.get(site.format(page_num)) 89 | pausechamp(r) 90 | soup = BeautifulSoup(r.text, "html.parser") 91 | packs = soup.find_all("a", {"class", "beatmap-pack__header js-accordion__item-header"}) 92 | for pack in packs: 93 | beatmaps=[] 94 | try: 95 | soup=BeautifulSoup(r.text, "lxml", parse_only=SoupStrainer('a')) 96 | mappack_id=pack['href'].split("/")[-1] 97 | if str(mappack_id) in pack_ids.keys(): 98 | # We can stop here, we got the rest 99 | stop=True 100 | break 101 | r=requests.get("https://osu.ppy.sh/beatmaps/packs/{}/raw".format(mappack_id)) 102 | pausechamp(r) 103 | soup=BeautifulSoup(r.text, "lxml", parse_only=SoupStrainer('a')) 104 | urls=[x['href'] for x in soup if x.has_attr('href')] 105 | urls.pop(0) # remove mediafire link 106 | for url in urls: 107 | url=url.replace('#', '/').split("/") 108 | # if url[-3]!="osu.ppy.sh": 109 | # beatmaps.append((url[-3], url[-1], url[-2])) # beatmapset_id, beatmap_id, gamemode 110 | # else: 111 | # beatmaps.append((url[-1], None, None)) 112 | if url[-1]!="Game_modifier": 113 | beatmaps.append(int(url[-1])) 114 | pack_ids[mappack_id]=[pack.find("div", {"class", "beatmap-pack__name"}).getText(), beatmaps] 115 | except: 116 | raise Exception(f"{pack['href']} mappack failed, the script will now terminate") 117 | mappacks[i]=pack_ids 118 | try: 119 | with open("mappack.json", "w") as f: 120 | json.dump(mappacks, f) 121 | except: 122 | pass 123 | print("updated mappack.json") 124 | 125 | # server 126 | # time.sleep(86400) # one day 127 | break 128 | except Exception as e: 129 | raise e 130 | -------------------------------------------------------------------------------- /api.py: -------------------------------------------------------------------------------- 1 | # for flask and osu api requests 2 | from pubsub import pub 3 | import time 4 | import requests 5 | import json 6 | import data, constants, oauth 7 | import webbrowser 8 | import asyncio 9 | 10 | # OSU API 11 | async def query_osu_user_beatmapsets(session, user_id, type): 12 | headers={ 13 | "Content-Type": "application/json", 14 | "Accept": "application/json", 15 | "Authorization": f"Bearer {get_token()}", 16 | } 17 | page=1 18 | jsons=[] 19 | while True: 20 | response=await session.get(f"{constants.OSU_API_URL}/users/{user_id}/beatmapsets/{type}?limit={page*100}&offset={(page-1)*100}", headers=headers) 21 | await asyncio.sleep(constants.api_get_interval) 22 | j=await response.json() # https://osu.ppy.sh/docs/index.html#get-user-beatmaps 23 | if str(j)=="[]": 24 | break 25 | jsons.append(j) 26 | page+=1 27 | return jsons 28 | 29 | 30 | # returns (hash, beatmapset_id) for validity check and use the output to write collections 31 | async def query_osu_beatmap(session, beatmap_id): 32 | headers={ 33 | "Content-Type": "application/json", 34 | "Accept": "application/json", 35 | "Authorization": f"Bearer {get_token()}", 36 | } 37 | 38 | response=await session.get(f"{constants.OSU_API_URL}/beatmaps/{beatmap_id}", headers=headers) 39 | await asyncio.sleep(constants.api_get_interval) 40 | j=await response.json() 41 | try: 42 | return j["checksum"], j["beatmapset_id"] 43 | except: 44 | # The beatmap is not hosted 45 | return None,None 46 | 47 | # check if beatmapset exist 48 | def query_osu_beatmapset(beatmapset_id): 49 | headers={ 50 | "Content-Type": "application/json", 51 | "Accept": "application/json", 52 | "Authorization": f"Bearer {get_token()}", 53 | } 54 | 55 | response=requests.get(f"{constants.OSU_API_URL}/beatmapsets/{beatmapset_id}", headers=headers) 56 | time.sleep(constants.api_get_interval) 57 | j=json.loads(response.text) 58 | return "error" in j 59 | 60 | def test_oauth(oauth): 61 | headers={ 62 | "Content-Type": "application/json", 63 | "Accept": "application/json", 64 | "Authorization": f"Bearer {oauth}" 65 | } 66 | response=requests.get(f"https://osu.ppy.sh/api/v2/beatmapsets/1", headers=headers) 67 | try: 68 | response.json()["artist"] 69 | except: 70 | return False 71 | return True 72 | 73 | def get_oauth(self): 74 | webbrowser.open(constants.oauth_url) 75 | oauth.ask_token() 76 | 77 | # Generate a oauth token 78 | def get_token(): 79 | if data.OAUTH_TOKEN is not None and test_oauth(data.OAUTH_TOKEN): 80 | return data.OAUTH_TOKEN 81 | else: 82 | get_oauth(None) 83 | pub.sendMessage("show.dialog", msg="Api access is required and is not granted. Please go to your web browser and click allow") 84 | return None 85 | 86 | async def check_cookies(session): 87 | url=constants.osu_beatmap_url_download.format(1) 88 | settings=data.Settings 89 | cookie={"XSRF-TOKEN":settings.xsrf_token,"osu_session":settings.osu_session} 90 | osu_header={ "referer":constants.osu_beatmap_url.format(1) } 91 | try: 92 | async with session.head(url, allow_redirects=True, cookies=cookie, headers=osu_header) as s: 93 | if s.status==200: 94 | settings.valid_osu_cookies=True 95 | except: 96 | settings.valid_osu_cookies=False 97 | pub.sendMessage("show.dialog", msg="Invalid XSRF-TOKEN or osu_session provided") -------------------------------------------------------------------------------- /assets/osu.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobermilk/osu-assistant/0b10aa6e8a75824b9e3138e4acaa4c15731f7047/assets/osu.ico -------------------------------------------------------------------------------- /autopy2exe.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "auto-py-to-exe-configuration_v1", 3 | "pyinstallerOptions": [ 4 | { 5 | "optionDest": "noconfirm", 6 | "value": true 7 | }, 8 | { 9 | "optionDest": "filenames", 10 | "value": "C:/Users/morga/Desktop/osu-assistant/main.py" 11 | }, 12 | { 13 | "optionDest": "onefile", 14 | "value": true 15 | }, 16 | { 17 | "optionDest": "console", 18 | "value": false 19 | }, 20 | { 21 | "optionDest": "icon_file", 22 | "value": "C:/Users/morga/Desktop/osu-assistant/assets/osu.ico" 23 | }, 24 | { 25 | "optionDest": "ascii", 26 | "value": false 27 | }, 28 | { 29 | "optionDest": "clean_build", 30 | "value": false 31 | }, 32 | { 33 | "optionDest": "strip", 34 | "value": false 35 | }, 36 | { 37 | "optionDest": "noupx", 38 | "value": false 39 | }, 40 | { 41 | "optionDest": "disable_windowed_traceback", 42 | "value": false 43 | }, 44 | { 45 | "optionDest": "embed_manifest", 46 | "value": true 47 | }, 48 | { 49 | "optionDest": "uac_admin", 50 | "value": false 51 | }, 52 | { 53 | "optionDest": "uac_uiaccess", 54 | "value": false 55 | }, 56 | { 57 | "optionDest": "win_private_assemblies", 58 | "value": false 59 | }, 60 | { 61 | "optionDest": "win_no_prefer_redirects", 62 | "value": false 63 | }, 64 | { 65 | "optionDest": "bootloader_ignore_signals", 66 | "value": false 67 | }, 68 | { 69 | "optionDest": "argv_emulation", 70 | "value": false 71 | }, 72 | { 73 | "optionDest": "datas", 74 | "value": "C:/Users/morga/Desktop/osu-assistant/assets/osu.ico;." 75 | } 76 | ], 77 | "nonPyinstallerOptions": { 78 | "increaseRecursionLimit": true, 79 | "manualArguments": "" 80 | } 81 | } -------------------------------------------------------------------------------- /buen.py: -------------------------------------------------------------------------------- 1 | import os 2 | import data 3 | import database 4 | import misc 5 | from strings import generate_random_name 6 | 7 | # Walks the specified subdirectory to a specified level 8 | def walklevel(some_dir, level=1): 9 | some_dir = some_dir.rstrip(os.path.sep) 10 | assert os.path.isdir(some_dir) 11 | num_sep = some_dir.count(os.path.sep) 12 | for root, dirs, files in os.walk(some_dir): 13 | yield root, dirs, files 14 | num_sep_this = root.count(os.path.sep) 15 | if num_sep + level <= num_sep_this: 16 | del dirs[:] 17 | 18 | # Buen 19 | def generate_beatmap(selections, current_collections): 20 | settings=data.Settings 21 | osudb=database.create_osudb2() 22 | new_folder=os.path.join(settings.osu_install_folder, "Songs", generate_random_name(8)) 23 | while os.path.isdir(new_folder): 24 | new_folder=os.path.join(settings.osu_install_folder, "Songs", generate_random_name(8)) 25 | 26 | os.mkdir(new_folder) 27 | if len(selections) > 0: 28 | for i, collection in enumerate(current_collections["collections"]): 29 | if i in selections: 30 | for checksum in collection["hashes"]: 31 | beatmap_id, song_title, mapper, folder = osudb[checksum] 32 | for dirpath, dirnames, filenames in misc.walklevel(os.path.join(settings.osu_install_folder, "Songs", folder)): 33 | for filename in filenames: 34 | filename=str(filename) 35 | if filename.endswith(".osu"): 36 | lines=None # file lines 37 | audio_file=None # AudioFilename 38 | title=(None, None) # Title, TitleUnicode 39 | artist=(None, None) # Artist, ArtistUnicode 40 | diff_name=None # Version 41 | found=False 42 | with open(os.path.join(settings.osu_install_folder, "Songs", folder, filename), "r") as f: 43 | lines=f.readlines() 44 | if "BeatmapID" and beatmap_id in lines: 45 | found=True 46 | if "Title" and song_title[0] in lines and "Creator" and mapper in lines: 47 | found=True 48 | # Now we extract the data 49 | if found: 50 | for line in lines: 51 | line=line.replace(":", " ") 52 | l=line.split() # l[0] = key, l[-1]=value 53 | 54 | if len(l)>1: 55 | if l[0]=="AudioFilename": 56 | audio_file=l[-1] 57 | if l[0]=="Title": 58 | title[0]=l[-1] 59 | if l[0]=="TitleUnicode": 60 | title[1]=l[-1] 61 | if l[0]=="Artist": 62 | artist[0]=l[-1] 63 | if l[0]=="ArtistUnicode": 64 | artist[1]=l[-1] 65 | if l[0]=="Version": 66 | diff_name=l[-1] 67 | print(audio_file, title, artist, diff_name) 68 | # if found and title!=None and audio_file!=None and artist!=None and diff_name!=None : 69 | # dest=os.path.join(settings.osu_install_folder, "Songs", new_folder) 70 | # while os.path.isfile(generate_random_name()) 71 | # shutil.copyfile(os.path.join(settings.osu_install_folder, "Songs", folder, audio_file),os.path.join(dest, )) 72 | # with open(os.path.join(dest, f"{artist} - {title} (osu-assistant) {diff_name}"), "w+") as f: 73 | # for line in lines: 74 | # line=line.replace(":", " ") 75 | # l=line.split() # l[0] = key, l[-1]=value 76 | 77 | # if len(l)>1: 78 | # if l=="AudioFilename:": 79 | 80 | # if l=="Title:": 81 | 82 | # if l=="TitleUnicode:": 83 | 84 | # if l=="Creator:": 85 | 86 | # if l=="BeatmapID:": 87 | # f.write("BeatmapID:0") 88 | 89 | # if l=="BeatmapSetID:": 90 | # f.write("BeatmapSetID:-1") -------------------------------------------------------------------------------- /buffer.py: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/jaasonw/osu-db-tools/blob/master/buffer.py 2 | import struct 3 | 4 | async def read_bool(buffer) -> bool: 5 | return struct.unpack(" int: 8 | return struct.unpack(" int: 11 | return struct.unpack(" int: 14 | return struct.unpack(" float: 17 | return struct.unpack(" float: 20 | return struct.unpack(" int: 23 | return struct.unpack(" str: 40 | strlen = 0 41 | strflag = await read_ubyte(buffer) 42 | if (strflag == 0x0b): 43 | strlen = 0 44 | shift = 0 45 | # uleb128 46 | # https://en.wikipedia.org/wiki/LEB128 47 | while True: 48 | byte = await read_ubyte(buffer) 49 | strlen |= ((byte & 0x7F) << shift) 50 | if (byte & (1 << 7)) == 0: 51 | break 52 | shift += 7 53 | if skip: 54 | await buffer.read(strlen) 55 | else: 56 | return (struct.unpack("<" + str(strlen) + "s", await buffer.read(strlen))[0]).decode("utf-8") 57 | class WriteBuffer: 58 | def __init__(self): 59 | self.offset = 0 60 | self.data = b"" 61 | 62 | def write_bool(self, data: bool): 63 | self.data += struct.pack(" 0): 85 | self.write_ubyte(0x0b) 86 | strlen = b"" 87 | value = len(data) 88 | while value != 0: 89 | byte = (value & 0x7F) 90 | value >>= 7 91 | if (value != 0): 92 | byte |= 0x80 93 | strlen += struct.pack(" maps=diff(job.beatmapsetids , db.get_data()) 257 | job_queue=[] 258 | for source_key, source in data.Sources.read(): 259 | downloads=misc.diff_local_and_source(source) 260 | job_queue.append(Job(source_key, downloads)) 261 | 262 | self.job_queue=job_queue 263 | pub.sendMessage("update.activity") 264 | 265 | def get_job_cnt(self): 266 | return len(self.job_queue) 267 | 268 | async def start_jobs(self): 269 | # refresh --> job_queue.pop() -> download(maps) -> write_collections -> progressbar+=1 270 | initial_job_cnt=self.get_job_cnt() 271 | success=1 272 | collections={} 273 | while self.get_job_cnt() > 0: 274 | job=self.job_queue.pop(0) 275 | job_source_key=job.get_job_source_key() 276 | job_source=data.Sources.get_source(job_source_key) 277 | if isinstance(job_source, UserpageSource) or isinstance(job_source, TournamentSource) or isinstance(job_source, OsucollectorSource) or isinstance(job_source, OsuweblinksSource): 278 | collections[job_source_key]=[x[2] for x in job_source.get_available_beatmaps() if x[2] != None] 279 | pub.sendMessage("update.progress", value=0, range=0, progress_message=f"Downloading {job_source_key} ({initial_job_cnt-self.get_job_cnt()}/{initial_job_cnt} jobs)") 280 | pub.sendMessage("enable.job_toggle_button") 281 | success=await misc.do_job(job) 282 | if data.cancel_jobs_toggle: 283 | break # Terminate all jobs 284 | pub.sendMessage("update.activity") 285 | # Check last success to see if we should show the No pending downloads 286 | if success: 287 | await database.update_collections(collections) 288 | pub.sendMessage("update.progress", value=None, range=None, progress_message=f"{initial_job_cnt} jobs completed successfully") 289 | pub.sendMessage("update.activity") 290 | pub.sendMessage("reset.job_toggle_button_text") 291 | pub.sendMessage("enable.job_toggle_button") 292 | if initial_job_cnt>0: 293 | pub.sendMessage("show.dialog", msg="Downloads complete! Open osu and check your collections") 294 | else: 295 | data.cancel_jobs_toggle=False 296 | pub.sendMessage("update.progress", value=None, range=None, progress_message=f"Cancelled running job") 297 | await self.refresh() 298 | pub.sendMessage("update.activity") 299 | pub.sendMessage("enable.job_toggle_button") 300 | 301 | 302 | # Settings 303 | class Settings: 304 | def __init__(self): 305 | self.osu_install_folder=None 306 | self.download_on_start=False 307 | self.download_from_osu=False 308 | self.xsrf_token="" 309 | self.osu_session="" 310 | self.download_interval=1000 311 | 312 | # not user inputs 313 | self.valid_osu_directory=False 314 | self.valid_oauth=False 315 | self.valid_osu_cookies=False -------------------------------------------------------------------------------- /gui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ########################################################################### 4 | ## Python code generated with wxFormBuilder (version 3.10.1-0-g8feb16b3) 5 | ## http://www.wxformbuilder.org/ 6 | ## 7 | ## PLEASE DO *NOT* EDIT THIS FILE! 8 | ########################################################################### 9 | 10 | import wx 11 | import wx.xrc 12 | import wx.adv 13 | 14 | ########################################################################### 15 | ## Class Main 16 | ########################################################################### 17 | 18 | class Main ( wx.Frame ): 19 | 20 | def __init__( self, parent ): 21 | wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = u"osu! assistant", pos = wx.DefaultPosition, size = wx.Size( 1143,757 ), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL ) 22 | 23 | self.SetSizeHints( wx.DefaultSize, wx.DefaultSize ) 24 | 25 | bSizer1 = wx.BoxSizer( wx.VERTICAL ) 26 | 27 | self.m_tabs = wx.Notebook( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) 28 | self.m_tabs.SetFont( wx.Font( 10, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Arial" ) ) 29 | 30 | self.m_panel_sources = wx.Panel( self.m_tabs, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) 31 | bSizer4 = wx.BoxSizer( wx.VERTICAL ) 32 | 33 | self.m_source_list = wx.Listbook( self.m_panel_sources, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LB_DEFAULT ) 34 | self.m_source_list.SetFont( wx.Font( 14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 35 | 36 | 37 | bSizer4.Add( self.m_source_list, 1, wx.ALL|wx.EXPAND, 5 ) 38 | 39 | bSizer241 = wx.BoxSizer( wx.HORIZONTAL ) 40 | 41 | self.m_add_source = wx.Button( self.m_panel_sources, wx.ID_ANY, u" Add source ", wx.DefaultPosition, wx.Size( -1,60 ), 0 ) 42 | self.m_add_source.SetFont( wx.Font( 21, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 43 | self.m_add_source.SetBackgroundColour( wx.Colour( 255, 121, 184 ) ) 44 | 45 | bSizer241.Add( self.m_add_source, 1, wx.ALL, 5 ) 46 | 47 | 48 | bSizer4.Add( bSizer241, 0, wx.EXPAND, 5 ) 49 | 50 | 51 | self.m_panel_sources.SetSizer( bSizer4 ) 52 | self.m_panel_sources.Layout() 53 | bSizer4.Fit( self.m_panel_sources ) 54 | self.m_tabs.AddPage( self.m_panel_sources, u"Sources", True ) 55 | self.m_panel_activity = wx.Panel( self.m_tabs, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) 56 | activity_box = wx.BoxSizer( wx.VERTICAL ) 57 | 58 | self.m_staticText23 = wx.StaticText( self.m_panel_activity, wx.ID_ANY, u"Download queue", wx.DefaultPosition, wx.DefaultSize, 0 ) 59 | self.m_staticText23.Wrap( -1 ) 60 | 61 | self.m_staticText23.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 62 | 63 | activity_box.Add( self.m_staticText23, 0, wx.TOP|wx.RIGHT|wx.LEFT, 5 ) 64 | 65 | m_activity_listChoices = [] 66 | self.m_activity_list = wx.ListBox( self.m_panel_activity, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, m_activity_listChoices, 0 ) 67 | activity_box.Add( self.m_activity_list, 1, wx.ALL|wx.EXPAND, 5 ) 68 | 69 | self.m_activity_progress = wx.StaticText( self.m_panel_activity, wx.ID_ANY, u"No active downloads!", wx.DefaultPosition, wx.DefaultSize, 0 ) 70 | self.m_activity_progress.Wrap( -1 ) 71 | 72 | self.m_activity_progress.SetFont( wx.Font( 16, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 73 | 74 | activity_box.Add( self.m_activity_progress, 0, wx.ALL|wx.EXPAND, 5 ) 75 | 76 | self.m_progressbar = wx.Gauge( self.m_panel_activity, wx.ID_ANY, 100, wx.DefaultPosition, wx.Size( -1,30 ), wx.GA_HORIZONTAL ) 77 | self.m_progressbar.SetValue( 0 ) 78 | activity_box.Add( self.m_progressbar, 0, wx.BOTTOM|wx.RIGHT|wx.LEFT|wx.EXPAND, 5 ) 79 | 80 | bSizer6 = wx.BoxSizer( wx.HORIZONTAL ) 81 | 82 | self.m_toggle_downloading = wx.Button( self.m_panel_activity, wx.ID_ANY, u"Start Downloading", wx.DefaultPosition, wx.Size( -1,60 ), 0 ) 83 | self.m_toggle_downloading.SetFont( wx.Font( 21, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 84 | self.m_toggle_downloading.SetBackgroundColour( wx.Colour( 255, 121, 184 ) ) 85 | 86 | bSizer6.Add( self.m_toggle_downloading, 1, wx.ALL, 5 ) 87 | 88 | 89 | activity_box.Add( bSizer6, 0, wx.EXPAND, 5 ) 90 | 91 | 92 | self.m_panel_activity.SetSizer( activity_box ) 93 | self.m_panel_activity.Layout() 94 | activity_box.Fit( self.m_panel_activity ) 95 | self.m_tabs.AddPage( self.m_panel_activity, u"Activity", False ) 96 | self.m_panel_settings = wx.Panel( self.m_tabs, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) 97 | bSizer25 = wx.BoxSizer( wx.VERTICAL ) 98 | 99 | bSizer26 = wx.BoxSizer( wx.HORIZONTAL ) 100 | 101 | self.m_staticText4 = wx.StaticText( self.m_panel_settings, wx.ID_ANY, u"osu install folder:\n(example: C:\\Users\\foo\\AppData\\Local\\osu!)", wx.DefaultPosition, wx.DefaultSize, 0 ) 102 | self.m_staticText4.Wrap( -1 ) 103 | 104 | self.m_staticText4.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 105 | 106 | bSizer26.Add( self.m_staticText4, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5 ) 107 | 108 | self.m_osu_dir = wx.DirPickerCtrl( self.m_panel_settings, wx.ID_ANY, wx.EmptyString, u"Select your osu install folder", wx.DefaultPosition, wx.DefaultSize, wx.DIRP_DEFAULT_STYLE ) 109 | self.m_osu_dir.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 110 | 111 | bSizer26.Add( self.m_osu_dir, 1, wx.ALL, 5 ) 112 | 113 | 114 | bSizer25.Add( bSizer26, 0, wx.EXPAND, 5 ) 115 | 116 | self.m_autodownload_toggle = wx.CheckBox( self.m_panel_settings, wx.ID_ANY, u"Automatically start downloading from sources on application start", wx.DefaultPosition, wx.DefaultSize, 0 ) 117 | self.m_autodownload_toggle.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 118 | 119 | bSizer25.Add( self.m_autodownload_toggle, 0, wx.ALL, 5 ) 120 | 121 | bSizer5511 = wx.BoxSizer( wx.HORIZONTAL ) 122 | 123 | 124 | bSizer25.Add( bSizer5511, 0, wx.EXPAND, 5 ) 125 | 126 | self.m_collapsiblePane1 = wx.CollapsiblePane( self.m_panel_settings, wx.ID_ANY, u"Advanced settings", wx.DefaultPosition, wx.DefaultSize, wx.CP_DEFAULT_STYLE ) 127 | self.m_collapsiblePane1.Collapse( False ) 128 | 129 | bSizer23 = wx.BoxSizer( wx.VERTICAL ) 130 | 131 | self.m_staticText9 = wx.StaticText( self.m_collapsiblePane1.GetPane(), wx.ID_ANY, u"Unless you know what you're doing, leave these alone", wx.DefaultPosition, wx.DefaultSize, 0 ) 132 | self.m_staticText9.Wrap( -1 ) 133 | 134 | bSizer23.Add( self.m_staticText9, 0, wx.ALL, 5 ) 135 | 136 | self.m_use_osu_mirror = wx.CheckBox( self.m_collapsiblePane1.GetPane(), wx.ID_ANY, u"Use osu website as mirror if chimu does not have beatmap (botting is against the osu terms of service, but there should be no issue with using this)", wx.DefaultPosition, wx.DefaultSize, 0 ) 137 | self.m_use_osu_mirror.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 138 | 139 | bSizer23.Add( self.m_use_osu_mirror, 0, wx.ALL, 5 ) 140 | 141 | bSizer26111 = wx.BoxSizer( wx.HORIZONTAL ) 142 | 143 | self.m_staticText4121 = wx.StaticText( self.m_collapsiblePane1.GetPane(), wx.ID_ANY, u"if option above is checked: \nobtain and fill (read help panel)\nXSRF_TOKEN and osu_session", wx.DefaultPosition, wx.DefaultSize, 0 ) 144 | self.m_staticText4121.Wrap( -1 ) 145 | 146 | self.m_staticText4121.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 147 | 148 | bSizer26111.Add( self.m_staticText4121, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5 ) 149 | 150 | self.m_settings_xsrf_token = wx.TextCtrl( self.m_collapsiblePane1.GetPane(), wx.ID_ANY, u"XSRF_TOKEN", wx.DefaultPosition, wx.DefaultSize, 0 ) 151 | self.m_settings_xsrf_token.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 152 | 153 | bSizer26111.Add( self.m_settings_xsrf_token, 1, wx.ALL, 5 ) 154 | 155 | self.m_settings_osu_session = wx.TextCtrl( self.m_collapsiblePane1.GetPane(), wx.ID_ANY, u"osu_session", wx.DefaultPosition, wx.DefaultSize, 0 ) 156 | self.m_settings_osu_session.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 157 | 158 | bSizer26111.Add( self.m_settings_osu_session, 1, wx.ALL, 5 ) 159 | 160 | 161 | bSizer23.Add( bSizer26111, 0, wx.EXPAND, 5 ) 162 | 163 | bSizer55 = wx.BoxSizer( wx.HORIZONTAL ) 164 | 165 | self.m_staticText411 = wx.StaticText( self.m_collapsiblePane1.GetPane(), wx.ID_ANY, u"download interval (ms): \ntoo low = rate limit\nrecommended: 1000", wx.DefaultPosition, wx.DefaultSize, 0 ) 166 | self.m_staticText411.Wrap( -1 ) 167 | 168 | self.m_staticText411.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 169 | 170 | bSizer55.Add( self.m_staticText411, 0, wx.ALL, 5 ) 171 | 172 | self.m_download_interval = wx.Slider( self.m_collapsiblePane1.GetPane(), wx.ID_ANY, 1000, 0, 2000, wx.DefaultPosition, wx.DefaultSize, wx.SL_HORIZONTAL|wx.SL_LABELS ) 173 | bSizer55.Add( self.m_download_interval, 1, wx.ALL, 5 ) 174 | 175 | 176 | bSizer23.Add( bSizer55, 0, wx.EXPAND, 5 ) 177 | 178 | self.m_export_to_beatmap = wx.Button( self.m_collapsiblePane1.GetPane(), wx.ID_ANY, u"Convert a in-game collection to beatmap", wx.DefaultPosition, wx.DefaultSize, 0 ) 179 | self.m_export_to_beatmap.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 180 | self.m_export_to_beatmap.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_WINDOW ) ) 181 | 182 | bSizer23.Add( self.m_export_to_beatmap, 0, wx.ALL|wx.EXPAND, 5 ) 183 | 184 | 185 | self.m_collapsiblePane1.GetPane().SetSizer( bSizer23 ) 186 | self.m_collapsiblePane1.GetPane().Layout() 187 | bSizer23.Fit( self.m_collapsiblePane1.GetPane() ) 188 | bSizer25.Add( self.m_collapsiblePane1, 1, wx.ALL|wx.EXPAND, 5 ) 189 | 190 | bSizer24 = wx.BoxSizer( wx.VERTICAL ) 191 | 192 | bSizer281 = wx.BoxSizer( wx.HORIZONTAL ) 193 | 194 | self.m_website = wx.Button( self.m_panel_settings, wx.ID_ANY, u"Wiki", wx.DefaultPosition, wx.DefaultSize, 0 ) 195 | self.m_website.SetFont( wx.Font( 14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 196 | self.m_website.SetBackgroundColour( wx.Colour( 255, 128, 64 ) ) 197 | 198 | bSizer281.Add( self.m_website, 0, wx.ALL|wx.ALIGN_BOTTOM, 5 ) 199 | 200 | self.m_discord = wx.Button( self.m_panel_settings, wx.ID_ANY, u"Discord", wx.DefaultPosition, wx.DefaultSize, 0 ) 201 | self.m_discord.SetFont( wx.Font( 14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 202 | self.m_discord.SetBackgroundColour( wx.Colour( 114, 137, 218 ) ) 203 | 204 | bSizer281.Add( self.m_discord, 0, wx.ALL|wx.ALIGN_BOTTOM, 5 ) 205 | 206 | self.m_github = wx.Button( self.m_panel_settings, wx.ID_ANY, u"Github", wx.DefaultPosition, wx.DefaultSize, 0 ) 207 | self.m_github.SetFont( wx.Font( 14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 208 | self.m_github.SetForegroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_WINDOW ) ) 209 | self.m_github.SetBackgroundColour( wx.Colour( 0, 0, 0 ) ) 210 | 211 | bSizer281.Add( self.m_github, 0, wx.ALL|wx.ALIGN_BOTTOM, 5 ) 212 | 213 | self.m_donate = wx.Button( self.m_panel_settings, wx.ID_ANY, u"Donate", wx.DefaultPosition, wx.DefaultSize, 0 ) 214 | self.m_donate.SetFont( wx.Font( 14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 215 | self.m_donate.SetForegroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNTEXT ) ) 216 | self.m_donate.SetBackgroundColour( wx.Colour( 128, 255, 255 ) ) 217 | 218 | bSizer281.Add( self.m_donate, 0, wx.ALL|wx.ALIGN_BOTTOM, 5 ) 219 | 220 | 221 | bSizer24.Add( bSizer281, 1, wx.ALIGN_CENTER_HORIZONTAL, 5 ) 222 | 223 | self.m_version = wx.StaticText( self.m_panel_settings, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) 224 | self.m_version.Wrap( -1 ) 225 | 226 | bSizer24.Add( self.m_version, 0, wx.ALL|wx.ALIGN_CENTER_HORIZONTAL, 5 ) 227 | 228 | 229 | bSizer25.Add( bSizer24, 1, wx.EXPAND, 5 ) 230 | 231 | self.m_save_settings = wx.Button( self.m_panel_settings, wx.ID_ANY, u"Save settings", wx.DefaultPosition, wx.Size( -1,60 ), 0 ) 232 | self.m_save_settings.SetFont( wx.Font( 21, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 233 | self.m_save_settings.SetBackgroundColour( wx.Colour( 255, 121, 184 ) ) 234 | 235 | bSizer25.Add( self.m_save_settings, 0, wx.ALL|wx.EXPAND, 5 ) 236 | 237 | 238 | self.m_panel_settings.SetSizer( bSizer25 ) 239 | self.m_panel_settings.Layout() 240 | bSizer25.Fit( self.m_panel_settings ) 241 | self.m_tabs.AddPage( self.m_panel_settings, u"Settings", False ) 242 | 243 | bSizer1.Add( self.m_tabs, 1, wx.EXPAND |wx.ALL, 5 ) 244 | 245 | 246 | self.SetSizer( bSizer1 ) 247 | self.Layout() 248 | 249 | self.Centre( wx.BOTH ) 250 | 251 | # Connect Events 252 | self.m_osu_dir.Bind( wx.EVT_DIRPICKER_CHANGED, self.update_osu_folder ) 253 | self.m_autodownload_toggle.Bind( wx.EVT_CHECKBOX, self.autodownload_toggle ) 254 | self.m_export_to_beatmap.Bind( wx.EVT_BUTTON, self.export_collection_to_beatmap ) 255 | self.m_website.Bind( wx.EVT_BUTTON, self.open_website ) 256 | self.m_discord.Bind( wx.EVT_BUTTON, self.open_discord ) 257 | self.m_github.Bind( wx.EVT_BUTTON, self.open_github ) 258 | self.m_donate.Bind( wx.EVT_BUTTON, self.open_donate ) 259 | self.m_save_settings.Bind( wx.EVT_BUTTON, self.save_settings ) 260 | 261 | def __del__( self ): 262 | pass 263 | 264 | 265 | # Virtual event handlers, override them in your derived class 266 | def update_osu_folder( self, event ): 267 | event.Skip() 268 | 269 | def autodownload_toggle( self, event ): 270 | event.Skip() 271 | 272 | def export_collection_to_beatmap( self, event ): 273 | event.Skip() 274 | 275 | def open_website( self, event ): 276 | event.Skip() 277 | 278 | def open_discord( self, event ): 279 | event.Skip() 280 | 281 | def open_github( self, event ): 282 | event.Skip() 283 | 284 | def open_donate( self, event ): 285 | event.Skip() 286 | 287 | def save_settings( self, event ): 288 | event.Skip() 289 | 290 | 291 | ########################################################################### 292 | ## Class AddSource 293 | ########################################################################### 294 | 295 | class AddSource ( wx.Frame ): 296 | 297 | def __init__( self, parent ): 298 | wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = u"Add source", pos = wx.DefaultPosition, size = wx.Size( 768,550 ), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL ) 299 | 300 | self.SetSizeHints( wx.DefaultSize, wx.DefaultSize ) 301 | 302 | bSizer1 = wx.BoxSizer( wx.VERTICAL ) 303 | 304 | self.m_notebook2 = wx.Notebook( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 ) 305 | self.m_notebook2.SetFont( wx.Font( 10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 306 | 307 | self.m_panel6 = wx.Panel( self.m_notebook2, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) 308 | bSizer7 = wx.BoxSizer( wx.VERTICAL ) 309 | 310 | bSizer35 = wx.BoxSizer( wx.HORIZONTAL ) 311 | 312 | self.m_staticText12 = wx.StaticText( self.m_panel6, wx.ID_ANY, u"Links of userpage links (add new line after a link)", wx.DefaultPosition, wx.DefaultSize, 0 ) 313 | self.m_staticText12.Wrap( -1 ) 314 | 315 | self.m_staticText12.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 316 | 317 | bSizer35.Add( self.m_staticText12, 0, wx.ALL|wx.ALIGN_BOTTOM, 5 ) 318 | 319 | self.m_subscribed_mappers = wx.Button( self.m_panel6, wx.ID_ANY, u" Get subscribed mappers ", wx.DefaultPosition, wx.DefaultSize, 0 ) 320 | self.m_subscribed_mappers.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 321 | self.m_subscribed_mappers.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_WINDOW ) ) 322 | 323 | bSizer35.Add( self.m_subscribed_mappers, 0, wx.TOP|wx.RIGHT|wx.LEFT, 5 ) 324 | 325 | 326 | bSizer7.Add( bSizer35, 0, wx.EXPAND, 5 ) 327 | 328 | self.m_userpages = wx.TextCtrl( self.m_panel6, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_AUTO_URL|wx.TE_MULTILINE ) 329 | self.m_userpages.SetFont( wx.Font( 12, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Arial" ) ) 330 | 331 | bSizer7.Add( self.m_userpages, 1, wx.ALL|wx.EXPAND, 5 ) 332 | 333 | bSizer10 = wx.BoxSizer( wx.HORIZONTAL ) 334 | 335 | self.m_user_top100 = wx.CheckBox( self.m_panel6, wx.ID_ANY, u"Top 100 plays (specify gamemode in url)", wx.DefaultPosition, wx.DefaultSize, 0 ) 336 | self.m_user_top100.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 337 | 338 | bSizer10.Add( self.m_user_top100, 0, wx.ALL, 5 ) 339 | 340 | self.m_user_favourites = wx.CheckBox( self.m_panel6, wx.ID_ANY, u"Favourites", wx.DefaultPosition, wx.DefaultSize, 0 ) 341 | self.m_user_favourites.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 342 | 343 | bSizer10.Add( self.m_user_favourites, 0, wx.ALL, 5 ) 344 | 345 | self.m_user_everything = wx.CheckBox( self.m_panel6, wx.ID_ANY, u"All beatmaps ever played", wx.DefaultPosition, wx.DefaultSize, 0 ) 346 | self.m_user_everything.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 347 | 348 | bSizer10.Add( self.m_user_everything, 0, wx.ALL, 5 ) 349 | 350 | 351 | bSizer7.Add( bSizer10, 0, wx.EXPAND, 5 ) 352 | 353 | bSizer101 = wx.BoxSizer( wx.HORIZONTAL ) 354 | 355 | self.m_user_ranked = wx.CheckBox( self.m_panel6, wx.ID_ANY, u"Ranked", wx.DefaultPosition, wx.DefaultSize, 0 ) 356 | self.m_user_ranked.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 357 | 358 | bSizer101.Add( self.m_user_ranked, 0, wx.ALL, 5 ) 359 | 360 | self.m_user_loved = wx.CheckBox( self.m_panel6, wx.ID_ANY, u"Loved", wx.DefaultPosition, wx.DefaultSize, 0 ) 361 | self.m_user_loved.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 362 | 363 | bSizer101.Add( self.m_user_loved, 0, wx.ALL, 5 ) 364 | 365 | self.m_user_pending = wx.CheckBox( self.m_panel6, wx.ID_ANY, u"Pending", wx.DefaultPosition, wx.DefaultSize, 0 ) 366 | self.m_user_pending.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 367 | 368 | bSizer101.Add( self.m_user_pending, 0, wx.ALL, 5 ) 369 | 370 | self.m_user_graveyarded = wx.CheckBox( self.m_panel6, wx.ID_ANY, u"Graveyarded", wx.DefaultPosition, wx.DefaultSize, 0 ) 371 | self.m_user_graveyarded.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 372 | 373 | bSizer101.Add( self.m_user_graveyarded, 0, wx.ALL, 5 ) 374 | 375 | 376 | bSizer7.Add( bSizer101, 0, wx.EXPAND, 5 ) 377 | 378 | self.m_add_userpage = wx.Button( self.m_panel6, wx.ID_ANY, u"Add Userpage(s)", wx.DefaultPosition, wx.Size( -1,60 ), 0 ) 379 | self.m_add_userpage.SetFont( wx.Font( 21, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 380 | self.m_add_userpage.SetBackgroundColour( wx.Colour( 255, 121, 184 ) ) 381 | 382 | bSizer7.Add( self.m_add_userpage, 0, wx.ALL|wx.EXPAND, 5 ) 383 | 384 | 385 | self.m_panel6.SetSizer( bSizer7 ) 386 | self.m_panel6.Layout() 387 | bSizer7.Fit( self.m_panel6 ) 388 | self.m_notebook2.AddPage( self.m_panel6, u"Userpage", True ) 389 | self.m_panel7 = wx.Panel( self.m_notebook2, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) 390 | bSizer14 = wx.BoxSizer( wx.VERTICAL ) 391 | 392 | self.m_tournament = wx.Listbook( self.m_panel7, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LB_DEFAULT ) 393 | self.m_tournament.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 394 | 395 | 396 | bSizer14.Add( self.m_tournament, 1, wx.EXPAND |wx.ALL, 5 ) 397 | 398 | self.m_add_tournament = wx.Button( self.m_panel7, wx.ID_ANY, u"Add Tournament", wx.DefaultPosition, wx.Size( -1,60 ), 0 ) 399 | self.m_add_tournament.SetFont( wx.Font( 21, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 400 | self.m_add_tournament.SetBackgroundColour( wx.Colour( 255, 121, 184 ) ) 401 | 402 | bSizer14.Add( self.m_add_tournament, 0, wx.ALL|wx.EXPAND, 5 ) 403 | 404 | 405 | self.m_panel7.SetSizer( bSizer14 ) 406 | self.m_panel7.Layout() 407 | bSizer14.Fit( self.m_panel7 ) 408 | self.m_notebook2.AddPage( self.m_panel7, u"Tournament", False ) 409 | self.m_panel8 = wx.Panel( self.m_notebook2, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) 410 | bSizer141 = wx.BoxSizer( wx.VERTICAL ) 411 | 412 | self.m_staticText41231 = wx.StaticText( self.m_panel8, wx.ID_ANY, u"Mappack section", wx.DefaultPosition, wx.DefaultSize, 0 ) 413 | self.m_staticText41231.Wrap( -1 ) 414 | 415 | self.m_staticText41231.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 416 | 417 | bSizer141.Add( self.m_staticText41231, 0, wx.TOP|wx.RIGHT|wx.LEFT, 5 ) 418 | 419 | m_mappack_sectionChoices = [ u"Standard", u"Spotlight", u"Theme", u"Artist/Album" ] 420 | self.m_mappack_section = wx.Choice( self.m_panel8, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, m_mappack_sectionChoices, 0 ) 421 | self.m_mappack_section.SetSelection( 0 ) 422 | self.m_mappack_section.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 423 | 424 | bSizer141.Add( self.m_mappack_section, 0, wx.ALL|wx.EXPAND, 5 ) 425 | 426 | self.m_staticText17 = wx.StaticText( self.m_panel8, wx.ID_ANY, u"Search results (hold control and click to select multiple packs)", wx.DefaultPosition, wx.DefaultSize, 0 ) 427 | self.m_staticText17.Wrap( -1 ) 428 | 429 | self.m_staticText17.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 430 | 431 | bSizer141.Add( self.m_staticText17, 0, wx.ALL, 5 ) 432 | 433 | m_mappack_listChoices = [] 434 | self.m_mappack_list = wx.ListBox( self.m_panel8, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, m_mappack_listChoices, wx.LB_MULTIPLE ) 435 | self.m_mappack_list.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 436 | 437 | bSizer141.Add( self.m_mappack_list, 1, wx.ALL|wx.EXPAND, 5 ) 438 | 439 | self.m_add_mappack = wx.Button( self.m_panel8, wx.ID_ANY, u"Add Mappack(s)", wx.DefaultPosition, wx.Size( -1,60 ), 0 ) 440 | self.m_add_mappack.SetFont( wx.Font( 21, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 441 | self.m_add_mappack.SetBackgroundColour( wx.Colour( 255, 121, 184 ) ) 442 | 443 | bSizer141.Add( self.m_add_mappack, 0, wx.ALL|wx.EXPAND, 5 ) 444 | 445 | 446 | self.m_panel8.SetSizer( bSizer141 ) 447 | self.m_panel8.Layout() 448 | bSizer141.Fit( self.m_panel8 ) 449 | self.m_notebook2.AddPage( self.m_panel8, u"Mappacks", False ) 450 | self.m_panel9 = wx.Panel( self.m_notebook2, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) 451 | bSizer241 = wx.BoxSizer( wx.VERTICAL ) 452 | 453 | self.m_staticText22 = wx.StaticText( self.m_panel9, wx.ID_ANY, u"osu collector collection links (add new line after a link)", wx.DefaultPosition, wx.DefaultSize, 0 ) 454 | self.m_staticText22.Wrap( -1 ) 455 | 456 | self.m_staticText22.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 457 | 458 | bSizer241.Add( self.m_staticText22, 0, wx.ALL, 5 ) 459 | 460 | self.m_osu_collector = wx.TextCtrl( self.m_panel9, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_AUTO_URL|wx.TE_MULTILINE ) 461 | self.m_osu_collector.SetFont( wx.Font( 12, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Arial" ) ) 462 | 463 | bSizer241.Add( self.m_osu_collector, 1, wx.ALL|wx.EXPAND, 5 ) 464 | 465 | self.m_add_osucollector = wx.Button( self.m_panel9, wx.ID_ANY, u"Add Collection(s)", wx.DefaultPosition, wx.Size( -1,60 ), 0 ) 466 | self.m_add_osucollector.SetFont( wx.Font( 21, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 467 | self.m_add_osucollector.SetBackgroundColour( wx.Colour( 255, 121, 184 ) ) 468 | 469 | bSizer241.Add( self.m_add_osucollector, 0, wx.ALL|wx.EXPAND, 5 ) 470 | 471 | 472 | self.m_panel9.SetSizer( bSizer241 ) 473 | self.m_panel9.Layout() 474 | bSizer241.Fit( self.m_panel9 ) 475 | self.m_notebook2.AddPage( self.m_panel9, u"osu!Collector", False ) 476 | self.m_panel10 = wx.Panel( self.m_notebook2, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) 477 | bSizer2411 = wx.BoxSizer( wx.VERTICAL ) 478 | 479 | self.m_staticText171 = wx.StaticText( self.m_panel10, wx.ID_ANY, u"In-game collection name", wx.DefaultPosition, wx.DefaultSize, 0 ) 480 | self.m_staticText171.Wrap( -1 ) 481 | 482 | self.m_staticText171.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 483 | 484 | bSizer2411.Add( self.m_staticText171, 0, wx.ALL, 5 ) 485 | 486 | self.m_osu_weblinks_key = wx.TextCtrl( self.m_panel10, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 ) 487 | self.m_osu_weblinks_key.SetFont( wx.Font( 12, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Arial" ) ) 488 | 489 | bSizer2411.Add( self.m_osu_weblinks_key, 0, wx.ALL|wx.EXPAND, 5 ) 490 | 491 | self.m_staticText221 = wx.StaticText( self.m_panel10, wx.ID_ANY, u"osu! website beatmap links (add new line after a link)", wx.DefaultPosition, wx.DefaultSize, 0 ) 492 | self.m_staticText221.Wrap( -1 ) 493 | 494 | self.m_staticText221.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 495 | 496 | bSizer2411.Add( self.m_staticText221, 0, wx.ALL, 5 ) 497 | 498 | self.m_osu_weblinks = wx.TextCtrl( self.m_panel10, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_AUTO_URL|wx.TE_MULTILINE ) 499 | self.m_osu_weblinks.SetFont( wx.Font( 12, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Arial" ) ) 500 | 501 | bSizer2411.Add( self.m_osu_weblinks, 1, wx.ALL|wx.EXPAND, 5 ) 502 | 503 | self.m_add_weblinks = wx.Button( self.m_panel10, wx.ID_ANY, u"Add Collection(s)", wx.DefaultPosition, wx.Size( -1,60 ), 0 ) 504 | self.m_add_weblinks.SetFont( wx.Font( 21, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 505 | self.m_add_weblinks.SetBackgroundColour( wx.Colour( 255, 121, 184 ) ) 506 | 507 | bSizer2411.Add( self.m_add_weblinks, 0, wx.ALL|wx.EXPAND, 5 ) 508 | 509 | 510 | self.m_panel10.SetSizer( bSizer2411 ) 511 | self.m_panel10.Layout() 512 | bSizer2411.Fit( self.m_panel10 ) 513 | self.m_notebook2.AddPage( self.m_panel10, u"Website Links", False ) 514 | 515 | bSizer1.Add( self.m_notebook2, 1, wx.EXPAND |wx.ALL, 5 ) 516 | 517 | 518 | self.SetSizer( bSizer1 ) 519 | self.Layout() 520 | 521 | self.Centre( wx.BOTH ) 522 | 523 | # Connect Events 524 | self.m_subscribed_mappers.Bind( wx.EVT_BUTTON, self.open_subscribed_mappers ) 525 | 526 | def __del__( self ): 527 | pass 528 | 529 | 530 | # Virtual event handlers, override them in your derived class 531 | def open_subscribed_mappers( self, event ): 532 | event.Skip() 533 | 534 | 535 | ########################################################################### 536 | ## Class ListPanel 537 | ########################################################################### 538 | 539 | class ListPanel ( wx.Panel ): 540 | 541 | def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.Size( 500,300 ), style = wx.TAB_TRAVERSAL, name = wx.EmptyString ): 542 | wx.Panel.__init__ ( self, parent, id = id, pos = pos, size = size, style = style, name = name ) 543 | 544 | bSizer31 = wx.BoxSizer( wx.VERTICAL ) 545 | 546 | m_listChoices = [] 547 | self.m_list = wx.ListBox( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, m_listChoices, 0 ) 548 | bSizer31.Add( self.m_list, 1, wx.ALL|wx.EXPAND, 5 ) 549 | 550 | 551 | self.SetSizer( bSizer31 ) 552 | self.Layout() 553 | 554 | # Connect Events 555 | self.m_list.Bind( wx.EVT_LISTBOX_DCLICK, self.open_beatmap_website ) 556 | 557 | def __del__( self ): 558 | pass 559 | 560 | 561 | # Virtual event handlers, override them in your derived class 562 | def open_beatmap_website( self, event ): 563 | event.Skip() 564 | 565 | 566 | ########################################################################### 567 | ## Class CollectionsSelection 568 | ########################################################################### 569 | 570 | class CollectionsSelection ( wx.Frame ): 571 | 572 | def __init__( self, parent ): 573 | wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = u"Select collections to export to beatmap", pos = wx.DefaultPosition, size = wx.Size( 500,300 ), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL ) 574 | 575 | self.SetSizeHints( wx.DefaultSize, wx.DefaultSize ) 576 | 577 | bSizer24 = wx.BoxSizer( wx.VERTICAL ) 578 | 579 | m_collections_selectionChoices = [] 580 | self.m_collections_selection = wx.ListBox( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, m_collections_selectionChoices, wx.LB_MULTIPLE ) 581 | bSizer24.Add( self.m_collections_selection, 1, wx.ALL|wx.EXPAND, 5 ) 582 | 583 | self.m_collections_select_btn = wx.Button( self, wx.ID_ANY, u"Export selected collection(s)", wx.DefaultPosition, wx.DefaultSize, 0 ) 584 | bSizer24.Add( self.m_collections_select_btn, 0, wx.ALL|wx.EXPAND, 5 ) 585 | 586 | 587 | self.SetSizer( bSizer24 ) 588 | self.Layout() 589 | 590 | self.Centre( wx.BOTH ) 591 | 592 | # Connect Events 593 | self.m_collections_select_btn.Bind( wx.EVT_BUTTON, self.export_collections_to_beatmap ) 594 | 595 | def __del__( self ): 596 | pass 597 | 598 | 599 | # Virtual event handlers, override them in your derived class 600 | def export_collections_to_beatmap( self, event ): 601 | event.Skip() 602 | 603 | 604 | ########################################################################### 605 | ## Class IntroWizard 606 | ########################################################################### 607 | 608 | class IntroWizard ( wx.adv.Wizard ): 609 | 610 | def __init__( self, parent ): 611 | wx.adv.Wizard.__init__ ( self, parent, id = wx.ID_ANY, title = wx.EmptyString, bitmap = wx.NullBitmap, pos = wx.DefaultPosition, style = wx.DEFAULT_DIALOG_STYLE|wx.MAXIMIZE_BOX|wx.STAY_ON_TOP ) 612 | 613 | self.SetSizeHints( wx.DefaultSize, wx.DefaultSize ) 614 | self.m_pages = [] 615 | 616 | self.m_wizPage1 = wx.adv.WizardPageSimple( self ) 617 | self.add_page( self.m_wizPage1 ) 618 | 619 | bSizer261 = wx.BoxSizer( wx.VERTICAL ) 620 | 621 | self.m_staticText181 = wx.StaticText( self.m_wizPage1, wx.ID_ANY, u"Welcome to osu! assistant", wx.DefaultPosition, wx.DefaultSize, 0 ) 622 | self.m_staticText181.Wrap( -1 ) 623 | 624 | self.m_staticText181.SetFont( wx.Font( 24, wx.FONTFAMILY_SCRIPT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, "Comic Sans MS" ) ) 625 | self.m_staticText181.SetMinSize( wx.Size( -1,50 ) ) 626 | 627 | bSizer261.Add( self.m_staticText181, 0, wx.ALL|wx.EXPAND, 5 ) 628 | 629 | self.m_staticline1 = wx.StaticLine( self.m_wizPage1, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ) 630 | bSizer261.Add( self.m_staticline1, 0, wx.EXPAND |wx.ALL, 5 ) 631 | 632 | self.m_staticText191 = wx.StaticText( self.m_wizPage1, wx.ID_ANY, u"Features:", wx.DefaultPosition, wx.DefaultSize, 0 ) 633 | self.m_staticText191.Wrap( -1 ) 634 | 635 | self.m_staticText191.SetFont( wx.Font( 18, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Arial" ) ) 636 | self.m_staticText191.SetMinSize( wx.Size( -1,50 ) ) 637 | 638 | bSizer261.Add( self.m_staticText191, 0, wx.ALL|wx.EXPAND, 5 ) 639 | 640 | self.m_staticText161 = wx.StaticText( self.m_wizPage1, wx.ID_ANY, u"• Bulk downloading from beatmap sources\n• Writing downloaded beatmaps to in-game collections\n• Beatmap sources allow future updates to be synced automatically", wx.DefaultPosition, wx.Size( 1000,400 ), 0 ) 641 | self.m_staticText161.Wrap( -1 ) 642 | 643 | self.m_staticText161.SetFont( wx.Font( 16, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Arial" ) ) 644 | 645 | bSizer261.Add( self.m_staticText161, 0, wx.ALL, 5 ) 646 | 647 | 648 | self.m_wizPage1.SetSizer( bSizer261 ) 649 | self.m_wizPage1.Layout() 650 | bSizer261.Fit( self.m_wizPage1 ) 651 | self.m_wizPage2 = wx.adv.WizardPageSimple( self ) 652 | self.add_page( self.m_wizPage2 ) 653 | 654 | bSizer26 = wx.BoxSizer( wx.VERTICAL ) 655 | 656 | self.m_staticText18 = wx.StaticText( self.m_wizPage2, wx.ID_ANY, u"Beatmap Sources", wx.DefaultPosition, wx.DefaultSize, 0 ) 657 | self.m_staticText18.Wrap( -1 ) 658 | 659 | self.m_staticText18.SetFont( wx.Font( 24, wx.FONTFAMILY_SCRIPT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, "Comic Sans MS" ) ) 660 | self.m_staticText18.SetMinSize( wx.Size( -1,50 ) ) 661 | 662 | bSizer26.Add( self.m_staticText18, 0, wx.ALL|wx.EXPAND, 5 ) 663 | 664 | self.m_staticline2 = wx.StaticLine( self.m_wizPage2, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ) 665 | bSizer26.Add( self.m_staticline2, 0, wx.EXPAND |wx.ALL, 5 ) 666 | 667 | self.m_staticText19 = wx.StaticText( self.m_wizPage2, wx.ID_ANY, u"osu! assistant adds beatmaps from:", wx.DefaultPosition, wx.DefaultSize, 0 ) 668 | self.m_staticText19.Wrap( -1 ) 669 | 670 | self.m_staticText19.SetFont( wx.Font( 18, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Arial" ) ) 671 | self.m_staticText19.SetMinSize( wx.Size( -1,50 ) ) 672 | 673 | bSizer26.Add( self.m_staticText19, 0, wx.ALL|wx.EXPAND, 5 ) 674 | 675 | self.m_staticText16 = wx.StaticText( self.m_wizPage2, wx.ID_ANY, u"• userpages on the osu website\n• tournaments mappools\n• mappacks\n• osu!Collector collection link\n", wx.DefaultPosition, wx.DefaultSize, 0 ) 676 | self.m_staticText16.Wrap( -1 ) 677 | 678 | self.m_staticText16.SetFont( wx.Font( 16, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 679 | 680 | bSizer26.Add( self.m_staticText16, 1, wx.ALL|wx.EXPAND, 5 ) 681 | 682 | 683 | self.m_wizPage2.SetSizer( bSizer26 ) 684 | self.m_wizPage2.Layout() 685 | bSizer26.Fit( self.m_wizPage2 ) 686 | self.m_wizPage3 = wx.adv.WizardPageSimple( self ) 687 | self.add_page( self.m_wizPage3 ) 688 | 689 | bSizer262 = wx.BoxSizer( wx.VERTICAL ) 690 | 691 | self.m_staticText182 = wx.StaticText( self.m_wizPage3, wx.ID_ANY, u"How to use?", wx.DefaultPosition, wx.DefaultSize, 0 ) 692 | self.m_staticText182.Wrap( -1 ) 693 | 694 | self.m_staticText182.SetFont( wx.Font( 24, wx.FONTFAMILY_SCRIPT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, "Comic Sans MS" ) ) 695 | self.m_staticText182.SetMinSize( wx.Size( -1,50 ) ) 696 | 697 | bSizer262.Add( self.m_staticText182, 0, wx.ALL|wx.EXPAND, 5 ) 698 | 699 | self.m_staticline3 = wx.StaticLine( self.m_wizPage3, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ) 700 | bSizer262.Add( self.m_staticline3, 0, wx.EXPAND |wx.ALL, 5 ) 701 | 702 | self.m_staticText192 = wx.StaticText( self.m_wizPage3, wx.ID_ANY, u"very ez:", wx.DefaultPosition, wx.DefaultSize, 0 ) 703 | self.m_staticText192.Wrap( -1 ) 704 | 705 | self.m_staticText192.SetFont( wx.Font( 18, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Arial" ) ) 706 | self.m_staticText192.SetMinSize( wx.Size( -1,50 ) ) 707 | 708 | bSizer262.Add( self.m_staticText192, 0, wx.ALL|wx.EXPAND, 5 ) 709 | 710 | self.m_staticText162 = wx.StaticText( self.m_wizPage3, wx.ID_ANY, u"1. Close osu!\n2. Add as many beatmaps sources as you want\n3. Start downloads \n4. Open osu! to see new collections", wx.DefaultPosition, wx.DefaultSize, 0 ) 711 | self.m_staticText162.Wrap( -1 ) 712 | 713 | self.m_staticText162.SetFont( wx.Font( 16, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 714 | 715 | bSizer262.Add( self.m_staticText162, 1, wx.ALL|wx.EXPAND, 5 ) 716 | 717 | 718 | self.m_wizPage3.SetSizer( bSizer262 ) 719 | self.m_wizPage3.Layout() 720 | bSizer262.Fit( self.m_wizPage3 ) 721 | self.m_wizPage4 = wx.adv.WizardPageSimple( self ) 722 | self.add_page( self.m_wizPage4 ) 723 | 724 | bSizer2621 = wx.BoxSizer( wx.VERTICAL ) 725 | 726 | self.m_staticText1821 = wx.StaticText( self.m_wizPage4, wx.ID_ANY, u"Before we begin...", wx.DefaultPosition, wx.DefaultSize, 0 ) 727 | self.m_staticText1821.Wrap( -1 ) 728 | 729 | self.m_staticText1821.SetFont( wx.Font( 24, wx.FONTFAMILY_SCRIPT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, "Comic Sans MS" ) ) 730 | self.m_staticText1821.SetMinSize( wx.Size( -1,50 ) ) 731 | 732 | bSizer2621.Add( self.m_staticText1821, 0, wx.ALL|wx.EXPAND, 5 ) 733 | 734 | self.m_staticline4 = wx.StaticLine( self.m_wizPage4, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL ) 735 | bSizer2621.Add( self.m_staticline4, 0, wx.EXPAND |wx.ALL, 5 ) 736 | 737 | self.m_staticText1921 = wx.StaticText( self.m_wizPage4, wx.ID_ANY, u"osu! assistant needs 2 things:", wx.DefaultPosition, wx.DefaultSize, 0 ) 738 | self.m_staticText1921.Wrap( -1 ) 739 | 740 | self.m_staticText1921.SetFont( wx.Font( 18, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Arial" ) ) 741 | self.m_staticText1921.SetMinSize( wx.Size( -1,50 ) ) 742 | 743 | bSizer2621.Add( self.m_staticText1921, 0, wx.ALL|wx.EXPAND, 5 ) 744 | 745 | bSizer263 = wx.BoxSizer( wx.HORIZONTAL ) 746 | 747 | self.m_staticText25 = wx.StaticText( self.m_wizPage4, wx.ID_ANY, u"1. osu install folder:\n(example:\nC:\\Users\\foo\\AppData\\Local\\osu!)", wx.DefaultPosition, wx.Size( 350,100 ), 0 ) 748 | self.m_staticText25.Wrap( -1 ) 749 | 750 | self.m_staticText25.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 751 | 752 | bSizer263.Add( self.m_staticText25, 0, wx.ALL, 5 ) 753 | 754 | self.m_osu_dir = wx.DirPickerCtrl( self.m_wizPage4, wx.ID_ANY, wx.EmptyString, u"Select your osu install folder", wx.DefaultPosition, wx.DefaultSize, wx.DIRP_DEFAULT_STYLE ) 755 | self.m_osu_dir.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 756 | 757 | bSizer263.Add( self.m_osu_dir, 1, wx.ALL, 5 ) 758 | 759 | 760 | bSizer2621.Add( bSizer263, 0, wx.EXPAND, 5 ) 761 | 762 | bSizer2611 = wx.BoxSizer( wx.HORIZONTAL ) 763 | 764 | self.m_oauth_btn = wx.Button( self.m_wizPage4, wx.ID_ANY, u"2. Grant assistant access to the osu! api", wx.DefaultPosition, wx.Size( -1,50 ), 0 ) 765 | self.m_oauth_btn.SetFont( wx.Font( 12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) ) 766 | self.m_oauth_btn.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_WINDOW ) ) 767 | 768 | bSizer2611.Add( self.m_oauth_btn, 1, wx.ALL, 5 ) 769 | 770 | 771 | bSizer2621.Add( bSizer2611, 0, wx.EXPAND, 5 ) 772 | 773 | 774 | self.m_wizPage4.SetSizer( bSizer2621 ) 775 | self.m_wizPage4.Layout() 776 | bSizer2621.Fit( self.m_wizPage4 ) 777 | self.Centre( wx.BOTH ) 778 | 779 | 780 | # Connect Events 781 | self.m_osu_dir.Bind( wx.EVT_DIRPICKER_CHANGED, self.update_osu_folder ) 782 | def add_page(self, page): 783 | if self.m_pages: 784 | previous_page = self.m_pages[-1] 785 | page.SetPrev(previous_page) 786 | previous_page.SetNext(page) 787 | self.m_pages.append(page) 788 | 789 | def __del__( self ): 790 | pass 791 | 792 | 793 | # Virtual event handlers, override them in your derived class 794 | def update_osu_folder( self, event ): 795 | event.Skip() 796 | 797 | 798 | -------------------------------------------------------------------------------- /gui_extra.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------------- # 2 | # PYBUSYINFO Control wxPython IMPLEMENTATION 3 | # Inspired By And Heavily Based On wxBusyInfo. 4 | # 5 | # Python Code By: 6 | # 7 | # Andrea Gavana, @ 10 December 2009 8 | # Latest Revision: 27 Dec 2012, 21.00 GMT 9 | # 10 | # 11 | # TODO List/Caveats 12 | # 13 | # 1. ? 14 | # 15 | # 16 | # For All Kind Of Problems, Requests Of Enhancements And Bug Reports, Please 17 | # Write To Me At: 18 | # 19 | # andrea.gavana@gmail.com 20 | # andrea.gavana@maerskoil.com 21 | # 22 | # Or, Obviously, To The wxPython Mailing List!!! 23 | # 24 | # Tags: phoenix-port, unittest, documented, py3-port 25 | # 26 | # End Of Comments 27 | # --------------------------------------------------------------------------- # 28 | 29 | """ 30 | :class:`~wx.lib.agw.pybusyinfo.PyBusyInfo` constructs a busy info window and displays a message in it. 31 | 32 | 33 | Description 34 | =========== 35 | 36 | :class:`PyBusyInfo` constructs a busy info window and displays a message in it. 37 | 38 | This class makes it easy to tell your user that the program is temporarily busy. 39 | Just create a :class:`PyBusyInfo` object, and within the current scope, a message window 40 | will be shown. 41 | 42 | For example:: 43 | 44 | busy = PyBusyInfo("Please wait, working...") 45 | 46 | for i in xrange(10000): 47 | DoACalculation() 48 | 49 | del busy 50 | 51 | 52 | It works by creating a window in the constructor, and deleting it in the destructor. 53 | You may also want to call :func:`Yield` () to refresh the window periodically (in case 54 | it had been obscured by other windows, for example). 55 | 56 | 57 | Usage 58 | ===== 59 | 60 | Usage example:: 61 | 62 | import wx 63 | import wx.lib.agw.pybusyinfo as PBI 64 | 65 | class MyFrame(wx.Frame): 66 | 67 | def __init__(self, parent): 68 | 69 | wx.Frame.__init__(self, parent, -1, "PyBusyInfo Demo") 70 | 71 | panel = wx.Panel(self) 72 | 73 | b = wx.Button(panel, -1, "Test PyBusyInfo ", (50,50)) 74 | self.Bind(wx.EVT_BUTTON, self.OnButton, b) 75 | 76 | 77 | def OnButton(self, event): 78 | 79 | message = "Please wait 5 seconds, working..." 80 | busy = PBI.PyBusyInfo(message, parent=self, title="Really Busy") 81 | 82 | wx.Yield() 83 | 84 | for indx in xrange(5): 85 | wx.MilliSleep(1000) 86 | 87 | del busy 88 | 89 | 90 | # our normal wxApp-derived class, as usual 91 | 92 | app = wx.App(0) 93 | 94 | frame = MyFrame(None) 95 | app.SetTopWindow(frame) 96 | frame.Show() 97 | 98 | app.MainLoop() 99 | 100 | 101 | 102 | Supported Platforms 103 | =================== 104 | 105 | :class:`PyBusyInfo` has been tested on the following platforms: 106 | * Windows (Windows XP). 107 | 108 | 109 | Window Styles 110 | ============= 111 | 112 | `No particular window styles are available for this class.` 113 | 114 | 115 | Events Processing 116 | ================= 117 | 118 | `No custom events are available for this class.` 119 | 120 | 121 | License And Version 122 | =================== 123 | 124 | :class:`PyBusyInfo` is distributed under the wxPython license. 125 | 126 | Latest Revision: Andrea Gavana @ 27 Dec 2012, 21.00 GMT 127 | 128 | Version 0.3 129 | 130 | """ 131 | 132 | # Version Info 133 | __version__ = "0.3" 134 | 135 | import wx 136 | 137 | _ = wx.GetTranslation 138 | 139 | 140 | class PyInfoFrame(wx.Frame): 141 | """ Base class for :class:`PyBusyInfo`. """ 142 | 143 | def __init__(self, parent, message, title, icon): 144 | """ 145 | Default class constructor. 146 | 147 | :param `parent`: the frame parent; 148 | :param `message`: the message to display in the :class:`PyBusyInfo`; 149 | :param `title`: the main :class:`PyBusyInfo` title; 150 | :param `icon`: an icon to draw as the frame icon, an instance of :class:`wx.Bitmap`. 151 | """ 152 | 153 | wx.Frame.__init__(self, parent, wx.ID_ANY, title, wx.DefaultPosition, 154 | wx.DefaultSize, wx.NO_BORDER|wx.FRAME_TOOL_WINDOW|wx.FRAME_SHAPED|wx.FRAME_FLOAT_ON_PARENT) 155 | 156 | panel = wx.Panel(self) 157 | panel.SetCursor(wx.HOURGLASS_CURSOR) 158 | 159 | self._message = message 160 | self._title = title 161 | self._icon = icon 162 | 163 | dc = wx.ClientDC(self) 164 | textWidth, textHeight, dummy = dc.GetFullMultiLineTextExtent(self._message) 165 | sizeText = wx.Size(textWidth, textHeight) 166 | 167 | self.SetClientSize((max(sizeText.x, 340) + 60, max(sizeText.y, 40) + 60)) 168 | # need to size the panel correctly first so that text.Centre() works 169 | panel.SetSize(self.GetClientSize()) 170 | 171 | # Bind the events to draw ourselves 172 | panel.Bind(wx.EVT_PAINT, self.OnPaint) 173 | panel.Bind(wx.EVT_ERASE_BACKGROUND, self.OnErase) 174 | 175 | self.Centre(wx.BOTH) 176 | 177 | # Create a non-rectangular region to set the frame shape 178 | size = self.GetSize() 179 | bmp = wx.Bitmap(size.x, size.y) 180 | dc = wx.BufferedDC(None, bmp) 181 | dc.SetBackground(wx.BLACK_BRUSH) 182 | dc.Clear() 183 | dc.SetPen(wx.BLACK_PEN) 184 | dc.DrawRoundedRectangle(0, 0, size.x, size.y, 12) 185 | r = wx.Region(bmp, wx.BLACK) 186 | # Store the non-rectangular region 187 | self.reg = r 188 | 189 | if wx.Platform == "__WXGTK__": 190 | self.Bind(wx.EVT_WINDOW_CREATE, self.SetBusyShape) 191 | else: 192 | self.SetBusyShape() 193 | 194 | # Add a custom bitmap at the top (if any) 195 | 196 | def SetBusyShape(self, event=None): 197 | """ 198 | Sets :class:`PyInfoFrame` shape using the region created from the bitmap. 199 | 200 | :param `event`: a :class:`wx.WindowCreateEvent` event (GTK only, as GTK supports setting 201 | the window shape only during window creation). 202 | """ 203 | 204 | self.SetShape(self.reg) 205 | if event: 206 | # GTK only 207 | event.Skip() 208 | 209 | 210 | def OnPaint(self, event): 211 | """ 212 | Handles the ``wx.EVT_PAINT`` event for :class:`PyInfoFrame`. 213 | 214 | :param `event`: a :class:`PaintEvent` to be processed. 215 | """ 216 | 217 | panel = event.GetEventObject() 218 | 219 | dc = wx.BufferedPaintDC(panel) 220 | dc.Clear() 221 | 222 | # Fill the background with a gradient shading 223 | endColour = wx.Colour(240,98,161) 224 | startColour = wx.Colour(255,135,198) 225 | 226 | rect = panel.GetRect() 227 | dc.GradientFillLinear(rect, startColour, endColour, wx.SOUTH) 228 | 229 | # Draw the label 230 | font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) 231 | dc.SetFont(font) 232 | 233 | # Draw the message 234 | rect2 = wx.Rect(*rect) 235 | rect2.height += 20 236 | dc.DrawLabel(self._message, rect2, alignment=wx.ALIGN_CENTER|wx.ALIGN_CENTER) 237 | 238 | # Draw the top title 239 | font.SetWeight(wx.FONTWEIGHT_BOLD) 240 | dc.SetFont(font) 241 | dc.SetPen(wx.Pen(wx.Colour(255,255,255))) 242 | dc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_CAPTIONTEXT)) 243 | 244 | if self._icon.IsOk(): 245 | iconWidth, iconHeight = self._icon.GetWidth(), self._icon.GetHeight() 246 | dummy, textHeight = dc.GetTextExtent(self._title) 247 | textXPos, textYPos = iconWidth + 10, (iconHeight-textHeight)//2 248 | dc.DrawBitmap(self._icon, 5, 5, True) 249 | else: 250 | textXPos, textYPos = 5, 0 251 | 252 | dc.DrawText(self._title, textXPos, textYPos+5) 253 | dc.DrawLine(5, 25, rect.width-5, 25) 254 | 255 | size = self.GetSize() 256 | dc.SetPen(wx.Pen(startColour, 1)) 257 | dc.SetBrush(wx.TRANSPARENT_BRUSH) 258 | dc.DrawRoundedRectangle(0, 0, size.x, size.y-1, 12) 259 | 260 | 261 | def OnErase(self, event): 262 | """ 263 | Handles the ``wx.EVT_ERASE_BACKGROUND`` event for :class:`PyInfoFrame`. 264 | 265 | :param `event`: a :class:`EraseEvent` event to be processed. 266 | 267 | :note: This method is intentionally empty to reduce flicker. 268 | """ 269 | 270 | # This is empty on purpose, to avoid flickering 271 | pass 272 | 273 | 274 | # -------------------------------------------------------------------- # 275 | # The actual PyBusyInfo implementation 276 | # -------------------------------------------------------------------- # 277 | 278 | class PyBusyInfo(object): 279 | """ 280 | Constructs a busy info window as child of parent and displays a message in it. 281 | """ 282 | 283 | def __init__(self, message, parent=None, title=_("Busy"), icon=wx.NullBitmap): 284 | """ 285 | Default class constructor. 286 | 287 | :param `parent`: the :class:`PyBusyInfo` parent; 288 | :param `message`: the message to display in the :class:`PyBusyInfo`; 289 | :param `title`: the main :class:`PyBusyInfo` title; 290 | :param `icon`: an icon to draw as the frame icon, an instance of :class:`wx.Bitmap`. 291 | 292 | :note: If `parent` is not ``None`` you must ensure that it is not closed 293 | while the busy info is shown. 294 | """ 295 | 296 | self._infoFrame = PyInfoFrame(parent, message, title, icon) 297 | 298 | if parent and parent.HasFlag(wx.STAY_ON_TOP): 299 | # we must have this flag to be in front of our parent if it has it 300 | self._infoFrame.SetWindowStyleFlag(wx.STAY_ON_TOP) 301 | 302 | # Added for the screenshot-taking tool 303 | self.Show() 304 | 305 | 306 | def __del__(self): 307 | """ Overloaded method, for compatibility with wxWidgets. """ 308 | 309 | self._infoFrame.Show(False) 310 | self._infoFrame.Destroy() 311 | 312 | 313 | def Show(self, show=True): 314 | """ 315 | Shows or hides the window. 316 | 317 | You may need to call `Raise` for a top level window if you want to bring it to 318 | top, although this is not needed if :meth:`PyBusyInfo.Show` is called immediately after the frame creation. 319 | 320 | :param bool `show`: ``True`` to show the :class:`PyBusyInfo` frame, ``False`` to hide it. 321 | 322 | :return: ``True`` if the window has been shown or hidden or ``False`` if nothing was done 323 | because it already was in the requested state. 324 | 325 | .. note:: 326 | 327 | Notice that the default state of newly created top level windows is hidden (to allow 328 | you to create their contents without flicker) unlike for all the other, not derived from 329 | :class:`TopLevelWindow`, windows that are by default created in the shown state. 330 | 331 | 332 | .. versionadded:: 0.9.5 333 | """ 334 | 335 | retVal = self._infoFrame.Show(show) 336 | 337 | if show: 338 | self._infoFrame.Refresh() 339 | self._infoFrame.Update() 340 | 341 | return retVal 342 | 343 | 344 | def Update(self): 345 | """ 346 | Calling this method immediately repaints the invalidated area of the window and all of its 347 | children recursively (this normally only happens when the flow of control returns to the 348 | event loop). 349 | 350 | :note: Notice that this function doesn't invalidate any area of the window so nothing happens 351 | if nothing has been invalidated (i.e. marked as requiring a redraw). Use `Refresh` first if 352 | you want to immediately redraw the window unconditionally. 353 | 354 | .. versionadded:: 0.9.5 355 | """ 356 | 357 | self._infoFrame.Update() 358 | 359 | def UpdateText(self, text): 360 | self._infoFrame._message=text 361 | self._infoFrame.Refresh() 362 | self._infoFrame.Update() 363 | 364 | if __name__ == '__main__': 365 | 366 | import wx 367 | 368 | class MyFrame(wx.Frame): 369 | 370 | def __init__(self, parent): 371 | 372 | wx.Frame.__init__(self, parent, -1, "PyBusyInfo Demo") 373 | 374 | panel = wx.Panel(self) 375 | 376 | b = wx.Button(panel, -1, "Test PyBusyInfo ", (50,50)) 377 | self.Bind(wx.EVT_BUTTON, self.OnButton, b) 378 | 379 | 380 | def OnButton(self, event): 381 | 382 | message = "Please wait 5 seconds, working..." 383 | busy = PyBusyInfo(message, parent=self, title="Really Busy") 384 | 385 | wx.Yield() 386 | 387 | for indx in range(5): 388 | wx.MilliSleep(1000) 389 | 390 | del busy 391 | 392 | 393 | # our normal wxApp-derived class, as usual 394 | 395 | app = wx.App(0) 396 | 397 | frame = MyFrame(None) 398 | app.SetTopWindow(frame) 399 | frame.Show() 400 | 401 | app.MainLoop() 402 | 403 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import webbrowser 2 | import wx 3 | from api import check_cookies, get_token, get_oauth 4 | import gui 5 | from wxasync import AsyncBind, WxAsyncApp, StartCoroutine 6 | import asyncio 7 | import data, constants, buen, database, misc 8 | from gui_extra import PyBusyInfo 9 | from pubsub import pub 10 | import sys 11 | import os 12 | import aiohttp 13 | 14 | def resource_path(relative_path): 15 | """ Get absolute path to resource, works for dev and for PyInstaller """ 16 | try: 17 | # PyInstaller creates a temp folder and stores path in _MEIPASS 18 | base_path = sys._MEIPASS 19 | except Exception: 20 | base_path = os.path.abspath(".") 21 | 22 | return os.path.join(base_path, relative_path) 23 | 24 | app = WxAsyncApp() 25 | 26 | # Used to allow updating of ui from outside using pub.sendMessage() 27 | def update_sources(): 28 | main_window.update_sources(None) 29 | 30 | def update_activity(): 31 | main_window.update_activity(None) 32 | 33 | def update_progress(value, range, progress_message): 34 | if range != None: 35 | # set job count to range 36 | main_window.m_progressbar.SetRange(range) 37 | if value != None: 38 | # Set current job progress 39 | main_window.m_progressbar.SetValue(value) 40 | else: 41 | # Job complete 42 | main_window.m_progressbar.SetRange(0) 43 | if progress_message != None: 44 | main_window.m_activity_progress.SetLabelText(progress_message) 45 | 46 | def enable_job_toggle_button(): 47 | # called when you are allowed to stop jobs 48 | main_window.m_toggle_downloading.Enable() 49 | 50 | def reset_job_toggle_button_text(): 51 | # called when jobs are completed 52 | main_window.m_toggle_downloading.SetLabelText(constants.activity_start) 53 | 54 | def show_dialog(msg, ok=None, focus_main=True): 55 | focus=main_window 56 | if not focus_main: 57 | focus=add_source_window 58 | # called when new release dropped on github 59 | dlg = wx.MessageDialog(focus, 60 | msg, 61 | "Alert OwO", wx.OK|wx.CANCEL|wx.ICON_QUESTION) 62 | result = dlg.ShowModal() 63 | if result == wx.ID_OK: 64 | if ok != None: 65 | ok() 66 | dlg.Destroy() 67 | return result == wx.ID_OK 68 | 69 | def show_loading(msg): 70 | global loading 71 | try: 72 | loading 73 | if msg == None: 74 | del loading 75 | else: 76 | loading.UpdateText(msg) 77 | except: 78 | if msg != None: 79 | loading=PyBusyInfo(msg, parent=main_window, title="Loading UwU") 80 | 81 | class MainWindow(gui.Main): 82 | """ 83 | Main window bro 84 | """ 85 | def __init__(self, parent=None): 86 | super(MainWindow, self).__init__(parent) 87 | self.Maximize(True) 88 | self.m_version.SetLabelText("App version: {}".format(constants.APP_VERSION)) 89 | pub.subscribe(update_sources, "update.sources") 90 | pub.subscribe(update_activity, "update.activity") 91 | pub.subscribe(update_progress, "update.progress") 92 | pub.subscribe(enable_job_toggle_button, "enable.job_toggle_button") 93 | pub.subscribe(reset_job_toggle_button_text, "reset.job_toggle_button_text") 94 | pub.subscribe(show_dialog, "show.dialog") 95 | pub.subscribe(show_loading, "show.loading") 96 | AsyncBind(wx.EVT_BUTTON, self.show_add_window, self.m_add_source) 97 | AsyncBind(wx.EVT_BUTTON, self.toggle_jobs, self.m_toggle_downloading) 98 | AsyncBind(wx.EVT_BUTTON, self.update_settings, self.m_save_settings) 99 | AsyncBind(wx.EVT_CLOSE, self.onDestroy, self) 100 | 101 | async def onDestroy(self, event): 102 | if show_dialog("Are you sure you want to close osu assistant?"): 103 | # pickle the data 104 | data.save_data() 105 | 106 | if add_source_window!=None: 107 | add_source_window.Destroy() 108 | self.Destroy() 109 | sys.exit(0) 110 | 111 | # used to repopulate the source list after a edit 112 | def update_sources(self, event): 113 | self.m_source_list.DeleteAllPages() 114 | 115 | source_list=data.Sources.read() 116 | for source_key, source in source_list: 117 | source_panel=ListPanel(self.m_source_list) 118 | missing_beatmaps=[beatmap[0] for beatmap in source.get_missing_beatmaps()] 119 | i=0 120 | for beatmap in source.get_available_beatmaps(): 121 | if beatmap[0] not in missing_beatmaps: 122 | source_panel.m_list.Insert("https://osu.ppy.sh/beatmapsets/"+str(beatmap[0]) ,i) 123 | i+=1 124 | for i, beatmap in enumerate(source.get_unavailable_beatmaps()): 125 | source_panel.m_list.Insert("https://osu.ppy.sh/b/"+str(beatmap[1]) +" (unavailable for download)",i) 126 | for i, beatmapset_id in enumerate(missing_beatmaps): 127 | source_panel.m_list.Insert("https://osu.ppy.sh/beatmapsets/"+str(beatmapset_id) +" (missing in-game)",i) 128 | self.m_source_list.AddPage(source_panel, f"#{data.Sources.collection_index[source_key]}: {source_key}") 129 | try: 130 | self.m_source_list.GetListView().SetColumnWidth(0, 750) 131 | self.m_source_list.GetListView().Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.delete_source) 132 | except: 133 | pass 134 | 135 | # used to repopulate the activity list after download completes 136 | def update_activity(self, event): 137 | self.m_activity_list.Clear() 138 | job_list=data.Jobs.read() 139 | i=0 140 | while len(job_list) > 0: 141 | job=job_list.pop(0) 142 | job_source_key=job.get_job_source_key() 143 | self.m_activity_list.Insert(str(job_source_key+f" ({job.get_job_downloads_cnt()} beatmaps will be downloaded)"), i) 144 | 145 | def export_collection_to_beatmap(self, event): 146 | if data.Settings.valid_osu_directory: 147 | if os.path.isfile(os.path.join(data.Settings.osu_install_folder, "collection.db")): 148 | # collection_window=CollectionSelectionWindow() 149 | # collection_window.SetIcon(wx.Icon(resource_path("osu.ico"))) 150 | # collection_window.Show() 151 | show_dialog("This feature is not implemented yet...") 152 | else: 153 | show_dialog("You have provided a osu install directory, but there is no collection.db in it") 154 | else: 155 | show_dialog("You need to set a osu install directory first") 156 | 157 | async def restore_settings(self, event): 158 | get_token() 159 | async with aiohttp.ClientSession() as session: 160 | await check_cookies(session) 161 | 162 | s=data.Settings 163 | if s.osu_install_folder!=None: 164 | # Initialize the cache db 165 | StartCoroutine(self.create_osudb, self) 166 | if s.osu_install_folder != None: 167 | self.m_osu_dir.SetPath(s.osu_install_folder) 168 | self.m_autodownload_toggle.SetValue(s.download_on_start) 169 | self.m_use_osu_mirror.SetValue(s.download_from_osu) 170 | self.m_settings_xsrf_token.SetHelpText("Inspect element on osu website to obtain") 171 | self.m_settings_osu_session.SetHelpText("Inspect element on osu website to obtain") 172 | if s.xsrf_token!="" and s.osu_session!="" and s.valid_osu_cookies == False: 173 | show_dialog("XSRF-TOKEN or osu_session provided has expired. You have to replace them") 174 | self.m_settings_xsrf_token.SetValue("XRSF_TOKEN") 175 | self.m_settings_osu_session.SetValue("osu_session") 176 | 177 | # Refresh sources and jobs (the views will update) 178 | await data.Sources.refresh() 179 | 180 | async def update_settings(self, event): 181 | s=data.Settings 182 | s.osu_install_folder=self.m_osu_dir.GetPath() 183 | s.download_on_start=self.m_autodownload_toggle.GetValue() 184 | s.download_from_osu=self.m_use_osu_mirror.GetValue() 185 | s.xsrf_token=self.m_settings_xsrf_token.GetValue() 186 | s.osu_session=self.m_settings_osu_session.GetValue() 187 | s.download_interval=self.m_download_interval.GetValue() 188 | 189 | if not os.path.isfile(os.path.join(s.osu_install_folder, "osu!.db")): 190 | show_dialog("Warning: The osu folder you selected is not the osu install directory. If you wish to just download beatmaps to the selected folder without collection updates, this is alright.") 191 | 192 | if s.valid_oauth==False: 193 | get_token() 194 | 195 | if s.osu_install_folder!=None: 196 | # Initialize the cache db 197 | StartCoroutine(self.create_osudb, self) 198 | 199 | if s.download_from_osu==True: 200 | async with aiohttp.ClientSession() as session: 201 | await check_cookies() 202 | 203 | data.save_data() 204 | 205 | # toggle jobs 206 | async def toggle_jobs(self, event=None): 207 | self.m_toggle_downloading.Disable() 208 | if self.m_toggle_downloading.GetLabel() == constants.activity_stop: 209 | data.cancel_jobs_toggle=True 210 | self.m_toggle_downloading.SetLabelText(constants.activity_start) 211 | elif not data.cancel_jobs_toggle: 212 | if data.Settings.valid_osu_directory: 213 | self.m_toggle_downloading.SetLabelText(constants.activity_stop) 214 | await data.Jobs.start_jobs() 215 | else: 216 | show_dialog("Set the correct osu install folder in settings!") 217 | self.m_toggle_downloading.Enable() 218 | 219 | async def show_add_window(self, event): 220 | global add_source_window 221 | if data.Settings.valid_oauth == True: 222 | add_source_window=AddSourceWindow(parent=None) 223 | add_source_window.SetIcon(wx.Icon(resource_path("osu.ico"))) 224 | add_source_window.Show() 225 | self.m_add_source.Disable() 226 | await add_source_window.populate_add_window(add_source_window) 227 | else: 228 | if data.Settings.valid_oauth==False: 229 | get_token() 230 | def delete_source(self, event): 231 | source_key=self.m_source_list.GetListView().GetItemText(self.m_source_list.GetListView().GetFocusedItem()) 232 | if show_dialog("Are you sure you want to delete {}".format(source_key)): 233 | StartCoroutine(data.Sources.delete_source(source_key[source_key.find(" ")+1:]), self) 234 | async def add_userpage(self, links, scope): 235 | await data.Sources.add_user_source(links, scope) 236 | async def add_tournament(self, selection): 237 | await data.Sources.add_tournament_source(selection) 238 | async def add_osucollector(self, links): 239 | await data.Sources.add_osucollector_source(links) 240 | async def add_osuweblinks(self, title, links): 241 | await data.Sources.add_osuweblinks_source(title, links) 242 | 243 | async def create_osudb(self): 244 | self.m_add_source.Disable() 245 | self.m_toggle_downloading.Disable() 246 | self.m_save_settings.Disable() 247 | await database.create_osudb() 248 | # Refresh sources and jobs (the views will update) 249 | await data.Sources.refresh() 250 | self.m_add_source.Enable() 251 | self.m_toggle_downloading.Enable() 252 | self.m_save_settings.Enable() 253 | 254 | # Initiate automatic downloads 255 | if data.Settings.download_on_start: 256 | pub.sendMessage("toggle.jobs") 257 | StartCoroutine(self.toggle_jobs, self) 258 | 259 | def open_discord(self, event): 260 | webbrowser.open(constants.link_discord) 261 | def open_donate(self, event): 262 | webbrowser.open(constants.link_paypal) 263 | def open_github(self, event): 264 | webbrowser.open(constants.link_github) 265 | def open_website(self, event): 266 | webbrowser.open(constants.link_website) 267 | 268 | class AddSourceWindow(gui.AddSource): 269 | """ 270 | Window for adding a new source 271 | """ 272 | def __init__(self, parent=None): 273 | super(AddSourceWindow, self).__init__(parent) 274 | self.Maximize(True) 275 | # bind the buttons to their respective callbacks 276 | AsyncBind(wx.EVT_BUTTON, self.add_userpage, self.m_add_userpage) 277 | AsyncBind(wx.EVT_BUTTON, self.add_tournament, self.m_add_tournament) 278 | AsyncBind(wx.EVT_BUTTON, self.add_mappack, self.m_add_mappack) 279 | AsyncBind(wx.EVT_BUTTON, self.add_osucollector, self.m_add_osucollector) 280 | AsyncBind(wx.EVT_BUTTON, self.add_osuweblinks, self.m_add_weblinks) 281 | AsyncBind(wx.EVT_CHOICE, self.change_mappack_section, self.m_mappack_section) 282 | AsyncBind(wx.EVT_WINDOW_DESTROY, self.onDestroy, self) 283 | 284 | async def onDestroy(self, event): 285 | global add_source_window 286 | global main_window 287 | add_source_window=None 288 | #User closed application 289 | if main_window != None: 290 | main_window.m_add_source.Enable() 291 | main_window.SetFocus() 292 | 293 | async def add_userpage(self, event): 294 | links=self.m_userpages.GetValue().strip() 295 | scope=[self.m_user_top100.GetValue(), self.m_user_favourites.GetValue(), self.m_user_everything.GetValue(), 296 | self.m_user_ranked.GetValue(), self.m_user_loved.GetValue(), self.m_user_pending.GetValue(), self.m_user_graveyarded.GetValue()] 297 | success=True 298 | if not "ppy.sh/users/" in links: 299 | show_dialog("You need to input a link to the osu user profile", focus_main=False) 300 | success=False 301 | if all(x==0 for x in scope): 302 | show_dialog("You need to select something to download from the userpage!", focus_main=False) 303 | success=False 304 | if success: 305 | self.Destroy() 306 | StartCoroutine(main_window.add_userpage(links, scope), main_window) 307 | 308 | async def add_tournament(self, event): 309 | selection=self.m_tournament.GetPageText(self.m_tournament.GetSelection()) 310 | self.Destroy() 311 | StartCoroutine(main_window.add_tournament(selection), main_window) 312 | 313 | async def add_mappack(self, event): 314 | labels=[self.m_mappack_list.GetString(x) for x in self.m_mappack_list.GetSelections()] 315 | ids=[int(x.split(":")[0]) for x in labels] 316 | self.Destroy() 317 | await data.Sources.add_mappack_source(ids) 318 | 319 | async def add_osucollector(self, event): 320 | success=True 321 | links=self.m_osu_collector.GetValue().strip() 322 | if not "osucollector.com/collections/" in links: 323 | show_dialog("You need to input osucollector collection url", focus_main=False) 324 | success=False 325 | if success: 326 | self.Destroy() 327 | StartCoroutine(main_window.add_osucollector(links), main_window) 328 | 329 | async def add_osuweblinks(self, event): 330 | success=True 331 | title=self.m_osu_weblinks_key.GetValue().strip() 332 | links=self.m_osu_weblinks.GetValue().strip() 333 | if not title: 334 | show_dialog("You need to name the collection", focus_main=False) 335 | success=False 336 | if not "ppy.sh/beatmapsets/" in links: 337 | show_dialog("You need to input beatmaps for this collection", focus_main=False) 338 | success=False 339 | if success: 340 | self.Destroy() 341 | StartCoroutine(main_window.add_osuweblinks(title, links), main_window) 342 | 343 | async def change_mappack_section(self, event): 344 | selection=str(self.m_mappack_section.GetSelection()) 345 | self.m_mappack_list.Clear() 346 | i=0 347 | for source_key, item in data.MappackJson[selection].items(): 348 | source_name=item[0] 349 | self.m_mappack_list.Insert(f"{source_key}: {source_name} ({len(item[1])} beatmapsets)", i) 350 | i+=1 351 | 352 | async def populate_add_window(self, event): 353 | for item in data.TournamentJson.items(): 354 | beatmap_list=[] 355 | add_tournament_panel=ListPanel(self.m_tournament) 356 | for beatmap in item[1][1]: 357 | beatmap_list.append(constants.osu_beatmap_url_full.format(beatmap[0], beatmap[2], beatmap[1])) 358 | tournament_key=item[0] + ": "+ item[1][0] 359 | if len(beatmap_list)>0: 360 | add_tournament_panel.m_list.InsertItems(beatmap_list,0) 361 | 362 | self.m_tournament.AddPage(add_tournament_panel, tournament_key) 363 | 364 | try: 365 | self.m_tournament.GetListView().SetColumnWidth(0,-1) 366 | except: 367 | pass 368 | 369 | mappack_list=[] 370 | for source_key, item in data.MappackJson["0"].items(): 371 | source_name=item[0] 372 | mappack_list.append(f"{source_key}: {source_name} ({len(item[1])} beatmapsets)") 373 | if len(mappack_list)>0: 374 | self.m_mappack_list.InsertItems(mappack_list,0) 375 | def open_subscribed_mappers(self, event): 376 | webbrowser.open(constants.link_mappers) 377 | 378 | class ListPanel(gui.ListPanel): 379 | def __init__(self, parent=None): 380 | super(ListPanel, self).__init__(parent) 381 | 382 | def open_beatmap_website(self, event): 383 | try: 384 | index = event.GetSelection() 385 | url=self.m_list.GetString(index).split()[0] 386 | webbrowser.open(url) 387 | except: 388 | pass 389 | 390 | class CollectionSelectionWindow(gui.CollectionsSelection): 391 | """ 392 | Window for adding a new source 393 | """ 394 | def __init__(self, parent=None): 395 | super(CollectionSelectionWindow, self).__init__(parent) 396 | self.current_collections=database.collection_to_dict() 397 | 398 | if not isinstance(self.current_collections, bool): 399 | for i, collection in enumerate(self.current_collections["collections"]): 400 | self.m_collections_selection.Insert(str(f"{i}: {collection['name']}"), i) 401 | 402 | def export_collections_to_beatmap(self, event): 403 | selections=self.m_collections_selection.GetSelections() 404 | buen.generate_beatmaps(selections, self.current_collections) 405 | self.Destroy() 406 | 407 | # Used for AddSourceWindow to call functions in main window 408 | # There can be only one instance at all times 409 | main_window = MainWindow() 410 | add_source_window=None 411 | 412 | async def main(): 413 | main_window.SetIcon(wx.Icon(resource_path("osu.ico"))) 414 | main_window.Show() 415 | app.SetTopWindow(main_window) 416 | has_savefile=await misc.init() 417 | 418 | if has_savefile==False: 419 | wizard=gui.IntroWizard(None) 420 | wizard.FitToPage(wizard.m_wizPage1) 421 | wizard.SetIcon(wx.Icon(resource_path("osu.ico"))) 422 | wizard.m_oauth_btn.Bind(wx.EVT_BUTTON, get_oauth) 423 | wizard.RunWizard(wizard.m_wizPage1) 424 | data.Settings.osu_install_folder=wizard.m_osu_dir.GetPath() 425 | await main_window.restore_settings(None) 426 | await app.MainLoop() 427 | asyncio.run(main()) -------------------------------------------------------------------------------- /misc.py: -------------------------------------------------------------------------------- 1 | import os 2 | import webbrowser 3 | import entity 4 | import requests 5 | import download 6 | import asyncio 7 | import data, database, constants, strings 8 | from pubsub import pub 9 | import aiohttp 10 | 11 | # Update sources/jobs on startup 12 | # 13 | async def init(): 14 | # Load app data 15 | has_savefile=data.load_data() 16 | # Check for update 17 | if requests.get("https://bobermilk.pythonanywhere.com/json/osu-assistant.json").json()["latest"]>constants.APP_VERSION: 18 | pub.sendMessage("show.dialog", msg="New update available! Download from github?", ok=lambda: webbrowser.open(constants.link_github_releases)) 19 | 20 | # Get the jsons 21 | data.TournamentJson=requests.get("https://bobermilk.pythonanywhere.com/json/tournament.json").json() 22 | data.MappackJson=requests.get("https://bobermilk.pythonanywhere.com/json/mappack.json").json() 23 | return has_savefile 24 | 25 | # WARNING: this function WILL hang the main thread, so remember to make it async in production 26 | async def do_job(job): 27 | downloads=job.get_job_downloads() 28 | 29 | source=data.Sources.get_source(job.get_job_source_key()) 30 | async with aiohttp.ClientSession() as session: 31 | for i, beatmap in enumerate(downloads, 1): 32 | # Check if cancel button has been pressed (user cancelled opration) 33 | if data.cancel_jobs_toggle: 34 | return False 35 | # Check source beatmap cache for availability of beatmap 36 | is_hosted=source.query_cache(beatmap) 37 | # Start downloading 38 | if is_hosted: 39 | success=await download.download_beatmap(session, beatmap[0]) 40 | 41 | # Intervals between jobs 42 | download_interval=data.Settings.download_interval/1000 43 | if success==2: 44 | download_interval+=constants.osu_get_interval # add 3 seconds if its downloading from osu website 45 | await asyncio.sleep(download_interval) 46 | 47 | # Update progressbar 48 | pub.sendMessage("update.progress", value=i, range=len(downloads), progress_message=None) 49 | 50 | return True 51 | 52 | # called by job refresh to find out what to download 53 | def diff_local_and_source(source): 54 | missing_beatmaps=[] 55 | for beatmap in source.get_available_beatmaps(): 56 | if beatmap[0] != None and not database.query_osudb(beatmap): 57 | missing_beatmaps.append(beatmap) 58 | return missing_beatmaps 59 | 60 | # The following creates source objects to be inserted into the Sources entities 61 | def create_userpage_source(links, scope): 62 | users=strings.parse_userpages_urlstrings(links) # returns set of (userid, gamemode) 63 | 64 | # Example entry in main screen 65 | # User: played=top&fav status=r&gp&p&g mode=m Polyester 66 | source_key=strings.generate_userpage_source_key(users, scope) 67 | 68 | return (source_key, entity.UserpageSource(users, scope)) 69 | 70 | def create_tournament_source(tournament_id, source_key): 71 | # Example 72 | # SOFT-4: Springtime Osu!mania Free-for-all Tournament 4 73 | return (source_key, entity.TournamentSource(tournament_id)) 74 | 75 | def create_mappack_source(ids): 76 | # Example entry in main screen 77 | # Mappack mode=m #109 #108 78 | 79 | source_key=strings.generate_mappack_source_key(ids) 80 | 81 | return (source_key, entity.MappackSource(ids)) 82 | 83 | def create_osucollector_source(links): 84 | # Example entry in main screen 85 | # Osucollector: DT SPEED 86 | 87 | # Get id 88 | ids=strings.parse_osucollector_urlstrings(links) 89 | 90 | # Generate source key 91 | source_key=strings.generate_osucollector_source_key(ids) 92 | 93 | # Generate new source 94 | new_source=entity.OsucollectorSource(ids) 95 | 96 | return (source_key, new_source) 97 | 98 | def create_osuweblinks_source(title, links): 99 | # Example entry in main screen 100 | # Osuweblinks: training packs my osu coach gave me 101 | 102 | # Get beatmapset_id, beatmapset_id -> beatmap_id 103 | beatmapset_ids, beatmap_ids=strings.parse_osuweblinks_urlstrings(links) 104 | 105 | # Generate source key 106 | source_key=strings.generate_osuweblinks_source_key(title) 107 | 108 | # Generate new source 109 | new_source=entity.OsuweblinksSource(beatmapset_ids, beatmap_ids) 110 | 111 | return (source_key, new_source) -------------------------------------------------------------------------------- /oauth.py: -------------------------------------------------------------------------------- 1 | from http.server import BaseHTTPRequestHandler, HTTPServer 2 | from urllib.parse import urlparse 3 | import data 4 | import threading 5 | 6 | # Have one instance of the server running only 7 | running=False 8 | 9 | hostName = "localhost" 10 | serverPort = 8080 11 | 12 | class AuthenticationLoopback(BaseHTTPRequestHandler): 13 | def do_GET(self): 14 | query = urlparse(self.path).query 15 | query_components = dict(qc.split("=") for qc in query.split("&")) 16 | data.OAUTH_TOKEN = query_components["token"] 17 | if data.OAUTH_TOKEN=="Failed": 18 | data.Settings.valid_oauth=False 19 | data.OAUTH_TOKEN=None 20 | else: 21 | data.Settings.valid_oauth=True 22 | 23 | self.send_response(200) 24 | self.send_header("Content-type", "text/html") 25 | self.end_headers() 26 | self.wfile.write(bytes("Success!", "utf-8")) 27 | self.wfile.write(bytes("", "utf-8")) 28 | self.wfile.write(bytes("

You may close this tab and return to the osu assistant application now.

", "utf-8")) 29 | self.wfile.write(bytes("", "utf-8")) 30 | fin() 31 | 32 | 33 | webServer = HTTPServer((hostName, serverPort), AuthenticationLoopback) 34 | 35 | def fin(): 36 | global running 37 | running=False 38 | webServer.shutdown() 39 | 40 | def ask_token(): 41 | global running 42 | if not running: 43 | thread = threading.Thread(target = webServer.serve_forever) 44 | thread.daemon=True 45 | thread.start() 46 | running=True 47 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # osu! assistant 2 | osu! assistant will sync beatmaps from userpages, tournaments, mappacks and osu!Collector and add them to your in-game collections. Supports all gamemodes! 3 | ![demo](https://cdn.discordapp.com/attachments/1010585418531622983/1015370254408491070/unknown.png) 4 | 5 | Discord server: https://discord.gg/2axQrK6d 6 | 7 | ## Youtube demo 8 | [![Youtube demo](https://img.youtube.com/vi/M5ghmbm08C0/0.jpg)](https://www.youtube.com/watch?v=M5ghmbm08C0) 9 | 10 | ## How to run? 11 | ### 1. Downloading my build (the exe is wrongly flagged as a virus, there is nothing I can do about it because I used python to write this) 12 | https://github.com/bobermilk/osu-assistant/releases/ 13 | 14 | ### 2. Run from source 15 | - install python 3 16 | - download the source code 17 | - use command prompt and cd into the directory containing the source code 18 | - `pip install -r requirements.txt` 19 | - `python main.py` 20 | 21 | # Screenshots 22 | ![add userpages](https://cdn.discordapp.com/attachments/962736678567571496/1018833852975816704/unknown.png) 23 | ![add tournaments](https://cdn.discordapp.com/attachments/962736678567571496/1018834093934391336/unknown.png) 24 | ![add mappacks](https://cdn.discordapp.com/attachments/962736678567571496/1018834167557005312/unknown.png) 25 | ![add osucollector](https://cdn.discordapp.com/attachments/962736678567571496/1018834257700982834/unknown.png) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | wheel 2 | requests 3 | pypubsub 4 | wxPython 5 | aiohttp[speedups] 6 | aiofiles 7 | wxasync -------------------------------------------------------------------------------- /scraper.py: -------------------------------------------------------------------------------- 1 | # for userpage and osucollector 2 | import requests 3 | import api 4 | import data 5 | import constants 6 | import aiohttp 7 | import asyncio 8 | from pubsub import pub 9 | # returns https://osu.ppy.sh/beatmapsets/ 10 | 11 | # TODO: write test 12 | async def get_userpage_beatmaps(source): 13 | all_beatmaps=set() 14 | 15 | scope=source.get_scope() 16 | async with aiohttp.ClientSession() as session: 17 | # Not from api 18 | if scope[0]: 19 | beatmaps=set() 20 | for user_id, gamemode in source.get_ids(): 21 | pub.sendMessage("show.loading", msg=f"Getting top 100 {gamemode} plays from {user_id}...") 22 | on_page=1 23 | if gamemode=="": 24 | url=constants.scrape_top_plays_defaultmode.format(user_id, on_page*100, (on_page-1)*100) 25 | else: 26 | url=constants.scrape_top_plays.format(user_id, gamemode, on_page*100, (on_page-1)*100) 27 | r=await session.get(url) 28 | j=await r.json() 29 | await asyncio.sleep(constants.osu_get_interval) 30 | cached_beatmap_ids=set() 31 | for beatmap in source.all_beatmaps: 32 | cached_beatmap_ids.add(beatmap[1]) 33 | for i, item in enumerate(j, 1): 34 | beatmap_id=item['beatmap_id'] 35 | pub.sendMessage("show.loading", msg=f"Getting beatmap data from osu! api ({i}/{len(j)})") 36 | if data.Settings.valid_oauth and not beatmap_id in cached_beatmap_ids: 37 | beatmap_checksum, beatmapset_id = await api.query_osu_beatmap(session, beatmap_id) 38 | if beatmapset_id == None: 39 | source.cache_unavailable_beatmap(beatmapset_id) 40 | else: 41 | beatmap=(beatmapset_id, beatmap_id, beatmap_checksum) 42 | beatmaps.add(beatmap) 43 | all_beatmaps.update(beatmaps) 44 | 45 | 46 | # From osu api 47 | 48 | # beatmaps=set() 49 | # for user_id, gamemode in source.get_ids(): 50 | # j=await api.query_osu_user_beatmapsets(user_id, gamemode, "loved") # list of jsons on each page 51 | # Sample json test: 52 | # with open("test.json", "w") as f: 53 | # f.write(str(j)) 54 | # for item in j: 55 | # for beatmap in item: 56 | # beatmapset_id=beatmap["beatmaps"][0]["beatmapset_id"] 57 | # beatmaps.add((beatmapset_id, None, None)) 58 | 59 | # with open("test.json", "w") as f: 60 | # f.write(str(beatmaps)) 61 | 62 | if scope[1]: 63 | # Favourites 64 | beatmaps=set() 65 | for user_id, gamemode in source.get_ids(): 66 | pub.sendMessage("show.loading", msg=f"Getting favourite maps from {user_id}...") 67 | if data.Settings.valid_oauth: 68 | j=await api.query_osu_user_beatmapsets(session, user_id, "favourite") # list of jsons on each page 69 | for item in j: 70 | for beatmap in item: 71 | beatmaps.add((beatmap["beatmaps"][0]["beatmapset_id"], None, None)) 72 | all_beatmaps.update(beatmaps) 73 | 74 | if scope[2]: 75 | # Everything played 76 | beatmaps=set() 77 | for user_id, gamemode in source.get_ids(): 78 | pub.sendMessage("show.loading", msg=f"Getting every map played by {user_id}...") 79 | if data.Settings.valid_oauth: 80 | j=await api.query_osu_user_beatmapsets(session, user_id, "most_played") 81 | for item in j: 82 | for beatmap in item: 83 | beatmapset_id=beatmap["beatmap"]["beatmapset_id"] 84 | beatmap_id=beatmap["beatmap_id"] 85 | beatmaps.add((beatmapset_id, beatmap_id, None)) 86 | all_beatmaps.update(beatmaps) 87 | 88 | 89 | if scope[3]: 90 | beatmaps=set() 91 | for user_id, gamemode in source.get_ids(): 92 | pub.sendMessage("show.loading", msg=f"Getting ranked maps from {user_id}...") 93 | if data.Settings.valid_oauth: 94 | j=await api.query_osu_user_beatmapsets(session, user_id, "ranked") 95 | for item in j: 96 | for beatmap in item: 97 | beatmapset_id=beatmap["beatmaps"][0]["beatmapset_id"] 98 | beatmaps.add((beatmapset_id, None, None)) 99 | all_beatmaps.update(beatmaps) 100 | 101 | 102 | if scope[4]: 103 | beatmaps=set() 104 | for user_id, gamemode in source.get_ids(): 105 | pub.sendMessage("show.loading", msg=f"Getting loved maps from {user_id}...") 106 | if data.Settings.valid_oauth: 107 | j=await api.query_osu_user_beatmapsets(session, user_id, "loved") 108 | for item in j: 109 | for beatmap in item: 110 | beatmapset_id=beatmap["beatmaps"][0]["beatmapset_id"] 111 | beatmaps.add((beatmapset_id, None, None)) 112 | all_beatmaps.update(beatmaps) 113 | 114 | if scope[5]: 115 | beatmaps=set() 116 | for user_id, gamemode in source.get_ids(): 117 | pub.sendMessage("show.loading", msg=f"Getting pending maps from {user_id}...") 118 | if data.Settings.valid_oauth: 119 | j=await api.query_osu_user_beatmapsets(session, user_id, "pending") 120 | for item in j: 121 | for beatmap in item: 122 | beatmapset_id=beatmap["beatmaps"][0]["beatmapset_id"] 123 | beatmaps.add((beatmapset_id, None, None)) 124 | all_beatmaps.update(beatmaps) 125 | 126 | if scope[6]: 127 | beatmaps=set() 128 | for user_id, gamemode in source.get_ids(): 129 | pub.sendMessage("show.loading", msg=f"Getting graveyarded maps from {user_id}...") 130 | if data.Settings.valid_oauth: 131 | j=await api.query_osu_user_beatmapsets(session, user_id, "graveyard") 132 | for item in j: 133 | for beatmap in item: 134 | beatmapset_id=beatmap["beatmaps"][0]["beatmapset_id"] 135 | beatmaps.add((beatmapset_id, None, None)) 136 | all_beatmaps.update(beatmaps) 137 | 138 | pub.sendMessage("show.loading", msg=None) 139 | return all_beatmaps 140 | 141 | async def get_tournament_beatmaps(source): 142 | all_beatmaps=set() 143 | async with aiohttp.ClientSession() as session: 144 | beatmaps=data.TournamentJson[source.get_id()][1] 145 | cached_beatmap_ids=set() 146 | for beatmap in source.all_beatmaps: 147 | cached_beatmap_ids.add(beatmap[1]) 148 | for i, beatmap in enumerate(beatmaps, 1): 149 | pub.sendMessage("show.loading", msg=f"Getting beatmap data from osu! api ({i}/{len(beatmaps)})") 150 | if data.Settings.valid_oauth and not beatmap[1] in cached_beatmap_ids: 151 | checksum, beatmapset_id=await api.query_osu_beatmap(session, beatmap[1]) 152 | if beatmapset_id == None: 153 | source.cache_unavailable_beatmap(beatmap[1]) 154 | else: 155 | all_beatmaps.add((beatmapset_id, beatmap[1], checksum)) 156 | pub.sendMessage("show.loading", msg=None) 157 | return all_beatmaps 158 | 159 | def get_mappack_beatmaps(source): 160 | all_beatmaps=set() 161 | for id in source.get_ids(): 162 | for i in range(0, 4): 163 | if str(id) in data.MappackJson[str(i)].keys(): 164 | for beatmapset_id in data.MappackJson[str(i)][str(id)][1]: 165 | all_beatmaps.add((beatmapset_id, None, None)) 166 | break # move on to next id 167 | return all_beatmaps 168 | 169 | async def get_osucollector_beatmaps(source): 170 | all_beatmaps=set() 171 | async with aiohttp.ClientSession() as session: 172 | for source_id in source.get_ids(): 173 | page=1 174 | cursor=None 175 | while True: 176 | pub.sendMessage("show.loading", msg=f"Getting collection maps from {source_id} ({page*100} maps retrieved)") 177 | url=constants.osucollector_url.format(source_id, page*100) 178 | if cursor!=None: 179 | url+="&cursor={}".format(cursor) 180 | r=await session.get(url) 181 | j=await r.json() 182 | await asyncio.sleep(1) 183 | for item in j['beatmaps']: 184 | beatmapset_id=item['beatmapset_id'] 185 | beatmap_id=item['id'] 186 | beatmap_checksum=item['checksum'] 187 | beatmap=(beatmapset_id, beatmap_id, beatmap_checksum) 188 | all_beatmaps.add(beatmap) 189 | cursor=j['nextPageCursor'] 190 | if cursor == None: 191 | break 192 | pub.sendMessage("show.loading", msg=None) 193 | return all_beatmaps 194 | 195 | async def get_osuweblinks_beatmaps(source): 196 | all_beatmaps=set() 197 | 198 | for beatmapset_id in source.get_beatmapset_ids(): 199 | all_beatmaps.add((beatmapset_id, None, None)) 200 | 201 | async with aiohttp.ClientSession() as session: 202 | for i, beatmap_id in enumerate(source.get_beatmap_ids(), 1): 203 | pub.sendMessage("show.loading", msg=f"Getting beatmap data from osu! api ({i}/{len(source.get_beatmap_ids())})") 204 | beatmap_checksum, beatmapset_id = await api.query_osu_beatmap(session, beatmap_id) 205 | if (beatmapset_id, None, None) in all_beatmaps: 206 | all_beatmaps.remove((beatmapset_id, None, None)) 207 | all_beatmaps.add((beatmapset_id, beatmap_id, beatmap_checksum)) 208 | 209 | pub.sendMessage("show.loading", msg=None) 210 | return all_beatmaps -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from cx_Freeze import setup, Executable 3 | 4 | # Dependencies are automatically detected, but it might need fine tuning. 5 | # "packages": ["os"] is used as example only 6 | build_exe_options = {"packages": ["os"], "excludes": ["tkinter"], 'include_files': ["assets"]} 7 | 8 | # base="Win32GUI" should be used only for Windows GUI app 9 | base = None 10 | if sys.platform == "win32": 11 | base = "Win32GUI" 12 | 13 | setup( 14 | name="osu! assistant", 15 | version="0.1", 16 | description="beatmap aggregator", 17 | options={"build_exe": build_exe_options}, 18 | executables=[Executable("main.py", base=base, icon="assets/osu.ico")], 19 | ) 20 | -------------------------------------------------------------------------------- /strings.py: -------------------------------------------------------------------------------- 1 | import re 2 | import constants 3 | import random, string 4 | 5 | # Create userpage ids 6 | def generate_userpage_source_key(users, scope): 7 | played="played=" 8 | if scope[0]: 9 | played+="top&" 10 | if scope[1]: 11 | played+="fav&" 12 | if scope[2]: 13 | played+="all" 14 | if played[-1]=='&': 15 | played=played[:-1] 16 | status="status=" 17 | if scope[3]: 18 | status+="r&" 19 | if scope[4]: 20 | status+="l&" 21 | if scope[5]: 22 | status+="p&" 23 | if scope[6]: 24 | status+="g" 25 | if status[-1]=='&': 26 | status=status[:-1] 27 | ids="ids=" 28 | for userid, gamemode in users: 29 | ids+=str(userid) 30 | ids+="," 31 | if ids[-1]==",": 32 | ids=ids[:-1] 33 | return f"User: {played} {status} {ids}" 34 | def generate_mappack_source_key(mappack_ids): 35 | ids="ids=" 36 | for id in mappack_ids: 37 | ids+=str(id) 38 | ids+="," 39 | if ids[-1]==",": 40 | ids=ids[:-1] 41 | return f"Mappack: {ids}" 42 | def generate_osucollector_source_key(collection_ids): 43 | ids="ids=" 44 | for id in collection_ids: 45 | ids+=str(id) 46 | ids+="," 47 | if ids[-1]==",": 48 | ids=ids[:-1] 49 | return f"Osucollector: {ids}" 50 | 51 | def generate_osuweblinks_source_key(title): 52 | return f"Osuweblinks: {title}" 53 | 54 | # Extract (user_id, gamemode) from osu beatmap urls 55 | # https://osu.ppy.sh/users/15656848/fruits 56 | def parse_userpages_urlstrings(urlstring): 57 | users=set() 58 | ra = "(?<=users\/)(.*)" # matches format /user_id/gamemode 59 | 60 | for user in re.findall(ra, urlstring): 61 | user=user.split("/") 62 | user_id=int(user[0]) 63 | 64 | # check if gamemode is specified 65 | if len(user) > 1 and user[1] in constants.gamemode_dict: 66 | gamemode=user[1] 67 | else: 68 | gamemode="" 69 | users.add((user_id, gamemode)) 70 | 71 | return users 72 | 73 | # for url in re.findall(rb, urlstring): 74 | # try: 75 | # r = requests.head(url, allow_redirects=True, timeout=10) 76 | # beatmapset_ids.append(re.findall(ra, r.url)[0]) 77 | # time.sleep(data.Settings.download_interval) 78 | # except: 79 | # pass 80 | # return beatmapset_ids 81 | 82 | # Extract beatmapset_ids from osu collector urls 83 | def parse_osucollector_urlstrings(urlstring): 84 | collections=set() 85 | ra = "(?<=collections\/)([0-9]*)" # matches format /collections/collection_id 86 | 87 | for collection_id in re.findall(ra, urlstring): 88 | collections.add(int(collection_id)) 89 | 90 | return collections 91 | 92 | # Extract beatmap_id, beatmapset_id from osu website urls 93 | def parse_osuweblinks_urlstrings(urlstring): 94 | beatmapset_ids=set() # beatmapset_id 95 | beatmap_ids=set() # (beatmapset_id, beatmap_id) cant use dict cuz multiple same key 96 | 97 | ra = "(?<=beatmapsets\/)(.*)" # matches format beatmapset_id#gamemode/beatmap_id 98 | 99 | for beatmap_data in re.findall(ra, urlstring): 100 | # beatmap_id 101 | data=re.findall(r'\d+', beatmap_data) 102 | if len(data)==1: 103 | beatmapset_ids.add(data[0]) 104 | elif len(data)==2: 105 | beatmapset_ids.add(data[0]) 106 | beatmap_ids.add(data[1]) 107 | 108 | return beatmapset_ids, beatmap_ids 109 | 110 | def generate_random_name(length): 111 | letters = string.ascii_lowercase 112 | return ''.join(random.choice(letters) for i in range(length)) 113 | 114 | def generate_collection_name(n): 115 | letters='' 116 | while n: 117 | letters+=(chr(64+n%25)) 118 | n//=25 # int division 119 | return letters --------------------------------------------------------------------------------