init
This commit is contained in:
commit
9ebd8f4936
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
wordclock_credentials.py
|
||||||
|
__pycache__
|
15
LICENSE.md
Normal file
15
LICENSE.md
Normal 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
71
android-touchpad.py
Executable 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 phone’s 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
35
cbz2ebook.sh
Executable 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
34
downloaders/comic-valkyrie.py
Executable 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
37
downloaders/mangadex.py
Executable 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
54
downloaders/mangarock.py
Executable 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
46
encoder-parameters.py
Executable 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
61
huion-tablet.py
Executable 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
24
latexpic.sh
Executable 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
43
ssim-stats.py
Executable 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
13
wordclock.py
Executable 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)
|
Loading…
Reference in a new issue