Last active
February 12, 2024 00:43
-
-
Save ariankordi/e3b0a12f7b2e713cd1bc2923ed2c50f8 to your computer and use it in GitHub Desktop.
python selenium script (meant to work in conjunction with cursed nso reverse proxy!!!!!) that shows splatoon stages on your kindle
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1.0"> | |
<dict> | |
<key>Label</key> | |
<string>arian.splatnet-kindle</string> | |
<key>Program</key> | |
<string>/opt/homebrew/bin/python3</string> | |
<key>ProgramArguments</key> | |
<array> | |
<string>/opt/homebrew/bin/python3</string> | |
<string>/Users/arian/Documents/splatnet-kindle/splatnet-kindle.py</string> | |
</array> | |
<key>EnvironmentVariables</key> | |
<dict> | |
<key>SSH_HOST</key> | |
<string>kindle-hostname-goes-here</string> | |
<key>SSH_PASS</key> | |
<string>kindle-password-goes-here</string> | |
</dict> | |
<key>StartCalendarInterval</key> | |
<!-- | |
you will need to switch to this when daylight savings time changes | |
TODO: either give a more elegant fix for this... | |
... or instead run every hour, | |
and let the script decide whether it's the correct second hour | |
<array> | |
<dict> | |
<key>Hour</key> | |
<integer>0</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>2</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>4</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>6</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>8</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>10</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>12</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>14</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>16</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>18</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>20</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>22</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
</array> | |
--> | |
<array> | |
<dict> | |
<key>Hour</key> | |
<integer>1</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>3</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>5</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>7</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>9</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>11</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>13</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>15</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>17</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>19</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>21</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
<dict> | |
<key>Hour</key> | |
<integer>23</integer> | |
<key>Minute</key> | |
<integer>0</integer> | |
</dict> | |
</array> | |
</dict> | |
</plist> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Unit] | |
Description=wip script to update kindle | |
# no more than 5 restarts within 3.4 mins | |
StartLimitInterval=300 | |
[Service] | |
#Type=oneshot | |
Environment=SSH_HOST=kindle-hostname-goes-here | |
Environment=SSH_PASS=kindle-password-goes-here | |
WorkingDirectory=/home/arian/Downloads/splatnet-kindle | |
ExecStart=/usr/bin/python3 splatnet-kindle.py | |
Restart=on-failure | |
RestartSec=30 | |
StartLimitBurst=5 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Unit] | |
Description=wip script to update kindle | |
[Timer] | |
#OnCalendar=*:0/2 | |
OnCalendar=0/2:00:00 UTC | |
#RandomizedDelaySec=1h | |
#Persistent=true | |
[Install] | |
WantedBy=timers.target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var style = document.createElement('style'); | |
style.innerHTML = ` | |
/* for testing | |
html {filter: grayscale(100%)} | |
*/ | |
/* no scrollbars */ | |
*::-webkit-scrollbar {display: none} | |
/* tighter */ | |
[class^=NavigationBar_container]{height: 0 !important; min-height: 20px !important} | |
[class^=Schedule_settings]{gap:0 !important; padding-top: 0 !important} | |
[class^=Schedule_Schedule]{padding-bottom: 5px !important} | |
[class^=Schedule_scheduleContainer]{padding-top : 15px !important} | |
/* smaller time */ | |
span[class^=Balloon_Balloon__]{margin-top: 9px !important; padding: 0 0 0 5px !important; font-size: 19px !important; font-family: var(--font-family-s2) !important;} | |
/* no bottom home bar */ | |
[class^=HomeIndicatorSpacer_HomeIndicatorSpacer]{display : none !important} | |
/* no side bar */ | |
[class^=NavigationTab_Wrapper__]{display: none !important} | |
/* black background */ | |
/*:root { --color-background: black !important; } | |
/* text color - white theme */ | |
:root {--color-background: white !important} | |
[class^=App_App__]{color: black !important} | |
[class^=Schedule_RuleName__]{text-shadow: none !important} | |
[class^=Schedule_bankaraMode__]{background-color: var(--color-yellow) !important} | |
[class^=DateLabel_time__]{color: black !important; text-shadow: none !important} | |
[class^=FestInfo_fesDate]{color: black !important} | |
[class^=FestInfo_fesContent__]{background-color: darkslategray !important} | |
[class^=FestInfo_fesLinkBtn__]{/*margin-top: 10px*/display: none !important} | |
[class^=FestInfo_fesStageName__]{background: var(--color-yellow) !important} | |
[class^=FestInfo_container__]{margin-bottom: /*-25px*/-15px !important;padding-top: 3px !important} | |
/* not adjusting size of splatfest header for now */ | |
[class^=SwipableView_swipableViewItem__] { background-color: var(--color-background) !important; } | |
/* big stage names */ | |
[class^=Schedule_StageName__]{line-height: 23px !important; font-size: 18px !important} | |
/* big icons */ | |
[class^=Schedule_ruleImg__]{height: 33px !important; width: 33px !important} | |
/* big mode names */ | |
[class^=Schedule_RuleName__]{font-size: 23px !important} | |
/* big anarchy modes */ | |
[class^=Schedule_bankaraMode__]{font-size: 18px !important} | |
` | |
document.head.appendChild(style); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Replace the following line with your JavaScript code | |
// persistent data request stubs | |
window.restorePersistentData = () => {window.console.log('restorepersistentdata called')}; | |
window.storePersistentData = input => {window.console.log('persistentdatastore called', input)}; | |
// game token (x-gamewebtoken) request | |
window.requestGameWebToken = () => { | |
window.console.log('requestgamewebtoken called'); | |
fetch('http://localhost:36017/_/request_gamewebtoken') | |
.then(response => { | |
return response.text(); | |
}) | |
.then(data => { | |
// data is now gtoken | |
window.onGameWebTokenReceive(data); | |
}); | |
}; | |
/* handle fetches for skipping one rotation ahead, more or less temporary? | |
* reasons this sucks: | |
* it's inefficient due to repetitive json en/decoding | |
* either use complicated string operations or hook JSON.unstringify LMAO (remember, only once!) | |
* it uses a static query hash for the time being | |
* i don't trust this method to not fail lol | |
* that should be all | |
*/ | |
const realFetch = fetch; | |
window.fetch = async function(...arg) { | |
//window.ase = arg;console.log(arg) | |
// check for a post (graphql) request that includes the hash of the query we want to edit | |
if( | |
window.oneAhead && | |
arg[0] && | |
arg[1] && | |
arg[1].method === "POST" && | |
arg[1].body && | |
arg[1].body.includes("f76dd61e08f4ce1d5d5b17762a243fec") | |
) { | |
const response = await realFetch(...arg); | |
const responseText = await response.text(); | |
const resultJSON = JSON.parse(responseText); | |
resultJSON.data.regularSchedules.nodes.shift(); | |
resultJSON.data.bankaraSchedules.nodes.shift(); | |
resultJSON.data.xSchedules.nodes.shift(); | |
resultJSON.data.festSchedules.nodes.shift(); | |
//resultJSON.data.bankaraSchedules.nodes.unshift(resultJSON.data.bankaraSchedules.nodes.pop()); | |
// const result = JSON.stringify(resultJSON); // Remove this line | |
// Create a custom Response object with the modified JSON data | |
const modifiedResponse = new Response(null, { | |
status: response.status, | |
statusText: response.statusText, | |
headers: response.headers, | |
}); | |
// Override the json() method of the custom Response object | |
modifiedResponse.json = async () => /*{console.log('hit our json function..!!!!');return */resultJSON//}; | |
// console.log('check ass');window.ass = resultJSON; | |
return modifiedResponse; | |
} | |
return realFetch(arg[0], arg[1]); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# inform about missing libraries potentially | |
try: | |
from selenium import webdriver | |
import selenium.common.exceptions | |
from selenium.webdriver.common.by import By | |
from selenium.webdriver.support.ui import WebDriverWait | |
from selenium.webdriver.support import expected_conditions as EC | |
# import the service so a custom executable path can be specified | |
from selenium.webdriver.chrome.service import Service | |
from shutil import which | |
# export image to jpeg and with minimal colors | |
from PIL import Image | |
from PIL.ImageOps import posterize | |
#from tempfile import gettempdir | |
# ssh client | |
import paramiko | |
except ImportError as e: | |
print('\033[91mthis script uses paramiko, selenium, and PIL ' \ | |
'(pillow/pillow-simd). please make sure you have them installed, ' \ | |
'refer to the script\'s imports for help\033[0m') | |
raise e | |
import io | |
import os | |
import sys | |
# our kb estimation | |
from math import ceil | |
from time import sleep | |
# this is where the profile of the chromedriver will be | |
# feel free to customize it if you don't use linux or don't like it | |
chromedriver_user_data_dir = f'--user-data-dir={os.environ["HOME"]}/.config/splatnet-kindle-chromedriver' | |
# check for environment variables involving ssh credentials | |
#if not all(v in os.environ for v in ['SSH_HOST', 'SSH_PASS']): # 'SSH_USER',]): | |
if not 'SSH_HOST' in os.environ: | |
print('make sure to define the following environment ' \ | |
'variables: SSH_HOST, SSH_USER (optional, default root), ' \ | |
'and SSH_PASS (optional, ssh key is used too). ' \ | |
'these represent the ssh credentials of the kindle ' \ | |
'for which this script will attempt to connect to. ' \ | |
'this script assumes your kindle has ssh enabled on wifi via the USBNetwork hack. OK?') | |
sys.exit(1) | |
# connect to kindle first things first | |
# because if this fails then there is no point of continuing | |
ssh = paramiko.SSHClient() | |
# you may comment this if you have already ssh'ed into your kindle | |
# NOTE: this isn't necessarily secure but this laso isn't mission critical so meh | |
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | |
# this should throw an exception on timeout, stopping the script from continuing | |
ssh.connect(os.environ['SSH_HOST'], | |
# use root as default username so i can be lazy and not define it | |
username=os.environ.get('SSH_USER', 'root'), | |
password=os.environ.get('SSH_PASS'), timeout=30) | |
# hardcoded link to ranked battle stages | |
url = 'https://api.lp1.av5ja.srv.nintendo.net/schedule/bankara' | |
options = webdriver.ChromeOptions() | |
# if you pass in any argv it will enable this debug mode | |
try: | |
# if there's any second argument then disable headless | |
if sys.argv[1]: | |
print('\033[1mDEBUG MODE ENABLED:\033[0m browser will not run headless. ' \ | |
'it will also stick around. if you get an error next time you run this it\'s ' \ | |
'probably because the chromium didn\'t launch because there is already one open. ok?') | |
# makes the browser hang around for testing | |
options.add_experimental_option("detach", True) | |
except IndexError: | |
# run webdriver in headless mode ideal for screenshots | |
# sometimes screenshots look wrong with this off? | |
options.add_argument("--headless") | |
pass | |
# options below are added somewhat dubiously | |
options.add_argument("--enable-gpu") | |
options.add_argument("--ozone-platform-hint=auto") | |
options.add_argument("--ignore-gpu-blocklist") | |
options.add_argument("--use-vulkan") | |
options.add_argument("--enable-raw-draw") | |
options.add_argument("--enable-gpu-rasterization") | |
options.add_argument("--enable-gpu-compositing") | |
# force scaling, bc on large systems the window is rendered at a larger scale | |
if 'SCALE' in os.environ: | |
options.add_argument("--force-device-scale-factor=" + os.environ['SCALE']) | |
else: | |
# hardcoded for chromium, not other browsers...!!! | |
options.add_argument("--force-device-scale-factor=1") | |
options.add_argument("--password-store=basic") | |
# not necessary to use user data dir if seemingly we cannot find a home directory | |
if 'HOME' in os.environ: | |
# set profile directory for the chromedriver in order to... | |
# ... cache and be faster. uses the template for this path | |
options.add_argument(chromedriver_user_data_dir) | |
else: | |
print('$HOME not found in the environment, not specifying a data ' \ | |
'directory for chromedriver (= no caches and this may be slower than it has to be.)') | |
# the result of which will be None when there is no chromedriver in PATH | |
which_path = which('chromedriver') | |
if which_path: | |
# specify custom service so that the executable path in the PATH can be used | |
# this bypasses the selenium-manager on purpose as I dislike selenium-manager | |
# it may be faster and easier to use the native chromedriver and the version does not really matter | |
service = Service(executable_path=which_path) | |
else: | |
# if chromedriver was not found in path then just use default method | |
if sys.platform == 'linux': | |
print('chromedriver not found in PATH. this is not a big deal it\'s just ' \ | |
'that I try to avoid using selenium-manager in this script, SO, if nothing ' \ | |
'happens for a while or you see it error out, it may be selenium-manager running, ' \ | |
'lol...\nif this causes problems then make sure `which chromedriver` returns something (i.e, it is in your PATH)') | |
service = Service() | |
# initialize webdriver with chrome, other backends are untested | |
# and I don't think they would due to the DevTools execute_cdp_cmd hook for the JS | |
driver = webdriver.Chrome(options=options, service=service) | |
#try: | |
# hardcoded size of these screens: kindle 4/touch, basic 5,7,8,10 | |
width = 600 | |
height = 800 | |
width = os.environ['WIDTH'] if 'WIDTH' in os.environ else width | |
height = os.environ['HEIGHT'] if 'HEIGHT' in os.environ else height | |
driver.set_window_size(width, height) | |
dirname = os.path.split(os.path.abspath(__file__))[0] | |
# JavaScript code to inject into the page | |
js_code = open(os.path.join(dirname, 'splatnet-kindle.js'), 'r').read() | |
# this is also injected later on | |
css_code = open(os.path.join(dirname, 'splatnet-kindle.css'), 'r').read() | |
# inject js on every page load (with driver.execute_script this is intermittent) | |
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {'source': js_code}) | |
# Visit the URL | |
driver.get(url) | |
# inject css into the page via JS | |
''' | |
driver.execute_script(""" | |
var style = document.createElement('style'); | |
style.innerHTML = `"""+ css_code +"""` | |
document.head.appendChild(style); | |
""") | |
''' | |
# inject css, except that the css file is actually a js file | |
# in order to avoid that concatenating so keep that in mind... | |
driver.execute_script(css_code) | |
# set the js to show one rotation ahead on the page | |
# ideally you use this by running the script e.g 10, 5 minutes early | |
# and schedule the kindle to show the image exACTLY when the rotation changes | |
#driver.execute_script('window.oneAhead = true;') | |
# wait for page to load by looking for first schedule entry | |
try: | |
element = WebDriverWait(driver, 30).until( | |
EC.visibility_of_element_located((By.CSS_SELECTOR, 'div[aria-hidden=false] div[class^=Schedule_scheduleContainer] > div[class^=Schedule_]:first-of-type')) | |
# old one | |
#EC.presence_of_element_located((By.CSS_SELECTOR, '[class^=Schedule_scheduleContainer]')) | |
# ISSUE?: this does NOT wait for images and fonts to load | |
# but apparently you would have to set up a timeout script for that so that's a lot more complex | |
# it might be more responsive since selenium polls for elements (idk if it would for a script) | |
) | |
except selenium.common.exceptions.TimeoutException as e: | |
# timeout = likely chance is that fetch failed | |
print('\033[1mtimed out - page may not have loaded, showing you the latest console entries:\033[0m') | |
for log in driver.get_log('browser'): | |
# highlight "net::" in bold, as to point to network related errors | |
# like if any of the given servers cannot be contacted | |
#message_net_highlighted = log['message'].replace('net::', '\033[1mnet::') | |
# actually highlighting this text represents pretty much all server errors | |
message_net_highlighted = log['message'].replace('Failed to load resource', '\033[1mFailed to load resource') | |
# make text red if the log level is severe | |
print(('\033[91m' if log['level'] == 'SEVERE' else '') + message_net_highlighted + '\033[0m') | |
raise e | |
# also, as a hack, select fest if it says "Splatfest Active!" | |
# that div should have a class in all circumstances, we just searched for it | |
# but see if the class is actually fest announcement... | |
if element.get_attribute('class').startswith('Schedule_festAnnouncement'): | |
# actually try to click on fest button and wait for it | |
# the button is invisible so just use js to click it (asynchronous!) | |
# the alternative is to make it visible and then click which would probably be slower tbh because selenium is just making http requests to chromedriver | |
# this should not cause problems since it is waited for anyway | |
#driver.find_element(By.XPATH, '//*[contains(@class, "swipeTabIconFest")]/..').click() | |
driver.execute_script("document.querySelector('[class^=swipeTabIconFest]').parentElement.click()") | |
# now wait for first tab being visible slash animation finishing (SPLATFEST SHOULD BE FIRST!!!! but don't wait long because now we know the page is loaded and gucci) | |
# by the way you can avoid the animation, search for `,skipAnimation` in the webpack bundle | |
# that would be faster but involve way more and this is already a hack as-is | |
WebDriverWait(driver, 3).until( | |
EC.visibility_of_element_located((By.CSS_SELECTOR, '#SwipeTab-panel-0[style="display: block; transform: none;"]')) | |
) | |
# Take a screenshot | |
#driver.save_screenshot("screenshot.png") | |
# TODO: REMOVE THIS 200ms DELAY AND REPLACE WITH SCRIPT THAT WAITS FOR IMAGES&FONTS TO BE READY | |
sleep(0.2) | |
# Take a screenshot as a binary stream | |
screenshot_stream = driver.get_screenshot_as_png() | |
# Convert the binary stream to a Pillow Image object | |
image = Image.open(io.BytesIO(screenshot_stream)) | |
# Convert the image to 4 bit grayscale (monochrome) | |
image = image.convert('L') | |
image = posterize(image, bits=4) | |
fname = 'splatnet-kindle-screenshot-mono' | |
# save monochrome screenshot to a BytesIO stream in memory | |
image_stream = io.BytesIO() | |
# save to system specific tmp folder | |
#local_file = os.path.join(gettempdir(), fname) | |
# eips supports jpeg and png but png averages 75 KB while jpeg averages 210 w/ artifacting | |
image.save(image_stream, format='png') # format='jpeg', quality=95) | |
# show size of image in kb for the sake of logging it | |
stream_size_kb = ceil(sys.getsizeof(image_stream) / 1024) | |
print(f'size of {fname}: {stream_size_kb} KB, now transferring to {os.environ["SSH_HOST"]}...') | |
image.close() | |
# this may not be necessary, it appears to exit with the script | |
driver.quit() | |
# finally, transfer the file to the kindle and run eips -g | |
with ssh.open_sftp() as sftp: | |
# kindles have /dev/shm as a tmpfs and it works ok | |
remote_fname = f'/dev/shm/{fname}' | |
# the file is never removed by anything (but it is overwritten) | |
with sftp.file(remote_fname, 'wb') as remote_file: | |
remote_file.write(image_stream.getvalue()) | |
# -c refreshes the screen so add/remove this if you want that | |
command = f'/usr/sbin/eips -c -g {remote_fname}' # && rm {remote_fname}') | |
# omit stdin and stderr, we will combine the latter | |
_, stdout, _ = ssh.exec_command(command) | |
stdout.channel.set_combine_stderr(True) | |
output = stdout.read() | |
if len(output): | |
print('output:', output.decode()) | |
image_stream.close() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment