commit 9ebd8f49365bb7e097c466cd2037388d1346cbec Author: Simon Bruder Date: Thu Jan 16 18:25:19 2020 +0000 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79d81df --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +wordclock_credentials.py +__pycache__ diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..62e8fd6 --- /dev/null +++ b/LICENSE.md @@ -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. diff --git a/android-touchpad.py b/android-touchpad.py new file mode 100755 index 0000000..b81b833 --- /dev/null +++ b/android-touchpad.py @@ -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]) diff --git a/cbz2ebook.sh b/cbz2ebook.sh new file mode 100755 index 0000000..b083e77 --- /dev/null +++ b/cbz2ebook.sh @@ -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 - diff --git a/downloaders/comic-valkyrie.py b/downloaders/comic-valkyrie.py new file mode 100755 index 0000000..2c6ae42 --- /dev/null +++ b/downloaders/comic-valkyrie.py @@ -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]) diff --git a/downloaders/mangadex.py b/downloaders/mangadex.py new file mode 100755 index 0000000..b9d6c78 --- /dev/null +++ b/downloaders/mangadex.py @@ -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 diff --git a/downloaders/mangarock.py b/downloaders/mangarock.py new file mode 100755 index 0000000..81b02b7 --- /dev/null +++ b/downloaders/mangarock.py @@ -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()) diff --git a/encoder-parameters.py b/encoder-parameters.py new file mode 100755 index 0000000..5500696 --- /dev/null +++ b/encoder-parameters.py @@ -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)) diff --git a/huion-tablet.py b/huion-tablet.py new file mode 100755 index 0000000..7da22b2 --- /dev/null +++ b/huion-tablet.py @@ -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) diff --git a/latexpic.sh b/latexpic.sh new file mode 100755 index 0000000..558ab72 --- /dev/null +++ b/latexpic.sh @@ -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 diff --git a/ssim-stats.py b/ssim-stats.py new file mode 100755 index 0000000..b682bab --- /dev/null +++ b/ssim-stats.py @@ -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'] +)) diff --git a/wordclock.py b/wordclock.py new file mode 100755 index 0000000..13fa40f --- /dev/null +++ b/wordclock.py @@ -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)