├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── Channels ├── Check_Mii_Out_Channel │ ├── DebugTools │ │ ├── TestUpdate.py │ │ ├── addToList.py │ │ ├── generateCountries.py │ │ ├── quicklist.py │ │ └── reset.py │ ├── ForceUpdate.sh │ ├── bestlist.py │ ├── cmoc.py │ ├── condetail.py │ ├── config.json.example │ ├── coninfo.py │ ├── conmail.py │ ├── contestCLI.py │ ├── entrylist.py │ ├── htmlcontest.py │ ├── htmlpopular.py │ ├── htmlrankings.py │ ├── htmltop50.py │ ├── jobs.cron │ ├── miikaitai.py │ ├── numberinfo.py │ ├── popcrafts.py │ ├── popular.py │ ├── randlist.py │ ├── sign_encrypt.py │ ├── top50.py │ └── wiisports.py ├── Everybody_Votes_Channel │ ├── __init__.py │ ├── config.json.example │ ├── jobs.cron │ ├── votes.py │ └── voteslists.py ├── Forecast_Channel │ ├── README.md │ ├── __init__.py │ ├── config.json.example │ ├── forecast.py │ ├── forecastlists.py │ ├── forecastregions.py │ └── jobs.cron ├── News_Channel │ ├── README.md │ ├── __init__.py │ ├── config.json.example │ ├── jobs.cron │ ├── logos │ │ ├── AFP_French.jpg │ │ ├── AFP_German.jpg │ │ ├── AFP_Spanish.jpg │ │ ├── ANP.jpg │ │ ├── AP.jpg │ │ ├── CanadianPress.jpg │ │ ├── Reuters.jpg │ │ └── SID.jpg │ ├── news.py │ ├── newsdownload.py │ └── newsmake.py ├── Nintendo_Channel │ ├── __init__.py │ ├── config.json.example │ ├── dllist.py │ ├── dstrial_header.py │ ├── info.py │ ├── ninch_thumb.py │ ├── ninfile.py │ ├── ninfile1.py │ ├── ninfile2.py │ ├── ninfile3.py │ └── ratings │ │ ├── BBFC │ │ ├── 12.jpg │ │ ├── 15.jpg │ │ ├── 18.jpg │ │ ├── PG.jpg │ │ └── U.jpg │ │ ├── CERO │ │ ├── A.jpg │ │ ├── B.jpg │ │ ├── C.jpg │ │ ├── D.jpg │ │ ├── Z.jpg │ │ ├── education.jpg │ │ └── scheduled.jpg │ │ ├── ESRB │ │ ├── E-small.jpg │ │ ├── E.jpg │ │ ├── E10-small.jpg │ │ ├── E10.jpg │ │ ├── EC.jpg │ │ ├── M.jpg │ │ ├── T.jpg │ │ ├── maycontain.jpg │ │ └── visitesrb.jpg │ │ └── PEGI │ │ ├── .DS_Store │ │ ├── 12.jpg │ │ ├── 16.jpg │ │ ├── 18.jpg │ │ ├── 3.jpg │ │ └── 7.jpg └── __init__.py ├── Games └── My_Aquarium │ └── myaquarium.py ├── LICENSE ├── README.md ├── Tools ├── Forecast │ └── parse.py ├── LZ Tools │ ├── DSDecmp │ │ ├── DSDecmp.exe │ │ └── Plugins │ │ │ ├── DSDecmp.xml │ │ │ ├── GoldenSunDD.dll │ │ │ └── LuminousArc.dll │ └── lzss │ │ ├── lzss │ │ ├── lzss-mac │ │ └── lzss.exe ├── Sign_Encrypt │ └── sign_encrypt.py └── Time │ └── convert.py ├── requirements.txt └── utils.py /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # RiiConnect24 Issue Template 2 | # Please delete all lines starting with # before submitting, this is just help text. 3 | 4 | # Please note issues regarding sign.py and config.py will not be read: those two files are RiiConnect24 Development's private property, and are kept secret for a reason. 5 | 6 | Short Description of Issue: 7 | 8 | Long Issue: 9 | 10 | Steps to Reproduce : 11 | - 12 | - 13 | - -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # RiiConnect24 Pull Request Template 2 | 3 | Please ensure your code works! It needs to be able to at least make files before we accept PRs. 4 | Please also ensure no config.py change has occured; it needs to be able to read our test configs for us to verify the code works. 5 | 6 | Author Name for Credits: FIRSTNAME (LASTNAME) 7 | 8 | Short Description: DESC. 9 | 10 | Long Description: DESCRIPTION. 11 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '0 5 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | 3 | .DS_Store 4 | *.pem 5 | *.pyc 6 | config.py 7 | config.json 8 | .DS_Store 9 | 10 | # Forecast Channel 11 | 12 | forecast.* 13 | short.* 14 | 15 | # News Channel 16 | 17 | newstime 18 | news.bin.* 19 | 20 | # Everybody Votes Channel 21 | 22 | voting.* 23 | *_q.* 24 | *_r.* 25 | 26 | 27 | # JetBrains IDE 28 | 29 | .idea/** 30 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/DebugTools/TestUpdate.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from os import system 3 | 4 | # temporary until we set up cron 5 | 6 | while True: 7 | system("./ForceUpdate.sh") 8 | sleep(1800) 9 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/DebugTools/addToList.py: -------------------------------------------------------------------------------- 1 | from cmoc import ResetList, QuickList, Prepare 2 | 3 | # example script gets every single mii in the DB, then adds it to spot_list 4 | 5 | ql = QuickList() 6 | pr = Prepare() 7 | 8 | miilist = [] 9 | artisanlist = [] 10 | miidata = "gAoAPwAAAAAAAAAAAAAAAAAAAAAAAF4AhonbB8JJnRIgBDxAuX0ookiKBEAAMZkEAIoAiiUEAAAAAAAAAAAAAAAAAAAAAAAAAAAaLw==" 11 | likes = 0 12 | skill = 0 13 | country = 49 14 | initial = "AA" 15 | 16 | artisandata = "gAsAUABlAGUAdwBlAGUAAAAAAAAAAAAAhorkD1RU1sYgADxAub0IPAiQCEAUabiQAIoAiiUEAAAAAAAAAAAAAAAAAAAAAAAAAAC68Q==" 17 | master = 0 18 | 19 | ResetList(b"NL") 20 | for i in range(499): 21 | miilist.append( 22 | (i, initial, likes, skill, country, miidata) + (artisandata, i, master) 23 | ) 24 | 25 | list_type = "NL" 26 | 27 | data = ql.build(list_type, miilist) 28 | 29 | with open("150/new_list01.ces", "wb") as file: 30 | file.write(pr.prepare(data)) 31 | 32 | with open("150/new_list01.dec", "wb") as file: 33 | file.write(data) 34 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/DebugTools/generateCountries.py: -------------------------------------------------------------------------------- 1 | from cmoc import Prepare 2 | from struct import pack 3 | 4 | # automatically generates all country codes for /first. sloppy code because it only needs to be ran once 5 | 6 | # codes = [1, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 128, 136, 144, 145, 152, 153, 154, 155, 156, 160, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177] 7 | # codes = [201, 202, 203, 204, 205, 206] 8 | codes = [100] 9 | p = Prepare() 10 | 11 | 12 | def u32(data): 13 | if not 0 <= data <= 4294967295: 14 | log("u32 out of range: %s" % data, "INFO") 15 | data = 0 16 | return pack(">I", data) 17 | 18 | 19 | for i in codes: 20 | with open(("addition/" + str(i) + ".ces"), "wb") as file: 21 | # data = bytes.fromhex('46440000') + u32(i) + bytes.fromhex('00000000000000000000000000000000FFFFFFFFFFFFFFFF4644000C0000000196000003') 22 | data = ( 23 | bytes.fromhex("4144000000000000") 24 | + u32(i) 25 | + bytes.fromhex("000000000000009600000096FFFFFFFFFFFFFFFF") 26 | ) 27 | file.write(p.prepare(data)) 28 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/DebugTools/quicklist.py: -------------------------------------------------------------------------------- 1 | from cmoc import QuickList, Prepare 2 | import MySQLdb 3 | from json import load 4 | 5 | with open("./config.json", "r") as f: 6 | config = load(f) 7 | 8 | # example script gets every single mii in the DB, then adds it to spot_list 9 | 10 | ql = QuickList() 11 | pr = Prepare() 12 | 13 | db = MySQLdb.connect("localhost", config["dbuser"], config["dbpass"], "cmoc") 14 | cursor = db.cursor() 15 | 16 | cursor.execute("SELECT craftsno,entryno FROM mii ORDER BY permlikes") 17 | numbers = cursor.fetchall() 18 | 19 | miilist = [] 20 | artisanlist = [] 21 | 22 | for i in range( 23 | len(numbers) 24 | ): # add the artisan data to each mii based on their craftsno 25 | cursor.execute( 26 | "SELECT entryno,initial,permlikes,skill,country,miidata FROM mii WHERE craftsno = %s AND entryno = %s ORDER BY permlikes", 27 | (numbers[i][0], numbers[i][1]), 28 | ) 29 | mii = cursor.fetchone() 30 | cursor.execute( 31 | "SELECT miidata,craftsno,master FROM artisan WHERE craftsno = %s", 32 | [numbers[i][0]], 33 | ) 34 | artisan = cursor.fetchone() 35 | miilist.append(mii + artisan) 36 | 37 | list_type = "SL" 38 | 39 | data = ql.build(list_type, miilist) 40 | 41 | with open("{}/150/spot_list.ces".format(config["miicontest_path"]), "wb") as file: 42 | file.write(pr.prepare(data)) 43 | 44 | with open("{}/150/spot_list.dec".format(config["miicontest_path"]), "wb") as file: 45 | file.write(data) 46 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/DebugTools/reset.py: -------------------------------------------------------------------------------- 1 | from sys import argv 2 | from cmoc import ResetList 3 | 4 | try: 5 | listname = argv[1].upper().encode() # converts to uppercase, then passed as bytes 6 | ResetList(listname) 7 | 8 | except IndexError as error: 9 | print("No list provided! Usage: python3 ./reset.py [type]") 10 | print("Available types are SL, PL, RL, NL, RL") 11 | print("This will remove all miis in the specified list!") 12 | 13 | except ValueError as error: 14 | print("Invalid List! Usage: python3 ./reset.py [type]") 15 | print("Available types are SL, PL, RL, NL, RL") 16 | print("This will remove all miis in the specified list!") 17 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/ForceUpdate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python3.7 ./top50.py 4 | python3.7 ./popcrafts.py 5 | python3.7 ./popular.py 6 | python3.7 ./randlist.py 7 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/bestlist.py: -------------------------------------------------------------------------------- 1 | from cmoc import BestList, Prepare 2 | import MySQLdb 3 | from json import load 4 | 5 | with open("/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/config.json", "r") as f: 6 | config = load(f) 7 | 8 | db = MySQLdb.connect( 9 | "localhost", config["dbuser"], config["dbpass"], "rc24_cmoc", charset="utf8mb4" 10 | ) 11 | cursor = db.cursor() 12 | bl = BestList() 13 | pr = Prepare() 14 | 15 | cursor.execute("SELECT id FROM contests WHERE status = 'results'") 16 | ids = cursor.fetchall() 17 | 18 | for id in ids: 19 | id = id[0] 20 | cursor.execute( 21 | "SELECT conmiis.entryno, conmiis.craftsno, conmiis.miidata, artisan.miidata, artisan.country, artisan.master FROM conmiis, artisan WHERE conmiis.craftsno = artisan.craftsno AND conmiis.contest = %s ORDER BY conmiis.likes DESC LIMIT 50", 22 | [id], 23 | ) 24 | miis = cursor.fetchall() 25 | 26 | build = bl.build(id, miis) 27 | 28 | with open( 29 | "{}/contest/{}/best_list.ces".format(config["miicontest_path"], id), "wb" 30 | ) as file: 31 | file.write(pr.prepare(build)) 32 | 33 | with open("decfiles/contests/{}/best_list.dec".format(id), "wb+") as file: 34 | file.write(build) 35 | 36 | db.close() 37 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/condetail.py: -------------------------------------------------------------------------------- 1 | from cmoc import ConDetail, Prepare, Thumbnail, Photo 2 | import MySQLdb 3 | from json import load 4 | from os import path, makedirs 5 | import requests 6 | 7 | with open("/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/config.json", "r") as f: 8 | config = load(f) 9 | 10 | db = MySQLdb.connect( 11 | "localhost", config["dbuser"], config["dbpass"], "rc24_cmoc", charset="utf8mb4" 12 | ) 13 | cursor = db.cursor() 14 | cd = ConDetail() 15 | pr = Prepare() 16 | th = Thumbnail() 17 | ph = Photo() 18 | 19 | cursor.execute( 20 | "SELECT id, start, end, status, entrycount, topic_japanese, topic, topic_german, topic_french, topic_spanish, topic_italian, topic_dutch, description_japanese, description, description_german, description_french, description_spanish, description_italian, description_dutch, thumbnail_url, souvenir_url FROM contests WHERE status != 'closed' AND status != 'waiting'" 21 | ) 22 | contests = cursor.fetchall() 23 | 24 | for i in contests: 25 | id = i[0] 26 | start = i[1] 27 | end = i[2] 28 | status = i[3] 29 | entrycount = i[4] - i[4] % 10 # must be rounded down to nearest 10 30 | topics = [i[5], i[6], i[7], i[8], i[9], i[10], i[11]] 31 | descriptions = [i[12], i[13], i[14], i[15], i[16], i[17], i[18]] 32 | thumbnail_url = i[19] 33 | souvenir_url = i[20] 34 | 35 | for language in range(0, 7): 36 | if not topics[language] or not descriptions[language]: 37 | continue 38 | 39 | data = cd.build(id, start, end, status, entrycount, topics[language], descriptions[language]) 40 | 41 | if not path.exists("{}/contest/{}".format(config["miicontest_path"], id)): 42 | makedirs("{}/contest/{}".format(config["miicontest_path"], id)) 43 | 44 | with open( 45 | "{}/contest/{}/con_detail{}.ces".format(config["miicontest_path"], id, language), "wb" 46 | ) as file: 47 | file.write(pr.prepare(data)) 48 | 49 | if not path.exists("decfiles/contests/{}".format(id)): 50 | makedirs("decfiles/contests/{}".format(id)) 51 | 52 | with open("decfiles/contests/{}/con_detail{}.dec".format(id, language), "wb+") as file: 53 | file.write(data) 54 | 55 | if path.exists("/var/rc24/thumbnail/{}.jpg".format(id)) or thumbnail_url: 56 | if thumbnail_url: 57 | thumbnail = requests.get(thumbnail_url).content 58 | 59 | with open("/var/rc24/thumbnail/{}.jpg".format(id), "wb") as f: 60 | f.write(thumbnail) 61 | 62 | data = th.build(id) 63 | 64 | if not path.exists("{}/contest/{}".format(config["miicontest_path"], id)): 65 | makedirs("{}/contest/{}".format(config["miicontest_path"], id)) 66 | 67 | with open( 68 | "{}/contest/{}/thumbnail.ces".format(config["miicontest_path"], id), "wb" 69 | ) as file: 70 | file.write(pr.prepare(data)) 71 | 72 | if not path.exists("decfiles/thumbnails/{}".format(id)): 73 | makedirs("decfiles/thumbnails/{}".format(id)) 74 | 75 | with open("decfiles/thumbnails/{}/thumbnail.dec".format(id), "wb+") as file: 76 | file.write(data) 77 | 78 | if path.exists("/var/rc24/souvenir/{}.jpg".format(id)): 79 | if souvenir_url: 80 | souvenir = requests.get(souvenir_url).content 81 | 82 | with open("/var/rc24/souvenir/{}.jpg".format(id), "wb") as f: 83 | f.write(souvenir) 84 | 85 | data = ph.build(id) 86 | 87 | with open( 88 | "{}/contest/{}/photo.ces".format(config["miicontest_path"], id), "wb" 89 | ) as file: 90 | file.write(pr.prepare(data)) 91 | 92 | if not path.exists("decfiles/souvenirs/{}".format(id)): 93 | makedirs("decfiles/souvenirs/{}".format(id)) 94 | 95 | with open("decfiles/souvenirs/{}/photo.dec".format(id), "wb+") as file: 96 | file.write(data) 97 | 98 | 99 | db.close() 100 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "file_path": "/path/to/save/file", 3 | "lzss_path": "/path/to/lzss", 4 | "production": true, 5 | "miicontest_path": "/var/www/wapp.wii.com/miicontest/public_html", 6 | "miicontestp_path": "/var/www/wapp.wii.com/miicontestp/public_html", 7 | "dbuser": "mysqluser", 8 | "dbpass": "mysqlpassword", 9 | } 10 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/coninfo.py: -------------------------------------------------------------------------------- 1 | from cmoc import ConInfo, Prepare 2 | import MySQLdb 3 | from json import load 4 | from time import mktime 5 | from datetime import datetime 6 | from subprocess import call 7 | 8 | with open("/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/config.json", "r") as f: 9 | config = load(f) 10 | 11 | 12 | def setRankings(id): # once voting is concluded, rank miis from 1 to 10 13 | cursor.execute( 14 | "UPDATE conmiis SET likes = (SELECT COUNT(*) FROM convotes WHERE id = %s AND (craftsno1 = conmiis.craftsno OR craftsno2 = conmiis.craftsno OR craftsno3 = conmiis.craftsno)) WHERE contest = %s", 15 | (id, id), 16 | ) # adds all the mii's votes 17 | db.commit() 18 | cursor.execute( 19 | "SELECT entryno FROM conmiis WHERE contest = %s ORDER BY likes ASC", [id] 20 | ) 21 | miis = cursor.fetchall() 22 | miiCount = len(miis) 23 | for i in range(miiCount): 24 | # idk how this works, but it does 25 | percentile = round((i / 10) / miiCount * 90) + 1 26 | cursor.execute( 27 | "UPDATE conmiis SET `rank` = %s WHERE entryno = %s", 28 | (percentile, miis[i][0]), 29 | ) 30 | 31 | 32 | currentTime = int(mktime(datetime.utcnow().timetuple())) - 946684800 33 | db = MySQLdb.connect( 34 | "localhost", config["dbuser"], config["dbpass"], "rc24_cmoc", charset="utf8mb4" 35 | ) 36 | cursor = db.cursor() 37 | ci = ConInfo() 38 | pr = Prepare() 39 | 40 | cursor.execute( 41 | "SELECT id, start, end, status, description FROM contests WHERE status != 'closed'" 42 | ) 43 | 44 | new = False 45 | 46 | for con in cursor.fetchall(): 47 | id = con[0] 48 | start = con[1] 49 | end = con[2] 50 | status = con[3] 51 | 52 | if ( 53 | status == "waiting" and start <= currentTime and end >= currentTime 54 | ): # contest is ready to be opened 55 | cursor.execute("UPDATE contests SET status = 'open' WHERE id = %s", [id]) 56 | 57 | new = True 58 | 59 | elif ( 60 | start <= currentTime and end <= currentTime 61 | ): # contest is ready to move to its next status 62 | # end time is increased by 7 days unless contest closes 63 | 64 | if status == "open": 65 | endTime = int(end + (7 * 24 * 60 * 60)) 66 | cursor.execute("SELECT COUNT(*) FROM conmiis WHERE contest = %s", [id]) 67 | cursor.execute( 68 | "UPDATE contests SET status = 'judging', end = %s, entrycount = %s, sent = 0 WHERE id = %s", 69 | (endTime, cursor.fetchone()[0], id), 70 | ) 71 | 72 | elif status == "judging": 73 | endTime = int(end + (30 * 24 * 60 * 60)) 74 | cursor.execute( 75 | "UPDATE contests SET status = 'results', end = %s, sent = 0 WHERE id = %s", 76 | (endTime, id), 77 | ) 78 | 79 | elif status == "results": 80 | cursor.execute("UPDATE contests SET status = 'closed' WHERE id = %s", [id]) 81 | 82 | new = True 83 | else: 84 | setRankings(id) 85 | 86 | 87 | db.commit() 88 | 89 | cursor.execute( 90 | "SELECT id, status FROM contests WHERE status != 'closed' AND status != 'waiting'" 91 | ) 92 | contests = cursor.fetchall() 93 | 94 | data = ci.build(contests) 95 | 96 | with open("{}/150/con_info.ces".format(config["miicontest_path"]), "wb") as file: 97 | file.write(pr.prepare(data)) 98 | 99 | with open("decfiles/con_info.dec", "wb") as file: 100 | file.write(data) 101 | 102 | db.close() 103 | 104 | if new: 105 | call(["python3", "/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/conmail.py"]) 106 | 107 | call(["python3", "/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/condetail.py"]) 108 | call(["python3", "/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/bestlist.py"]) 109 | call(["python3", "/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/entrylist.py"]) 110 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/conmail.py: -------------------------------------------------------------------------------- 1 | import calendar 2 | import MySQLdb 3 | import time 4 | from base64 import encodebytes 5 | from datetime import datetime 6 | from io import BytesIO 7 | from json import load 8 | from os import system, remove, path, makedirs 9 | from PIL import Image 10 | from random import randint 11 | from requests import post 12 | 13 | from textwrap import wrap 14 | 15 | with open("/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/config.json", "r") as f: 16 | config = load(f) 17 | 18 | db = MySQLdb.connect( 19 | "localhost", config["dbuser"], config["dbpass"], "rc24_cmoc", charset="utf8mb4" 20 | ) 21 | cursor = db.cursor() 22 | 23 | cursor.execute( 24 | "SELECT id, status, description, sent, photo_url FROM contests WHERE (status != 'closed' AND status != 'waiting')" 25 | ) 26 | contests = cursor.fetchall() 27 | 28 | now = datetime.utcnow() 29 | boundary = now.strftime("--BoundaryForDL%Y%m%d%H%m/" + str(randint(1000000, 9999999))) 30 | date = now.strftime("%a, %d %b %Y %H:%M:%S GMT") 31 | message = "{}\r\nContent-Type: text/plain\r\n\r\nThis part is ignored.\r\n\r\n\r\n\r\n".format( 32 | boundary 33 | ) 34 | 35 | 36 | def enc(text, description): 37 | return str( 38 | encodebytes(text.format(description).encode("utf-16be")) 39 | .replace(b"\n", b"\r\n") 40 | .decode("utf-8") 41 | ) 42 | 43 | 44 | def convert_to_baseline(path): 45 | """If for some reason the image has an alpha channel (probably a PNG), fill the background with white.""" 46 | 47 | with open(path, "rb") as f: 48 | picture = f.read() 49 | f.close() 50 | 51 | image = Image.open(BytesIO(picture)) 52 | 53 | image = image.convert("RGB") 54 | 55 | data = list(image.getdata()) 56 | image_without_exif = Image.new(image.mode, image.size) 57 | image_without_exif.putdata(data) 58 | 59 | buffer = BytesIO() 60 | image_without_exif.save(buffer, format="jpeg") 61 | 62 | return str(encodebytes(buffer.getvalue()).replace(b"\n", b"\r\n").decode("utf-8")) 63 | 64 | 65 | i = 0 66 | 67 | if contests == (): 68 | exit() 69 | 70 | # get the con_detail stuff which will show on the Wii Menu icon 71 | 72 | sorted_contests = [] 73 | 74 | for c in contests: 75 | sorted_contests.append(c[0]) 76 | 77 | contest_id = max(sorted_contests) 78 | 79 | decfilename = "/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/decfiles/contests/{}/con_detail1.dec".format( 80 | str(contest_id) 81 | ) 82 | 83 | if path.exists(decfilename): 84 | boundary2 = "----=_CMOC_Contest_Details" 85 | 86 | with open(decfilename, "rb") as f: 87 | con_detail = str(encodebytes(f.read()).replace(b"\n", b"\r\n").decode("utf-8")) 88 | 89 | thumbnail_path = "/var/rc24/thumbnail/{}".format(str(contest_id)) 90 | 91 | message += ( 92 | boundary 93 | + "\r\n" 94 | + "Content-Type: text/plain\r\n\r\n" 95 | + "Date: {}\r\n".format(date) 96 | + "From: w9999999900000000@rc24.xyz\r\n" 97 | + "To: allusers@rc24.xyz\r\n" 98 | + "Message-ID: <776DCLBHYHD.2QBO4Y3I2Y04S@JavaMail.w9999999900000000@rc24.xyz>\r\n" 99 | + "Subject: \r\n" 100 | + "MIME-Version: 1.0\r\n" 101 | + 'Content-Type: multipart/mixed; boundary="{}"\r\n'.format(boundary2) 102 | + "Content-Transfer-Encoding: base64\r\n" 103 | + "X-Wii-AppID: 3-48415041-3031\r\n" 104 | + "X-Wii-Tag: 00000001\r\n" 105 | + "X-Wii-Cmd: 00080001\r\n\r\n" 106 | + "--" 107 | + boundary2 108 | + "\r\n" 109 | + "Content-Type: application/octet-stream;\r\n" 110 | + " name=con_detail1.bin\r\n" 111 | + "Content-Transfer-Encoding: base64\r\n" 112 | + "Content-Disposition: attachment;\r\n" 113 | + " filename=con_detail1.bin\r\n\r\n" 114 | + con_detail 115 | + "\r\n\r\n" 116 | ) 117 | 118 | if path.exists(thumbnail_path + ".jpg"): 119 | system("convert " + thumbnail_path + ".jpg " + thumbnail_path + ".png") 120 | system("wimgt ENCODE " + thumbnail_path + ".png -x TPL.RGB565") 121 | system("mv " + thumbnail_path + " " + thumbnail_path + ".tpl") 122 | 123 | with open(thumbnail_path + ".tpl", "rb") as f: 124 | con_icon = str( 125 | encodebytes(f.read()[64:]).replace(b"\n", b"\r\n").decode("utf-8") 126 | ) 127 | 128 | message += ( 129 | "--" 130 | + boundary2 131 | + "\r\n" 132 | + "Content-Type: application/octet-stream;\r\n" 133 | + " name=thumbnail_" 134 | + str(contest_id) 135 | + ".tpl\r\n" 136 | + "Content-Transfer-Encoding: base64\r\n" 137 | + "Content-Disposition: attachment;\r\n" 138 | + " filename=thumbnail_" 139 | + str(contest_id) 140 | + ".tpl\r\n\r\n" 141 | + con_icon 142 | + "\r\n\r\n" 143 | ) 144 | 145 | message += "--" + boundary2 + "--\r\n\r\n" 146 | 147 | # make text, photo and attachment 148 | 149 | for c in contests: 150 | i += 1 151 | contest_id = c[0] 152 | status = c[1] 153 | description = c[2] 154 | sent = c[3] 155 | photo_url = c[4] 156 | 157 | if not path.exists("/var/rc24/photo/{}.jpg".format(id)) and photo_url: 158 | if photo_url: 159 | photo = requests.get(photo_url).content 160 | 161 | with open("/var/rc24/photo/{}.jpg".format(id), "wb") as f: 162 | f.write(photo) 163 | 164 | if sent == 0: 165 | boundary2 = "----=_CMOC_Contest_{}".format(i) 166 | 167 | contest_posting = "*******************************\r\nA New Contest is Under Way\r\n*******************************\r\n\r\nCare to test your Mii-making skills\r\nby designing a Mii on a particular\r\ntheme?\r\n\r\n\u25c6Contest Theme:\r\n{}\r\n\r\n\u25c6How to Submit an Entry\r\n1. Design a Mii in the Mii\r\n Channel.\r\n2. Go to the Check Mii Out\r\n Channel and submit your Mii.\r\n\r\n\r\nP.S. Check out https://mii.rc24.xyz/,\r\nit's the official companion website\r\nfor the Check Mii Out Channel!\r\n\r\n\r\n----------------------------------\r\nThis message is regarding the\r\nCheck Mii Out Channel.\r\n\r\nIf you do not wish to receive further\r\ncommercial messages from RiiConnect24,\r\nplease click the opt-out icon on the \r\nupper-right corner of the screen.\r\n\r\nYou can opt out of either (1) \r\nmessages for the Check Mii Out\r\nChannel only or (2) all messages for\r\nall channels and games." 168 | contest_judging = "*******************************\r\nCome and Judge a Contest\r\n*******************************\r\n\r\nCome over to the Check Mii Out\r\nChannel and judge a few Miis\r\nfor a contest.\r\n\r\n\u25c6Contest Theme:\r\n{}\r\n\r\n\r\nP.S. Check out https://mii.rc24.xyz/,\r\nit's the official companion website\r\nfor the Check Mii Out Channel!\r\n\r\n\r\n----------------------------------\r\n\r\n\r\nThis message is regarding the\r\nCheck Mii Out Channel.\r\n\r\nIf you do not wish to receive further\r\ncommercial messages from RiiConnect24,\r\nplease click the opt-out icon on the \r\nupper-right corner of the screen.\r\n\r\nYou can opt out of either (1) \r\nmessages for the Check Mii Out\r\nChannel only or (2) all messages for\r\nall channels and games." 169 | contest_results = "*******************************\r\nContest Results\r\n*******************************\r\n\r\nWe've tallied up all the votes, and\r\nthe winners for this contest have\r\nbeen decided!\r\n\r\n\u25c6Contest Theme:\r\n{}\r\n\r\n\r\nP.S. Check out https://mii.rc24.xyz/,\r\nit's the official companion website\r\nfor the Check Mii Out Channel!\r\n\r\n\r\n----------------------------------\r\nThis message is regarding the\r\nCheck Mii Out Channel.\r\n\r\nIf you do not wish to receive further\r\ncommercial messages from RiiConnect24,\r\nplease click the opt-out icon on the \r\nupper-right corner of the screen.\r\n\r\nYou can opt out of either (1) \r\nmessages for the Check Mii Out\r\nChannel only or (2) all messages for\r\nall channels and games." 170 | 171 | if status == "open": 172 | contest_text = enc(contest_posting, description) 173 | contest_status = "A New Contest is Under Way\n\nCare to test your Mii-making skills\nby designing a Mii on a particular\ntheme?\n\nContest Theme: {}".format( 174 | description 175 | ) 176 | elif status == "judging": 177 | contest_text = enc(contest_judging, description) 178 | contest_status = "Come and Judge a Contest\n\nCome over to the Check Mii Out\nChannel and judge a few Miis\nfor a contest.\n\nContest Theme: {}".format( 179 | description 180 | ) 181 | elif status == "results": 182 | contest_text = enc(contest_results, description) 183 | contest_status = "Contest Results\n\nWe've tallied up all the votes, and\nthe winners for this contest have\nbeen decided!\n\nContest Theme: {}".format( 184 | description 185 | ) 186 | 187 | message += ( 188 | boundary 189 | + "\r\n" 190 | + "Content-Type: text/plain\r\n\r\n" 191 | + "Date: {}\r\n".format(date) 192 | + "From: w9999999900000000@rc24.xyz\r\n" 193 | + "To: allusers@rc24.xyz\r\n" 194 | + "Message-ID: <776DCLBHYHD.2QBO4Y3I2Y04S@JavaMail.w9999999900000000@rc24.xyz>\r\n" 195 | + "Subject: \r\n" 196 | + "MIME-Version: 1.0\r\n" 197 | + 'Content-Type: multipart/mixed; boundary="{}"\r\n'.format(boundary2) 198 | + "Content-Transfer-Encoding: base64\r\n" 199 | + "X-Wii-AltName: AEMAaABlAGMAawAgAE0AaQBpACAATwB1AHQAIABDAGgAYQBuAG4AZQBs=\r\n" 200 | + "X-Wii-MB-OptOut: 1\r\n" 201 | + "X-Wii-MB-NoReply: 1\r\n" 202 | + "X-Wii-AppID: 3-48415041-3031\r\n\r\n" 203 | + "--" 204 | + boundary2 205 | + "\r\n" 206 | + "Content-Type: text/plain; charset=utf-16BE\r\n" 207 | + "Content-Transfer-Encoding: base64\r\n\r\n" 208 | + contest_text 209 | + "\r\n\r\n\r\n\r\n\r\n" 210 | + "--" 211 | + boundary2 212 | + "\r\n" 213 | + "Content-Type: application/x-wii-msgboard;\r\n" 214 | + " name=cmoc_letterform.arc\r\n" 215 | + "Content-Transfer-Encoding: base64\r\n" 216 | + "Content-Disposition: attachment;\r\n" 217 | + " filename=cmoc_letterform.arc\r\n\r\n" 218 | + "Vao4LQAAACAAAAAkAAAAYAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAIAAAABAAAAYAAAACMA\r\n" 219 | + "Y2hqdW1wLmJpbgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ2hKcAAAACMAAAABAAAAAAAB\r\n" 220 | + "AAFIQVBBAAAAIAAAAANhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n\r\n" 221 | ) 222 | 223 | photo_path = "/var/rc24/photo/{}.jpg".format(contest_id) 224 | 225 | if status == "open" and path.exists(photo_path): 226 | message += ( 227 | "--" 228 | + boundary2 229 | + "\r\n" 230 | + "Content-Type: image/jpeg;" 231 | + "\r\n" 232 | + " name=contest{}.jpg\r\n".format(str(i)) 233 | + "Content-Transfer-Encoding: base64\r\n" 234 | + "Content-Disposition: attachment;\r\n" 235 | + " filename=contest{}.jpg\r\n\r\n".format(str(i)) 236 | + convert_to_baseline(photo_path) 237 | + "\r\n\r\n" 238 | ) 239 | 240 | message += "--" + boundary2 + "--\r\n\r\n" 241 | 242 | data = { 243 | "username": "CMOC Bot", 244 | "content": contest_status, 245 | "avatar_url": "http://rc24.xyz/images/logo-small.png", 246 | "attachments": [ 247 | { 248 | "fallback": contest_status, 249 | "color": "#CB7931", 250 | "author_name": "RiiConnect24 CMOC Script", 251 | "author_icon": "https://miicontest.wii.rc24.xyz/apple-touch-icon.png", 252 | "text": contest_status, 253 | "title": "Update!", 254 | "fields": [ 255 | { 256 | "title": "Script", 257 | "value": "Check Mii Out Channel", 258 | "short": "false", 259 | } 260 | ], 261 | "thumb_url": "https://miicontest.wii.rc24.xyz/photo/{}.jpg".format( 262 | contest_id 263 | ), 264 | "footer": "RiiConnect24 Script", 265 | "footer_icon": "https://rc24.xyz/images/logo-small.png", 266 | "ts": int(calendar.timegm(datetime.utcnow().timetuple())), 267 | } 268 | ], 269 | } 270 | 271 | for url in config["webhook_urls"]: 272 | post_webhook = post(url, json=data, allow_redirects=True) 273 | 274 | cursor.execute("UPDATE contests SET sent = 1 WHERE id = %s", [contest_id]) 275 | 276 | db.commit() 277 | 278 | message += boundary + "--\r\n" 279 | 280 | path = config["miicontest_path"] 281 | 282 | task_file = "{}/150/con_task1".format(path) 283 | 284 | print(message) 285 | 286 | with open(task_file + ".txt", "w") as f: 287 | f.write(message) 288 | f.close() 289 | 290 | system( 291 | "python3 ./sign_encrypt.py -t enc -in '{}.txt' -out '{}.bin' -key BE3715C308F341A8F16F0EF4FB1497AF -rsa /var/rc24/key/cmoc.pem".format( 292 | task_file, task_file 293 | ) 294 | ) 295 | 296 | remove(task_file + ".txt") 297 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/contestCLI.py: -------------------------------------------------------------------------------- 1 | from time import mktime 2 | from datetime import date, datetime 3 | import MySQLdb 4 | from json import load 5 | 6 | with open("/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/config.json", "r") as f: 7 | config = load(f) 8 | 9 | db = MySQLdb.connect( 10 | "localhost", config["dbuser"], config["dbpass"], "rc24_cmoc", charset="utf8mb4" 11 | ) 12 | cursor = db.cursor() 13 | 14 | 15 | class colors: 16 | HEADER = "\033[95m" 17 | BLUE = "\033[94m" 18 | GREEN = "\033[92m" 19 | WARNING = "\033[93m" 20 | FAIL = "\033[91m" 21 | END = "\033[0m" 22 | BOLD = "\033[1m" 23 | UNDERLINE = "\033[4m" 24 | 25 | 26 | def searchContest(): 27 | search = str( 28 | input( 29 | "-> Please enter a keyword or phrase in the contest's topic or description: " 30 | ) 31 | ) 32 | formattedSearch = "%" + search.replace(" ", "%") + "%" 33 | cursor.execute( 34 | "SELECT id, description FROM contests WHERE topic LIKE %s OR description LIKE %s", 35 | (formattedSearch, formattedSearch), 36 | ) 37 | result = cursor.fetchall() 38 | 39 | print(len(result), 'results for "' + str(search) + '"') 40 | print("ID | Description\n-------------------") 41 | for i in result: 42 | print(str(i[0]) + " " * (4 - len(str(i[0]))), "|", i[1]) 43 | 44 | print() 45 | 46 | 47 | def showInfo(id): 48 | cursor.execute( 49 | "SELECT start, end, status, entrycount, topic, description FROM contests WHERE id = %s", 50 | [id], 51 | ) 52 | result = cursor.fetchone() 53 | if result == None: 54 | print(f"{colors.FAIL}ID:", id, f"does not exist!{colors.END}") 55 | 56 | else: 57 | print("ID:", id) 58 | print("Start:", datetime.fromtimestamp(result[0] + 946684800).date()) 59 | print("End:", datetime.fromtimestamp(result[1] + 946684800).date()) 60 | print("Status:", result[2].capitalize()) 61 | print("Submissions:", result[3]) 62 | print("Topic:", result[4]) 63 | print("Description:", result[5]) 64 | print() 65 | 66 | 67 | print( 68 | f"{colors.BOLD}CMOC Contest Editor\nRiiConnect24 - Created by Josh{colors.END}\n1 - Add\n2 - Edit\n3 - Remove\n4 - View" 69 | ) 70 | entry = int(input("-> Selection: ")) 71 | 72 | if entry == 1: 73 | print("\nCreating new contest ->") 74 | while True: 75 | month = input( 76 | "-> Enter starting month (leave blank to start when next contest changes status): " 77 | ) 78 | 79 | if len(month) == 0: 80 | currentTime = int(mktime(datetime.utcnow().timetuple())) - 946684800 81 | cursor.execute( 82 | "SELECT end, status, description FROM contests WHERE end > %s AND status != 'closed' ORDER BY end ASC LIMIT 1", 83 | [currentTime], 84 | ) 85 | nextContest = cursor.fetchone() 86 | 87 | if len(nextContest) == 0: 88 | print( 89 | "There are no future contests scheduled. Please try again and enter the date manually." 90 | ) 91 | exit() 92 | 93 | start = int(nextContest[0]) # set start time to next contest's end time 94 | startDate = datetime.fromtimestamp(start + 946684800).date() 95 | 96 | end = int( 97 | start + (7 * 24 * 60 * 60) 98 | ) # change status every 7 days until closure 99 | endDate = datetime.fromtimestamp(end + 946684800).date() 100 | 101 | if nextContest[1] == "waiting": 102 | nextStatus = "open" 103 | elif nextContest[1] == "open": 104 | nextStatus = "judging" 105 | elif nextContest[1] == "judging": 106 | nextStatus = "results" 107 | elif nextContest[1] == "results": 108 | nextStatus = "closed" 109 | 110 | print( 111 | "\nThe contest '{}' will become status '{}' on {}.".format( 112 | nextContest[2], nextStatus, startDate 113 | ) 114 | ) 115 | choice = str( 116 | input( 117 | "-> This new contest will begin at this date. Is this correct? [Y/N]: " 118 | ) 119 | ) 120 | if choice.upper() != "Y": 121 | print(f"{colors.FAIL}Contest adding aborted.{colors.END}") 122 | exit() 123 | 124 | break 125 | 126 | else: 127 | try: 128 | month = int(month) 129 | except: 130 | print( 131 | f"{colors.FAIL}Invalid month!{colors.END} Make sure you are entering the month by its number." 132 | ) 133 | continue 134 | 135 | if month < datetime.now().month and datetime.now().month != 12: 136 | print("That month has already passed.") 137 | exit() 138 | 139 | day = int(input("-> Enter starting day: ")) 140 | if ( 141 | (month != 1 and datetime.now().month == 12) 142 | and day < datetime.now().day 143 | and month < datetime.now().month 144 | ): 145 | print("That date has already passed.") 146 | exit() 147 | 148 | year = datetime.now().year 149 | if datetime.now().month == 12 and month == 1: 150 | year = datetime.now().year + 1 151 | 152 | try: 153 | d = date(year, month, day) 154 | 155 | except ValueError as e: 156 | print(e) 157 | exit() 158 | 159 | start = int(mktime(d.timetuple()) - 946684800) 160 | startDate = datetime.fromtimestamp(start + 946684800).date() 161 | 162 | end = int(start + (7 * 24 * 60 * 60)) 163 | endDate = datetime.fromtimestamp(end + 946684800).date() 164 | break 165 | 166 | while True: 167 | topic = str(input("-> Enter topic: ")) 168 | if len(topic) > 10: 169 | print( 170 | "The contest topic cannot be longer than 10 characters. That topic is", 171 | len(topic), 172 | "characters.", 173 | ) 174 | 175 | else: 176 | break 177 | 178 | while True: 179 | description = str(input("-> Enter description: ")) 180 | if len(description) > 64: 181 | print( 182 | "The contest description cannot be longer than 64 characters. That topic is", 183 | len(description), 184 | "characters.", 185 | ) 186 | 187 | else: 188 | break 189 | 190 | print() 191 | print("Description:", description) 192 | print("Topic:", topic) 193 | print("Start date:", startDate) 194 | print("End date:", endDate) 195 | choice = str(input("-> Is this information correct? [Y/N]: ")) 196 | if choice.upper() == "Y": 197 | cursor.execute( 198 | "INSERT INTO contests(start, end, status, entrycount, topic, description) VALUES (%s, %s, %s, %s, %s, %s)", 199 | (start, end, "waiting", 0, topic, description), 200 | ) 201 | db.commit() 202 | print(f"{colors.GREEN}Contest added successfully.{colors.END}") 203 | 204 | else: 205 | print(f"{colors.FAIL}Contest adding aborted.{colors.END}") 206 | exit() 207 | 208 | elif entry == 2: 209 | stop = False 210 | print("\nEditing contest ->") 211 | 212 | cursor.execute("SELECT id, description FROM contests ORDER BY id DESC limit 5") 213 | print("---5 most recently created contests---") 214 | print("ID | Description\n-------------------") 215 | for i in cursor.fetchall(): 216 | print(str(i[0]) + " " * (4 - len(str(i[0]))), "|", i[1]) 217 | print() 218 | 219 | while not stop: 220 | print("1 - Enter a contest ID\n2 - Search for a contest by keyword") 221 | selection = int(input("-> Selection: ")) 222 | if selection == 1: 223 | id = int(input("-> Enter contest ID: ")) 224 | cursor.execute("SELECT COUNT(*) FROM contests WHERE id = %s", [id]) 225 | if cursor.fetchone()[0] == 0: 226 | print(f"{colors.FAIL}ID:", id, f"does not exist!{colors.END}\n") 227 | continue 228 | 229 | showInfo(id) 230 | 231 | print("1 - Change topic\n2 - Change description") 232 | editSelection = int(input("-> Selection: ")) 233 | 234 | if editSelection == 1: 235 | while True: 236 | topic = str(input("-> Enter a new topic for the contest: ")) 237 | if len(topic) > 10: 238 | print( 239 | "The contest topic cannot be longer than 10 characters. That topic is", 240 | len(topic), 241 | "characters.", 242 | ) 243 | else: 244 | break 245 | 246 | cursor.execute( 247 | "UPDATE contests SET topic = %s WHERE id = %s", (topic, id) 248 | ) 249 | db.commit() 250 | print(f"{colors.GREEN}Contest topic updated successfully.{colors.END}") 251 | stop = True 252 | 253 | elif editSelection == 2: 254 | while True: 255 | description = str( 256 | input("-> Enter a new description for the contest: ") 257 | ) 258 | if len(description) > 64: 259 | print( 260 | "The contest topic cannot be longer than 64 characters. That topic is", 261 | len(description), 262 | "characters.", 263 | ) 264 | else: 265 | break 266 | 267 | cursor.execute( 268 | "UPDATE contests SET description = %s WHERE id = %s", 269 | (description, id), 270 | ) 271 | db.commit() 272 | print( 273 | f"{colors.GREEN}Contest description updated successfully.{colors.END}" 274 | ) 275 | stop = True 276 | 277 | else: 278 | print(f"{colors.FAIL}Invalid entry!{colors.END}") 279 | stop = True 280 | 281 | elif selection == 2: 282 | searchContest() 283 | 284 | else: 285 | print(f"{colors.FAIL}Invalid entry!{colors.END}\n") 286 | 287 | elif entry == 3: 288 | stop = False 289 | print("\nRemoving contest ->") 290 | 291 | cursor.execute("SELECT id, description FROM contests ORDER BY id DESC limit 5") 292 | print("---5 most recently created contests---") 293 | print("ID | Description\n-------------------") 294 | for i in cursor.fetchall(): 295 | print(str(i[0]) + " " * (4 - len(str(i[0]))), "|", i[1]) 296 | 297 | while not stop: 298 | print("\n1 - Enter a contest ID\n2 - Search for a contest by keyword") 299 | selection = int(input("-> Selection: ")) 300 | 301 | if selection == 1: 302 | id = int(input("-> Enter contest ID: ")) 303 | cursor.execute( 304 | "SELECT status, entrycount FROM contests WHERE id = %s", [id] 305 | ) 306 | result = cursor.fetchone() 307 | if result == None: 308 | print(f"ID:{colors.FAIL}", id, f"does not exist!{colors.END}") 309 | continue 310 | 311 | showInfo(id) 312 | 313 | if result[0] != "closed": 314 | print( 315 | "WARNING: This contest is still active with status '{}'.".format( 316 | result[0] 317 | ) 318 | ) 319 | 320 | if int(result[1]) != 0: 321 | print( 322 | "This contest and its", 323 | result[1], 324 | "miis will be permanently deleted.", 325 | ) 326 | 327 | else: 328 | print("This contest will be permanently deleted.") 329 | 330 | choice = str( 331 | input( 332 | f"-> Are you sure you want to {colors.UNDERLINE}delete{colors.END} contest #{id}? [Y/N]: " 333 | ) 334 | ) 335 | 336 | if choice.upper() == "Y": # REMEMBER TO DELETE THE MIIS TOO IDIOT 337 | cursor.execute("DELETE FROM contests WHERE id = %s", [id]) 338 | db.commit() 339 | print(f"{colors.GREEN}Contest", id, f"deleted.{colors.END}") 340 | stop = True 341 | 342 | else: 343 | print(f"{colors.FAIL}Contest deletion aborted.{colors.END}") 344 | stop = True 345 | 346 | elif selection == 2: 347 | searchContest() 348 | 349 | else: 350 | print(f"{colors.FAIL}Invalid entry!{colors.END}\n") 351 | 352 | elif entry == 4: 353 | print("\nViewing contest information ->") 354 | cursor.execute("SELECT id, description FROM contests ORDER BY id DESC limit 5") 355 | print("---5 most recently created contests---") 356 | print("ID | Description\n-------------------") 357 | for i in cursor.fetchall(): 358 | print(str(i[0]) + " " * (4 - len(str(i[0]))), "|", i[1]) 359 | print() 360 | 361 | while True: 362 | print("1 - Enter a contest ID\n2 - Search for a contest by keyword\n3 - Exit") 363 | selection = int(input("-> Selection: ")) 364 | 365 | if selection == 1: 366 | id = int(input("-> Enter contest ID: ")) 367 | showInfo(id) 368 | 369 | elif selection == 2: 370 | searchContest() 371 | 372 | elif selection == 3: 373 | break 374 | 375 | else: 376 | print(f"{colors.FAIL}Invalid entry!{colors.END}\n") 377 | 378 | db.close() 379 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/entrylist.py: -------------------------------------------------------------------------------- 1 | from cmoc import EntryList, Prepare 2 | import MySQLdb 3 | from json import load 4 | 5 | with open("/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/config.json", "r") as f: 6 | config = load(f) 7 | 8 | db = MySQLdb.connect( 9 | "localhost", config["dbuser"], config["dbpass"], "rc24_cmoc", charset="utf8mb4" 10 | ) 11 | cursor = db.cursor() 12 | el = EntryList() 13 | pr = Prepare() 14 | 15 | cursor.execute("SELECT id FROM contests WHERE status = 'judging'") 16 | ids = cursor.fetchall() 17 | 18 | for id in ids: 19 | id = id[0] 20 | cursor.execute( 21 | "SELECT craftsno, miidata FROM conmiis WHERE contest = %s ORDER BY RAND()", [id] 22 | ) 23 | result = cursor.fetchall() 24 | miis = [] 25 | for i in result: 26 | miis.append((i[0], i[1])) 27 | 28 | build = el.build(id, miis) 29 | 30 | for e in range(len(build)): 31 | with open( 32 | "{}/contest/{}/entry_list{}.ces".format( 33 | config["miicontest_path"], id, e + 1 34 | ), 35 | "wb", 36 | ) as file: 37 | file.write(pr.prepare(build[e])) 38 | 39 | with open( 40 | "decfiles/contests/{}/entry_list{}.dec".format(id, e + 1), "wb+" 41 | ) as file: 42 | file.write(build[e]) 43 | 44 | db.close() 45 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/htmlcontest.py: -------------------------------------------------------------------------------- 1 | import MySQLdb 2 | from json import load 3 | from time import mktime 4 | from datetime import date, datetime 5 | 6 | with open("/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/config.json", "r") as f: 7 | config = load(f) 8 | date = str(datetime.today().strftime("%B %d, %Y")) 9 | 10 | beginning = ( 11 | '\n\n\n\n\n\n\nCheck Mii Out Channel - Contest Info\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n

Contest Info

\n

Check Mii Out Channel - RiiConnect24

\n

These are the contests currently running on the Check Mii Out Channel.

\nBack to Homepage\n

' 12 | + date 13 | + '

\n

All times are measured in UTC

\n\n' 14 | ) 15 | 16 | db = MySQLdb.connect( 17 | "localhost", 18 | config["dbuser"], 19 | config["dbpass"], 20 | "rc24_cmoc", 21 | use_unicode=True, 22 | charset="utf8mb4", 23 | ) 24 | cursor = db.cursor() 25 | 26 | cursor.execute( 27 | "SELECT start,end,status,description FROM contests WHERE status != 'closed' AND status != 'waiting' ORDER BY status ASC, end ASC" 28 | ) 29 | list = cursor.fetchall() 30 | 31 | month = int(datetime.now().month) 32 | day = int(datetime.now().day) 33 | 34 | tables = ( 35 | beginning 36 | + "\t\n\t\t\n\t\t\n\t\t\n\t\t\n" 37 | ) 38 | 39 | for i in range(len(list)): 40 | start = datetime.fromtimestamp(int(list[i][0]) + 946684800) 41 | end = datetime.fromtimestamp(int(list[i][1]) + 946684800) 42 | startDate = start.date() 43 | endDate = end.date() 44 | startTime = str(start.time())[:-3] 45 | endTime = str(end.time())[:-3] 46 | 47 | tables += "\t\n" 48 | tables += "\t\t\n".format(startDate, startTime) 49 | tables += "\t\t\n".format(endDate, endTime) 50 | tables += "\t\t\n".format(list[i][2].capitalize()) 51 | tables += "\t\t\n".format(list[i][3]) 52 | 53 | tables += "\t\n" 54 | # tables += str('\n' + list[i][0] + '' + str(list[i][1])) 55 | 56 | tables += "\n
StartNext RotationStatusDescription
{} {}{} {}{}{}
\n\n" 57 | 58 | with open("/var/www/rc24/wapp.wii.com/miicontest/public_html/contest.html", "w") as file: 59 | file.write(tables) 60 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/htmlpopular.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import MySQLdb 3 | from lz4.block import decompress 4 | from base64 import b64encode, b64decode 5 | from os.path import exists 6 | from subprocess import call 7 | from json import load 8 | from datetime import datetime 9 | from cmoc import wii2studio 10 | from random import shuffle 11 | import sentry_sdk 12 | 13 | with open("/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/config.json", "r") as f: 14 | config = load(f) 15 | 16 | sentry_sdk.init(config["sentry_url"]) 17 | 18 | 19 | def decodeMii(data): 20 | return decompress(b64decode(data.encode()), uncompressed_size=76) 21 | 22 | 23 | def decToEntry(num): # takes decimal int, outputs 12 digit entry number string 24 | num ^= ((num << 0x1E) ^ (num << 0x12) ^ (num << 0x18)) & 0xFFFFFFFF 25 | num ^= (num & 0xF0F0F0F) << 4 26 | num ^= (num >> 0x1D) ^ (num >> 0x11) ^ (num >> 0x17) ^ 0x20070419 27 | 28 | crc = (num >> 8) ^ (num >> 24) ^ (num >> 16) ^ (num & 0xFF) ^ 0xFF 29 | if 232 < (0xD4A50FFF < num) + (crc & 0xFF): 30 | crc &= 0x7F 31 | 32 | crc &= 0xFF 33 | return str(int((format(crc, "08b") + format(num, "032b")), 2)).zfill(12) 34 | 35 | 36 | date = str(datetime.today().strftime("%B %d, %Y")) 37 | db = MySQLdb.connect( 38 | "localhost", 39 | config["dbuser"], 40 | config["dbpass"], 41 | "rc24_cmoc", 42 | use_unicode=True, 43 | charset="utf8mb4", 44 | ) 45 | cursor = db.cursor() 46 | 47 | headers = ["Mii", "Entry Number", "Nickname", "Initials", "Likes", "Creator"] 48 | for h in range(len(headers)): 49 | headers[h] = "\t\t" + headers[h] + "\n" 50 | headers = "\t\n" + "".join(headers) + "\t\n" 51 | 52 | cursor.execute("SELECT COUNT(*) FROM mii WHERE likes > 0") 53 | count = int(cursor.fetchone()[0]) 54 | print("Popular Count:", count) 55 | 56 | # popular is always sorted by volatile likes first, but we combine miis ordered by permlikes to fill in the rest to equal 100 total miis 57 | if count >= 1000: 58 | extraCount = 0 59 | count = 1000 60 | 61 | else: 62 | extraCount = 1000 - count 63 | 64 | cursor.execute( 65 | "SELECT mii.entryno, mii.initial, mii.permlikes, mii.miidata, mii.nickname, mii.craftsno, artisan.nickname, artisan.master FROM mii, artisan WHERE mii.craftsno = artisan.craftsno AND likes > 0 ORDER BY likes DESC LIMIT %s", 66 | [count], 67 | ) 68 | popularMiis = cursor.fetchall() 69 | 70 | cursor.execute( 71 | "SELECT mii.entryno, mii.initial, mii.permlikes, mii.miidata, mii.nickname, mii.craftsno, artisan.nickname, artisan.master FROM mii, artisan WHERE mii.permlikes > 21 AND mii.craftsno=artisan.craftsno ORDER BY mii.permlikes DESC LIMIT %s", 72 | [extraCount], 73 | ) 74 | extraMiis = cursor.fetchall() 75 | 76 | row = list(set(popularMiis + extraMiis))[:500] 77 | shuffle(row) 78 | row = tuple(row) 79 | 80 | table = ( 81 | f'

These are all of the popular Miis that currently appear on the Check Mii Out Channel. Only the top 100 popular Miis are shown. Click on a Mii to download it.

\nBack to Homepage\n

{date}

\n\n' 82 | + headers 83 | ) 84 | for i in range(len(row)): 85 | artisan = row[i][6] 86 | entryno = row[i][0] 87 | initial = row[i][1] 88 | mii_filename = ( 89 | "/var/www/rc24/wapp.wii.com/miicontest/public_html/render/entry-{}.mii".format( 90 | entryno 91 | ) 92 | ) 93 | if not exists(mii_filename): 94 | with open(mii_filename, "wb") as f: 95 | miidata = decodeMii(row[i][3])[:-2] 96 | miidata = ( 97 | miidata[:28] + b"\x00\x00\x00\x00" + miidata[32:] 98 | ) # remove mac address from mii data 99 | f.write(miidata) 100 | 101 | # if not exists(mii_filename + '.png'): 102 | # call(['mono', '/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/MiiRender.exe', mii_filename]) 103 | 104 | if len(initial) == 1: 105 | initial += "." 106 | elif len(initial) == 2: 107 | initial = initial[0] + "." + initial[1] + "." 108 | 109 | if bool(row[i][7]): 110 | artisan += '
' 111 | 112 | longentry = decToEntry(entryno) 113 | longentry = longentry[:4] + "-" + longentry[4:8] + "-" + longentry[8:12] 114 | 115 | table += "\t\n" 116 | table += f'\t\t\n' 117 | table += f"\t\t\n" 118 | table += f"\t\t\n" 119 | table += f"\t\t\n" 120 | table += f"\t\t\n" 121 | table += f'\t\t\n' 122 | 123 | table += "\t\n" 124 | 125 | table += "
{longentry}{row[i][4]}{initial}{row[i][2]}{artisan}
\n" 126 | 127 | with open( 128 | "/var/www/rc24/wapp.wii.com/miicontest/public_html/tables/popular.html", "w" 129 | ) as file: 130 | file.write(table) 131 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/htmlrankings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import MySQLdb 3 | from lz4.block import decompress 4 | from base64 import b64encode, b64decode 5 | from os.path import exists 6 | from subprocess import call 7 | from json import load 8 | from datetime import datetime 9 | from cmoc import wii2studio 10 | 11 | with open("/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/config.json", "r") as f: 12 | config = load(f) 13 | 14 | 15 | def decodeMii(data): 16 | return decompress(b64decode(data.encode()), uncompressed_size=76) 17 | 18 | 19 | date = str(datetime.today().strftime("%B %d, %Y")) 20 | db = MySQLdb.connect( 21 | "localhost", 22 | config["dbuser"], 23 | config["dbpass"], 24 | "rc24_cmoc", 25 | use_unicode=True, 26 | charset="utf8mb4", 27 | ) 28 | cursor = db.cursor() 29 | 30 | countries = { 31 | 1: "jp", 32 | 8: "ai", 33 | 9: "ag", 34 | 10: "ar", 35 | 11: "aw", 36 | 12: "bs", 37 | 13: "bb", 38 | 14: "bz", 39 | 15: "bo", 40 | 16: "br", 41 | 17: "vg", 42 | 18: "ca", 43 | 19: "ky", 44 | 20: "cl", 45 | 21: "co", 46 | 22: "cr", 47 | 23: "dm", 48 | 24: "do", 49 | 25: "ec", 50 | 26: "sv", 51 | 27: "gf", 52 | 28: "gd", 53 | 29: "gp", 54 | 30: "gt", 55 | 31: "gy", 56 | 32: "ht", 57 | 33: "hn", 58 | 34: "jm", 59 | 35: "mq", 60 | 36: "mx", 61 | 37: "ms", 62 | 38: "cw", 63 | 39: "ni", 64 | 40: "pa", 65 | 41: "py", 66 | 42: "pe", 67 | 43: "kn", 68 | 44: "lc", 69 | 45: "vc", 70 | 46: "sr", 71 | 47: "tt", 72 | 48: "tc", 73 | 49: "us", 74 | 50: "uy", 75 | 51: "vi", 76 | 52: "ve", 77 | 64: "al", 78 | 65: "au", 79 | 66: "at", 80 | 67: "be", 81 | 68: "ba", 82 | 69: "bw", 83 | 70: "bg", 84 | 71: "hr", 85 | 72: "cy", 86 | 73: "cz", 87 | 74: "dk", 88 | 75: "ee", 89 | 76: "fi", 90 | 77: "fr", 91 | 78: "de", 92 | 79: "gr", 93 | 80: "hu", 94 | 81: "is", 95 | 82: "ie", 96 | 83: "it", 97 | 84: "lv", 98 | 85: "ls", 99 | 86: "li", 100 | 87: "lt", 101 | 88: "lu", 102 | 89: "mk", 103 | 90: "mt", 104 | 91: "me", 105 | 92: "mz", 106 | 93: "na", 107 | 94: "nl", 108 | 95: "nz", 109 | 96: "no", 110 | 97: "pl", 111 | 98: "pt", 112 | 99: "ro", 113 | 100: "ru", 114 | 101: "rs", 115 | 102: "sk", 116 | 103: "si", 117 | 104: "za", 118 | 105: "es", 119 | 106: "sz", 120 | 107: "se", 121 | 108: "ch", 122 | 109: "tr", 123 | 110: "gb", 124 | 111: "zm", 125 | 112: "zw", 126 | 113: "az", 127 | 114: "mr", 128 | 115: "ml", 129 | 116: "ne", 130 | 117: "td", 131 | 118: "sd", 132 | 119: "er", 133 | 120: "dj", 134 | 121: "so", 135 | 128: "tw", 136 | 136: "kr", 137 | 144: "hk", 138 | 145: "mo", 139 | 152: "id", 140 | 153: "sg", 141 | 154: "th", 142 | 155: "ph", 143 | 156: "my", 144 | 160: "cn", 145 | 168: "ae", 146 | 169: "in", 147 | 170: "eg", 148 | 171: "om", 149 | 172: "qa", 150 | 173: "kw", 151 | 174: "sa", 152 | 175: "sy", 153 | 176: "bh", 154 | 177: "jo", 155 | } 156 | 157 | headers = ["Rank", "Mii", "Artisan", "Votes", "Posts"] 158 | for h in range(len(headers)): 159 | headers[h] = "\t\t" + headers[h] + "\n" 160 | headers = "\t\n" + "".join(headers) + "\n" 161 | 162 | cursor.execute( 163 | "SELECT artisan.craftsno, artisan.miidata, artisan.nickname, artisan.country, artisan.votes, (SELECT COUNT(*) FROM mii WHERE mii.craftsno = artisan.craftsno) FROM artisan WHERE votes >= 3 AND craftsno != 100000993 ORDER BY votes DESC LIMIT 300" 164 | ) 165 | row = cursor.fetchall() 166 | 167 | table = ( 168 | f'

These are currently the top Mii Artisans of the Check Mii Out Channel, ranked. Only the top 300 Mii Artisans are shown. Click on a Mii to download it.

\nBack to Homepage\n

{date}

\n\n' 169 | + headers 170 | ) 171 | for i in range(len(row)): 172 | master = "" 173 | nickname = row[i][2] 174 | craftsno = row[i][0] 175 | mii_filename = ( 176 | "/var/www/rc24/wapp.wii.com/miicontest/public_html/render/crafts-{}.mii".format( 177 | craftsno 178 | ) 179 | ) 180 | if not exists(mii_filename): 181 | with open(mii_filename, "wb") as f: 182 | miidata = decodeMii(row[i][1])[:-2] 183 | miidata = ( 184 | miidata[:28] + b"\x00\x00\x00\x00" + miidata[32:] 185 | ) # remove mac address from mii data 186 | f.write(miidata) 187 | f.close() 188 | 189 | # if not exists(mii_filename + '.png'): 190 | # call(['mono', 'MiiRender.exe', mii_filename]) 191 | 192 | if int(row[i][4]) >= 1000: 193 | master = '
' 194 | 195 | try: 196 | country = ( 197 | ' ' 198 | ) 199 | 200 | except KeyError: 201 | country = "" 202 | 203 | table += "\t\n" 204 | table += f"\t\t\n" 205 | table += f'\t\t\n' 206 | table += f'\t\t\n' 207 | table += f"\t\t\n" 208 | table += f"\t\t\n" 209 | table += "\t\n" 210 | 211 | table += "
{i + 1}{country + nickname}
{master}
{row[i][4]}{row[i][5]}
\n" 212 | 213 | with open( 214 | "/var/www/rc24/wapp.wii.com/miicontest/public_html/tables/rankings.html", "w" 215 | ) as file: 216 | file.write(table) 217 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/htmltop50.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import MySQLdb 3 | from lz4.block import decompress 4 | from base64 import b64encode, b64decode 5 | from os.path import exists 6 | from subprocess import call 7 | from json import load 8 | from datetime import datetime 9 | from cmoc import wii2studio 10 | 11 | 12 | with open("/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/config.json", "r") as f: 13 | config = load(f) 14 | 15 | 16 | def decodeMii(data): 17 | return decompress(b64decode(data.encode()), uncompressed_size=76) 18 | 19 | 20 | def decToEntry(num): # takes decimal int, outputs 12 digit entry number string 21 | num ^= ((num << 0x1E) ^ (num << 0x12) ^ (num << 0x18)) & 0xFFFFFFFF 22 | num ^= (num & 0xF0F0F0F) << 4 23 | num ^= (num >> 0x1D) ^ (num >> 0x11) ^ (num >> 0x17) ^ 0x20070419 24 | 25 | crc = (num >> 8) ^ (num >> 24) ^ (num >> 16) ^ (num & 0xFF) ^ 0xFF 26 | if 232 < (0xD4A50FFF < num) + (crc & 0xFF): 27 | crc &= 0x7F 28 | 29 | crc &= 0xFF 30 | return str(int((format(crc, "08b") + format(num, "032b")), 2)).zfill(12) 31 | 32 | 33 | date = str(datetime.today().strftime("%B %d, %Y")) 34 | db = MySQLdb.connect( 35 | "localhost", 36 | config["dbuser"], 37 | config["dbpass"], 38 | "rc24_cmoc", 39 | use_unicode=True, 40 | charset="utf8mb4", 41 | ) 42 | cursor = db.cursor() 43 | 44 | headers = ["Mii", "Entry Number", "Nickname", "Initials", "Likes", "Creator"] 45 | for h in range(len(headers)): 46 | headers[h] = "\t\t" + headers[h] + "\n" 47 | headers = "\t\n" + "".join(headers) + "\n" 48 | 49 | cursor.execute( 50 | "SELECT mii.entryno, mii.initial, mii.permlikes, mii.miidata, mii.nickname, mii.craftsno, artisan.nickname, artisan.master FROM mii, artisan WHERE mii.craftsno = artisan.craftsno ORDER BY permlikes DESC LIMIT 50" 51 | ) 52 | row = cursor.fetchall() 53 | 54 | table = ( 55 | f'

These are currently the top rated Miis on the Check Mii Out Channel, ranked. Click on a Mii to download it.

\nBack to Homepage\n

{date}

\n\n' 56 | + headers 57 | ) 58 | for i in range(len(row)): 59 | artisan = row[i][6] 60 | entryno = row[i][0] 61 | initial = row[i][1] 62 | mii_filename = ( 63 | "/var/www/rc24/wapp.wii.com/miicontest/public_html/render/entry-{}.mii".format( 64 | entryno 65 | ) 66 | ) 67 | if not exists(mii_filename): 68 | with open(mii_filename, "wb") as f: 69 | miidata = decodeMii(row[i][3])[:-2] 70 | miidata = ( 71 | miidata[:28] + b"\x00\x00\x00\x00" + miidata[32:] 72 | ) # remove mac address from mii data 73 | f.write(miidata) 74 | 75 | # if not exists(mii_filename + '.png'): 76 | # call(['mono', 'MiiRender.exe', mii_filename]) 77 | 78 | if len(initial) == 1: 79 | initial += "." 80 | elif len(initial) == 2: 81 | initial = initial[0] + "." + initial[1] + "." 82 | 83 | if bool(row[i][7]): 84 | artisan += '
' 85 | 86 | longentry = decToEntry(entryno) 87 | longentry = longentry[:4] + "-" + longentry[4:8] + "-" + longentry[8:12] 88 | 89 | table += "\t\n" 90 | table += f'\t\t\n' 91 | table += f"\t\t\n" 92 | table += f"\t\t\n" 93 | table += f"\t\t\n" 94 | table += f"\t\t\n" 95 | table += f'\t\t\n' 96 | 97 | table += "\t\n" 98 | 99 | table += "
{longentry}{row[i][4]}{initial}{row[i][2]}{artisan}
\n" 100 | 101 | with open( 102 | "/var/www/rc24/wapp.wii.com/miicontest/public_html/tables/top50.html", "w" 103 | ) as file: 104 | file.write(table) 105 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/jobs.cron: -------------------------------------------------------------------------------- 1 | #generate top 50 miis once a week 2 | 0 0 */5 * * cd /var/rc24/File-Maker/Channels/Check_Mii_Out_Channel && /usr/local/bin/python3.7 ./top50.py > /var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/logs/top50.log 3 | 4 | #generate top 50 web miis once a week 5 | 5 0 */5 * * cd /var/rc24/File-Maker/Channels/Check_Mii_Out_Channel && /usr/local/bin/python3.7 ./htmltop50.py > /dev/null 2>&1 6 | 7 | #generate popular list once a day 8 | 0 0 * * * cd /var/rc24/File-Maker/Channels/Check_Mii_Out_Channel && /usr/local/bin/python3.7 ./popular.py > /var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/logs/popular.log 9 | 10 | #generate popular web list once a day 11 | 5 0 * * * cd /var/rc24/File-Maker/Channels/Check_Mii_Out_Channel && /usr/local/bin/python3.7 ./htmlpopular.py > /var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/logs/popular2.log 2>&1 12 | 13 | #generate grab bag every 30 minutes 14 | */30 * * * * cd /var/rc24/File-Maker/Channels/Check_Mii_Out_Channel && /usr/local/bin/python3.7 ./randlist.py > /var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/logs/randlist.log 15 | 16 | #generate popcrafts every 2 hours 17 | 0 */2 * * * cd /var/rc24/File-Maker/Channels/Check_Mii_Out_Channel && /usr/local/bin/python3.7 ./popcrafts.py > /var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/logs/popcrafts.log 18 | 19 | #generate artisan web list every 2 hours 20 | 5 */2 * * * cd /var/rc24/File-Maker/Channels/Check_Mii_Out_Channel && /usr/local/bin/python3.7 ./htmlrankings.py >/dev/null 2>&1 21 | 22 | #generate contest web list every 2 hours 23 | 5 */2 * * * cd /var/rc24/File-Maker/Channels/Check_Mii_Out_Channel && /usr/local/bin/python3.7 ./htmlcontest.py >/dev/null 2>&1 24 | 25 | #generate wii sports resort popular list every day 26 | 0 0 * * * cd /var/rc24/File-Maker/Channels/Check_Mii_Out_Channel && /usr/local/bin/python3.7 ./wiisports.py > /var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/logs/wsr.log 27 | 28 | #generate coninfo and condetail every 2 hours 29 | 0 */2 * * * cd /var/rc24/File-Maker/Channels/Check_Mii_Out_Channel && /usr/local/bin/python3.7 ./coninfo.py > /var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/logs/coninfo.log -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/miikaitai.py: -------------------------------------------------------------------------------- 1 | # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild 2 | 3 | from pkg_resources import parse_version 4 | from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO 5 | 6 | 7 | if parse_version(ks_version) < parse_version("0.7"): 8 | raise Exception( 9 | "Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" 10 | % (ks_version) 11 | ) 12 | 13 | 14 | class Mii(KaitaiStruct): 15 | def __init__(self, _io, _parent=None, _root=None): 16 | self._io = _io 17 | self._parent = _parent 18 | self._root = _root if _root else self 19 | self._read() 20 | 21 | def _read(self): 22 | self.invalid = self._io.read_bits_int(1) != 0 23 | self.gender = self._io.read_bits_int(1) != 0 24 | self.birth_month = self._io.read_bits_int(4) 25 | self.birth_day = self._io.read_bits_int(5) 26 | self.favorite_color = self._io.read_bits_int(4) 27 | self.favorite = self._io.read_bits_int(1) != 0 28 | self._io.align_to_byte() 29 | self.mii_name = (self._io.read_bytes(20)).decode(u"utf-16be") 30 | self.body_height = self._io.read_u1() 31 | self.body_weight = self._io.read_u1() 32 | self.avatar_id = [None] * (4) 33 | for i in range(4): 34 | self.avatar_id[i] = self._io.read_u1() 35 | 36 | self.client_id = [None] * (4) 37 | for i in range(4): 38 | self.client_id[i] = self._io.read_u1() 39 | 40 | self.face_type = self._io.read_bits_int(3) 41 | self.face_color = self._io.read_bits_int(3) 42 | self.facial_feature = self._io.read_bits_int(4) 43 | self.unknown = self._io.read_bits_int(3) 44 | self.mingle = self._io.read_bits_int(1) != 0 45 | self.unknown_2 = self._io.read_bits_int(1) != 0 46 | self.downloaded = self._io.read_bits_int(1) != 0 47 | self.hair_type = self._io.read_bits_int(7) 48 | self.hair_color = self._io.read_bits_int(3) 49 | self.hair_flip = self._io.read_bits_int(1) != 0 50 | self.unknown_3 = self._io.read_bits_int(5) 51 | self.eyebrow_type = self._io.read_bits_int(5) 52 | self.unknown_4 = self._io.read_bits_int(1) != 0 53 | self.eyebrow_rotation = self._io.read_bits_int(4) 54 | self.unknown_5 = self._io.read_bits_int(6) 55 | self.eyebrow_color = self._io.read_bits_int(3) 56 | self.eyebrow_size = self._io.read_bits_int(4) 57 | self.eyebrow_vertical = self._io.read_bits_int(5) 58 | self.eyebrow_horizontal = self._io.read_bits_int(4) 59 | self.eye_type = self._io.read_bits_int(6) 60 | self.unknown_6 = self._io.read_bits_int(2) 61 | self.eye_rotation = self._io.read_bits_int(3) 62 | self.eye_vertical = self._io.read_bits_int(5) 63 | self.eye_color = self._io.read_bits_int(3) 64 | self.unknown_7 = self._io.read_bits_int(1) != 0 65 | self.eye_size = self._io.read_bits_int(3) 66 | self.eye_horizontal = self._io.read_bits_int(4) 67 | self.unknown_8 = self._io.read_bits_int(5) 68 | self.nose_type = self._io.read_bits_int(4) 69 | self.nose_size = self._io.read_bits_int(4) 70 | self.nose_vertical = self._io.read_bits_int(5) 71 | self.unknown_9 = self._io.read_bits_int(3) 72 | self.mouth_type = self._io.read_bits_int(5) 73 | self.mouth_color = self._io.read_bits_int(2) 74 | self.mouth_size = self._io.read_bits_int(4) 75 | self.mouth_vertical = self._io.read_bits_int(5) 76 | self.glasses_type = self._io.read_bits_int(4) 77 | self.glasses_color = self._io.read_bits_int(3) 78 | self.unknown_10 = self._io.read_bits_int(1) != 0 79 | self.glasses_size = self._io.read_bits_int(3) 80 | self.glasses_vertical = self._io.read_bits_int(5) 81 | self.facial_hair_mustache = self._io.read_bits_int(2) 82 | self.facial_hair_beard = self._io.read_bits_int(2) 83 | self.facial_hair_color = self._io.read_bits_int(3) 84 | self.facial_hair_size = self._io.read_bits_int(4) 85 | self.facial_hair_vertical = self._io.read_bits_int(5) 86 | self.mole_enable = self._io.read_bits_int(1) != 0 87 | self.mole_size = self._io.read_bits_int(4) 88 | self.mole_vertical = self._io.read_bits_int(5) 89 | self.mole_horizontal = self._io.read_bits_int(5) 90 | self.unknown_11 = self._io.read_bits_int(1) != 0 91 | self._io.align_to_byte() 92 | self.creator_name = (self._io.read_bytes(20)).decode(u"utf-16be") 93 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/numberinfo.py: -------------------------------------------------------------------------------- 1 | from cmoc import QuickList, Prepare, ResetList 2 | import MySQLdb 3 | from json import load 4 | from time import sleep 5 | 6 | with open("/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/config.json", "r") as f: 7 | config = load(f) 8 | # get the top 50 most popular miis sorted by their permanent likes and add them to pop_list 9 | 10 | 11 | db = MySQLdb.connect("localhost", config["dbuser"], config["dbpass"], "rc24_cmoc") 12 | cursor = db.cursor() 13 | 14 | cursor.execute('SELECT COUNT(*) FROM mii') 15 | mii_count = int(cursor.fetchone()[0]) 16 | 17 | cursor.execute('SELECT COUNT(*) FROM artisan') 18 | artisan_count = int(cursor.fetchone()[0]) 19 | 20 | ql = QuickList() 21 | pr = Prepare() 22 | 23 | data = ql.numberinfoBuild(mii_count, artisan_count) 24 | 25 | with open( 26 | "{}/150/number_info.ces".format(config["miicontest_path"]), "wb" 27 | ) as file: 28 | file.write(pr.prepare(data)) 29 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/popcrafts.py: -------------------------------------------------------------------------------- 1 | from cmoc import QuickList, Prepare 2 | from datetime import timezone, datetime 3 | import MySQLdb 4 | from json import load 5 | 6 | with open("/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/config.json", "r") as f: 7 | config = load(f) 8 | 9 | # get the top 99 artisans and adds them to popcrafts_list 10 | 11 | ql = QuickList() 12 | pr = Prepare() 13 | 14 | db = MySQLdb.connect( 15 | "localhost", config["dbuser"], config["dbpass"], "rc24_cmoc", charset="utf8mb4" 16 | ) 17 | cursor = db.cursor() 18 | 19 | # 100000993 is the RC24 gold pants artisan 20 | cursor.execute( 21 | "SELECT craftsno FROM artisan WHERE craftsno !=100000993 ORDER BY votes DESC LIMIT 100" 22 | ) 23 | count = cursor.fetchall() 24 | 25 | artisanlist = [] 26 | 27 | for i in range(len(count)): # add the artisan data to each mii based on their craftsno 28 | cursor.execute( 29 | "SELECT master,lastpost FROM artisan WHERE craftsno = %s", [count[i][0]] 30 | ) 31 | result = cursor.fetchone() 32 | 33 | lastpost = int( 34 | ( 35 | datetime.now().replace(tzinfo=timezone.utc).timestamp() 36 | - result[1].replace(tzinfo=timezone.utc).timestamp() 37 | ) 38 | / 60 39 | / 60 40 | ) # hours since artisan last posted 41 | master = int(result[0]) 42 | 43 | if lastpost < 24: # if a new mii was uploaded within 24 hours, add new mii flag 44 | if master == 1: 45 | master = 3 # master artisan with new mii flag 46 | 47 | else: 48 | master = 2 # new mii flag 49 | 50 | cursor.execute( 51 | "SELECT craftsno,miidata,popularity,country FROM artisan WHERE craftsno = %s", 52 | [count[i][0]], 53 | ) 54 | artisanData = cursor.fetchone() 55 | artisanData = ( 56 | artisanData[:2] + (master,) + artisanData[2:] 57 | ) # insert master into the tuple where it would normally be retrieved from the db lmao 58 | artisanlist.append(artisanData) 59 | 60 | data = ql.popcraftsBuild(artisanlist) 61 | with open("{}/150/popcrafts_list.ces".format(config["miicontest_path"]), "wb") as file: 62 | file.write(pr.prepare(data)) 63 | 64 | with open("decfiles/popcrafts_list.dec", "wb") as file: 65 | file.write(data) 66 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/popular.py: -------------------------------------------------------------------------------- 1 | from cmoc import QuickList, Prepare 2 | import MySQLdb 3 | from json import load 4 | from time import sleep 5 | from random import shuffle 6 | 7 | with open("/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/config.json", "r") as f: 8 | config = load(f) 9 | 10 | db = MySQLdb.connect("localhost", config["dbuser"], config["dbpass"], "rc24_cmoc") 11 | cursor = db.cursor() 12 | 13 | cursor.execute("SELECT COUNT(*) FROM mii WHERE likes > 0") 14 | count = int(cursor.fetchone()[0]) 15 | print("Popular Count:", count) 16 | 17 | # popular is always sorted by volatile likes first, but we combine miis ordered by permlikes to fill in the rest to equal 100 total miis 18 | if count >= 1000: 19 | extraCount = 0 20 | count = 1000 21 | 22 | else: 23 | extraCount = 1000 - count 24 | 25 | cursor.execute( 26 | "SELECT mii.entryno, mii.initial, mii.permlikes, mii.skill, mii.country, mii.miidata, artisan.miidata, artisan.craftsno, artisan.master FROM mii, artisan WHERE mii.craftsno=artisan.craftsno ORDER BY mii.likes DESC LIMIT %s", 27 | [count], 28 | ) 29 | popularMiis = cursor.fetchall() 30 | 31 | cursor.execute( 32 | "SELECT mii.entryno, mii.initial, mii.permlikes, mii.skill, mii.country, mii.miidata, artisan.miidata, artisan.craftsno, artisan.master FROM mii, artisan WHERE mii.permlikes > 21 AND mii.craftsno=artisan.craftsno ORDER BY mii.entryno DESC LIMIT %s", 33 | [extraCount], 34 | ) 35 | extraMiis = cursor.fetchall() 36 | 37 | cursor.execute( 38 | "UPDATE mii SET likes = 0" 39 | ) # reset everyone's likes, but not their permlikes 40 | 41 | db.commit() 42 | db.close() 43 | 44 | res = [] 45 | combined = list(set(popularMiis + extraMiis))[:500] 46 | shuffle(combined) 47 | combined = tuple(combined) 48 | 49 | for country in [0, 150]: 50 | # gets the most popular miis ordered by their volatile likes which resets to 0 when spot_list resets 51 | ql = QuickList() 52 | pr = Prepare() 53 | 54 | data = ql.build("SL", combined, country) 55 | 56 | with open( 57 | "{}/{}/spot_list.ces".format(config["miicontest_path"], country), "wb" 58 | ) as file: 59 | file.write(pr.prepare(data)) 60 | 61 | with open("decfiles/spot_list.dec", "wb") as file: 62 | file.write(data) 63 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/randlist.py: -------------------------------------------------------------------------------- 1 | from cmoc import NumberedList, Prepare 2 | import MySQLdb 3 | from json import load 4 | 5 | with open("/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/config.json", "r") as f: 6 | config = load(f) 7 | 8 | ql = NumberedList() 9 | pr = Prepare() 10 | 11 | db = MySQLdb.connect("localhost", config["dbuser"], config["dbpass"], "rc24_cmoc") 12 | cursor = db.cursor() 13 | 14 | # grab bag is extremely unpredictable and can cause server spam or crash wiis if done incorrectly 15 | # the code below is basically all just trial and error 16 | 17 | a = [150, 150, 150, 0, 0, 0, 0, 0, 0, 0] 18 | 19 | bl = 0 20 | for n in a: 21 | bl += 1 22 | 23 | cursor.execute( 24 | "SELECT mii.entryno, mii.initial, mii.permlikes, mii.skill, mii.country, mii.miidata, artisan.miidata, artisan.craftsno, artisan.master FROM mii, artisan WHERE mii.craftsno = artisan.craftsno AND mii.permlikes<25 ORDER BY RAND() LIMIT %s", 25 | [n], 26 | ) # idk how this works it needs to be figured out 27 | miilist = cursor.fetchall() 28 | 29 | list_type = "RL" + str(bl) 30 | 31 | for country in [0, 150]: 32 | ql = NumberedList() 33 | 34 | data = ql.build(list_type, miilist, country) 35 | 36 | with open( 37 | "{}/{}/bargain_list{}.ces".format( 38 | config["miicontest_path"], country, str(bl).zfill(2) 39 | ), 40 | "wb", 41 | ) as file: 42 | file.write(pr.prepare(data)) 43 | 44 | with open( 45 | "decfiles/bargain_list/bargain_list{}.dec".format(str(bl).zfill(2)), "wb" 46 | ) as file: 47 | file.write(data) 48 | 49 | a = [150, 150, 150, 0, 0, 0, 0, 0, 0, 0] 50 | nl = 0 51 | for t in a: 52 | nl += 1 53 | 54 | cursor.execute( 55 | "SELECT mii.entryno, mii.initial, mii.permlikes, mii.skill, mii.country, mii.miidata, artisan.miidata, artisan.craftsno, artisan.master FROM mii, artisan WHERE mii.craftsno = artisan.craftsno ORDER BY entryno DESC LIMIT %s", 56 | [t], 57 | ) 58 | miilist = cursor.fetchall() 59 | 60 | list_type = "NL" + str(nl) 61 | 62 | for country in [0, 150]: 63 | ql = NumberedList() 64 | 65 | data = ql.build(list_type, miilist, country) 66 | 67 | with open( 68 | "{}/{}/new_list{}.ces".format( 69 | config["miicontest_path"], country, str(nl).zfill(2) 70 | ), 71 | "wb", 72 | ) as file: 73 | file.write(pr.prepare(data)) 74 | 75 | with open( 76 | "decfiles/bargain_list/new_list{}.dec".format(str(nl).zfill(2)), "wb" 77 | ) as file: 78 | file.write(data) 79 | 80 | db.close() 81 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/sign_encrypt.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import binascii 3 | import collections 4 | import os 5 | import pyaes 6 | import rsa 7 | import struct 8 | import subprocess 9 | 10 | 11 | def u8(data): 12 | return struct.pack(">B", data) 13 | 14 | 15 | def u16(data): 16 | return struct.pack(">H", data) 17 | 18 | 19 | def u32(data): 20 | return struct.pack(">I", data) 21 | 22 | 23 | parser = argparse.ArgumentParser(description="Sign / Encrypt WiiConnect24 files.") 24 | parser.add_argument( 25 | "-t", 26 | "--type", 27 | type=str, 28 | nargs="+", 29 | help="Type of file. Set either enc for encrypted file, or dec for decrypted (compressed) file.", 30 | ) 31 | parser.add_argument("-in", "--input", type=str, nargs="+", help="Input file.") 32 | parser.add_argument("-out", "--output", type=str, nargs="+", help="Output file.") 33 | parser.add_argument( 34 | "-c", 35 | "--compress", 36 | type=str, 37 | nargs="+", 38 | help="If set, this will compress the file before signing.", 39 | ) 40 | parser.add_argument( 41 | "-key", "--aes-key", type=str, nargs="+", help="AES key in hex or a path." 42 | ) 43 | parser.add_argument( 44 | "-iv", "--iv-key", type=str, nargs="+", help="AES IV in hex or a path." 45 | ) 46 | parser.add_argument( 47 | "-rsa", 48 | "--rsa-key-path", 49 | type=str, 50 | nargs="+", 51 | help="RSA private key path. If not specified, it will use the private key in Private.pem if it exists.", 52 | ) 53 | 54 | args = parser.parse_args() 55 | 56 | if args.compress is not None: 57 | subprocess.call(["cp", args.input[0], "temp"]) 58 | subprocess.call(["lzss", "-evf", "temp"]) 59 | 60 | if args.type[0] == "enc": 61 | filename = args.input[0] 62 | elif args.type[0] == "dec": 63 | filename = "temp" 64 | 65 | with open(filename, "rb") as f: 66 | data = f.read() 67 | 68 | """RSA sign the file.""" 69 | 70 | if args.rsa_key_path is not None: 71 | rsa_key_path = args.rsa_key_path[0] 72 | else: 73 | rsa_key_path = "Private.pem" 74 | 75 | with open(rsa_key_path, "rb") as source_file: 76 | private_key_data = source_file.read() 77 | 78 | private_key = rsa.PrivateKey.load_pkcs1(private_key_data, "PEM") 79 | 80 | signature = rsa.sign(data, private_key, "SHA-1") 81 | 82 | if args.type[0] == "enc": 83 | if args.iv_key is not None: 84 | try: 85 | iv = binascii.unhexlify(args.iv_key[0]) 86 | except: 87 | iv = open(args.iv_key[0], "rb").read() 88 | else: 89 | iv = os.urandom(16) 90 | 91 | try: 92 | key = binascii.unhexlify(args.aes_key[0]) 93 | except: 94 | key = open(args.aes_key[0], "rb").read() 95 | 96 | aes = pyaes.AESModeOfOperationOFB(key, iv=iv) 97 | processed = aes.encrypt(data) 98 | elif args.type[0] == "dec": 99 | processed = data 100 | 101 | content = collections.OrderedDict() 102 | 103 | content["magic"] = b"WC24" if args.type[0] == "enc" else u32(0) 104 | content["version"] = u32(1) if args.type[0] == "enc" else u32(0) 105 | content["filler"] = u32(0) 106 | content["crypt_type"] = u8(1) if args.type[0] == "enc" else u8(0) 107 | content["pad"] = u8(0) * 3 108 | content["reserved"] = u8(0) * 32 109 | content["iv"] = iv if args.type[0] == "enc" else u8(0) * 16 110 | content["signature"] = signature 111 | content["data"] = processed 112 | 113 | if os.path.exists(args.output[0]): 114 | os.remove(args.output[0]) 115 | 116 | if args.type[0] == "dec": 117 | os.remove("temp") 118 | 119 | for values in content.values(): 120 | with open(args.output[0], "ab+") as f: 121 | f.write(values) 122 | 123 | print("Completed Successfully") 124 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/top50.py: -------------------------------------------------------------------------------- 1 | from cmoc import QuickList, Prepare, ResetList 2 | import MySQLdb 3 | from json import load 4 | from time import sleep 5 | 6 | with open("/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/config.json", "r") as f: 7 | config = load(f) 8 | # get the top 50 most popular miis sorted by their permanent likes and add them to pop_list 9 | 10 | 11 | db = MySQLdb.connect("localhost", config["dbuser"], config["dbpass"], "rc24_cmoc") 12 | cursor = db.cursor() 13 | 14 | cursor.execute( 15 | "SELECT mii.entryno, mii.initial, mii.permlikes, mii.skill, mii.country, mii.miidata, artisan.miidata, artisan.craftsno, artisan.master FROM mii, artisan WHERE mii.craftsno=artisan.craftsno ORDER BY permlikes DESC LIMIT 50" 16 | ) 17 | miilist = cursor.fetchall() 18 | 19 | for country in [0, 150]: 20 | ql = QuickList() 21 | pr = Prepare() 22 | 23 | data = ql.build("PL", miilist, country) 24 | 25 | with open( 26 | "{}/{}/pop_list.ces".format(config["miicontest_path"], country), "wb" 27 | ) as file: 28 | file.write(pr.prepare(data)) 29 | -------------------------------------------------------------------------------- /Channels/Check_Mii_Out_Channel/wiisports.py: -------------------------------------------------------------------------------- 1 | import MySQLdb 2 | from json import load 3 | from cmoc import WSR 4 | from os import system 5 | from base64 import b64decode, b64encode 6 | from crc16 import crc16xmodem as crc 7 | 8 | with open("/var/rc24/File-Maker/Channels/Check_Mii_Out_Channel/config.json", "r") as f: 9 | config = load(f) 10 | 11 | db = MySQLdb.connect("localhost", config["dbuser"], config["dbpass"], "rc24_cmoc") 12 | cursor = db.cursor() 13 | 14 | cursor.execute("SELECT COUNT(*) FROM mii WHERE likes > 0 LIMIT 100") 15 | count = int(cursor.fetchone()[0]) 16 | print("Popular Count:", count) 17 | 18 | # popular is always sorted by volatile likes first, but we combine miis ordered by permlikes to fill in the rest to equal 100 total miis 19 | if count >= 100: 20 | extraCount = 0 21 | count = 100 22 | 23 | else: 24 | extraCount = 100 - count 25 | 26 | cursor.execute( 27 | "SELECT mii.initial, mii.miidata, artisan.miidata, mii.entryno FROM mii, artisan WHERE mii.craftsno=artisan.craftsno ORDER BY mii.likes DESC LIMIT %s", 28 | [count], 29 | ) 30 | popularMiis = cursor.fetchall() 31 | 32 | cursor.execute( 33 | "SELECT mii.initial, mii.miidata, artisan.miidata, mii.entryno FROM mii, artisan WHERE mii.permlikes < 25 AND mii.craftsno=artisan.craftsno ORDER BY mii.permlikes DESC LIMIT %s", 34 | [extraCount], 35 | ) 36 | extraMiis = cursor.fetchall() 37 | 38 | db.close() 39 | 40 | ql = WSR().build(popularMiis + extraMiis) 41 | with open("decfiles/wiisports.dec", "wb") as file: 42 | file.write(ql) 43 | 44 | path = config["miicontest_path"] 45 | 46 | with open("{}/dd/wiisports.dec".format(path), "wb") as file: 47 | file.write(ql) 48 | 49 | # symlink all miidd country code files to wiisports.enc with its FULL directory path 50 | system( 51 | "python3 ./sign_encrypt.py -t enc -in '{}/dd/wiisports.dec' -out '{}/dd/wiisports.enc' -key 91D9A5DD10AAB467491A066EAD9FDD6F -rsa /var/rc24/key/miidd.pem".format( 52 | path, path 53 | ) 54 | ) 55 | -------------------------------------------------------------------------------- /Channels/Everybody_Votes_Channel/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Everybody_Votes_Channel/__init__.py -------------------------------------------------------------------------------- /Channels/Everybody_Votes_Channel/config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "key_path": "/path/to/key", 3 | "file_path": "/path/to/save/file", 4 | "production": true, 5 | "mysql_user": "user", 6 | "mysql_password": "password", 7 | "mysql_database": "database", 8 | "sentry_url": "http://status.domain.tld/", 9 | "webhook_urls": [ 10 | "http://status.domain.tld/" 11 | ], 12 | "packVFF": true 13 | } -------------------------------------------------------------------------------- /Channels/Everybody_Votes_Channel/jobs.cron: -------------------------------------------------------------------------------- 1 | 0 0 1,16 * 2,4,6 cd /var/rc24/File-Maker/ && /usr/local/bin/python3.7 -m Channels.Everybody_Votes_Channel.votes v >/dev/null 2>&1 2 | 0 0 * * 2,4,6 cd /var/rc24/File-Maker/ && /usr/local/bin/python3.7 -m Channels.Everybody_Votes_Channel.votes q n >/dev/null 2>&1 3 | 0 0 * * 2,4,6 cd /var/rc24/File-Maker/ && /usr/local/bin/python3.7 -m Channels.Everybody_Votes_Channel.votes r n >/dev/null 2>&1 4 | 0 0 1,16 * * cd /var/rc24/File-Maker/ && /usr/local/bin/python3.7 -m Channels.Everybody_Votes_Channel.votes q w >/dev/null 2>&1 5 | 0 0 1,16 * * cd /var/rc24/File-Maker/ && /usr/local/bin/python3.7 -m Channels.Everybody_Votes_Channel.votes r w >/dev/null 2>&1 6 | 5 0 1,16 * 2,4,6 cd /var/rc24/pack/ && /usr/local/bin/python3.7 votespack.py >/dev/null 2>&1 -------------------------------------------------------------------------------- /Channels/Everybody_Votes_Channel/voteslists.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import collections 5 | 6 | """List of countries the EVC uses.""" 7 | 8 | countries = collections.OrderedDict() 9 | 10 | countries["Japan"] = ["日本", "Japan", "Japan", "Japon", "Japón", "Giappone", "Japan"] 11 | countries["Argentina"] = ["アルゼンチン", "Argentina", "Argentinien", "Argentine", "Argentina", "Argentina", "Argentinië"] 12 | countries["Brazil"] = ["ブラジル", "Brazil", "Brasilien", "Brésil", "Brasil", "Brasile", "Brazilië"] 13 | countries["Canada"] = ["カナダ", "Canada", "Kanada", "Canada", "Canadá", "Canada", "Canada"] 14 | countries["Chile"] = ["チリ", "Chile", "Chile", "Chili", "Chile", "Cile", "Chili"] 15 | countries["Colombia"] = ["コロンビア", "Colombia", "Kolumbien", "Colombie", "Colombia", "Colombia", "Colombia"] 16 | countries["Costa Rica"] = ["コスタリカ", "Costa Rica", "Costa Rica", "Costa Rica", "Costa Rica", "Costa Rica", "Costa Rica"] 17 | countries["Ecuador"] = ["エクアドル", "Ecuador", "Ecuador", "Equateur", "Ecuador", "Ecuador", "Ecuador"] 18 | countries["Guatemala"] = ["グアテマラ", "Guatemala", "Guatemala", "Guatemala", "Guatemala", "Guatemala", "Guatemala"] 19 | countries["Mexico"] = ["メキシコ", "Mexico", "Mexiko", "Mexique", "México", "Messico", "Mexico"] 20 | countries["Panama"] = ["パナマ", "Panama", "Panama", "Panama", "Panamá", "Panamá", "Panama"] 21 | countries["Peru"] = ["ペルー", "Peru", "Peru", "Pérou", "Perú", "Perù", "Peru"] 22 | countries["United States"] = ["アメリカ", "United States", "Vereinigte Staaten", "Etats-Unis d’Amérique", "Estados Unidos de América", "Stati Uniti d'America", "Verenigde Staten"] 23 | countries["Venezuela"] = ["ベネズエラ", "Venezuela", "Venezuela", "Venezuela", "Venezuela", "Venezuela", "Venezuela"] 24 | countries["Australia"] = ["オーストラリア", "Australia", "Australien", "Australie", "Australia", "Australia", "Australië"] 25 | countries["Austria"] = ["オーストリア", "Austria", "Österreich", "Autriche", "Austria", "Austria", "Oostenrijk"] 26 | countries["Belgium"] = ["ベルギー", "Belgium", "Belgien", "Belgique", "Bélgica", "Belgio", "België"] 27 | countries["Denmark"] = ["デンマーク", "Denmark", "Dänemark", "Danemark", "Dinamarca", "Danimarca", "Denemarken"] 28 | countries["Finland"] = ["フィンランド", "Finland", "Finnland", "Finlande", "Finlandia", "Finlandia", "Finland"] 29 | countries["France"] = ["フランス", "France", "Frankreich", "France", "Francia", "Francia", "Frankrijk"] 30 | countries["Germany"] = ["ドイツ", "Germany", "Deutschland", "Allemagne", "Alemania", "Germania", "Duitsland"] 31 | countries["Greece"] = ["ギリシャ", "Greece", "Griechenland", "Grèce", "Grecia", "Grecia", "Griekenland"] 32 | countries["Ireland"] = ["アイルランド", "Ireland", "Irland", "Irlande", "Irlanda", "Irlanda", "Ierland"] 33 | countries["Italy"] = ["イタリア", "Italy", "Italien", "Italie", "Italia", "Italia", "Italië"] 34 | countries["Luxembourg"] = ["ルクセンブルク", "Luxembourg", "Luxemburg", "Luxembourg", "Luxemburgo", "Lussemburgo", "Luxemburg"] 35 | countries["Netherlands"] = ["オランダ", "Netherlands", "Niederlande", "Pays-Bas", "Países Bajos", "Paesi Bassi", "Nederland"] 36 | countries["New Zealand"] = ["ニュージーランド", "New Zealand", "Neuseeland", "Nouvelle-Zélande", "Nueva Zelanda", "Nuova Zelanda", "Nieuw-Zeeland"] 37 | countries["Norway"] = ["ノルウェー", "Norway", "Norwegen", "Norvège", "Noruega", "Norvegia", "Noorwegen"] 38 | countries["Poland"] = ["ポーランド", "Poland", "Polen", "Pologne", "Polonia", "Polonia", "Polen"] 39 | countries["Portugal"] = ["ポルトガル", "Portugal", "Portugal", "Portugal", "Portugal", "Portogallo", "Portugal"] 40 | countries["Spain"] = ["スペイン", "Spain", "Spanien", "Espagne", "España", "Spagna", "Spanje"] 41 | countries["Sweden"] = ["スウェーデン", "Sweden", "Schweden", "Suède", "Suecia", "Svezia", "Zweden"] 42 | countries["Switzerland"] = ["スイス", "Switzerland", "Schweiz", "Suisse", "Suiza", "Svizzera", "Zwitserland"] 43 | countries["United Kingdom"] = ["イギリス", "United Kingdom", "Großbritannien", "Royaume-Uni", "Reino Unido", "Regno Unito", "Verenigd Koninkrijk"] 44 | 45 | """List of country codes.""" 46 | 47 | country_codes = [1, 10, 16, 18, 20, 21, 22, 25, 30, 36, 40, 42, 49, 52, 65, 66, 67, 74, 76, 77, 78, 79, 82, 83, 88, 94, 95, 96, 97, 98, 105, 107, 108, 110] 48 | 49 | """These lists tell the script how many entries are used for the position tables.""" 50 | """(if it's more than 1, that must mean the region is split up into multiple parts)""" 51 | 52 | position_table = collections.OrderedDict() 53 | 54 | position_table[1] = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2] 55 | position_table[16] = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1] 56 | position_table[18] = [1, 1, 2, 1, 1, 3, 1, 1, 1, 1, 1, 4, 3] 57 | position_table[21] = [1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0] 58 | position_table[36] = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 59 | position_table[40] = [2, 0, 1, 1, 1, 0, 0, 1, 1, 2] 60 | position_table[49] = [1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 61 | position_table[77] = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0] 62 | position_table[78] = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 63 | position_table[83] = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 64 | position_table[94] = [1, 1, 1, 3, 1, 1, 1, 1, 1, 2, 1, 1] 65 | position_table[105] = [1, 1, 1, 1, 3, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 66 | position_table[110] = [1, 2, 2, 1, 1] 67 | 68 | """Data for the position table. Nintendo mixed these up to match the order votes were submitted in (I think).""" 69 | """That would be more effort to re-arrange the data in the position table, so I just made it read the values only if there is any votes for the region.""" 70 | 71 | position_data = collections.OrderedDict() 72 | 73 | position_data[1] = "A2A4C828AF52B964B478AA64AA73AA87AD9BA5969B96A09EADA5A2A987947F8E78A096A5919B9B8782A591AF82AF7AB978AA6EAA6DB364AF73B96BC05AA546AA55AF4BB437B95FC358BA46C350C82DBE26C623CD2DD237C837D728E14849395A" 74 | position_data[16] = "A4862664E8648E1E4141C873D746CD9E7DA0B4467878B99B8746E35385BEC855C2AEE94D82DC4B6996C8A5AAE3699687E15AA064" 75 | position_data[18] = "87BE3CA009981EA064AAC8C3F0A8E1AAC89BD7C3D4BDAAAA50AF1E695C405649505A3C787841647D8E89" 76 | position_data[21] = "7C7D78739BC8695AAA5A71247D468D6B6E6E579887326946969BC896649B9119782D8C8C4BA58D4864B2677B647328194E19875A733E6E825A87" 77 | position_data[36] = "37508FB0786914465A5A69A54B7D98B69B9E8AAF9687E6A07DAF82918C787DA2649B91B476988BA1EBAA5F7D8CBE91A52B6F67B2A5C8C8C899AE738CC8B9D7B4" 78 | position_data[40] = "A05DAF7B1E7373737D5A739BAA5250823AA0" 79 | position_data[49] = "D25E78D252E748E1AA87917D3C7819645A64E04EDC5FC8A0BE872EE628DF18D98C5A3C46A064AA5F7869B46C9191E249DC64EB37A53FAF5087419169A08C5037D2737337735AE440DC55557D2D5AD746E254B95D7D7D2341CD55E84CC87D714BAA7878914164CD69DC3F272F9B46C3645550F0BE" 80 | position_data[77] = "8246DC465AB49196463CA06E28467864AA46E6E6C86E6E3296C87896C84678C88C14505A8C2D508CC8C8BE96" 81 | position_data[78] = "B95A64966EDC9BC8C86E5F417837AF2D7350467841AA3CBEBE919664781E8C8C" 82 | position_data[83] = "7D822328283C324B463264196432821E64466464786E82649682A08CA0A0BE96B9AABEBE96E63CB4" 83 | position_data[94] = "645AC8418C6496288214B40AAA82D223BE08A0C882B4B46E32C8788232C8" 84 | position_data[105] = "6E5F64E6A03C3C1EF852E65FCA739AD9A7E6B4E1C8E6EBE1641E7878503CC832AA73468C1E32A0968C28781E7832" 85 | position_data[110] = "B4B4738732E67846D71E82B4507D" 86 | 87 | """Number of regions for each country.""" 88 | 89 | region_number = collections.OrderedDict() 90 | 91 | region_number[1] = 47 92 | region_number[10] = 24 93 | region_number[16] = 27 94 | region_number[18] = 13 95 | region_number[20] = 13 96 | region_number[21] = 33 97 | region_number[22] = 7 98 | region_number[25] = 22 99 | region_number[30] = 22 100 | region_number[36] = 32 101 | region_number[40] = 10 102 | region_number[42] = 25 103 | region_number[49] = 52 104 | region_number[52] = 25 105 | region_number[65] = 8 106 | region_number[66] = 9 107 | region_number[67] = 3 108 | region_number[74] = 17 109 | region_number[76] = 6 110 | region_number[77] = 26 111 | region_number[78] = 16 112 | region_number[79] = 13 113 | region_number[82] = 8 114 | region_number[83] = 20 115 | region_number[88] = 3 116 | region_number[94] = 12 117 | region_number[95] = 13 118 | region_number[96] = 5 119 | region_number[97] = 16 120 | region_number[98] = 7 121 | region_number[105] = 17 122 | region_number[107] = 21 123 | region_number[108] = 23 124 | region_number[110] = 5 125 | 126 | language_num = collections.OrderedDict() 127 | 128 | language_num[0] = "Japanese" 129 | language_num[1] = "English" 130 | language_num[2] = "German" 131 | language_num[3] = "French" 132 | language_num[4] = "Spanish" 133 | language_num[5] = "Italian" 134 | language_num[6] = "Dutch" 135 | language_num[7] = "Portuguese" 136 | language_num[8] = "French Canada" 137 | 138 | """Languages each country uses. The numbers correspond to the ones in the dictionary above.""" 139 | 140 | country_language = collections.OrderedDict() 141 | 142 | country_language[1] = [1] 143 | country_language[10] = [1, 4, 8] 144 | country_language[16] = [1, 4, 7, 8] 145 | country_language[18] = [1, 4, 8] 146 | country_language[20] = [1, 4, 8] 147 | country_language[21] = [1, 4, 8] 148 | country_language[22] = [1, 4, 8] 149 | country_language[25] = [1, 4, 8] 150 | country_language[30] = [1, 4, 8] 151 | country_language[36] = [1, 4, 8] 152 | country_language[40] = [1, 4, 8] 153 | country_language[42] = [1, 4, 8] 154 | country_language[49] = [1, 4, 8] 155 | country_language[52] = [1, 4, 8] 156 | country_language[65] = [1] 157 | country_language[66] = [2, 3, 5, 6] 158 | country_language[67] = [2, 3, 5, 6] 159 | country_language[74] = [1] 160 | country_language[76] = [1] 161 | country_language[77] = [3] 162 | country_language[78] = [2] 163 | country_language[79] = [1, 4, 7] 164 | country_language[82] = [1] 165 | country_language[83] = [5] 166 | country_language[88] = [2, 3, 5, 6] 167 | country_language[94] = [6] 168 | country_language[95] = [1] 169 | country_language[96] = [1] 170 | country_language[97] = [1] 171 | country_language[98] = [1, 4, 7] 172 | country_language[105] = [4] 173 | country_language[107] = [1] 174 | country_language[108] = [2, 3, 5, 6] 175 | country_language[110] = [1] 176 | 177 | category_text = collections.OrderedDict() 178 | 179 | category_text[0] = "Thoughts" 180 | category_text[1] = "Personality" 181 | category_text[2] = "Surroundings" 182 | category_text[3] = "Experience" 183 | category_text[4] = "Knowledge" 184 | 185 | """Poll categories. The keys correspond to the ones above.""" 186 | 187 | categories = collections.OrderedDict() 188 | 189 | categories[0] = 3 190 | categories[1] = 5 191 | categories[2] = 7 192 | categories[3] = 9 193 | categories[4] = 10 194 | -------------------------------------------------------------------------------- /Channels/Forecast_Channel/README.md: -------------------------------------------------------------------------------- 1 | # Forecast Channel 2 | 3 | Thanks for your interest in helping out with RiiConnect24's File Maker. 4 | 5 | Please read on to learn how to add your own forecast city (and PR it to us). 6 | 7 | ## Files 8 | 9 | ### Scripts 10 | 11 | + forecast.py downloads the forecast from AccuWeather. 12 | + forecastlists.py has a list of the forecast cities that will be included. 13 | + forecastregions.py has region data for countries. 14 | 15 | ## How to Contribute 16 | 17 | If you would like to add your own forecast city, here are some instructions. 18 | 19 | We are not accepting new cities for these following countries: 20 | 21 | + Argentina 22 | + Australia 23 | + Brazil 24 | + Canada 25 | + Finland 26 | + France 27 | + Germany 28 | + Italy 29 | + Mexico 30 | + Portugal 31 | + United Kingdom 32 | + United States 33 | 34 | Your city needs to be popular enough in order for it to be added. If it has a decent population, that's good. 35 | 36 | To add a city, find the dictionary with the country code of the country you want to add a city for ([list of country codes are here](https://wiibrew.org/wiki/Country_Codes)) then make a dictionary entry. Make sure to fit it in the right place. 37 | 38 | Then comes the entries in this order: 39 | 40 | 1. A list of the city names in 7 languages in this order: (Japanese, English, German, French, Spanish, Italian, Dutch) 41 | 1. A list of the region names in 7 languages in the same order. You can find this in forecastregions.py or from another value. 42 | 1. A list of the country names in 7 languages in the same order. You can also find this in forecastregions.py or from another value. 43 | 1. Coordinates. You can get the latitude and longitude of the city, and convert it to what the Wii uses with this Python code: 44 | 45 | ```python 46 | def coord_decode(value): 47 | value = int(value,16) 48 | if value >= 0x8000: value -= 0x10000 49 | return value*0.0054931640625 50 | ``` 51 | 52 | Then comes the first zoom factor. You can put it as any value from 0x0-0x9, as it's only something trivial for how the cities appear on the Globe. 53 | Put "030000" after that. 54 | -------------------------------------------------------------------------------- /Channels/Forecast_Channel/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Forecast_Channel/__init__.py -------------------------------------------------------------------------------- /Channels/Forecast_Channel/config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "key_path": "/path/to/key", 3 | "file_path": "/path/to/save/file", 4 | "production": true, 5 | "multithreaded": true, 6 | "wii_u_generation": true, 7 | "check_coordinates": false, 8 | "send_logs": true, 9 | "send_stats": true, 10 | "send_webhooks": true, 11 | "sentry_url": "http://example.com/", 12 | "webhook_urls": [ 13 | "http://example.com/" 14 | ], 15 | "packVFF": true, 16 | "download_locations": false, 17 | "cloudflare_cache_purge": true, 18 | "cloudflare_zone_name": "id", 19 | "cloudflare_token": "token", 20 | "cloudflare_hostname": "http://example.com/" 21 | } -------------------------------------------------------------------------------- /Channels/Forecast_Channel/jobs.cron: -------------------------------------------------------------------------------- 1 | 0 * * * * cd /var/rc24/File-Maker/ && /usr/bin/timeout 10m /usr/local/bin/python3.7 -m Channels.Forecast_Channel.forecast >/dev/null 2>&1 2 | 5 * * * * cd /var/rc24/pack/ && /usr/local/bin/python3.7 forecastpack.py >/dev/null 2>&1 -------------------------------------------------------------------------------- /Channels/News_Channel/README.md: -------------------------------------------------------------------------------- 1 | # News Channel 2 | 3 | These are the scripts we use to run the News Channel. 4 | 5 | ## Files 6 | 7 | ### Scripts 8 | 9 | + newsdownload.py downloads the news from the news site. 10 | + newsmake.py parses the news data into a data file the News Channel loads. 11 | + news.py calls newsdownload and newsmake with the news source to download from. 12 | 13 | The "logos" folder contains the news source logos that will show up on the News Channel. 14 | 15 | ## How to Add Your Own News Source 16 | 17 | If you want to add your own news source (if you have a valid reason to do so) here are some instructions. 18 | 19 | NOTE: I recommend you have a little bit of Python experience before you go any further. 20 | 21 | First, go in newsdownload.py and take a look at the `sources` dictionary: 22 | 23 | + Key: ID used for the news source. 24 | + Values: 25 | + name: Name of the news source. If using a custom logo for the news source, it needs to use the same name for the JPEG. 26 | + url: Base URL for the RSS feeds. We use `%s` substitutions for the categories (will be explained later) 27 | + lang: Language that the news is in. It's used for the newspaper module in order for it to know what language to get the news in. You can find the list of available languages with the `newspaper.languages()` command, but we only support 7 (Japanese, English, German, French, Spanish, Italian, and Dutch). 28 | + cat: Categories for the news: 29 | + Key: RSS feed name to substitute in url. 30 | + Value: ID used internally for the news category. 31 | 32 | Next, jump to the `Parse` class. In the `__init__` function, add an entry: 33 | 34 | + Key: Needs to match the key used for the `sources` dictionary (the news source ID). 35 | + Value: Name of the function we will define to manage special things for the news source. 36 | 37 | Now define the function for the `Parse` class. The two main things you usually have to manage in this function are: 38 | 39 | + Caption. newspaper unfortunately doesn't automatically parse captions, so normally you can grab it from the HTML with BeautifulSoup. 40 | + Location. Hopefully you are using a news source that has the location at the start of the article. 41 | 42 | Confused? Look at the other functions to see how it's done. 43 | 44 | Now for newsmake.py, take a look at the `sources` dictionary there: 45 | 46 | + Key: Needs to match the key used for the other `sources` dictionary (the news source ID). 47 | + Values: 48 | + topics_news: News topics used. 49 | + Key: Topic name used in the actual News Channel. 50 | + Value: Needs to match the value used in the `cat` dictionary inside the other `sources` dictionary. 51 | + languages: List of languages that is used for a hidden language selection screen that can only be toggled with an integer in the file. The way we do it is have it be [0] if the source is to be used in Japan, [1, 3, 4] for America, and [1, 2, 3, 4, 5, 6] for Europe. 52 | + language_code: Used to indicate the language used for the source. 53 | + 0: Japanese 54 | + 1: English 55 | + 2: German 56 | + 3: French 57 | + 4: Spanish 58 | + 5: Italian 59 | + 6: Dutch 60 | + country_code: Used to indicate the country code for the file. Since the news files we make are identical per country in a region, we just set it to 1 for Japan, 49 for America, and 110 for Europe. 61 | 62 | A couple more things, and you're done: 63 | 64 | In the `source_nums` dictionary in the `make_source_nums` function, you need to add an entry: 65 | 66 | + Key: Needs to match the key used for the `sources` dictionary from newsdownload.py (the news source ID). 67 | + Value: This needs to be an array: 68 | + 1st Entry: Logo to use. 69 | + 0: Custom Logo 70 | + 1: Mainichi 71 | + 2: News24 72 | + 3: Associated Press 73 | + 4: AFP 74 | + 5: ANP 75 | + 6: ANSA 76 | + 2nd Entry: Position for the logo to use. 77 | + 1: https://imgur.com/I3H59qp 78 | + 2: https://imgur.com/uzwwgsy 79 | + 3: https://imgur.com/heqzeTz 80 | + 4: https://imgur.com/AJAQndN 81 | + 5: https://imgur.com/3rQxpVJ 82 | + 6: https://imgur.com/f1O8VjS 83 | 84 | If using a custom logo, add the ID that matches the one used in `sources` from newsdownload.py to the `sources` array in the `make_source_pictures` function 85 | 86 | That's it! If you made it this far, I'd give you a few cookies if I could, unless you just skipped all the way to the end of here. -------------------------------------------------------------------------------- /Channels/News_Channel/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/News_Channel/__init__.py -------------------------------------------------------------------------------- /Channels/News_Channel/config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "key_path": "/path/to/key", 3 | "file_path": "/path/to/save/file", 4 | "force_all": false, 5 | "google_maps_api_key": "key", 6 | "production": true, 7 | "sentry_url": "http://status.domain.tld/", 8 | "webhook_urls": [ 9 | "http://status.domain.tld/" 10 | ], 11 | "packVFF": true 12 | } -------------------------------------------------------------------------------- /Channels/News_Channel/jobs.cron: -------------------------------------------------------------------------------- 1 | 0 * * * * cd /var/rc24/File-Maker/ && /usr/local/bin/python3.7 -m Channels.News_Channel.news >/dev/null 2>&1 2 | 5 * * * * cd /var/rc24/pack/ && /usr/local/bin/python3.7 newspack.py >/dev/null 2>&1 -------------------------------------------------------------------------------- /Channels/News_Channel/logos/AFP_French.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/News_Channel/logos/AFP_French.jpg -------------------------------------------------------------------------------- /Channels/News_Channel/logos/AFP_German.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/News_Channel/logos/AFP_German.jpg -------------------------------------------------------------------------------- /Channels/News_Channel/logos/AFP_Spanish.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/News_Channel/logos/AFP_Spanish.jpg -------------------------------------------------------------------------------- /Channels/News_Channel/logos/ANP.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/News_Channel/logos/ANP.jpg -------------------------------------------------------------------------------- /Channels/News_Channel/logos/AP.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/News_Channel/logos/AP.jpg -------------------------------------------------------------------------------- /Channels/News_Channel/logos/CanadianPress.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/News_Channel/logos/CanadianPress.jpg -------------------------------------------------------------------------------- /Channels/News_Channel/logos/Reuters.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/News_Channel/logos/Reuters.jpg -------------------------------------------------------------------------------- /Channels/News_Channel/logos/SID.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/News_Channel/logos/SID.jpg -------------------------------------------------------------------------------- /Channels/News_Channel/news.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from Channels.News_Channel import newsdownload, newsmake 5 | from .newsdownload import News 6 | from .newsmake import NewsMake 7 | import sys 8 | import threading 9 | from utils import * 10 | 11 | 12 | def main(): 13 | print("News Channel File Generator \nBy Larsen Vallecillo / www.rc24.xyz\n") 14 | if len(sys.argv) > 1: 15 | download(sys.argv[1]) 16 | else: 17 | threads = [] 18 | 19 | sources = [ 20 | "ap_english", 21 | "ap_spanish", 22 | "reuters_europe_english", 23 | "afp_french", 24 | "afp_german", 25 | "afp_spanish", 26 | "ansa_italian", 27 | "anp_dutch", 28 | "reuters_japanese", 29 | "ap_canada", 30 | "ap_australia", 31 | ] 32 | 33 | for source in sources: 34 | t = threading.Thread(target=download, args=(source,)) 35 | threads.append(t) 36 | t.start() 37 | 38 | for t in threads: 39 | t.join() 40 | 41 | 42 | def download(source): 43 | try: 44 | if source == "ap_english": 45 | NewsMake("AP English", "ap_english", 1, "America", News("ap_english")) 46 | elif source == "ap_spanish": 47 | NewsMake("AP Spanish", "ap_spanish", 4, "America", News("ap_spanish")) 48 | elif source == "reuters_europe_english": 49 | NewsMake( 50 | "Reuters Europe English", 51 | "reuters_europe_english", 52 | 1, 53 | "Europe", 54 | News("reuters_europe_english"), 55 | ) 56 | elif source == "afp_french": 57 | NewsMake("AFP French", "afp_french", 3, "International", News("afp_french")) 58 | elif source == "afp_german": 59 | NewsMake("AFP German", "afp_german", 2, "Europe", News("afp_german")) 60 | elif source == "afp_spanish": 61 | NewsMake("AFP Spanish", "afp_spanish", 4, "Europe", News("afp_spanish")) 62 | elif source == "ansa_italian": 63 | NewsMake("ANSA Italian", "ansa_italian", 5, "Europe", News("ansa_italian")) 64 | elif source == "anp_dutch": 65 | NewsMake("ANP Dutch", "anp_dutch", 6, "Europe", News("anp_dutch")) 66 | elif source == "reuters_japanese": 67 | NewsMake( 68 | "Reuters Japanese", 69 | "reuters_japanese", 70 | 0, 71 | "Japan", 72 | News("reuters_japanese"), 73 | ) 74 | elif source == "ap_canada": 75 | NewsMake("AP Canada", "ap_canada", 1, "Canada", News("ap_canada")) 76 | elif source == "ap_australia": 77 | NewsMake( 78 | "AP Australia", "ap_australia", 1, "Australia", News("ap_australia") 79 | ) 80 | else: 81 | print("Invalid source specified.") 82 | exit() 83 | except Exception as e: 84 | print("Failed to make news for " + source + ".") 85 | raise e 86 | 87 | 88 | if __name__ == "__main__": 89 | main() 90 | -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/__init__.py -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "file_path": "/path/to/ninchannel/folder", 3 | "make_info": true, 4 | "production": false 5 | } -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/dstrial_header.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import collections 3 | import struct 4 | import sys 5 | 6 | 7 | def u8(data): 8 | if not 0 <= data <= 255: 9 | log("u8 out of range: %s" % data, "INFO") 10 | data = 0 11 | return struct.pack(">B", data) 12 | 13 | 14 | def u16(data): 15 | if not 0 <= data <= 65535: 16 | log("u16 out of range: %s" % data, "INFO") 17 | data = 0 18 | return struct.pack(">H", data) 19 | 20 | 21 | def u32(data): 22 | if not 0 <= data <= 4294967295: 23 | log("u32 out of range: %s" % data, "INFO") 24 | data = 0 25 | return struct.pack(">I", data) 26 | 27 | 28 | def u32_littleendian(data): 29 | if not 0 <= data <= 4294967295: 30 | log("u32 little endian out of range: %s" % data, "INFO") 31 | data = 0 32 | return struct.pack(" ") 37 | sys.exit(1) 38 | elif len(sys.argv[2]) > 98: 39 | print("Error: Name must be less than or equal to 98 characters.") 40 | sys.exit(1) 41 | elif len(sys.argv[3]) != 4: 42 | print("Error: Title ID must be 4 characters.") 43 | sys.exit(1) 44 | 45 | 46 | class make_rom: 47 | def __init__(self): 48 | self.open_rom() 49 | self.make_rom() 50 | self.write_file() 51 | 52 | print("Completed Successfully") 53 | 54 | def open_rom(self): 55 | self.rom = open(sys.argv[1], "rb").read() 56 | 57 | def make_rom(self): 58 | self.header = collections.OrderedDict() 59 | 60 | self.header["unknown"] = u16(0) 61 | self.header["version"] = u8(6) 62 | self.header["unknown_region"] = u8(2) 63 | self.header["filesize"] = u32(332 + len(self.rom)) 64 | self.header["crc32"] = u32(0) 65 | self.header["country_code"] = u32(49) 66 | self.header["language_code"] = u32(1) 67 | self.header["rom_offset"] = u32(332) 68 | self.header["rom_size"] = u32(len(self.rom)) 69 | self.header["game_title"] = sys.argv[2].encode("utf-16be").ljust(98, b"\0") 70 | self.header["game_description"] = "Nintendo Channel Demo".encode( 71 | "utf-16be" 72 | ).ljust(194, b"\0") 73 | self.header["removal_year"] = u16(65535) # No removal year 74 | self.header["removal_month"] = u8(255) # No removal month 75 | self.header["removal_day"] = u8(255) # No removal day 76 | self.header["company_id"] = u32(1) 77 | self.header["title_id"] = sys.argv[3].encode() 78 | 79 | def write_file(self): 80 | self.writef = open(sys.argv[1] + "-output.bin", "wb") 81 | 82 | for values in self.header.values(): 83 | self.writef.write(values) 84 | 85 | self.writef.write(self.rom) 86 | 87 | self.readf = open(sys.argv[1] + "-output.bin", "rb") 88 | 89 | self.writef.seek(8) 90 | self.writef.write( 91 | binascii.unhexlify( 92 | format(binascii.crc32(self.readf.read()) & 0xFFFFFFFF, "08x") 93 | ) 94 | ) 95 | 96 | self.writef.close() 97 | 98 | 99 | make_rom() 100 | -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/info.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import glob 3 | import json 4 | import os 5 | import struct 6 | import sys 7 | import textwrap 8 | import requests 9 | from PIL import Image 10 | from ninfile2 import GameTDB 11 | 12 | with open("./config.json", "rb") as f: 13 | config = json.load(f) 14 | 15 | 16 | def u8(data): 17 | if not 0 <= data <= 255: 18 | print("u8 out of range: %s" % data, "INFO") 19 | data = 0 20 | return struct.pack(">B", data) 21 | 22 | 23 | def u16(data): 24 | if not 0 <= data <= 65535: 25 | print("u16 out of range: %s" % data, "INFO") 26 | data = 0 27 | return struct.pack(">H", data) 28 | 29 | 30 | def u32(data): 31 | if not 0 <= data <= 4294967295: 32 | print("u32 out of range: %s" % data, "INFO") 33 | data = 0 34 | return struct.pack(">I", data) 35 | 36 | 37 | def enc(text, length): 38 | if len(text) > length: 39 | print("Error: Text too long.") 40 | return text.encode("utf-16be").ljust(length, b"\0")[:length] 41 | 42 | 43 | if len(sys.argv) != 3: 44 | print("Usage: info.py <platform> <title id>") 45 | sys.exit(1) 46 | elif len(sys.argv[2]) != 4 and len(sys.argv[2]) != 5: 47 | print("Error: Title ID must be 4 or 5 characters.") 48 | sys.exit(1) 49 | 50 | 51 | class MakeInfo: 52 | def __init__(self, databases): 53 | self.header = {} 54 | self.databases = databases 55 | 56 | self.make_header() 57 | self.write_gametdb_info() 58 | self.write_jpegs() 59 | self.write_file() 60 | 61 | print("Completed Successfully") 62 | 63 | def offset_count(self): 64 | """ 65 | This function returns the offset of where the selected table is 66 | """ 67 | return sum(len(values) for values in list(self.header.values()) if values) 68 | 69 | def make_header(self): 70 | self.header["unknown"] = u16(0) 71 | self.header["version"] = u8(6) 72 | self.header["unknown_region"] = u8(2) 73 | self.header["filesize"] = u32(0) 74 | self.header["crc32"] = u32(0) 75 | self.header["dllistid"] = u32(0) 76 | self.header["country_code"] = u32(49) 77 | self.header["language_code"] = u32(1) 78 | self.header["ratings_table_offset"] = u32(0) 79 | self.header["times_played_table_offset"] = u32(0) 80 | self.header["people_who_liked_this_also_liked_entry_number"] = u32(0) 81 | self.header["people_who_liked_this_also_liked_table_offset"] = u32(0) 82 | self.header["related_titles_entry_number"] = u32(0) 83 | self.header["related_titles_table_offset"] = u32(0) 84 | self.header["videos_entry_number"] = u32(0) 85 | self.header["videos_table_offset"] = u32(0) 86 | self.header["demos_entry_number"] = u32(0) 87 | self.header["demos_table_offset"] = u32(0) 88 | self.header["unknown_2"] = u32(0) * 2 89 | self.header["picture_offset"] = u32(0) 90 | self.header["picture_size"] = u32(0) 91 | self.header["unknown_3"] = u32(0) 92 | self.header["rating_picture_offset"] = u32(0) 93 | self.header["rating_picture_size"] = u32(0) 94 | for i in range(1, 8): 95 | self.header["rating_detail_picture_%s_offset" % i] = u32(0) 96 | self.header["rating_detail_picture_%s_size" % i] = u32(0) 97 | self.header["unknown_4"] = u32(0) * 2 98 | self.header["soft_id"] = u32(0) 99 | self.header["game_id"] = u32(0) 100 | self.header["platform_flag"] = u8(0) 101 | self.header["company_id"] = u32(2) 102 | self.header["unknown_5"] = u16(1) 103 | self.header["unknown_6"] = u16(1) 104 | self.header["unknown_7"] = u8(1) 105 | self.header["wii_shop_channel_button_flag"] = u8(1) 106 | self.header["purchase_button_flag"] = u8(0) 107 | self.header["release_year"] = u16(0) 108 | self.header["release_month"] = u8(0) 109 | self.header["release_day"] = u8(0) 110 | self.header["shop_points"] = u32(0) 111 | # Seems to have enabled 112 | self.header["unknown_8_0"] = u8(4) 113 | self.header["unknown_8_1"] = u8(1) 114 | self.header["unknown_8_2"] = u8(0) 115 | self.header["unknown_8_3"] = u8(4) 116 | self.header["wii_remote_flag"] = u8(0) 117 | self.header["nunchuk_flag"] = u8(0) 118 | self.header["classic_controller_flag"] = u8(0) 119 | self.header["gamecube_controller_flag"] = u8(0) 120 | self.header["mii_flag"] = u8(0) 121 | self.header["online_flag"] = u8(0) 122 | self.header["wiiconnect24_flag"] = u8(0) 123 | self.header["nintendo_wifi_connection_flag"] = u8(0) 124 | self.header["downloadable_content_flag"] = u8(0) 125 | self.header["wireless_play_flag"] = u8(0) 126 | self.header["download_play_flag"] = u8(0) 127 | self.header["touch_generations_flag"] = u8(0) 128 | self.header["language_chinese_flag"] = u8(0) 129 | self.header["language_korean_flag"] = u8(0) 130 | self.header["language_japanese_flag"] = u8(0) 131 | self.header["language_english_flag"] = u8(0) 132 | self.header["language_french_flag"] = u8(0) 133 | self.header["language_spanish_flag"] = u8(0) 134 | self.header["language_german_flag"] = u8(0) 135 | self.header["language_italian_flag"] = u8(0) 136 | self.header["language_dutch_flag"] = u8(0) 137 | for i in range(1, 11): 138 | self.header["unknown_9_%s" % i] = u8(0) 139 | self.header["title"] = b"\0" * 62 140 | self.header["subtitle"] = b"\0" * 62 141 | self.header["short_title"] = b"\0" * 62 142 | for i in range(1, 4): 143 | self.header["description_text_%s" % i] = b"\0" * 82 144 | self.header["genre_text"] = b"\0" * 58 145 | self.header["players_text"] = b"\0" * 82 146 | self.header["peripherals_text"] = b"\0" * 88 147 | self.header["unknown_10"] = b"\0" * 80 148 | self.header["disclaimer_text"] = b"\0" * 4800 149 | self.header["unknown_11"] = u8(9) 150 | self.header["distribution_date_text"] = b"\0" * 82 151 | self.header["wii_points_text"] = b"\0" * 82 152 | for i in range(1, 11): 153 | self.header["custom_field_text_%s" % i] = b"\0" * 82 154 | 155 | def write_gametdb_info(self): 156 | for s in self.databases[sys.argv[1]][1].findall("game"): 157 | if ( 158 | sys.argv[1] == "Switch" 159 | and s.find("id").text == sys.argv[2] 160 | or s.find("id").text[:4] == sys.argv[2] 161 | and s.find("type") != "CUSTOM" 162 | ): 163 | print("Found {}!".format(sys.argv[2])) 164 | 165 | for c in self.databases["Wii"][1].findall("companies"): 166 | for i, company in enumerate(c.findall("company")): 167 | # Firstly, we will try to find the company by the company code. 168 | # This method will only work for disc games. As such, methods below exist 169 | if s.find("id").text[4:] != "": 170 | if s.find("id").text[4:] == company.get("code"): 171 | id = company.get("code").encode() 172 | id = int(id.hex(), base=16) 173 | self.header["company_id"] = u32(id) 174 | break 175 | 176 | # If None we will default to Nintendo as well 177 | if s.find("publisher").text is None: 178 | self.header["company_id"] = u32(0x3031) 179 | break 180 | if company.get("name") in s.find("publisher").text: 181 | id = company.get("code").encode() 182 | id = int(id.hex(), base=16) 183 | self.header["company_id"] = u32(id) 184 | break 185 | 186 | self.header["game_id"] = sys.argv[2].encode("utf-8")[:4] 187 | 188 | # Get the game type 189 | game_type = { 190 | "3DS": { 191 | None: 0x12, 192 | "3DS": 0x12, 193 | "3DSWare": 0x13, 194 | "New3DS": 0x18, 195 | "New3DSWare": 0x19, 196 | "VC-NES": 0x1A, 197 | "VC-GB": 0x1B, 198 | "VC-GBC": 0x1C, 199 | "VC-GBA": 0x1D, 200 | "VC-GG": 0x1E, 201 | }, 202 | "NDS": { 203 | None: 0x0A, 204 | "DS": 0x0A, 205 | "DSi": 0x10, 206 | "DSiWare": 0x11, 207 | }, 208 | "Switch": { 209 | None: 0x17, 210 | "Switch": 0x17, 211 | "eShop": 0x27, 212 | }, 213 | "Wii": { 214 | None: 0x01, 215 | "Wii": 0x01, 216 | "Channel": 0x02, 217 | "WiiWare": 0x0B, 218 | "VC-NES": 0x03, 219 | "VC-SNES": 0x04, 220 | "VC-N64": 0x05, 221 | "VC-SMS": 0x0C, 222 | "VC-MD": 0x07, 223 | "VC-PCE": 0x06, 224 | "VC-NEOGEO": 0x08, 225 | "VC-Arcade": 0x0E, 226 | "VC-C64": 0x0D, 227 | "VC-MSX": 0x28, 228 | }, 229 | "WiiU": { 230 | None: 0x15, 231 | "WiiU": 0x15, 232 | "eShop": 0x16, 233 | "VC-NES": 0x1F, 234 | "VC-SNES": 0x20, 235 | "VC-N64": 0x21, 236 | "VC-GBA": 0x22, 237 | "VC-DS": 0x23, 238 | "VC-PCE": 0x24, 239 | "VC-MSX": 0x25, 240 | "Channel": 0x26, 241 | } 242 | } # The XML returns None for disc games when we query the type. 243 | if s.find("type").text in game_type[sys.argv[1]]: 244 | if s.find("type").text == "WiiU" or s.find("type").text == "Switch": 245 | self.header["platform_flag"] = u8(0) 246 | else: 247 | self.header["platform_flag"] = u8( 248 | game_type[sys.argv[1]][s.find("type").text] 249 | ) 250 | else: 251 | print("Could not find game type") 252 | sys.exit(1) 253 | 254 | self.header["purchase_button_flag"] = u8( 255 | 1 256 | ) # we'll make it go to gametdb 257 | # Some games, more notably DSiWare and some 3DS games do not have a release date in the xml. 258 | # Due to this, we must set defaults in the case of no date. 259 | if s.find("date").get("year") == "": 260 | release_year = 65535 261 | else: 262 | release_year = s.find("date").get("year") 263 | if s.find("date").get("month") == "": 264 | release_month = 255 265 | else: 266 | release_month = s.find("date").get("month") 267 | if s.find("date").get("day") == "": 268 | release_day = 255 269 | else: 270 | release_day = s.find("date").get("day") 271 | 272 | self.header["release_year"] = u16(int(release_year)) 273 | self.header["release_month"] = u8(int(release_month) - 1) 274 | self.header["release_day"] = u8(int(release_day)) 275 | 276 | controllers = { 277 | "wiimote": "wii_remote", 278 | "nunchuk": "nunchuk", 279 | "classiccontroller": "classic_controller", 280 | "gamecube": "gamecube_controller", 281 | "mii": "mii", 282 | } 283 | controllers2 = { 284 | "wheel": "Wii Wheel", 285 | "balanceboard": "Wii Balance Board", 286 | "wiispeak": "Wii Speak", 287 | "microphone": "Microphone", 288 | "guitar": "Guitar", 289 | "drums": "Drums", 290 | "dancepad": "Dance Pad", 291 | "keyboard": "Keyboard", 292 | "udraw": "uDraw", 293 | } 294 | 295 | other_peripherals = False 296 | 297 | for controller in s.find("input").findall("control"): 298 | if controller.get("type") in controllers: 299 | self.header[ 300 | "{}_flag".format(controllers[controller.get("type")]) 301 | ] = u8(1) 302 | elif controller.get("type") in controllers2: 303 | if not other_peripherals: 304 | self.header["peripherals_text"] = "" 305 | 306 | if ( 307 | sys.argv[1] != "Wii" 308 | and controllers2[controller.get("type")] == "Wii Wheel" 309 | ): 310 | self.header["peripherals_text"] + "Wheel" + ", " 311 | else: 312 | self.header["peripherals_text"] += ( 313 | controllers2[controller.get("type")] + ", " 314 | ) 315 | 316 | other_peripherals = True 317 | 318 | if other_peripherals: 319 | # find a way to get rid of the "ZZ" part later 320 | self.header["peripherals_text"] = enc( 321 | "ZZ" + self.header["peripherals_text"][:-2], 88 322 | ) 323 | 324 | for feature in s.find("wi-fi").findall("feature"): 325 | if "online" in feature.text: 326 | self.header["online_flag"] = u8(1) 327 | self.header["nintendo_wifi_connection_flag"] = u8(1) 328 | 329 | # what languages does this game support? 330 | languages = { 331 | "ZHCN": "chinese", 332 | "KO": "korean", 333 | "JA": "japanese", 334 | "EN": "english", 335 | "FR": "french", 336 | "ES": "spanish", 337 | "DE": "german", 338 | "IT": "italian", 339 | "NL": "dutch", 340 | } 341 | languages_list = s.find("languages").text.split(",") 342 | 343 | for l in languages.keys(): 344 | if l in languages_list: 345 | self.header["language_{}_flag".format(languages[l])] = u8(1) 346 | 347 | # write the synopsis, and text wrap it properly 348 | try: 349 | wrap = textwrap.wrap( 350 | s.find("locale", {"lang": "EN"}).find("synopsis").text, 40 351 | ) 352 | 353 | if len(wrap) <= 4: 354 | text_type = ( 355 | "description" # put the synopsis at the top of the page 356 | ) 357 | else: 358 | text_type = ( 359 | "custom_field" # put the synopsis in the middle of the page 360 | ) 361 | 362 | # let's shorten the synopsis until it fits 363 | 364 | synopsis_text = ( 365 | s.find("locale", {"lang": "EN"}) 366 | .find("synopsis") 367 | .text[:400] 368 | .replace("\n", "") 369 | .replace(" ", " ") 370 | .split(".") 371 | ) 372 | 373 | if ( 374 | len(s.find("locale", {"lang": "EN"}).find("synopsis").text) 375 | > 400 376 | ): 377 | try: 378 | synopsis_text = synopsis_text[:-1] 379 | synopsis_text[-1] += "." 380 | except: 381 | pass 382 | 383 | i = len("".join(textwrap.wrap(". ".join(synopsis_text), 40))) + 1 384 | j = len(synopsis_text) 385 | 386 | while i > 11: 387 | j -= 1 388 | sentences = ". ".join(synopsis_text[:j]) 389 | if len(sentences) != 0: 390 | sentences += "." 391 | wrap = textwrap.wrap(sentences, 40) 392 | i = len(wrap) 393 | 394 | i = 1 395 | 396 | for w in wrap: 397 | self.header["{}_text_{}".format(text_type, i)] = enc(w, 82) 398 | i += 1 399 | if i == 10 and len(wrap) > 10: 400 | self.header["{}_text_{}".format(text_type, i)] += b"..." 401 | break 402 | except AttributeError: 403 | pass 404 | 405 | self.header["title"] = title = ( 406 | s.find("locale", {"lang": "EN"}).find("title").text 407 | ) 408 | 409 | # make separator in game name have a subtitle too 410 | 411 | if ": " in self.header["title"]: 412 | self.header["title"] = title.split(": ")[0] 413 | self.header["subtitle"] = enc(title.split(": ")[1], 62) 414 | 415 | elif " - " in self.header["title"]: 416 | self.header["title"] = title.split(" - ")[0] 417 | self.header["subtitle"] = enc(title.split(" - ")[1], 62) 418 | 419 | self.header["title"] = enc(self.header["title"], 62) 420 | 421 | try: 422 | self.header["genre_text"] = enc( 423 | s.find("genre").text.title().replace(",", ", "), 58 424 | ) 425 | 426 | except AttributeError: 427 | pass 428 | 429 | players_local = s.find("input").get("players") 430 | players_online = s.find("wi-fi").get("players") 431 | 432 | if sys.argv[1] == "Wii": 433 | self.header["players_text"] = players_local 434 | 435 | if players_local != "1": 436 | self.header["players_text"] += "s" 437 | 438 | """if players_online != "0": 439 | self.header["players_text"] += " (Local), " + \ 440 | players_online + \ 441 | " (Online)""" 442 | 443 | self.header["players_text"] = enc(self.header["players_text"], 82) 444 | 445 | self.header["disclaimer_text"] = enc( 446 | 'Game information is provided by GameTDB. Press the\n"Purchase" button to get redirected ' 447 | "to the GameTDB page.", 448 | 4800, 449 | ) 450 | 451 | title_id = s.find("id").text 452 | 453 | # Download cover and convert to JPEG 454 | """print("Downloading cover art...") 455 | url = f"https://art.gametdb.com/ds/box/US/{title_id}.png" 456 | if sys.argv[1] == "Wii": 457 | url = f"https://art.gametdb.com/wii/cover/US/{title_id}.png" 458 | if sys.argv[1] == "3DS": 459 | url = f"https://art.gametdb.com/3ds/box/US/{title_id}.png" 460 | 461 | r = requests.get( 462 | url, headers={"User-Agent": "Nintendo Channel Info Downloader"})""" 463 | 464 | # Grab the cover image, and resize and center it 465 | if len(glob.glob(f"covers/{sys.argv[1]}/US/{title_id}*")) > 0: 466 | img = Image.new(mode="RGB", size=(384, 384), color=(255, 255, 255)) 467 | cover_img = Image.open( 468 | glob.glob(f"covers/{sys.argv[1]}/US/{title_id}*")[0] 469 | ) 470 | cover_img_w, cover_img_h = cover_img.size 471 | if sys.argv[1] != "3DS" and sys.argv[1] != "NDS": 472 | cover_img_resized = cover_img.resize( 473 | (int(cover_img_w * (384 / cover_img_h)), 384) 474 | ) 475 | else: 476 | cover_img_resized = cover_img.resize( 477 | (384, int(cover_img_h * (384 / cover_img_w))) 478 | ) 479 | cover_img_w, cover_img_h = cover_img_resized.size 480 | cover_rgb_img = img.convert("RGB") 481 | offset = ((384 - cover_img_w) // 2, (384 - cover_img_h) // 2) 482 | img.paste(cover_img_resized, offset) 483 | img.save(f"{sys.argv[1]}-{title_id}.jpg") 484 | 485 | return 486 | 487 | print("Error: Could not find {}.".format(sys.argv[2])) 488 | sys.exit(1) 489 | 490 | def write_jpegs(self): 491 | game_id = sys.argv[2] 492 | 493 | if len(glob.glob(f"{sys.argv[1]}-{game_id}*")) > 0: 494 | with open(glob.glob(f"{sys.argv[1]}-{game_id}*")[0], "rb") as cover: 495 | self.header["picture_offset"] = u32(self.offset_count()) 496 | self.header["coverArt"] = cover.read() 497 | cover.seek(0, os.SEEK_END) 498 | self.header["picture_size"] = u32(cover.tell()) 499 | 500 | # TODO: Figure out the ratings so I can write the correct images 501 | with open("ratings/ESRB/E-small.jpg", "rb") as rating: 502 | self.header["rating_picture_offset"] = u32(self.offset_count()) 503 | self.header["ratingArt"] = rating.read() 504 | rating.seek(0, os.SEEK_END) 505 | self.header["rating_picture_size"] = u32(rating.tell()) 506 | 507 | """with open("ratings/ESRB/rating.jpg", "rb") as detailed: 508 | self.header["rating_detail_picture_1_offset"] = u32(self.offset_count()) 509 | self.header["detailed_rating_picture"] = detailed.read() 510 | detailed.seek(0, os.SEEK_END) 511 | self.header["rating_detail_picture_1_size"] = u32(detailed.tell())""" 512 | 513 | if os.path.exists(f"{sys.argv[1]}-{game_id}.jpg"): 514 | os.remove(f"{sys.argv[1]}-{game_id}.jpg") 515 | 516 | def write_file(self): 517 | self.header["filesize"] = u32(self.offset_count()) 518 | id = int(binascii.hexlify(sys.argv[2][:4].encode("utf-8")).decode("utf-8"), 16) 519 | 520 | if sys.argv[1] == "NDS": 521 | id ^= 0x22222222 522 | 523 | elif sys.argv[1] == "3DS": 524 | id ^= 0x33333333 525 | 526 | elif sys.argv[1] == "WiiU": 527 | id ^= 0x44444444 528 | 529 | elif sys.argv[1] == "Switch": 530 | id ^= 0x55555555 531 | 532 | filename = str(id) + ".info" 533 | 534 | if os.path.exists(filename + "-1"): 535 | os.remove(filename + "-1") 536 | 537 | if os.path.exists(filename): 538 | os.remove(filename) 539 | 540 | self.writef = open(filename + "-1", "ab") 541 | 542 | for values in self.header.values(): 543 | self.writef.write(values) 544 | 545 | self.writef.close() 546 | 547 | self.readf = open(filename + "-1", "rb") 548 | 549 | read = self.readf.read() 550 | 551 | self.writef2 = open(config["file_path"] + "/soft/US/en/" + filename, "wb") 552 | 553 | self.writef2.write(read) 554 | self.writef2.seek(8) 555 | self.writef2.write( 556 | binascii.unhexlify(format(binascii.crc32(read) & 0xFFFFFFFF, "08x")) 557 | ) 558 | 559 | os.remove(filename + "-1") 560 | 561 | self.writef2.close() 562 | 563 | 564 | MakeInfo(GameTDB(True).parse()) 565 | -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ninch_thumb.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import os 3 | 4 | 5 | def u8(data): 6 | if not 0 <= data <= 255: 7 | print("u8 out of range: %s" % data, "INFO") 8 | data = 0 9 | return struct.pack(">B", data) 10 | 11 | 12 | def u16(data): 13 | if not 0 <= data <= 65535: 14 | print("u16 out of range: %s" % data, "INFO") 15 | data = 0 16 | return struct.pack(">H", data) 17 | 18 | 19 | def u32(data): 20 | if not 0 <= data <= 4294967295: 21 | print("u32 out of range: %s" % data, "INFO") 22 | data = 0 23 | return struct.pack(">I", data) 24 | 25 | 26 | class MakeThumb: 27 | def __init__(self): 28 | print("Generating File...") 29 | self.header = {} 30 | self.make_header() 31 | self.make_jpeg_table() 32 | self.write_jpegs() 33 | self.write_file() 34 | 35 | def offset_count(self): 36 | """ 37 | This function returns the offset of where the selected table is 38 | """ 39 | return sum(len(values) for values in list(self.header.values()) if values) 40 | 41 | def make_header(self): 42 | self.header["unknown"] = u16(0) 43 | self.header["version"] = u8(6) 44 | self.header["unknownRegion"] = u8(2) 45 | self.header["fileSize"] = u32(0) 46 | self.header[f"unknown1_1"] = u32(601820255) 47 | self.header[f"unknown1_2"] = u32(1) 48 | self.header[f"unknown1_3"] = u32(49) 49 | self.header[f"unknown1_4"] = u32(1) 50 | self.header[f"unknown1_5"] = u32(1252951207) 51 | self.header[f"number_of_images"] = u32(120) 52 | 53 | def make_jpeg_table(self): 54 | for i in range(120): 55 | self.header[f"jpegSize_{i}"] = u32(0) 56 | self.header[f"jpegOffset_{i}"] = u32(0) 57 | 58 | def write_jpegs(self): 59 | deadbeef = {0: 0xDE, 1: 0xAD, 2: 0xBE, 3: 0xEF} 60 | for i in range(120): 61 | with open("testing.jpeg", "rb") as image: 62 | self.header[f"jpegOffset_{i}"] = u32(self.offset_count()) 63 | self.header[f"JPEGData{i}"] = image.read() 64 | counter = 0 65 | while self.offset_count() % 32 != 0: 66 | self.header[f"deadbeef_{i}_{counter}"] = u8(deadbeef[counter % 4]) 67 | counter += 1 68 | # Seek to end of file to set filesize 69 | image.seek(0, os.SEEK_END) 70 | self.header[f"jpegSize_{i}"] = u32(image.tell()) 71 | image.close() 72 | 73 | def write_file(self): 74 | # Now that all the file contents are written, calculate filesize 75 | self.header["fileSize"] = u32(self.offset_count()) 76 | 77 | filename = "testing.thumb" 78 | 79 | if os.path.exists(filename): 80 | os.remove(filename) 81 | 82 | self.writef = open(filename, "ab") 83 | 84 | for values in self.header.values(): 85 | self.writef.write(values) 86 | 87 | self.writef.close() 88 | 89 | 90 | MakeThumb() 91 | -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ninfile1.py: -------------------------------------------------------------------------------- 1 | import ninfile 2 | from kaitaistruct import KaitaiStream, BytesIO 3 | 4 | file = ninfile.NinchDllist.from_file("434968891.LZ") 5 | 6 | nintendo_channel_file = {} 7 | 8 | # header 9 | 10 | nintendo_channel_file["unknown"] = file.unknown 11 | nintendo_channel_file["version"] = file.version 12 | nintendo_channel_file["unknown_region"] = file.unknown_region 13 | nintendo_channel_file["filesize"] = file.filesize 14 | nintendo_channel_file["crc32"] = file.crc32 15 | nintendo_channel_file["dllistid"] = file.dllistid 16 | nintendo_channel_file["thumbnail_id"] = file.thumbnail_id 17 | nintendo_channel_file["country_code"] = file.country_code 18 | nintendo_channel_file["language_code"] = file.language_code 19 | nintendo_channel_file["unknown_2"] = file.unknown_2 20 | nintendo_channel_file["ratings_entry_number"] = file.ratings_entry_number 21 | nintendo_channel_file["ratings_table_offset"] = file.ratings_table_offset 22 | nintendo_channel_file["title_types_entry_number"] = file.title_types_entry_number 23 | nintendo_channel_file["title_types_table_offset"] = file.title_types_table_offset 24 | nintendo_channel_file["company_entry_number"] = file.company_entry_number 25 | nintendo_channel_file["company_table_offset"] = file.company_table_offset 26 | nintendo_channel_file["title_entry_number"] = file.title_entry_number 27 | nintendo_channel_file["title_table_offset"] = file.title_table_offset 28 | nintendo_channel_file["new_title_entry_number"] = file.new_title_entry_number 29 | nintendo_channel_file["new_title_table_offset"] = file.new_title_table_offset 30 | nintendo_channel_file["videos_1_entry_number"] = file.videos_1_entry_number 31 | nintendo_channel_file["videos_1_table_offset"] = file.videos_1_table_offset 32 | nintendo_channel_file["new_video_entry_number"] = file.new_video_entry_number 33 | nintendo_channel_file["new_video_table_offset"] = file.new_video_table_offset 34 | nintendo_channel_file["demos_entry_number"] = file.demos_entry_number 35 | nintendo_channel_file["demos_table_offset"] = file.demos_table_offset 36 | nintendo_channel_file["unknown_5"] = file.unknown_5 37 | nintendo_channel_file["unknown_6"] = file.unknown_6 38 | nintendo_channel_file[ 39 | "recommendations_entry_number" 40 | ] = file.recommendations_entry_number 41 | nintendo_channel_file[ 42 | "recommendations_table_offset" 43 | ] = file.recommendations_table_offset 44 | nintendo_channel_file["unknown_7"] = file.unknown_7 45 | nintendo_channel_file[ 46 | "recent_recommendations_entry_number" 47 | ] = file.recent_recommendations_entry_number 48 | nintendo_channel_file[ 49 | "recent_recommendations_table_offset" 50 | ] = file.recent_recommendations_table_offset 51 | nintendo_channel_file["unknown_8"] = file.unknown_8 52 | nintendo_channel_file["popular_videos_entry_number"] = file.popular_videos_entry_number 53 | nintendo_channel_file["popular_videos_table_offset"] = file.popular_videos_table_offset 54 | nintendo_channel_file[ 55 | "detailed_ratings_entry_number" 56 | ] = file.detailed_ratings_entry_number 57 | nintendo_channel_file[ 58 | "detailed_ratings_table_offset" 59 | ] = file.detailed_ratings_table_offset 60 | nintendo_channel_file["last_update"] = file.last_update 61 | nintendo_channel_file["unknown_9"] = file.unknown_9 62 | nintendo_channel_file["dl_url_ids"] = file.dl_url_ids 63 | nintendo_channel_file["unknown_10"] = file.unknown_10 64 | 65 | i = 0 66 | 67 | nintendo_channel_file["ratings_table"] = {} 68 | 69 | for f in file.ratings_table: 70 | nintendo_channel_file["ratings_table"][i] = {} 71 | nintendo_channel_file["ratings_table"][i]["rating_id"] = f.rating_id 72 | nintendo_channel_file["ratings_table"][i]["unknown"] = f.unknown 73 | nintendo_channel_file["ratings_table"][i]["age"] = f.age 74 | nintendo_channel_file["ratings_table"][i]["unknown2"] = f.unknown2 75 | nintendo_channel_file["ratings_table"][i]["jpeg_offset"] = f.jpeg_offset 76 | nintendo_channel_file["ratings_table"][i]["jpeg_size"] = f.jpeg_size 77 | nintendo_channel_file["ratings_table"][i]["title"] = f.title 78 | nintendo_channel_file["ratings_table"][i]["jpeg"] = f.jpeg 79 | 80 | i += 1 81 | 82 | i = 0 83 | 84 | nintendo_channel_file["title_types_table"] = {} 85 | 86 | for f in file.title_types_table: 87 | nintendo_channel_file["title_types_table"][i] = {} 88 | nintendo_channel_file["title_types_table"][i]["type_id"] = f.type_id 89 | nintendo_channel_file["title_types_table"][i]["console_model"] = f.console_model 90 | nintendo_channel_file["title_types_table"][i]["title"] = f.title 91 | nintendo_channel_file["title_types_table"][i]["group_id"] = f.group_id 92 | nintendo_channel_file["title_types_table"][i]["unknown"] = f.unknown 93 | 94 | i += 1 95 | 96 | i = 0 97 | 98 | nintendo_channel_file["company_table"] = {} 99 | 100 | for f in file.company_table: 101 | nintendo_channel_file["company_table"][i] = {} 102 | nintendo_channel_file["company_table"][i]["id"] = f.id 103 | nintendo_channel_file["company_table"][i]["dev_title"] = f.dev_title 104 | nintendo_channel_file["company_table"][i]["pub_title"] = f.pub_title 105 | 106 | i += 1 107 | 108 | i = 0 109 | 110 | nintendo_channel_file["title_table"] = {} 111 | 112 | for f in file.title_table: 113 | nintendo_channel_file["title_table"][i] = {} 114 | nintendo_channel_file["title_table"][i]["id"] = f.id 115 | nintendo_channel_file["title_table"][i]["title_id"] = f.title_id 116 | nintendo_channel_file["title_table"][i]["title_type"] = f.title_type 117 | nintendo_channel_file["title_table"][i]["genre"] = f.genre 118 | nintendo_channel_file["title_table"][i]["company_offset"] = f.company_offset 119 | nintendo_channel_file["title_table"][i]["release_date_year"] = f.release_date_year 120 | nintendo_channel_file["title_table"][i]["release_date_month"] = f.release_date_month 121 | nintendo_channel_file["title_table"][i]["release_date_day"] = f.release_date_day 122 | nintendo_channel_file["title_table"][i]["rating_id"] = f.rating_id 123 | nintendo_channel_file["title_table"][i]["unknown_4"] = f.unknown_4 124 | nintendo_channel_file["title_table"][i]["title"] = f.title 125 | nintendo_channel_file["title_table"][i]["subtitle"] = f.subtitle 126 | nintendo_channel_file["title_table"][i]["short_title"] = f.short_title 127 | 128 | i += 1 129 | 130 | i = 0 131 | 132 | nintendo_channel_file["new_title_table"] = {} 133 | 134 | for f in file.new_title_table: 135 | nintendo_channel_file["new_title_table"][i] = {} 136 | nintendo_channel_file["new_title_table"][i]["new_title_offset"] = f.new_title_offset 137 | 138 | i += 1 139 | 140 | i = 0 141 | 142 | nintendo_channel_file["videos_1_table"] = {} 143 | 144 | for f in file.videos_1_table: 145 | nintendo_channel_file["videos_1_table"][i] = {} 146 | nintendo_channel_file["videos_1_table"][i]["id"] = f.id 147 | nintendo_channel_file["videos_1_table"][i]["time_length"] = f.time_length 148 | nintendo_channel_file["videos_1_table"][i]["title_id"] = f.title_id 149 | nintendo_channel_file["videos_1_table"][i]["unknown"] = f.unknown 150 | nintendo_channel_file["videos_1_table"][i]["unknown_2"] = f.unknown_2 151 | nintendo_channel_file["videos_1_table"][i]["rating_id"] = f.rating_id 152 | nintendo_channel_file["videos_1_table"][i]["unknown_3"] = f.unknown_3 153 | nintendo_channel_file["videos_1_table"][i]["new_tag"] = f.new_tag 154 | nintendo_channel_file["videos_1_table"][i]["video_index"] = f.video_index 155 | nintendo_channel_file["videos_1_table"][i]["unknown_4"] = f.unknown_4 156 | nintendo_channel_file["videos_1_table"][i]["title"] = f.title 157 | 158 | i += 1 159 | 160 | i = 0 161 | 162 | nintendo_channel_file["new_video_table"] = {} 163 | 164 | for f in file.new_video_table: 165 | nintendo_channel_file["new_video_table"][i] = {} 166 | nintendo_channel_file["new_video_table"][i]["id"] = f.id 167 | nintendo_channel_file["new_video_table"][i]["unknown"] = f.unknown 168 | nintendo_channel_file["new_video_table"][i]["title_id"] = f.title_id 169 | nintendo_channel_file["new_video_table"][i]["unknown_2"] = f.unknown_2 170 | nintendo_channel_file["new_video_table"][i]["title"] = f.title 171 | 172 | i += 1 173 | 174 | i = 0 175 | 176 | nintendo_channel_file["demos_table"] = {} 177 | 178 | for f in file.demos_table: 179 | nintendo_channel_file["demos_table"][i] = {} 180 | nintendo_channel_file["demos_table"][i]["id"] = f.id 181 | nintendo_channel_file["demos_table"][i]["title"] = f.title 182 | nintendo_channel_file["demos_table"][i]["subtitle"] = f.subtitle 183 | nintendo_channel_file["demos_table"][i]["titleid"] = f.titleid 184 | nintendo_channel_file["demos_table"][i]["company_offset"] = f.company_offset 185 | nintendo_channel_file["demos_table"][i]["removal_year"] = f.removal_year 186 | nintendo_channel_file["demos_table"][i]["removal_month"] = f.removal_month 187 | nintendo_channel_file["demos_table"][i]["removal_day"] = f.removal_day 188 | nintendo_channel_file["demos_table"][i]["unknown"] = f.unknown 189 | nintendo_channel_file["demos_table"][i]["rating_id"] = f.rating_id 190 | nintendo_channel_file["demos_table"][i]["new_tag"] = f.new_tag 191 | nintendo_channel_file["demos_table"][i]["new_tag_index"] = f.new_tag_index 192 | nintendo_channel_file["demos_table"][i]["unknown_2"] = f.unknown_2 193 | 194 | i += 1 195 | 196 | i = 0 197 | 198 | nintendo_channel_file["recommendations_table"] = {} 199 | 200 | for f in file.recommendations_table: 201 | nintendo_channel_file["recommendations_table"][i] = {} 202 | nintendo_channel_file["recommendations_table"][i][ 203 | "recommendation_title_offset" 204 | ] = f.recommendation_title_offset 205 | 206 | i += 1 207 | 208 | nintendo_channel_file["recent_recommendations_table"] = {} 209 | 210 | for f in file.recent_recommendations_table: 211 | nintendo_channel_file["recent_recommendations_table"][i] = {} 212 | nintendo_channel_file["recent_recommendations_table"][i][ 213 | "recent_recommendation_title_offset" 214 | ] = f.recent_recommendation_title_offset 215 | nintendo_channel_file["recent_recommendations_table"][i]["unknown"] = f.unknown 216 | 217 | i += 1 218 | 219 | i = 0 220 | 221 | nintendo_channel_file["popular_videos_table"] = {} 222 | 223 | for f in file.popular_videos_table: 224 | nintendo_channel_file["popular_videos_table"][i] = {} 225 | nintendo_channel_file["popular_videos_table"][i]["id"] = f.id 226 | nintendo_channel_file["popular_videos_table"][i]["time_length"] = f.time_length 227 | nintendo_channel_file["popular_videos_table"][i]["title_id"] = f.title_id 228 | nintendo_channel_file["popular_videos_table"][i]["bar_color"] = f.bar_color 229 | nintendo_channel_file["popular_videos_table"][i]["unknown_2"] = f.unknown_2 230 | nintendo_channel_file["popular_videos_table"][i]["rating_id"] = f.rating_id 231 | nintendo_channel_file["popular_videos_table"][i]["unknown_3"] = f.unknown_3 232 | nintendo_channel_file["popular_videos_table"][i]["video_rank"] = f.video_rank 233 | nintendo_channel_file["popular_videos_table"][i]["unknown_4"] = f.unknown_4 234 | nintendo_channel_file["popular_videos_table"][i]["title"] = f.title 235 | 236 | i += 1 237 | 238 | i = 0 239 | 240 | nintendo_channel_file["detailed_ratings_table"] = {} 241 | 242 | for f in file.detailed_ratings_table: 243 | nintendo_channel_file["detailed_ratings_table"][i] = {} 244 | nintendo_channel_file["detailed_ratings_table"][i]["rating_group"] = f.rating_group 245 | nintendo_channel_file["detailed_ratings_table"][i]["rating_id"] = f.rating_id 246 | nintendo_channel_file["detailed_ratings_table"][i]["title"] = f.title 247 | 248 | i += 1 249 | -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ninfile2.py: -------------------------------------------------------------------------------- 1 | import lxml.etree as et 2 | import ninfile1 3 | import os 4 | import requests 5 | import struct 6 | import zipfile 7 | 8 | """Pack integers to specific type.""" 9 | 10 | 11 | # Unsigned integers 12 | 13 | 14 | def u8(data): 15 | if not 0 <= data <= 255: 16 | log("u8 out of range: %s" % data, "INFO") 17 | data = 0 18 | return struct.pack(">B", data) 19 | 20 | 21 | def u16(data): 22 | if not 0 <= data <= 65535: 23 | log("u16 out of range: %s" % data, "INFO") 24 | data = 0 25 | return struct.pack(">H", data) 26 | 27 | 28 | def u32(data): 29 | if not 0 <= data <= 4294967295: 30 | log("u32 out of range: %s" % data, "INFO") 31 | data = 0 32 | return struct.pack(">I", data) 33 | 34 | 35 | def u32_littleendian(data): 36 | if not 0 <= data <= 4294967295: 37 | log("u32 little endian out of range: %s" % data, "INFO") 38 | data = 0 39 | return struct.pack("<I", data) 40 | 41 | 42 | # Signed integer 43 | 44 | 45 | def s8(data): 46 | if not -128 <= data <= 127: 47 | log("s8 out of range: %s" % data, "INFO") 48 | data = 0 49 | return struct.pack(">b", data) 50 | 51 | 52 | def s16(data): 53 | if not -32768 <= data <= 32767: 54 | log("s16 out of range: %s" % data, "INFO") 55 | data = 0 56 | return struct.pack(">h", data) 57 | 58 | 59 | def s32(data): 60 | if not -2147483648 <= data <= 2147483647: 61 | log("s32 out of range: %s" % data, "INFO") 62 | data = 0 63 | return struct.pack(">i", data) 64 | 65 | 66 | def strIDToint(id): 67 | return (ord(id[0]) << 24 | ord(id[1]) << 16 | ord(id[2]) << 8) ^ 0x52433234 68 | 69 | 70 | def intTostrID(id): 71 | id ^= 0x52433234 72 | return chr(id >> 24) + chr(id >> 16 & 0xFF) + chr(id >> 8 & 0xFF) + chr(id & 0xFF) 73 | 74 | 75 | class GameTDB: 76 | def __init__(self, cache): 77 | self.databases = { 78 | "Wii": ["wii", None], 79 | "3DS": ["3ds", None], 80 | "NDS": ["ds", None], 81 | "WiiU": ["wiiu", None], 82 | "Switch": ["switch", None], 83 | } 84 | self.download() 85 | 86 | def download(self): 87 | for k, v in self.databases.items(): 88 | print("Downloading {} Database from GameTDB...".format(k)) 89 | filename = v[0] + "tdb" 90 | if not os.path.exists(filename + ".xml"): 91 | url = "https://www.gametdb.com/{}".format(filename + ".zip") 92 | # It's blocked for the "python-requests" user-agent to encourage setting a different user-agent for different apps, to get an idea of the origin of the requests. (according to the GameTDB admin). 93 | r = requests.get( 94 | url, headers={"User-Agent": "Nintendo Channel Info Downloader"} 95 | ) 96 | open(filename + ".zip", "wb").write(r.content) 97 | self.zip = zipfile.ZipFile(filename + ".zip") 98 | self.zip.extractall(".") 99 | self.zip.close() 100 | 101 | def parse(self): 102 | for k, v in self.databases.items(): 103 | filename = v[0] + "tdb" 104 | 105 | print("Loading {}...".format(k)) 106 | v[1] = et.parse(filename + ".xml") 107 | 108 | return self.databases 109 | 110 | 111 | class NintendoChannel: 112 | def __init__(self, ninfile): 113 | self.dictionaries = {} 114 | self.ninfile = ninfile 115 | self.build() 116 | self.write() 117 | 118 | def build(self): 119 | print("Generating list file...") 120 | 121 | self.dictionaries["header"] = self.make_header() 122 | self.dictionaries["ratings_table"] = self.make_ratings_table() 123 | self.dictionaries["title_types_table"] = self.make_title_types_table() 124 | self.dictionaries["company_table"] = self.make_company_table() 125 | self.dictionaries["title_table"] = self.make_title_table() 126 | self.dictionaries["new_title_table"] = self.make_new_title_table() 127 | self.dictionaries["videos_1_table"] = self.make_videos_1_table() 128 | self.dictionaries["new_video_table"] = self.make_new_video_table() 129 | self.dictionaries["demos_table"] = self.make_demos_table() 130 | self.dictionaries["recommendations_table"] = self.make_recommendations_table() 131 | self.dictionaries[ 132 | "recent_recommendations_table" 133 | ] = self.make_recent_recommendations_table() 134 | self.dictionaries["popular_videos_table"] = self.make_popular_videos_table() 135 | self.dictionaries["jpeg"] = self.make_jpeg() 136 | self.dictionaries["detailed_ratings_table"] = self.make_detailed_ratings_table() 137 | 138 | self.dictionaries["header"]["filesize"] = u32(self.offset_count1()) 139 | 140 | def offset_count1(self): 141 | return sum( 142 | len(values) 143 | for dictionary in self.dictionaries 144 | for values in list(self.dictionaries[dictionary].values()) 145 | if values 146 | ) 147 | 148 | def offset_count2(self, dictionary): 149 | return sum(len(values) for values in list(dictionary.values()) if values) 150 | 151 | def offset_count(self, dictionary): 152 | return self.offset_count1() + self.offset_count2(dictionary) 153 | 154 | def write(self): 155 | filename = "434968893.LZ" 156 | 157 | if os.path.exists(filename): 158 | os.remove(filename) 159 | 160 | with open(filename, "ab+") as f: 161 | for dictionary in self.dictionaries: 162 | for v in self.dictionaries[dictionary].values(): 163 | f.write(v) 164 | 165 | def make_header(self): 166 | header = {} 167 | 168 | header["unknown"] = u16(self.ninfile["unknown"]) 169 | header["version"] = u8(self.ninfile["version"]) 170 | header["unknown_region"] = u8(self.ninfile["unknown_region"]) 171 | header["filesize"] = u32(0) 172 | header["crc32"] = u32(self.ninfile["crc32"]) 173 | header["dllistid"] = u32(self.ninfile["dllistid"]) 174 | header["thumbnail_id"] = u32(self.ninfile["thumbnail_id"]) 175 | header["country_code"] = u32(self.ninfile["country_code"]) 176 | header["language_code"] = u32(self.ninfile["language_code"]) 177 | 178 | for i in range(0, 9): 179 | header["unknown_2_" + str(i)] = u8(self.ninfile["unknown_2"][i]) 180 | 181 | header["ratings_entry_number"] = u32(len(self.ninfile["ratings_table"])) 182 | header["ratings_table_offset"] = u32(0) 183 | header["title_types_entry_number"] = u32(len(self.ninfile["title_types_table"])) 184 | header["title_types_table_offset"] = u32(0) 185 | header["company_entry_number"] = u32(len(self.ninfile["company_table"])) 186 | header["company_table_offset"] = u32(0) 187 | header["title_entry_number"] = u32(len(self.ninfile["title_table"])) 188 | header["title_table_offset"] = u32(0) 189 | header["new_title_entry_number"] = u32(len(self.ninfile["new_title_table"])) 190 | header["new_title_table_offset"] = u32(0) 191 | header["videos_1_entry_number"] = u32(len(self.ninfile["videos_1_table"])) 192 | header["videos_1_table_offset"] = u32(0) 193 | header["new_video_entry_number"] = u32(len(self.ninfile["new_video_table"])) 194 | header["new_video_table_offset"] = u32(0) 195 | header["demos_entry_number"] = u32(len(self.ninfile["demos_table"])) 196 | header["demos_table_offset"] = u32(0) 197 | header["unknown_5"] = u32(self.ninfile["unknown_5"]) 198 | header["unknown_6"] = u32(self.ninfile["unknown_6"]) 199 | header["recommendations_entry_number"] = u32( 200 | len(self.ninfile["recommendations_table"]) 201 | ) 202 | header["recommendations_table_offset"] = u32(0) 203 | 204 | for i in range(0, 4): 205 | header["unknown_7_" + str(i)] = u32(self.ninfile["unknown_7"][i]) 206 | 207 | header["recent_recommendations_entry_number"] = u32( 208 | len(self.ninfile["recent_recommendations_table"]) 209 | ) 210 | header["recent_recommendations_table_offset"] = u32(0) 211 | 212 | for i in range(0, 2): 213 | header["unknown_8_" + str(i)] = u32(self.ninfile["unknown_8"][i]) 214 | 215 | header["popular_videos_entry_number"] = u32( 216 | len(self.ninfile["popular_videos_table"]) 217 | ) 218 | header["popular_videos_table_offset"] = u32(0) 219 | header["detailed_ratings_entry_number"] = u32( 220 | len(self.ninfile["detailed_ratings_table"]) 221 | ) 222 | header["detailed_ratings_table_offset"] = u32(0) 223 | header["last_update"] = ( 224 | self.ninfile["last_update"].encode("utf-16be").rjust(62, b"\x00") 225 | ) 226 | 227 | for i in range(0, 3): 228 | header["unknown_9_" + str(i)] = u8(self.ninfile["unknown_9"][i]) 229 | 230 | for i in range(0, 5): 231 | header["dl_url_ids_" + str(i)] = ( 232 | self.ninfile["dl_url_ids"][i].encode("utf-8").rjust(256, b"\x00") 233 | ) 234 | 235 | for i in range(0, 4): 236 | header["unknown_10_" + str(i)] = u8(self.ninfile["unknown_10"][i]) 237 | 238 | return header 239 | 240 | def make_ratings_table(self): 241 | ratings_table = {} 242 | 243 | i = 0 244 | 245 | self.dictionaries["header"]["ratings_table_offset"] = u32(self.offset_count1()) 246 | 247 | for r in self.ninfile["ratings_table"]: 248 | r = self.ninfile["ratings_table"][r] 249 | 250 | i += 1 251 | 252 | ratings_table["rating_id_" + str(i)] = u8(r["rating_id"]) 253 | ratings_table["unknown_" + str(i)] = u8(r["unknown"]) 254 | ratings_table["age_" + str(i)] = u8(r["age"]) 255 | ratings_table["unknown2_" + str(i)] = u8(r["unknown2"]) 256 | ratings_table["jpeg_offset_" + str(i)] = u32(r["jpeg_offset"]) 257 | ratings_table["jpeg_size_" + str(i)] = u32(r["jpeg_size"]) 258 | ratings_table["title_" + str(i)] = ( 259 | r["title"].encode("utf-16be").rjust(22, b"\x00") 260 | ) 261 | 262 | return ratings_table 263 | 264 | def make_title_types_table(self): 265 | title_types_table = {} 266 | 267 | i = 0 268 | 269 | self.dictionaries["header"]["title_types_table_offset"] = u32( 270 | self.offset_count1() 271 | ) 272 | 273 | for t in self.ninfile["title_types_table"]: 274 | t = self.ninfile["title_types_table"][t] 275 | 276 | i += 1 277 | 278 | title_types_table["type_id_" + str(i)] = u8(t["type_id"]) 279 | title_types_table["console_model_" + str(i)] = ( 280 | t["console_model"].encode("utf-8").rjust(3, b"\x00") 281 | ) 282 | title_types_table["title_" + str(i)] = ( 283 | t["title"].encode("utf-16be").rjust(102, b"\x00") 284 | ) 285 | title_types_table["group_id_" + str(i)] = u8(t["group_id"]) 286 | title_types_table["unknown_" + str(i)] = u8(t["unknown"]) 287 | 288 | return title_types_table 289 | 290 | def make_company_table(self): 291 | company_table = {} 292 | 293 | i = 0 294 | 295 | self.dictionaries["header"]["company_table_offset"] = u32(self.offset_count1()) 296 | 297 | for c in self.ninfile["company_table"]: 298 | c = self.ninfile["company_table"][c] 299 | 300 | i += 1 301 | 302 | company_table["id_" + str(i)] = u32(c["id"]) 303 | company_table["dev_title_" + str(i)] = ( 304 | c["dev_title"].encode("utf-16be").rjust(62, b"\x00") 305 | ) 306 | company_table["pub_title_" + str(i)] = ( 307 | c["pub_title"].encode("utf-16be").rjust(62, b"\x00") 308 | ) 309 | 310 | return company_table 311 | 312 | def make_title_table(self): 313 | title_table = {} 314 | 315 | i = 0 316 | 317 | self.dictionaries["header"]["title_table_offset"] = u32(self.offset_count1()) 318 | 319 | for t in self.ninfile["title_table"]: 320 | t = self.ninfile["title_table"][t] 321 | 322 | i += 1 323 | 324 | title_table["id_" + str(i)] = u32(t["id"]) 325 | title_table["title_id_" + str(i)] = ( 326 | t["title_id"].encode("utf-8").rjust(4, b"\x00") 327 | ) 328 | title_table["title_type_" + str(i)] = u8(t["title_type"]) 329 | 330 | for j in range(0, 3): 331 | title_table["genre_" + str(i) + "_" + str(j)] = u8(t["genre"][j]) 332 | 333 | title_table["company_offset_" + str(i)] = u32(t["company_offset"]) 334 | title_table["release_date_year_" + str(i)] = u16(t["release_date_year"]) 335 | title_table["release_date_month_" + str(i)] = u8(t["release_date_month"]) 336 | title_table["release_date_day_" + str(i)] = u8(t["release_date_day"]) 337 | title_table["rating_id_" + str(i)] = u8(t["rating_id"]) 338 | 339 | for j in range(0, 29): 340 | title_table["unknown_4_" + str(i) + "_" + str(j)] = u8( 341 | t["unknown_4"][j] 342 | ) 343 | 344 | title_table["title_" + str(i)] = ( 345 | t["title"].encode("utf-16be").rjust(62, b"\x00") 346 | ) 347 | title_table["subtitle_" + str(i)] = ( 348 | t["subtitle"].encode("utf-16be").rjust(62, b"\x00") 349 | ) 350 | title_table["short_title_" + str(i)] = ( 351 | t["short_title"].encode("utf-16be").rjust(62, b"\x00") 352 | ) 353 | 354 | return title_table 355 | 356 | def make_new_title_table(self): 357 | new_title_table = {} 358 | 359 | i = 0 360 | 361 | self.dictionaries["header"]["new_title_table_offset"] = u32( 362 | self.offset_count1() 363 | ) 364 | 365 | for n in self.ninfile["new_title_table"]: 366 | n = self.ninfile["new_title_table"][n] 367 | 368 | i += 1 369 | 370 | new_title_table["new_title_offset_" + str(i)] = u32(n["new_title_offset"]) 371 | 372 | return new_title_table 373 | 374 | def make_videos_1_table(self): 375 | videos_1_table = {} 376 | 377 | i = 0 378 | 379 | self.dictionaries["header"]["videos_1_table_offset"] = u32(self.offset_count1()) 380 | 381 | for v in self.ninfile["videos_1_table"]: 382 | v = self.ninfile["videos_1_table"][v] 383 | 384 | i += 1 385 | 386 | videos_1_table["id_" + str(i)] = u32(v["id"]) 387 | videos_1_table["time_length_" + str(i)] = u16(v["time_length"]) 388 | videos_1_table["title_id_" + str(i)] = u32(v["title_id"]) 389 | 390 | for j in range(0, 15): 391 | videos_1_table["unknown_" + str(i) + "_" + str(j)] = u8(v["unknown"][j]) 392 | 393 | videos_1_table["unknown2_" + str(i)] = u8(v["unknown_2"]) 394 | videos_1_table["rating_id_" + str(i)] = u8(v["rating_id"]) 395 | videos_1_table["unknown3_" + str(i)] = u8(v["unknown_3"]) 396 | videos_1_table["new_tag_" + str(i)] = u8(v["new_tag"]) 397 | videos_1_table["video_index_" + str(i)] = u8(v["video_index"]) 398 | 399 | for j in range(0, 2): 400 | videos_1_table["unknown4_" + str(i) + "_" + str(j)] = u8( 401 | v["unknown_4"][j] 402 | ) 403 | 404 | videos_1_table["title_" + str(i)] = ( 405 | v["title"].encode("utf-16be").rjust(246, b"\x00") 406 | ) 407 | 408 | return videos_1_table 409 | 410 | def make_new_video_table(self): 411 | new_video_table = {} 412 | 413 | i = 0 414 | 415 | self.dictionaries["header"]["new_video_table_offset"] = u32( 416 | self.offset_count1() 417 | ) 418 | 419 | for n in self.ninfile["new_video_table"]: 420 | n = self.ninfile["new_video_table"][n] 421 | 422 | i += 1 423 | 424 | new_video_table["id_" + str(i)] = u32(n["id"]) 425 | new_video_table["unknown_" + str(i)] = u16(n["unknown"]) 426 | new_video_table["title_id_" + str(i)] = u32(n["title_id"]) 427 | 428 | for j in range(0, 18): 429 | new_video_table["unknown2_" + str(i) + "_" + str(j)] = u8( 430 | n["unknown_2"][j] 431 | ) 432 | 433 | new_video_table["title_" + str(i)] = ( 434 | n["title"].encode("utf-16be").rjust(204, b"\x00") 435 | ) 436 | 437 | return new_video_table 438 | 439 | def make_demos_table(self): 440 | demos_table = {} 441 | 442 | i = 0 443 | 444 | self.dictionaries["header"]["demos_table_offset"] = u32(self.offset_count1()) 445 | 446 | for d in self.ninfile["demos_table"]: 447 | d = self.ninfile["demos_table"][d] 448 | 449 | i += 1 450 | 451 | demos_table["id_" + str(i)] = u32(d["id"]) 452 | demos_table["title_" + str(i)] = ( 453 | d["title"].encode("utf-16be").rjust(62, b"\x00") 454 | ) 455 | demos_table["subtitle_" + str(i)] = ( 456 | d["subtitle"].encode("utf-16be").rjust(62, b"\x00") 457 | ) 458 | demos_table["titleid_" + str(i)] = u32(d["titleid"]) 459 | demos_table["company_offset_" + str(i)] = u32(d["company_offset"]) 460 | demos_table["removal_year_" + str(i)] = u16(d["removal_year"]) 461 | demos_table["removal_month_" + str(i)] = u8(d["removal_month"]) 462 | demos_table["removal_day_" + str(i)] = u8(d["removal_day"]) 463 | demos_table["unknown_" + str(i)] = u32(d["unknown"]) 464 | demos_table["rating_id_" + str(i)] = u8(d["rating_id"]) 465 | demos_table["new_tag_" + str(i)] = u8(d["new_tag"]) 466 | demos_table["new_tag_index_" + str(i)] = u8(d["new_tag_index"]) 467 | 468 | for j in range(0, 205): 469 | demos_table["unknown2_" + str(i) + "_" + str(j)] = u8(d["unknown_2"][j]) 470 | 471 | return demos_table 472 | 473 | def make_recommendations_table(self): 474 | recommendations_table = {} 475 | 476 | i = 0 477 | 478 | self.dictionaries["header"]["recommendations_table_offset"] = u32( 479 | self.offset_count1() 480 | ) 481 | 482 | for r in self.ninfile["recommendations_table"]: 483 | r = self.ninfile["recommendations_table"][r] 484 | 485 | i += 1 486 | 487 | recommendations_table["recommendation_table_offset_" + str(i)] = u32( 488 | r["recommendation_title_offset"] 489 | ) 490 | 491 | return recommendations_table 492 | 493 | def make_recent_recommendations_table(self): 494 | recent_recommendations_table = {} 495 | 496 | i = 0 497 | 498 | self.dictionaries["header"]["recent_recommendations_table_offset"] = u32( 499 | self.offset_count1() 500 | ) 501 | 502 | for r in self.ninfile["recent_recommendations_table"]: 503 | r = self.ninfile["recent_recommendations_table"][r] 504 | 505 | i += 1 506 | 507 | recent_recommendations_table[ 508 | "recent_recommendation_title_offset_" + str(i) 509 | ] = u32(r["recent_recommendation_title_offset"]) 510 | recent_recommendations_table["unknown_" + str(i)] = u16(r["unknown"]) 511 | 512 | return recent_recommendations_table 513 | 514 | def make_popular_videos_table(self): 515 | popular_videos_table = {} 516 | 517 | i = 0 518 | 519 | self.dictionaries["header"]["popular_videos_table_offset"] = u32( 520 | self.offset_count1() 521 | ) 522 | 523 | for p in self.ninfile["popular_videos_table"]: 524 | p = self.ninfile["popular_videos_table"][p] 525 | 526 | i += 1 527 | 528 | popular_videos_table["id_" + str(i)] = u32(p["id"]) 529 | popular_videos_table["time_length_" + str(i)] = u16(p["time_length"]) 530 | popular_videos_table["title_id_" + str(i)] = u32(p["title_id"]) 531 | popular_videos_table["bar_color_" + str(i)] = u8(p["bar_color"]) 532 | 533 | for j in range(0, 15): 534 | popular_videos_table["unknown2_" + str(i) + "_" + str(j)] = u8( 535 | p["unknown_2"][j] 536 | ) 537 | 538 | popular_videos_table["rating_id_" + str(i)] = u8(p["rating_id"]) 539 | popular_videos_table["unknown3_" + str(i)] = u8(p["unknown_3"]) 540 | popular_videos_table["video_rank_" + str(i)] = u8(p["video_rank"]) 541 | popular_videos_table["unknown4_" + str(i)] = u8(p["unknown_4"]) 542 | popular_videos_table["title_" + str(i)] = ( 543 | p["title"].encode("utf-16be").rjust(204, b"\x00") 544 | ) 545 | 546 | return popular_videos_table 547 | 548 | def make_detailed_ratings_table(self): 549 | detailed_ratings_table = {} 550 | 551 | i = 0 552 | 553 | self.dictionaries["header"]["detailed_ratings_table_offset"] = u32( 554 | self.offset_count1() 555 | ) 556 | 557 | for d in self.ninfile["detailed_ratings_table"]: 558 | d = self.ninfile["detailed_ratings_table"][d] 559 | 560 | i += 1 561 | 562 | detailed_ratings_table["rating_group_" + str(i)] = u8(d["rating_group"]) 563 | detailed_ratings_table["rating_id_" + str(i)] = u8(d["rating_id"]) 564 | detailed_ratings_table["title_" + str(i)] = ( 565 | d["title"].encode("utf-16be").rjust(204, b"\x00") 566 | ) 567 | 568 | return detailed_ratings_table 569 | 570 | def deadbeef(self, i): 571 | k = 0 572 | 573 | while (self.offset_count(self.jpeg) % 32) != 0: 574 | bytes = {0: 0xDE, 1: 0xAD, 2: 0xBE, 3: 0xEF} 575 | 576 | self.jpeg["deadbeef_" + str(i) + "_" + str(k)] = u8(bytes[k % 4]) 577 | 578 | k += 1 579 | 580 | def make_jpeg(self): 581 | self.jpeg = {} 582 | 583 | i = 0 584 | 585 | for j in self.ninfile["ratings_table"]: 586 | j = self.ninfile["ratings_table"][j] 587 | 588 | if j["jpeg_offset"] != 0: 589 | i += 1 590 | 591 | self.deadbeef(i) 592 | 593 | self.jpeg["jpeg_" + str(i)] = j["jpeg"] 594 | 595 | self.deadbeef(i) 596 | 597 | return self.jpeg 598 | 599 | 600 | NintendoChannel(ninfile1.nintendo_channel_file) 601 | -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/BBFC/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/BBFC/12.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/BBFC/15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/BBFC/15.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/BBFC/18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/BBFC/18.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/BBFC/PG.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/BBFC/PG.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/BBFC/U.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/BBFC/U.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/CERO/A.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/CERO/A.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/CERO/B.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/CERO/B.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/CERO/C.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/CERO/C.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/CERO/D.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/CERO/D.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/CERO/Z.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/CERO/Z.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/CERO/education.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/CERO/education.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/CERO/scheduled.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/CERO/scheduled.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/ESRB/E-small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/ESRB/E-small.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/ESRB/E.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/ESRB/E.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/ESRB/E10-small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/ESRB/E10-small.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/ESRB/E10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/ESRB/E10.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/ESRB/EC.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/ESRB/EC.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/ESRB/M.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/ESRB/M.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/ESRB/T.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/ESRB/T.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/ESRB/maycontain.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/ESRB/maycontain.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/ESRB/visitesrb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/ESRB/visitesrb.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/PEGI/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/PEGI/.DS_Store -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/PEGI/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/PEGI/12.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/PEGI/16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/PEGI/16.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/PEGI/18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/PEGI/18.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/PEGI/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/PEGI/3.jpg -------------------------------------------------------------------------------- /Channels/Nintendo_Channel/ratings/PEGI/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/Nintendo_Channel/ratings/PEGI/7.jpg -------------------------------------------------------------------------------- /Channels/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Channels/__init__.py -------------------------------------------------------------------------------- /Games/My_Aquarium/myaquarium.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # =========================================================================== 5 | # MY AQUARIUM - AQUARIUM ATTACHMENT GENERATOR 6 | # VERSION 1.0 7 | # AUTHORS: JOHN PANSERA 8 | # **************************************************************************** 9 | # Copyright (c) 2015-2023 RiiConnect24, and its (Lead) Developers 10 | # =========================================================================== 11 | 12 | import binascii 13 | import collections 14 | import io 15 | import struct 16 | 17 | bytes = 0 18 | 19 | print "My Aquarium Custom Attachment Generator" 20 | print "By John Pansera / Version 1.0\n" 21 | 22 | 23 | def u8(data): 24 | if data < 0 or data > 255: 25 | print "[+] Invalid Entry: %s" % data 26 | exit() 27 | return struct.pack(">B", data) 28 | 29 | 30 | def u16(data): 31 | if data < 0 or data > 65535: 32 | print "[+] Invalid Entry: %s" % data 33 | exit() 34 | return struct.pack(">H", data) 35 | 36 | 37 | def u32(data): 38 | if data < 0 or data > 4294967295: 39 | print "[+] Invalid Entry: %s" % data 40 | exit() 41 | return struct.pack(">I", data) 42 | 43 | 44 | def pad(amnt): 45 | buffer = "" 46 | for _ in range(amnt): buffer += "\0" 47 | return buffer 48 | 49 | 50 | print "AQUARIUM SIZE" 51 | print "0 - Small" 52 | print "1 - Medium" 53 | print "2 - Large" 54 | print "\n" 55 | aquarium_size = raw_input('Enter Selection: ') 56 | print "GLASS TYPE" 57 | print "0 - Normal" 58 | print "1 - Gift 1" 59 | print "2 - Gift 2" 60 | print "3 - Special Date 1" 61 | print "4 - Special Date 2" 62 | print "5 - Special Date 3" 63 | print "\n" 64 | glass_type = raw_input('Enter Selection : ') 65 | print "FLOOR TYPE" 66 | print "0 - River Bed" 67 | print "1 - Sand" 68 | print "2 - White Sand" 69 | print "3 - Gravel 1" 70 | print "4 - Gravel 2" 71 | print "\n" 72 | floor_type = raw_input('Enter Selection : ') 73 | print "BACKGROUND TYPE" 74 | print "0 - Underwater Theme" 75 | print "1 - Emerald Theme" 76 | print "2 - Coral Theme" 77 | print "3 - Quiet Ocean Theme" 78 | print "4 - Riverbed Theme" 79 | print "\n" 80 | background_type = raw_input('Enter Selection : ') 81 | print "LIGHT TYPE" 82 | print "0 - Normal" 83 | print "1 - Brightly Lit" 84 | print "2 - Very Bright" 85 | print "3 - Blue" 86 | print "4 - Green" 87 | print "5 - Orange" 88 | print "6 - Spotted" 89 | print "\n" 90 | light_type = raw_input('Enter Selection : ') 91 | print "\nSPECIAL DATE 1" 92 | specialdate1_month = raw_input('Enter Month Number : ') 93 | specialdate1_day = raw_input('Enter Day Number : ') 94 | print "\nSPECIAL DATE 2" 95 | specialdate2_month = raw_input('Enter Month Number : ') 96 | specialdate2_day = raw_input('Enter Day Number : ') 97 | print "\nSPECIAL DATE 3" 98 | specialdate3_month = raw_input('Enter Month Number : ') 99 | specialdate3_day = raw_input('Enter Day Number : ') 100 | print "\n" 101 | print "NOTE: This script does not currently generate objects in the tank" 102 | 103 | header = collections.OrderedDict() 104 | header["unknown"] = u8(0) # Version? 105 | header["aquarium_size"] = u8(int(aquarium_size)) 106 | header["glass_type"] = u8(int(glass_type)) 107 | header["floor_type"] = u8(int(floor_type)) 108 | header["background_type"] = u8(int(background_type)) 109 | header["light_type"] = u8(int(light_type)) 110 | header["unknown_1"] = u16(0) 111 | header["breeding_date_counter"] = u32(0) 112 | header["unknown_date_counter"] = u32(0) 113 | header["special_date_1_padding"] = u8(0) 114 | header["special_date_1_month"] = u8(int(specialdate1_month)) 115 | header["special_date_1_day"] = u8(int(specialdate1_day)) 116 | header["special_date_1_padding_1"] = u8(0) 117 | header["special_date_2_padding"] = u8(0) 118 | header["special_date_2_month"] = u8(int(specialdate2_month)) 119 | header["special_date_2_day"] = u8(int(specialdate2_day)) 120 | header["special_date_2_padding_2"] = u8(0) 121 | header["special_date_3_padding"] = u8(0) 122 | header["special_date_3_month"] = u8(int(specialdate3_month)) 123 | header["special_date_3_day"] = u8(int(specialdate3_day)) 124 | header["special_date_3_padding_1"] = u8(0) 125 | 126 | if raw_input('Would you like to add fish? [y/n] ') is 'y': 127 | amnt = int(raw_input('Enter amount of fish to add [MAX 15]: ')) 128 | if amnt <= 15: 129 | print "\n" 130 | print "FISH SELECTION [0-39]" 131 | print "Note: There are 40 different types of fish, however there are more listed here (found in the game's files - they may or may not be able to be used)" 132 | print "01 Achilles Tang" 133 | print "02 Asian Arowana" 134 | print "03 Red Asian Arowana" 135 | print "04 Blueface Angelfish" 136 | print "05 Emperor Angelfish" 137 | print "06 Yellow Piranha" 138 | print "07 Freshwater Angelfish" 139 | print "08 Ocellaris Clownfish" 140 | print "09 Blue Jellyfish" 141 | print "10 Clown Killifish" 142 | print "11 Clown Loach" 143 | print "12 Pakistani Loach" 144 | print "13 Sea Angel" 145 | print "14 Golden Gourami" 146 | print "15 Dwarf Hawkfish" 147 | print "16 Humpback Grouper" 148 | print "17 Coral Grouper" 149 | print "18 Killifish" 150 | print "19 Papuan Jellyfish" 151 | print "20 Checkered Barb" 152 | print "21 Jellyfish Choukurage" 153 | print "22 Tinkeri Butterfly" 154 | print "23 Corydoras Narcissus" 155 | print "24 Flagtail Surgeonfish" 156 | print "25 Neon Tetra" 157 | print "26 Cardinal Tetra" 158 | print "27 Pastaza Corydoras" 159 | print "28 Red Lionfish" 160 | print "29 Lyretail Hogfish" 161 | print "30 Wrasse" 162 | print "31 Blue Gourami" 163 | print "32 Black Molly" 164 | print "33 Blue Grass Guppy" 165 | print "34 Red Grass Guppy" 166 | print "35 Blue Discus" 167 | print "36 Betta" 168 | print "37 Argentine Pearlfish" 169 | print "38 Moon Jellyfish" 170 | print "39 Spotted Green Puffer" 171 | print "40 Sea Horse" 172 | print "41 Rainbow Rockfish" 173 | print "42 Archerfish" 174 | print "43 Jaw Characin" 175 | print "44 White Spotted Cichlid" 176 | print "45 Rainbow Fish" 177 | print "46 Leopard Bushfish" 178 | print "47 Redtail Catfish" 179 | print "48 Japanese Bullhead Shark" 180 | print "49 Lookdown Fish" 181 | print "\n" 182 | for i in range(amnt): 183 | sel = int(raw_input('Selection %s: ' % i)) 184 | if sel < 40: 185 | header["fish_amount_%s" % i] = u8(1) 186 | header["fish_id_%s" % i] = u8(sel) 187 | header["fish_growth_level_%s" % i] = u16(0) 188 | header["fish_hungry_degree_%s" % i] = u16(0) 189 | header["fish_padding_%s" % i] = u16(1) 190 | header["fish_birthday_%s" % i] = u32(1) # Birthday is day 1 191 | header["current_day_%s" % i] = u32(1) # Current day set to 1 192 | else: 193 | print "Error: Invalid selection" 194 | else: 195 | print "Error: Invalid amount, skipping" 196 | amnt = 0 197 | 198 | header["fish_tables"] = pad(240 - (amnt * 16)) 199 | header["object_tables"] = pad(160) # TODO: Add in object tables 200 | 201 | print "Processing ..." 202 | 203 | f = io.BytesIO() 204 | for k, v in header.items(): f.write(v) 205 | f.flush() 206 | f.seek(0) 207 | copy = f.read() 208 | crc32 = format(binascii.crc32(copy) & 0xFFFFFFFF, '08x') 209 | f.close() 210 | 211 | file = open('a0014682.dat', 'wb') # Not sure how the name is generated yet 212 | file.write(binascii.unhexlify('08051400')) # Magic Value 213 | file.write(binascii.unhexlify(crc32)) # CRC32 214 | file.write(copy) # Rest of File 215 | file.flush() 216 | file.close() 217 | 218 | print "\n" 219 | print "Completed Successfully" 220 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # File-Maker 2 | [![License](https://img.shields.io/github/license/riiconnect24/file-maker.svg?style=flat-square)](http://www.gnu.org/licenses/agpl-3.0) 3 | [![Discord](https://img.shields.io/discord/206934458954153984.svg?style=flat-square)](https://discord.com/invite/b4Y7jfD) 4 | 5 | These scripts will create static data files for these Wii Channels: 6 | 7 | - Everybody Votes Channel 8 | - Forecast Channel 9 | - News Channel 10 | - Nintendo Channel 11 | - Check Mii Out Channel/Mii Contest Channel 12 | 13 | These files are downloaded on the Wii, and contain news, weather info, etc that the Channel(s) display, as well as influencing some games' environments. 14 | 15 | ## Services and Modules 16 | 17 | We use the following services for this project: 18 | 19 | - [Datadog](https://datadoghq.com/) for analytics. 20 | - For News Channel, [Google Maps Geocoding API](https://developers.google.com/maps/documentation/geocoding/intro) to get location coordinates. 21 | - For Everybody Votes Channel and Check Mii Out Channel, [MySQL](https://www.mysql.com/) to hold votes and suggestions. 22 | - [Sentry](https://sentry.io/) for error logging. 23 | - Webhooks to log when a script has been ran. 24 | 25 | Some notable Python modules used in the project are: 26 | 27 | - [BeautifulSoup](https://www.crummy.com/software/BeautifulSoup/) for HTML parsing. 28 | - [feedparser](https://pypi.python.org/pypi/feedparser) to parse RSS feeds. 29 | - [newspaper](http://newspaper.readthedocs.io/en/latest/) for news article scraping. 30 | - [nlzss](https://github.com/DorkmasterFlek/python-nlzss) for LZ compressing the files. 31 | - [requests](http://docs.python-requests.org/en/master/) for various HTTP requests. 32 | - [rsa](https://pypi.python.org/pypi/rsa) to create an RSA signed SHA-1 (that the Wii verifies downloaded files with). 33 | 34 | [AccuWeather](https://accuweather.com/) is used as the weather source for the Forecast Channel. For a list of news sources we use for the News Channel, [refer to this webpage](https://rc24.xyz/services/news.html). 35 | 36 | All files are LZ10 compressed. 37 | 38 | If you want to know the format of the files used by the Channels, you can [look at our Kaitais](https://github.com/RiiConnect24/Kaitai-Files), check the wiki, or look at the code. 39 | 40 | ## Installing Requirements 41 | 42 | These scripts run on Python 3. 43 | 44 | Just run `pip install -r requirements.txt` in the root folder and it'll install. You might have to run as `sudo` due to permissions. 45 | 46 | It's required to have a `config.json` for each Channel in the `Channels` folder. Fill out `config.json.template` for the Channels you want to run this script for, and rename it to `config.json`. 47 | 48 | Run the scripts as modules, e.g. `python -m Channels.Forecast_Channel.forecast`. 49 | -------------------------------------------------------------------------------- /Tools/Forecast/parse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import binascii 5 | import collections 6 | import os 7 | import pickle 8 | import sys 9 | import collections 10 | 11 | print "Forecast File Parser" 12 | print "By John Pansera (2017-2020) - www.rc24.xyz" 13 | print "Parsing ...\n" 14 | 15 | filename = "forecast.bin" 16 | amnt = None 17 | offset = None 18 | file = None 19 | names = collections.OrderedDict() 20 | 21 | 22 | def read_int(x): 23 | return int(binascii.hexlify(file.read(x)), 16) 24 | 25 | 26 | def coord_decode(value): 27 | value = int(value, 16) 28 | if value >= 0x8000: 29 | value -= 0x10000 30 | return value * 0.0054931640625 31 | 32 | 33 | def parse_offsets(dict, count, offset): 34 | file.seek(offset) 35 | for i in range(count): 36 | code = read_int(1) 37 | padding = read_int(3) 38 | offset = read_int(4) 39 | dict[i] = [code, offset] 40 | 41 | def parse_weather_offsets(dict, count, offset): 42 | file.seek(offset) 43 | for i in range(count): 44 | international_code_1 = binascii.hexlify(file.read(2)) 45 | international_code_2 = binascii.hexlify(file.read(2)) 46 | international_offset = read_int(4) 47 | japan_code_1 = binascii.hexlify(file.read(2)) 48 | japan_code_2 = binascii.hexlify(file.read(2)) 49 | japan_offset = read_int(4) 50 | dict[i] = [international_code_1.upper(), international_code_2.upper(), international_offset, japan_code_1.upper(), japan_code_2.upper(), japan_offset] 51 | 52 | 53 | def get_text(): 54 | str = "" 55 | null_last = False 56 | while True: 57 | char = file.read(1) 58 | str += char 59 | if char == '\00': 60 | if not null_last: 61 | null_last = True 62 | else: 63 | null_last = False 64 | break 65 | else: null_last = False 66 | return str 67 | 68 | 69 | file = open(filename, "rb") 70 | 71 | # Print file header information 72 | 73 | version = read_int(4) 74 | print "Version: %s" % version 75 | file_size = read_int(4) 76 | print "File Size: %s" % file_size 77 | crc32 = binascii.hexlify(file.read(4)) 78 | print "CRC32: %s" % crc32 79 | timestamp_1 = read_int(4) 80 | timestamp_2 = read_int(4) 81 | print "Valid until: %s (for %s minutes)" % (timestamp_1,int(timestamp_1-timestamp_2)) 82 | print "Generated at: %s" % timestamp_2 83 | file.seek(20) # Country Code 84 | country_code = binascii.hexlify(file.read(1)) 85 | print "Country Code: %s (%s)" % (country_code, str(int(country_code, 16)).zfill(3)) 86 | file.seek(21) # Language Code 87 | language_code = read_int(4) 88 | print "Language Code: %s" % language_code 89 | file.seek(28) # Message Offset 90 | message_offset = read_int(4) 91 | if message_offset == 0: print "No Message in file" 92 | else: print "Message exists in file" 93 | file.seek(32) # Long Forecast Entry Number 94 | long_count = read_int(4) 95 | file.seek(36) # Long Forecast Table Offset 96 | long_offset = read_int(4) 97 | print "%s long forecast table entries @ %s" % (long_count, hex(long_offset)) 98 | file.seek(40) # Short Forecast Entry Number 99 | short_count = read_int(4) 100 | file.seek(44) # Short Forecast Table Offset 101 | short_offset = read_int(4) 102 | print "%s short forecast table entries @ %s" % (short_count, hex(short_offset)) 103 | file.seek(48) # Weather Condition Codes Entry Number 104 | weatherconditions_count = read_int(4) 105 | file.seek(52) # Weather Condition Codes Table Offset 106 | weatherconditions_offset = read_int(4) 107 | print "%s weather condition entries @ %s" % (weatherconditions_count, hex(weatherconditions_offset)) 108 | file.seek(56) # UV Index Entry Number 109 | uvindex_count = read_int(4) 110 | file.seek(60) # UV Index Table Offset 111 | uvindex_offset = read_int(4) 112 | print "%s uv index entries @ %s" % (uvindex_count, hex(uvindex_offset)) 113 | file.seek(64) # Laundry Index Entry Number 114 | laundry_count = read_int(4) 115 | file.seek(68) # Laundry Index Table Offset 116 | laundry_offset = read_int(4) 117 | print "%s laundry index entries @ %s" % (laundry_count, hex(laundry_offset)) 118 | file.seek(72) # Pollen Count Entry Number 119 | pollen_count = read_int(4) 120 | file.seek(76) # Pollen Count Entry Offset 121 | pollen_offset = read_int(4) 122 | print "%s pollen index entries @ %s" % (pollen_count, hex(pollen_offset)) 123 | file.seek(80) # Location Entry Number 124 | amnt = read_int(4) 125 | file.seek(84) # Location Table Offset 126 | offset = read_int(4) 127 | print "%s location entries @ %s" % (amnt, hex(offset)) 128 | 129 | # Parse Entries 130 | uvindex = collections.OrderedDict() 131 | laundry = collections.OrderedDict() 132 | pollen = collections.OrderedDict() 133 | weather = collections.OrderedDict() 134 | parse_offsets(uvindex, uvindex_count, uvindex_offset) 135 | parse_offsets(laundry, laundry_count, laundry_offset) 136 | parse_offsets(pollen, pollen_count, pollen_offset) 137 | parse_weather_offsets(weather, weatherconditions_count / 2, weatherconditions_offset) 138 | 139 | file.seek(uvindex[0][1]) 140 | print "\nUV Index Entries:" 141 | for i in range(uvindex_count): 142 | uvindex[i][1] = get_text() 143 | print " %s : %s" % (uvindex[i][0], uvindex[i][1].decode('utf-16be')) 144 | 145 | laundry_str = "" 146 | file.seek(laundry[0][1]) 147 | for i in range(laundry_count): 148 | laundry[i][1] = get_text() 149 | laundry_str += str(laundry[i][0]) + " " 150 | print "\nLaundry Index Entries: " + laundry_str 151 | 152 | pollen_str = "" 153 | file.seek(pollen[0][1]) 154 | for i in range(pollen_count): 155 | pollen[i][1] = get_text() 156 | pollen_str += str(pollen[i][0]) + " " 157 | print "\nPollen Index Entries: " + pollen_str 158 | 159 | print "\nWeather Conditions:" 160 | print "Code 1 / Code 2 (International) / Code 1 / Code 2 (Japan)\n" 161 | file.seek(weather[0][2]) 162 | for i in range(weatherconditions_count / 2): 163 | weather[i][2] = get_text() 164 | weather[i][5] = get_text() 165 | print " %s/%s/%s/%s : %s" % (weather[i][0], weather[i][1], weather[i][3], weather[i][4], weather[i][2].decode('utf-16be').replace('\n', ' ')) 166 | 167 | raw_input("\nParsing location entries:") 168 | 169 | # Parse location entries 170 | file.seek(offset) 171 | for i in range(amnt): 172 | loc_name = binascii.hexlify(file.read(4)) 173 | city = read_int(4) 174 | region = read_int(4) 175 | country = read_int(4) 176 | lat = coord_decode(binascii.hexlify(file.read(2))) 177 | lng = coord_decode(binascii.hexlify(file.read(2))) 178 | zoom1 = read_int(1) 179 | zoom2 = read_int(1) 180 | file.seek(2, 1) 181 | names[i] = [loc_name, city, region, country, lat, lng, zoom1, zoom2] 182 | 183 | 184 | file.seek(names[0][1]) 185 | for k in names.keys(): 186 | print "Location ID: %s" % names[k][0].upper() 187 | names[k][1] = get_text() 188 | print "City: %s" % names[k][1].decode('utf-16be').encode('utf-8') 189 | if names[k][2] != 0: 190 | names[k][2] = get_text() 191 | print "Region: %s" % names[k][2].decode('utf-16be').encode('utf-8') 192 | else: print "No Region" 193 | if names[k][3] != 0: 194 | names[k][3] = get_text() 195 | print "Country: %s" % names[k][3].decode('utf-16be').encode('utf-8') 196 | else: print "No Country" 197 | print "Latitude Coordinate: %s" % names[k][4] 198 | print "Longitude Coordinate: %s" % names[k][5] 199 | print "Zoom 1: %s" % names[k][6] 200 | print "Zoom 2: %s" % names[k][7] 201 | 202 | 203 | print "\nDumping Locations Database ..." 204 | with open('locations.db', 'wb') as file: 205 | pickle.dump(names, file) 206 | 207 | print "Completed Sucessfully" 208 | -------------------------------------------------------------------------------- /Tools/LZ Tools/DSDecmp/DSDecmp.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Tools/LZ Tools/DSDecmp/DSDecmp.exe -------------------------------------------------------------------------------- /Tools/LZ Tools/DSDecmp/Plugins/GoldenSunDD.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Tools/LZ Tools/DSDecmp/Plugins/GoldenSunDD.dll -------------------------------------------------------------------------------- /Tools/LZ Tools/DSDecmp/Plugins/LuminousArc.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Tools/LZ Tools/DSDecmp/Plugins/LuminousArc.dll -------------------------------------------------------------------------------- /Tools/LZ Tools/lzss/lzss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Tools/LZ Tools/lzss/lzss -------------------------------------------------------------------------------- /Tools/LZ Tools/lzss/lzss-mac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Tools/LZ Tools/lzss/lzss-mac -------------------------------------------------------------------------------- /Tools/LZ Tools/lzss/lzss.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RiiConnect24/File-Maker/fe41a6ac4bddd1fd1e67030fb646cc200826fad9/Tools/LZ Tools/lzss/lzss.exe -------------------------------------------------------------------------------- /Tools/Sign_Encrypt/sign_encrypt.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import binascii 3 | import collections 4 | import os 5 | import pyaes 6 | import rsa 7 | import struct 8 | import subprocess 9 | 10 | 11 | def u8(data): 12 | return struct.pack(">B", data) 13 | 14 | 15 | def u16(data): 16 | return struct.pack(">H", data) 17 | 18 | 19 | def u32(data): 20 | return struct.pack(">I", data) 21 | 22 | parser = argparse.ArgumentParser(description="Sign / Encrypt WiiConnect24 files.") 23 | parser.add_argument("-t", "--type", 24 | type=str, nargs="+", 25 | help="Type of file. Set either enc for encrypted file, or dec for decrypted (compressed) file.") 26 | parser.add_argument("-in", "--input", 27 | type=str, nargs="+", 28 | help="Input file.") 29 | parser.add_argument("-out", "--output", 30 | type=str, nargs="+", 31 | help="Output file.") 32 | parser.add_argument("-c", "--compress", 33 | type=str, nargs="+", 34 | help="If set, this will compress the file before signing.") 35 | parser.add_argument("-key", "--aes-key", 36 | type=str, nargs="+", 37 | help="AES key in hex or a path.") 38 | parser.add_argument("-iv", "--iv-key", 39 | type=str, nargs="+", 40 | help="AES IV in hex or a path.") 41 | parser.add_argument("-rsa", "--rsa-key-path", 42 | type=str, nargs="+", 43 | help="RSA private key path. If not specified, it will use the private key in Private.pem if it exists.") 44 | 45 | args = parser.parse_args() 46 | 47 | if args.compress is not None: 48 | subprocess.call(["cp", args.input[0], "temp"]) 49 | subprocess.call(["lzss", "-evf", "temp"]) 50 | 51 | if args.type[0] == "enc": 52 | filename = args.input[0] 53 | elif args.type[0] == "dec": 54 | filename = "temp" 55 | 56 | with open(filename, "rb") as f: 57 | data = f.read() 58 | 59 | """RSA sign the file.""" 60 | 61 | if args.rsa_key_path is not None: 62 | rsa_key_path = args.rsa_key_path[0] 63 | else: 64 | rsa_key_path = "Private.pem" 65 | 66 | with open(rsa_key_path, "rb") as source_file: 67 | private_key_data = source_file.read() 68 | 69 | private_key = rsa.PrivateKey.load_pkcs1(private_key_data, "PEM") 70 | 71 | signature = rsa.sign(data, private_key, "SHA-1") 72 | 73 | if args.type[0] == "enc": 74 | if args.iv_key is not None: 75 | try: 76 | iv = binascii.unhexlify(args.iv_key[0]) 77 | except: 78 | iv = open(args.iv_key[0], "rb").read() 79 | else: 80 | iv = os.urandom(16) 81 | 82 | try: 83 | key = binascii.unhexlify(args.aes_key[0]) 84 | except: 85 | key = open(args.aes_key[0], "rb").read() 86 | 87 | aes = pyaes.AESModeOfOperationOFB(key, iv=iv) 88 | processed = aes.encrypt(data) 89 | elif args.type[0] == "dec": 90 | processed = data 91 | 92 | content = collections.OrderedDict() 93 | 94 | content["magic"] = b"WC24" if args.type[0] == "enc" else u32(0) 95 | content["version"] = u32(1) if args.type[0] == "enc" else u32(0) 96 | content["filler"] = u32(0) 97 | content["crypt_type"] = u8(1) if args.type[0] == "enc" else u8(0) 98 | content["pad"] = u8(0) * 3 99 | content["reserved"] = u8(0) * 32 100 | content["iv"] = iv if args.type[0] == "enc" else u8(0) * 16 101 | content["signature"] = signature 102 | content["data"] = processed 103 | 104 | if os.path.exists(args.output[0]): 105 | os.remove(args.output[0]) 106 | 107 | if args.type[0] == "dec": 108 | os.remove("temp") 109 | 110 | for values in content.values(): 111 | with open(args.output[0], "ab+") as f: 112 | f.write(values) 113 | 114 | print("Completed Successfully") 115 | -------------------------------------------------------------------------------- /Tools/Time/convert.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import sys 3 | 4 | 5 | def main(): 6 | wii_to_timestamp = (int(sys.argv[1], 16) * 60) + 946684800 7 | date = datetime.datetime.utcfromtimestamp(wii_to_timestamp).strftime('%Y-%m-%d %H:%M') 8 | print "Timestamp: %s" % wii_to_timestamp 9 | print "Date: %s" % date 10 | 11 | 12 | if __name__ == "__main__": 13 | main() 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bs4 2 | cloudflare 3 | datadog 4 | feedparser 5 | ftfy==4.4.3 6 | googlemaps 7 | lxml 8 | newspaper3k 9 | nlzss 10 | pyaes 11 | requests 12 | rsa 13 | sentry_sdk 14 | unidecode 15 | pillow 16 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import logging 3 | import os 4 | import requests 5 | import sentry_sdk 6 | import struct 7 | from sentry_sdk.integrations.logging import LoggingIntegration 8 | 9 | """Unification of utilities used by all scripts.""" 10 | 11 | requests.packages.urllib3.disable_warnings() # This is so we don't get some warning about SSL. 12 | 13 | production = False 14 | p_errors = False 15 | 16 | def setup_log(sentry_url, print_errors): 17 | global logger, production 18 | sentry_logging = LoggingIntegration( 19 | level=logging.INFO, 20 | event_level=logging.INFO 21 | ) 22 | sentry_sdk.init(dsn=sentry_url, integrations=[sentry_logging]) 23 | logger = logging.getLogger(__name__) 24 | p_errors = print_errors 25 | production = True 26 | 27 | def log(msg, level): # TODO: Use number levels, strings are annoying 28 | if p_errors: 29 | print(msg) 30 | 31 | if production: 32 | if level == "VERBOSE": 33 | logger.debug(msg) 34 | elif level == "INFO": 35 | logger.info(msg) 36 | elif level == "WARNING": 37 | logger.warning(msg) 38 | elif level == "CRITICAL": 39 | logger.critical(msg) 40 | 41 | def mkdir_p(path): 42 | try: 43 | os.makedirs(path) 44 | except: 45 | pass 46 | 47 | 48 | """Pack integers to specific type.""" 49 | 50 | # Unsigned integers 51 | 52 | def u8(data): 53 | if not 0 <= data <= 255: 54 | log("u8 out of range: %s" % data, "INFO") 55 | data = 0 56 | return struct.pack(">B", data) 57 | 58 | 59 | def u16(data): 60 | if not 0 <= data <= 65535: 61 | log("u16 out of range: %s" % data, "INFO") 62 | data = 0 63 | return struct.pack(">H", data) 64 | 65 | 66 | def u32(data): 67 | if not 0 <= data <= 4294967295: 68 | log("u32 out of range: %s" % data, "INFO") 69 | data = 0 70 | return struct.pack(">I", data) 71 | 72 | 73 | def u32_littleendian(data): 74 | if not 0 <= data <= 4294967295: 75 | log("u32 little endian out of range: %s" % data, "INFO") 76 | data = 0 77 | return struct.pack("<I", data) 78 | 79 | # Signed integer 80 | 81 | def s8(data): 82 | if not -128 <= data <= 127: 83 | log("s8 out of range: %s" % data, "INFO") 84 | data = 0 85 | return struct.pack(">b", data) 86 | 87 | 88 | def s16(data): 89 | if not -32768 <= data <= 32767: 90 | log("s16 out of range: %s" % data, "INFO") 91 | data = 0 92 | return struct.pack(">h", data) 93 | 94 | 95 | def s32(data): 96 | if not -2147483648 <= data <= 2147483647: 97 | log("s32 out of range: %s" % data, "INFO") 98 | data = 0 99 | return struct.pack(">i", data) 100 | --------------------------------------------------------------------------------