master
Simon Bruder 2020-01-16 18:25:19 +00:00
commit 9ebd8f4936
No known key found for this signature in database
GPG Key ID: 6F03E0000CC5B62F
12 changed files with 435 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
wordclock_credentials.py
__pycache__

15
LICENSE.md Normal file
View File

@ -0,0 +1,15 @@
ISC License (ISC)
Copyright 2020 Simon Bruder
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

71
android-touchpad.py Executable file
View File

@ -0,0 +1,71 @@
#!/usr/bin/env python3
# made for samsung galaxy note 4
# only works with stylus
# open phone app, dial *#0*#, choose “Black”
import argparse
import subprocess
from xdo import Xdo
parser = argparse.ArgumentParser(description='Use android phone stylus as input method')
parser.add_argument('--pos', type=str, default='100x100+0x0', help='Position on phone to use in percent (e.g. 40x40+10x10)')
parser.add_argument('--screen', type=str, default='1920x1080+0x0', help='Screen resolution and offset')
parser.add_argument('--phone', type=str, default='12544x7056', help='Phone resolution (not necessarily equal to the phones screen resolution)')
args = parser.parse_args()
screen_res = tuple(map(int, args.screen.split('+')[0].split('x')))
offset = tuple(map(int, args.screen.split('+')[1].split('x')))
phone_res = tuple(map(int, args.phone.split('x')))
limits = [[0, 0], [0, 0]]
limits[0][0] = int(args.pos.split('+')[1].split('x')[0]) / 100 * phone_res[0]
limits[0][1] = limits[0][0] + int(args.pos.split('+')[0].split('x')[0]) / 100 * phone_res[0]
limits[1][0] = int(args.pos.split('+')[1].split('x')[1]) / 100 * phone_res[1]
limits[1][1] = limits[1][0] + int(args.pos.split('+')[0].split('x')[1]) / 100 * phone_res[1]
def real_value(axis, value):
if axis == 1:
value = phone_res[1] - value
# https://stackoverflow.com/a/929107
return int(((value - limits[axis][0]) * screen_res[axis]) / (limits[axis][1] - limits[axis][0]))
update = 0
x = 0
y = 0
pressure = 0
xdo = Xdo()
process = subprocess.Popen(['adb', 'shell', 'getevent', '-q', '-l'], stdout=subprocess.PIPE)
for line in process.stdout:
line = line.decode('utf-8')
line = line.split()
if line[1] != 'EV_ABS':
continue
event = line[2]
value = int('0x' + line[3], 16)
if event == 'ABS_PRESSURE':
if value == 0 and pressure != 0:
xdo.mouse_up(0, 1)
elif pressure == 0:
xdo.mouse_down(0, 1)
pressure = value
# Y and X flipped (landscape)
elif event == 'ABS_Y':
old_x = x
x = real_value(0, value)
if old_x != x:
update += 1
elif event == 'ABS_X':
old_y = y
y = real_value(1, value)
if old_y != y:
update += 1
if update == 2:
update = 0
if 0 <= x < screen_res[0] and 0 <= y < screen_res[1]:
xdo.move_mouse(x + offset[0], y + offset[1])

35
cbz2ebook.sh Executable file
View File

@ -0,0 +1,35 @@
#!/bin/zsh -i
set -e
SIZE="1440x1920" # Kobo Forma
#SIZE="768x1024" # Amazon Kindle Paperwhite
infile="$(realpath $1)"
outfile="$(realpath $2)"
tmpdir=$(mktemp -d)
function cleanup {
rm -rf "$tmpdir"
}
trap cleanup EXIT INT SIGTERM
cd "$tmpdir"
unzip "$infile"
renumber 4 **/*
length=$(ls -1 **/* | wc -l)
position=0
for image in **/*; do
width=$(identify -format "%W" "$image")
height=$(identify -format "%H" "$image")
if (($width > $height)); then
mogrify -resize "$SIZE" -rotate 270 "$image"
else
mogrify -resize "$SIZE" "$image"
fi
position=$(($position + 1))
echo -ne "$(printf '%3s' $((100 * $position / $length))) % "$(printf "%0*d" "$((72 * $position / $length))" 0 | tr '0' '#')'\r'
done
echo
zip "$outfile" **/*
cd -

34
downloaders/comic-valkyrie.py Executable file
View File

@ -0,0 +1,34 @@
#!/usr/bin/env python3
from PIL import Image
from bs4 import BeautifulSoup
from tqdm import tqdm
import re
import requests
import sys
def parse_filename(filename):
split = re.split(':|,|\+|>', filename)
return map(int, split[1:])
def get_image_urls():
url = sys.argv[1]
soup = BeautifulSoup(requests.get(url).text, 'html.parser')
for div in soup.find(id='content').find_all('div'):
yield url + div.get('data-ptimg')
pages = list(get_image_urls())
for metadata_url in tqdm(pages):
image_url = re.sub('\.ptimg\.json$', '.jpg', metadata_url)
ptimg_data = requests.get(metadata_url).json()
image_data = requests.get(image_url, stream=True).raw
scrambled_image = Image.open(image_data)
combined_image = Image.new('RGB', (ptimg_data['views'][0]['width'], ptimg_data['views'][0]['height']))
for from_x, from_y, width, height, to_x, to_y in map(parse_filename, ptimg_data['views'][0]['coords']):
chunk_data = scrambled_image.crop((from_x, from_y, from_x+width, from_y+height))
combined_image.paste(chunk_data, (to_x, to_y))
combined_image.save(image_url.split('/')[-1])

37
downloaders/mangadex.py Executable file
View File

@ -0,0 +1,37 @@
#!/usr/bin/env python3
from tqdm import tqdm
import requests
import shutil
import simplejson.errors
import sys
import os
if len(sys.argv) < 2:
raise Exception('sys.argv[1]')
def parse_chapter_api(data):
if not data['server'].startswith('http'):
data['server'] = 'https://mangadex.org/data/'
base_url = data['server'] + data['hash'] + '/'
return list(map(lambda page: base_url + page, data['page_array']))
for chapter in tqdm(sys.argv[1:]):
chapter_api_response = requests.get(
f'https://mangadex.org/api/?id={chapter}&type=chapter'
)
chapter_number = chapter_api_response.json()['chapter']
os.makedirs(chapter_number, exist_ok=True)
try:
for image in tqdm(parse_chapter_api(chapter_api_response.json())):
r = requests.get(image, stream=True)
with open(chapter_number + '/' + image.split('/')[-1], 'wb') as f:
r.raw.decode_content = True
shutil.copyfileobj(r.raw, f)
except simplejson.errors.JSONDecodeError as e:
if chapter_api_response.status_code != 200:
raise Exception(f'API request failed with HTTP status code {chapter_api_response.status_code}') from None
else:
raise e

54
downloaders/mangarock.py Executable file
View File

@ -0,0 +1,54 @@
#!/usr/bin/env python3
from io import BytesIO
from tqdm import tqdm
import os
import requests
import sys
# Decoding thanks to https://github.com/bake/mri/blob/master/mri.go#L34
XOR_KEY = 101
def get_pages(chapter):
api_response = requests.get(
'https://api.mangarockhd.com/query/web401/pagesv2',
params={
'oid': 'mrs-chapter-' + chapter
}
).json()
for item in api_response['data']:
yield item['url']
def decode_ciphertext(byte):
return byte ^ XOR_KEY
def get_image(url):
ciphertext = requests.get(url).content
size = len(ciphertext) + 7
cleartext = BytesIO()
cleartext.write('RIFF'.encode('ascii'))
cleartext.write(bytes([
size >> 0 & 255,
size >> 8 & 255,
size >> 16 & 255,
size >> 24 & 255
]))
cleartext.write('WEBPVP8'.encode('ascii'))
cleartext.write(bytes(list(map(decode_ciphertext, ciphertext))))
cleartext.seek(0)
return cleartext
requested_chapters = sys.argv[1:]
for chapter_idx, chapter in tqdm(list(enumerate(requested_chapters))):
chapter_dir = str(chapter_idx+1)
os.makedirs(chapter_dir, exist_ok=True)
pages = get_pages(chapter)
for idx, page in tqdm(list(enumerate(pages))):
filename = os.path.join(chapter_dir, f'{idx+1:04}.webp')
if os.path.isfile(filename):
continue
image = get_image(page)
with open(filename, 'wb') as f:
f.write(image.read())

46
encoder-parameters.py Executable file
View File

@ -0,0 +1,46 @@
#!/usr/bin/env python3
import sys
import subprocess
import json
def is_number(value):
numeric_chars = [*map(str, range(10)), '-']
numeric_charcodes = list(map(ord, numeric_chars))
return all(ord(char) in numeric_charcodes for char in value)
def parse_encoder_params(encoder_params):
for param in encoder_params.split(' / '):
if '=' in param:
key, value = param.split('=', 1)
if is_number(value):
value = int(value)
elif is_number(value.replace('.', '', 1)):
value = float(value)
else:
key = param
value = True
yield key, value
def run_mediainfo(file):
cmd = [
'mediainfo',
'--Inform=Video;%Encoded_Library%\\n%Encoded_Library_Settings%',
file
]
process = subprocess.run(cmd, stdout=subprocess.PIPE)
output = process.stdout.decode('utf-8').split('\n')
encoder = output[0]
params = dict(parse_encoder_params(output[1]))
return {
'file': file,
'encoder': encoder,
'params': params
}
info = list(map(run_mediainfo, sys.argv[1:]))
print(json.dumps(info, indent=2, sort_keys=True))

61
huion-tablet.py Executable file
View File

@ -0,0 +1,61 @@
#!/usr/bin/env python3
# Set Huion H430P parameters from saved profiles using xsetwacom
# Driver: https://github.com/DIGImend/digimend-kernel-drivers
#
# Write your config into ~/.config/huion-tablet-yml
# Example:
#
# presets:
# my-preset:
# screen: DP2-2
# area: 0 0 24384 15240
# buttons:
# pad:
# 1: key Ctrl Z
# 2: key Ctrl Shift Z
# 3: key +
# 8: key -
# then run huion-tablet.py my-preset
from subprocess import run
import argparse
import os.path
import yaml
def set_parameter(device, *args):
if device == 'stylus':
device = 'HUION Huion Tablet Pen stylus'
elif device == 'pad':
device = 'HUION Huion Tablet Pad pad'
args = map(str, args)
run(['xsetwacom', 'set', device, *args], check=True)
parser = argparse.ArgumentParser(description='setup huion h430p tablet')
parser.add_argument('--area', type=str, default='')
parser.add_argument('--screen', type=str, default='')
parser.add_argument('preset', metavar='PRESET', type=str, nargs='?', help='a preset')
args = parser.parse_args()
area = '0 0 24384 15240'
screen = ''
if args.preset is not None:
with open(os.path.expanduser('~/.config/huion-tablet.yml'), 'r') as f:
config = yaml.load(f, Loader=yaml.SafeLoader)
preset = config['presets'][args.preset]
area = preset['area']
screen = preset['screen']
if 'buttons' in preset:
for device in preset['buttons']:
for button, mapping in preset['buttons'][device].items():
set_parameter(device, 'button', button, *str(mapping).split(' '))
if args.area != '':
area = args.area
if args.screen != '':
screen = args.screen
area = tuple(map(int, area.split(' ')))
set_parameter('stylus', 'Area', ' '.join(map(str, area)))
set_parameter('stylus', 'MapToOutput', screen)

24
latexpic.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
tmpdir=$(mktemp -d)
cd $tmpdir
cat > doc.tex << EOF
\documentclass[varwidth]{standalone}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{siunitx}
\sisetup{
per-mode=fraction,
quotient-mode=fraction
}
\renewcommand*\familydefault{\sfdefault}
\begin{document}
$(cat $OLDPWD/$1)
\end{document}
EOF
lualatex doc.tex
convert -density 600 doc.pdf $OLDPWD/$2
cd -
rm -rf $tmpdir

43
ssim-stats.py Executable file
View File

@ -0,0 +1,43 @@
#!/usr/bin/env python3
import pandas as pd
import numpy as np
import sys
from pprint import pprint
from tabulate import tabulate
def process_raw_line(line):
fields = line.rstrip().split(' ')
fields = [field.split(':')[-1] for field in fields]
fields[5] = fields[5][1:-1]
fields[0] = int(fields[0])
fields[1:] = list(map(np.float, fields[1:]))
return fields
with open(sys.argv[1], 'r') as f:
data = list(map(process_raw_line, f.readlines()))
df = pd.DataFrame(data)
df.columns = ['frame', 'Y', 'U', 'V', 'All', 'dB']
df['inv'] = [1 - value for value in df['All']]
print(f'Mean overall SSIM: {df["All"].mean()}')
print(f'Median overall SSIM: {df["All"].median()}')
print(f'Frame with worst SSIM: {df.idxmin()["All"]+1}')
print(f'Frame with best SSIM: {df.idxmax()["All"]+1}')
print(tabulate(
[(key, value * 100) for key, value in [
['best', df['All'].max()],
[50, 1 - df['inv'].quantile(0.50)],
[66.6, 1 - df['inv'].quantile(0.666)],
[75, 1 - df['inv'].quantile(0.75)],
[80, 1 - df['inv'].quantile(0.80)],
[90, 1 - df['inv'].quantile(0.90)],
[95, 1 - df['inv'].quantile(0.95)],
[98, 1 - df['inv'].quantile(0.98)],
[99, 1 - df['inv'].quantile(0.99)],
[99.9, 1 - df['inv'].quantile(0.999)],
[100, df['All'].min()]
]],
headers=['% Frames', '≥ SSIM']
))

13
wordclock.py Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env python3
import sys
import paho.mqtt.client as mqtt
import wordclock_credentials as creds
client = mqtt.Client('wordclock.py')
client.username_pw_set(creds.USER, creds.PASSWORD)
client.connect(creds.MQTT_HOST, 1883, 60)
client.publish('wordclock/color/red', sys.argv[2], retain=True)
client.publish('wordclock/color/green', sys.argv[1], retain=True)
client.publish('wordclock/color/blue', sys.argv[3], retain=True)