From f44e46aab587d5ea23dd4048dff177867c2cc672 Mon Sep 17 00:00:00 2001 From: Ethan Dalool Date: Tue, 11 Jun 2019 22:41:31 -0700 Subject: [PATCH] Initial commit. --- README.md | 6 + allexecutables.py | 15 +++ bitrate_chart.py | 50 ++++++++ brename.py | 112 +++++++++++++++++ breplace.py | 24 ++++ clipboard.py | 14 +++ contentreplace.py | 65 ++++++++++ crlf.py | 31 +++++ crop.py | 21 ++++ do_cmd.py | 7 ++ drawn_quartered.py | 73 +++++++++++ eval.py | 28 +++++ filenameorderedrandomness.pyw | 26 ++++ filenamescramble.py | 23 ++++ filenamescrambleint.pyw | 21 ++++ fileprefix.py | 112 +++++++++++++++++ filepull.py | 57 +++++++++ fluidsynth.bat | 5 + forline.py | 14 +++ getcrx.py | 142 +++++++++++++++++++++ gif_mp4.bat | 3 + head.py | 16 +++ hexdump.py | 63 ++++++++++ hexpng.py | 46 +++++++ hms_s.py | 57 +++++++++ kbps.py | 75 ++++++++++++ linenumbers.py | 12 ++ lowercase.py | 7 ++ mp3slice.py | 171 ++++++++++++++++++++++++++ randomfile.py | 46 +++++++ rejpg.py | 48 ++++++++ repeat.py | 16 +++ replace.py | 12 ++ repr.py | 5 + resize.py | 52 ++++++++ reverse.py | 7 ++ sdate.py | 25 ++++ search.py | 224 ++++++++++++++++++++++++++++++++++ sleep.py | 6 + sorted.py | 19 +++ subtitle_shift.py | 67 ++++++++++ subtitle_shift_pointsync.py | 197 ++++++++++++++++++++++++++++++ timestampfilename.pyw | 25 ++++ touch.py | 24 ++++ unique.py | 20 +++ zerofile.py | 36 ++++++ 46 files changed, 2125 insertions(+) create mode 100644 README.md create mode 100644 allexecutables.py create mode 100644 bitrate_chart.py create mode 100644 brename.py create mode 100644 breplace.py create mode 100644 clipboard.py create mode 100644 contentreplace.py create mode 100644 crlf.py create mode 100644 crop.py create mode 100644 do_cmd.py create mode 100644 drawn_quartered.py create mode 100644 eval.py create mode 100644 filenameorderedrandomness.pyw create mode 100644 filenamescramble.py create mode 100644 filenamescrambleint.pyw create mode 100644 fileprefix.py create mode 100644 filepull.py create mode 100644 fluidsynth.bat create mode 100644 forline.py create mode 100644 getcrx.py create mode 100644 gif_mp4.bat create mode 100644 head.py create mode 100644 hexdump.py create mode 100644 hexpng.py create mode 100644 hms_s.py create mode 100644 kbps.py create mode 100644 linenumbers.py create mode 100644 lowercase.py create mode 100644 mp3slice.py create mode 100644 randomfile.py create mode 100644 rejpg.py create mode 100644 repeat.py create mode 100644 replace.py create mode 100644 repr.py create mode 100644 resize.py create mode 100644 reverse.py create mode 100644 sdate.py create mode 100644 search.py create mode 100644 sleep.py create mode 100644 sorted.py create mode 100644 subtitle_shift.py create mode 100644 subtitle_shift_pointsync.py create mode 100644 timestampfilename.pyw create mode 100644 touch.py create mode 100644 unique.py create mode 100644 zerofile.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..8cfb0e3 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +cmd +=== + +This is a collection of programs that I use from the command line. + +Wherever you download this repository, don't forget to add that directory to your PATH environment variable. diff --git a/allexecutables.py b/allexecutables.py new file mode 100644 index 0000000..1de07c3 --- /dev/null +++ b/allexecutables.py @@ -0,0 +1,15 @@ +import os + +paths = os.getenv('PATH').split(';') +paths = [p for p in paths if os.path.exists(p)] + +extensions = os.getenv('PATHEXT').split(';') +extensions = [e.lower() for e in extensions] +print('Extensions according to PATHEXT:', extensions) + +for path in paths: + print(path) + files = os.listdir(path) + files = [f for f in files if any(f.lower().endswith(e) for e in extensions)] + files = [' ' + f for f in files] + print('\n'.join(files)) diff --git a/bitrate_chart.py b/bitrate_chart.py new file mode 100644 index 0000000..72581b3 --- /dev/null +++ b/bitrate_chart.py @@ -0,0 +1,50 @@ +''' + bitrate | 01 | 1:00 | 30:00 | 1:00:00 | 1:30:00 | 2:00:00 + -: | -: | -: | -: | -: | -: | -: + 128 kbps | 16.000 KiB | 960.000 KiB | 28.125 MiB | 56.250 MiB | 84.375 MiB | 112.500 MiB + 256 kbps | 32.000 KiB | 1.875 MiB | 56.250 MiB | 112.500 MiB | 168.750 MiB | 225.000 MiB + 320 kbps | 40.000 KiB | 2.344 MiB | 70.312 MiB | 140.625 MiB | 210.938 MiB | 281.250 MiB + 500 kbps | 62.500 KiB | 3.662 MiB | 109.863 MiB | 219.727 MiB | 329.590 MiB | 439.453 MiB + 640 kbps | 80.000 KiB | 4.688 MiB | 140.625 MiB | 281.250 MiB | 421.875 MiB | 562.500 MiB + 738 kbps | 92.250 KiB | 5.405 MiB | 162.158 MiB | 324.316 MiB | 486.475 MiB | 648.633 MiB + 1024 kbps | 128.000 KiB | 7.500 MiB | 225.000 MiB | 450.000 MiB | 675.000 MiB | 900.000 MiB + 2048 kbps | 256.000 KiB | 15.000 MiB | 450.000 MiB | 900.000 MiB | 1.318 GiB | 1.758 GiB + 2330 kbps | 291.271 KiB | 17.067 MiB | 512.000 MiB | 1.000 GiB | 1.500 GiB | 2.000 GiB + 3072 kbps | 384.000 KiB | 22.500 MiB | 675.000 MiB | 1.318 GiB | 1.978 GiB | 2.637 GiB + 4096 kbps | 512.000 KiB | 30.000 MiB | 900.000 MiB | 1.758 GiB | 2.637 GiB | 3.516 GiB + 4660 kbps | 582.543 KiB | 34.133 MiB | 1.000 GiB | 2.000 GiB | 3.000 GiB | 4.000 GiB + 6144 kbps | 768.000 KiB | 45.000 MiB | 1.318 GiB | 2.637 GiB | 3.955 GiB | 5.273 GiB + 8192 kbps | 1.000 MiB | 60.000 MiB | 1.758 GiB | 3.516 GiB | 5.273 GiB | 7.031 GiB +12288 kbps | 1.500 MiB | 90.000 MiB | 2.637 GiB | 5.273 GiB | 7.910 GiB | 10.547 GiB +16384 kbps | 2.000 MiB | 120.000 MiB | 3.516 GiB | 7.031 GiB | 10.547 GiB | 14.062 GiB +''' +import sys +import kbps + +from voussoirkit import bytestring + +times = ['01', '1:00', '30:00', '1:00:00', '1:30:00', '2:00:00'] +rates = [128, 256, 320, 500, 640, 738, 1024, 2048, 3072, 4096, 6144, 8192, 12288, 16384, 2330.17, 4660.34] + +times.sort(key=lambda x: kbps.hms_s(x)) +rates.sort() + +table = [] +table.append('bitrate | ' + ' | '.join(times)) +table.append('-: | ' * (len(times)+1)) +for r in rates: + l = [] + l.append('%d kbps' % r) + for t in times: + l.append(kbps.kbps(time=t, kbps=r)) + l = ' | '.join(l) + table.append(l) + +#print('\n'.join(table)) +columns = [[b.strip() for b in a] for a in zip(*[x.split('|') for x in table])] +for (index, column) in enumerate(columns): + width = max((len(x) for x in column)) + columns[index] = [x.rjust(width, ' ') for x in column] +table = [' | '.join(a) for a in zip(*columns)] +#print(columns) +print('\n'.join(table)) diff --git a/brename.py b/brename.py new file mode 100644 index 0000000..3706a7b --- /dev/null +++ b/brename.py @@ -0,0 +1,112 @@ +''' +Batch rename files by providing a string to be `eval`ed, using variable `x` as +the current filename. +Yes I know this is weird, but for certain tasks it's just too quick and easy to pass up. + +For example: + +Prefix all the files: +brename.py "'Test_' + x" + +Keep the first word and extension: +brename.py "(x.split(' ')[0] + '.' + x.split('.')[-1]) if ' ' in x else x" +''' +import argparse +import os +import random +import re +import sys + +from voussoirkit import safeprint + +dot = '.' +quote = '"' +apostrophe = "'" +space = ' ' + +def brename(transformation, autoyes=False): + old = os.listdir() + new = [] + for (index, x) in enumerate(old): + (noext, ext) = os.path.splitext(x) + x = eval(transformation) + new.append(x) + pairs = [] + for (x, y) in zip(old, new): + if x == y: + continue + pairs.append((x, y)) + + if not loop(pairs, dry=True): + print('Nothing to replace') + return + + ok = autoyes + if not ok: + print('Is this correct? y/n') + ok = input('>').lower() in ('y', 'yes', 'yeehaw') + + if ok: + loop(pairs, dry=False) + +def excise(s, mark_left, mark_right): + ''' + Remove the text between the left and right landmarks, inclusive, returning + the rest of the text. + + excise('What a wonderful day [soundtrack].mp3', ' [', ']') -> + returns 'What a wonderful day.mp3' + ''' + return s.split(mark_left)[0] + s.split(mark_right)[-1] + +def longest_length(li): + longest = 0 + for item in li: + longest = max(longest, len(item)) + return longest + +def loop(pairs, dry=False): + has_content = False + for (x, y) in pairs: + if dry: + line = '{old}\n{new}\n' + line = line.format(old=x, new=y) + #print(line.encode('utf-8')) + safeprint.safeprint(line) + has_content = True + else: + os.rename(x, y) + return has_content + +def title(text): + (text, extension) = os.path.splitext(text) + text = text.title() + if ' ' in text: + (first, rest) = text.split(' ', 1) + else: + (first, rest) = (text, '') + rest = ' %s ' % rest + for article in ['The', 'A', 'An', 'At', 'To', 'In', 'Of', 'From', 'And']: + article = ' %s ' % article + rest = rest.replace(article, article.lower()) + rest = rest.strip() + if rest != '': + rest = ' ' + rest + text = first + rest + extension + return text + +def brename_argparse(args): + brename(args.transformation, autoyes=args.autoyes) + +def main(argv): + parser = argparse.ArgumentParser(description=__doc__) + + parser.add_argument('transformation', help='python command using x as variable name') + parser.add_argument('-y', '--yes', dest='autoyes', action='store_true', help='accept results without confirming') + parser.set_defaults(func=brename_argparse) + + args = parser.parse_args(argv) + args.func(args) + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) diff --git a/breplace.py b/breplace.py new file mode 100644 index 0000000..e65de2d --- /dev/null +++ b/breplace.py @@ -0,0 +1,24 @@ +''' +Batch rename files by replacing the first argument with the second. +''' +import argparse +import brename +import sys + +def breplace_argparse(args): + command = f'x.replace("{args.replace_from}", "{args.replace_to}")' + brename.brename(command, autoyes=args.autoyes) + +def main(argv): + parser = argparse.ArgumentParser(__doc__) + + parser.add_argument('replace_from') + parser.add_argument('replace_to') + parser.add_argument('-y', '--yes', dest='autoyes', action='store_true', help='accept results without confirming') + parser.set_defaults(func=breplace_argparse) + + args = parser.parse_args(argv) + args.func(args) + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) diff --git a/clipboard.py b/clipboard.py new file mode 100644 index 0000000..2355a6e --- /dev/null +++ b/clipboard.py @@ -0,0 +1,14 @@ +''' +Dump the clipboard to stdout. I use this for redirecting to files. +''' +import pyperclip +import sys + +if len(sys.argv) > 1: + from voussoirkit import clipext + stuff = clipext.resolve(sys.argv[1]) + pyperclip.copy(stuff) +else: + text = pyperclip.paste() + text = text.replace('\r', '') + print(text) diff --git a/contentreplace.py b/contentreplace.py new file mode 100644 index 0000000..fcdeb18 --- /dev/null +++ b/contentreplace.py @@ -0,0 +1,65 @@ +import argparse +import codecs +import glob +import sys +import pyperclip + + +def contentreplace(filename, replace_from, replace_to, autoyes=False): + f = open(filename, 'r', encoding='utf-8') + with f: + content = f.read() + + occurances = content.count(replace_from) + + print(f'{filename}: Found {occurances} occurences.') + if occurances == 0: + return + + permission = autoyes or (input('Replace? ').lower() in ('y', 'yes')) + if not permission: + return + + content = content.replace(replace_from, replace_to) + + f = open(filename, 'w', encoding='utf-8') + with f: + f.write(content) + +def contentreplace_argparse(args): + filenames = glob.glob(args.filename_glob) + + if args.clip_prompt: + replace_from = input('Ready from') + if not replace_from: + replace_from = pyperclip.paste() + replace_to = input('Ready to') + if not replace_to: + replace_to = pyperclip.paste() + else: + replace_from = codecs.decode(args.replace_from, 'unicode_escape') + replace_to = codecs.decode(args.replace_to, 'unicode_escape') + + for filename in filenames: + contentreplace( + filename, + replace_from, + replace_to, + autoyes=args.autoyes, + ) + +def main(argv): + parser = argparse.ArgumentParser(description=__doc__) + + parser.add_argument('filename_glob') + parser.add_argument('replace_from') + parser.add_argument('replace_to') + parser.add_argument('-y', '--yes', dest='autoyes', action='store_true', help='accept results without confirming') + parser.add_argument('--clip_prompt', dest='clip_prompt', action='store_true') + parser.set_defaults(func=contentreplace_argparse) + + args = parser.parse_args(argv) + args.func(args) + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) diff --git a/crlf.py b/crlf.py new file mode 100644 index 0000000..2155e82 --- /dev/null +++ b/crlf.py @@ -0,0 +1,31 @@ +''' +Convert LF line endings to CRLF. +''' + +import glob +import sys + +from voussoirkit import pipeable + + +CR = b'\x0D' +LF = b'\x0A' +CRLF = CR + LF + +def crlf(filename): + with open(filename, 'rb') as handle: + content = handle.read() + content = content.replace(CRLF, LF) + content = content.replace(LF, CRLF) + with open(filename, 'wb') as handle: + handle.write(content) + +def main(args): + for line in pipeable.go(args, strip=True, skip_blank=True): + for filename in glob.glob(line): + pipeable.output(filename) + crlf(filename) + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) + diff --git a/crop.py b/crop.py new file mode 100644 index 0000000..60fbebe --- /dev/null +++ b/crop.py @@ -0,0 +1,21 @@ +import os +import sys +from PIL import Image + +filename = sys.argv[1] +(name, extension) = os.path.splitext(filename) +newname = '%s_cropped%s' % (name, extension) + +crops = sys.argv[2:] +crops = ' '.join(crops) +crops = crops.replace(',', ' ') +crops = crops.replace(' ', ' ') +crops = crops.split(' ') +crops = [int(x) for x in crops] +crops = list(crops) +print(crops) +i = Image.open(filename) +if len(crops) == 2: + crops.extend(i.size) +i = i.crop(crops) +i.save(newname, quality=100) diff --git a/do_cmd.py b/do_cmd.py new file mode 100644 index 0000000..10e573b --- /dev/null +++ b/do_cmd.py @@ -0,0 +1,7 @@ +import os + +from voussoirkit import pipeable + + +for line in pipeable.go(): + os.system(line) diff --git a/drawn_quartered.py b/drawn_quartered.py new file mode 100644 index 0000000..1e22412 --- /dev/null +++ b/drawn_quartered.py @@ -0,0 +1,73 @@ +''' +This script takes an image and splits it up into pieces as separate files. + +drawn_quartered test.jpg --width 2 --height 2 +drawn_quartered test.jpg outputname.jpg --width 3 --height 4 +''' + +import argparse +import math +import os +import PIL.Image +import sys + +from voussoirkit import pathclass + +def drawquarter(image, width=2, height=2): + pieces = [] + (image_width, image_height) = image.size + step_x = image_width / width + step_y = image_height / height + if (step_x != int(step_x)): + print('Warning: Imperfect x', step_x) + if (step_y != int(step_y)): + print('Warning: Imperfect y', step_y) + step_x = math.ceil(step_x) + step_y = math.ceil(step_y) + for y in range(height): + end_y = y + 1 + for x in range(width): + end_x = x + 1 + coords = (step_x * x, step_y * y, step_x * end_x, step_y * end_y) + piece = image.crop(coords) + pieces.append(piece) + return pieces + +def drawquarter_argparse(args): + image = PIL.Image.open(args.input_filename) + + if args.output_filename is not None: + output_filename = args.output_filename + else: + output_filename = args.input_filename + + output_path = pathclass.Path(output_filename) + output_directory = output_path.parent + os.makedirs(output_directory.absolute_path, exist_ok=True) + output_filename_format = output_path.basename + output_filename_format = output_filename_format.rsplit('.', 1)[0] + output_filename_format += '_%dx%d_{ycoord}-{xcoord}.' % (args.width, args.height) + output_filename_format += args.input_filename.rsplit('.', 1)[1] + + pieces = drawquarter(image, width=args.width, height=args.height) + for (index, piece) in enumerate(pieces): + (ycoord, xcoord) = divmod(index, args.height) + output_filename = output_filename_format.format(xcoord=xcoord, ycoord=ycoord) + output_filename = output_directory.with_child(output_filename) + print(output_filename.relative_path) + piece.save(output_filename.absolute_path) + +def main(argv): + parser = argparse.ArgumentParser() + + parser.add_argument('input_filename') + parser.add_argument('output_filename', nargs='?', default=None) + parser.add_argument('--width', dest='width', type=int, default=2) + parser.add_argument('--height', dest='height', type=int, default=2) + parser.set_defaults(func=drawquarter_argparse) + + args = parser.parse_args(argv) + args.func(args) + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/eval.py b/eval.py new file mode 100644 index 0000000..3928aa3 --- /dev/null +++ b/eval.py @@ -0,0 +1,28 @@ +import glob +import math +import os +import random +import re +import sys + +from voussoirkit import pipeable + + +lines = pipeable.input(sys.argv[1]) +pattern = sys.argv[2] + +def quote(s): + return '"%s"' % s + +def apostrophe(s): + return "'%s'" % s + +def random_hex(length=12): + randbytes = os.urandom(math.ceil(length / 2)) + token = ''.join('{:02x}'.format(x) for x in randbytes) + token = token[:length] + return token + +for line in lines: + x = line + pipeable.output(eval(pattern)) diff --git a/filenameorderedrandomness.pyw b/filenameorderedrandomness.pyw new file mode 100644 index 0000000..fb4fc1b --- /dev/null +++ b/filenameorderedrandomness.pyw @@ -0,0 +1,26 @@ +''' +Drag multiple files on top of this .py file. The first file will have its +name randomly scrambled into 12 digits. The others will increment that number b +1. +''' + +print('hi') +import os +import random +import string +import sys + +argv = sys.argv[1:] +print(''.join(c for c in argv if c in string.printable)) + +randname = [random.choice(string.digits) for x in range(12)] +randname = int(''.join(randname)) +for filepath in argv: + folder = os.path.dirname(filepath) + basename = os.path.basename(filepath) + extension = os.path.splitext(basename)[1] + newname = str(randname).rjust(12, '0') + randname += 1 + newname = '%s\\%s%s' % (folder, newname, extension) + os.rename(filepath, newname) + print('%s -> %s' % (filepath, newname)) diff --git a/filenamescramble.py b/filenamescramble.py new file mode 100644 index 0000000..86f4394 --- /dev/null +++ b/filenamescramble.py @@ -0,0 +1,23 @@ +''' +Drag a file on top of this .py file, and it will have its +filename scrambled into a combination of 16 upper and lowercase +letters. +''' + +import os +import random +import string +import sys + +argv = sys.argv[1:] +print(''.join(c for c in argv if c in string.printable)) +for filepath in argv: + folder = os.path.dirname(filepath) + basename = os.path.basename(filepath) + extension = os.path.splitext(basename)[1] + newname = [random.choice(string.ascii_lowercase) for x in range(9)] + newname = ''.join(newname) + newname = newname + extension + newname = os.path.join(folder, newname) + #os.rename(filepath, newname) + print('%s -> %s' % (filepath, newname)) diff --git a/filenamescrambleint.pyw b/filenamescrambleint.pyw new file mode 100644 index 0000000..21e82c0 --- /dev/null +++ b/filenamescrambleint.pyw @@ -0,0 +1,21 @@ +''' +Drag a file on top of this .py file, and it will have its +filename scrambled into a combination of 12 digits. +''' + +import os +import random +import string +import sys + +argv = sys.argv[1:] +print(''.join(c for c in argv if c in string.printable)) +for filepath in argv: + folder = os.path.dirname(filepath) + basename = os.path.basename(filepath) + extension = os.path.splitext(basename)[1] + newname = [random.choice(string.digits) for x in range(12)] + newname = ''.join(newname) + newname = '%s\\%s%s' % (folder, newname, extension) + os.rename(filepath, newname) + print('%s -> %s' % (filepath, newname)) diff --git a/fileprefix.py b/fileprefix.py new file mode 100644 index 0000000..c8e5f4c --- /dev/null +++ b/fileprefix.py @@ -0,0 +1,112 @@ +''' +When you run this file from the commandline given a single argument, all +of the files in the current working directory will be renamed in the format +{argument}_{count} where argument is your cmd input and count is a zero-padded +integer that counts each file in the folder. +''' + +import argparse +import os +import random +import string +import re +import sys + +from voussoirkit import pathclass +from voussoirkit import safeprint + +IGNORE_EXTENSIONS = ['py', 'lnk', 'ini'] + + +def natural_sorter(x): + ''' + http://stackoverflow.com/a/11150413 + ''' + convert = lambda text: int(text) if text.isdigit() else text.lower() + alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)] + return alphanum_key(x) + +def fileprefix( + prefix='', + sep='_', + ctime=False, + autoyes=False, + ): + current_directory = pathclass.Path('.') + + prefix = prefix.strip() + if prefix == ':': + prefix = current_directory.basename + ' - ' + elif prefix != '': + prefix += sep + + filepaths = current_directory.listdir() + filepaths = [f for f in filepaths if f.is_file] + filepaths = [f for f in filepaths if f.extension.lower() not in IGNORE_EXTENSIONS] + + try: + pyfile = pathclass.Path(__file__) + filepaths.remove(pyfile) + except ValueError: + pass + + # trust me on this. + zeropadding = len(str(len(filepaths))) + zeropadding = max(2, zeropadding) + zeropadding = str(zeropadding) + + format = '{{prefix}}{{index:0{pad}d}}{{extension}}'.format(pad=zeropadding) + + if ctime: + print('Sorting by time') + filepaths.sort(key=lambda x: x.stat.st_ctime) + else: + print('Sorting by name') + filepaths.sort(key=lambda x: natural_sorter(x.basename)) + + rename_pairs = [] + + for (index, filepath) in enumerate(filepaths): + extension = filepath.extension + if extension != '': + extension = '.' + extension + + newname = format.format(prefix=prefix, index=index, extension=extension) + if filepath.basename != newname: + rename_pairs.append((filepath.absolute_path, newname)) + for (oldname, newname) in rename_pairs: + message = f'{oldname} -> {newname}' + safeprint.safeprint(message) + + ok = autoyes + if not ok: + print('Is this correct? y/n') + ok = input('>').lower() in ('y', 'yes', 'yeehaw') + + if ok: + for (oldname, newname) in rename_pairs: + os.rename(oldname, newname) + + +def fileprefix_argparse(args): + return fileprefix( + prefix=args.prefix, + sep=args.sep, + ctime=args.ctime, + autoyes=args.autoyes, + ) + +def main(argv): + parser = argparse.ArgumentParser() + + parser.add_argument('prefix', nargs='?', default='') + parser.add_argument('--sep', dest='sep', default=' ', help='the character between the prefix and remainder') + parser.add_argument('--ctime', dest='ctime', action='store_true', help='sort by ctime instead of filename') + parser.add_argument('-y', '--yes', dest='autoyes', action='store_true', help='accept results without confirming') + parser.set_defaults(func=fileprefix_argparse) + + args = parser.parse_args(argv) + args.func(args) + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/filepull.py b/filepull.py new file mode 100644 index 0000000..f8adee4 --- /dev/null +++ b/filepull.py @@ -0,0 +1,57 @@ +''' +Pull all of the files in nested directories into the current directory. +''' +import argparse +import os +import sys + +from voussoirkit import spinal + +def filepull(pull_from='.', autoyes=False): + files = list(spinal.walk_generator(pull_from)) + cwd = os.getcwd() + files = [f for f in files if os.path.split(f.absolute_path)[0] != cwd] + + if len(files) == 0: + print('No files to move') + return + + duplicate_count = {} + for f in files: + basename = f.basename + duplicate_count.setdefault(basename, []) + duplicate_count[basename].append(f.absolute_path) + + duplicates = ['\n'.join(sorted(copies)) for (basename, copies) in duplicate_count.items() if len(copies) > 1] + duplicates = sorted(duplicates) + if len(duplicates) > 0: + raise Exception('duplicate names:\n' + '\n'.join(duplicates)) + + for f in files: + print(f.basename) + + ok = autoyes + if not ok: + print('Move %d files?' % len(files)) + ok = input('> ').lower() in ['y', 'yes'] + + if ok: + for f in files: + local = os.path.join('.', f.basename) + os.rename(f.absolute_path, local) + +def filepull_argparse(args): + filepull(pull_from=args.pull_from, autoyes=args.autoyes) + +def main(argv): + parser = argparse.ArgumentParser() + + parser.add_argument('pull_from', nargs='?', default='.') + parser.add_argument('-y', '--yes', dest='autoyes', action='store_true') + parser.set_defaults(func=filepull_argparse) + + args = parser.parse_args(argv) + args.func(args) + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/fluidsynth.bat b/fluidsynth.bat new file mode 100644 index 0000000..301fd6f --- /dev/null +++ b/fluidsynth.bat @@ -0,0 +1,5 @@ +@echo off +set filename=%1 +set filename=%filename:.mid=.wav% + +D:\software\fluidsynth\fluidsynth.exe --gain 1 -F %filename% D:\software\fluidsynth\Scc1t2.sf2 %1% \ No newline at end of file diff --git a/forline.py b/forline.py new file mode 100644 index 0000000..e21fe63 --- /dev/null +++ b/forline.py @@ -0,0 +1,14 @@ +import os +import sys + +from voussoirkit import clipext + +text = sys.argv[1] +command = sys.argv[2:] +command = ['"%s"' % x if (' ' in x or x == '%x') else x for x in command] +command = ' '.join(command) +text = clipext.resolve(text) + +for line in text.splitlines(): + thiscomm = command.replace('%x', line) + os.system(thiscomm) diff --git a/getcrx.py b/getcrx.py new file mode 100644 index 0000000..5289570 --- /dev/null +++ b/getcrx.py @@ -0,0 +1,142 @@ +import argparse +import io +import json +import os +import requests +import sys +import time +import traceback +import zipfile + +from voussoirkit import clipext + +FILENAME_BADCHARS = '\\/:*?<>|"' + +WEBSTORE_URL = 'https://chrome.google.com/webstore/detail/x/{extension_id}' +CRX_URL = 'https://clients2.google.com/service/update2/crx?response=redirect&prodversion=59.0&x=id%3D{extension_id}%26installsource%3Dondemand%26uc' + +def sanitize_filename(name): + for c in FILENAME_BADCHARS: + name = name.replace(c, '-') + return name + +def prompt_permission(prompt): + answer = input(prompt) + return answer.lower() in {'yes', 'y'} + +def get_webstore_name_version(extension_id): + url = WEBSTORE_URL.format(extension_id=extension_id) + response = requests.get(url) + try: + name = response.text + name = name.split('meta property="og:title" content="')[1] + name = name.split('"')[0] + except IndexError: + name = None + + try: + version = response.text + version = version.split('meta itemprop="version" content="')[1] + version = version.split('"')[0] + except IndexError: + version = None + + return (name, version) + +def get_crx_name_version(crx_bytes): + crx_handle = io.BytesIO(crx_bytes) + crx_archive = zipfile.ZipFile(crx_handle) + manifest = json.loads(crx_archive.read('manifest.json')) + name = manifest.get('name', None) + version = manifest.get('version', None) + return (name, version) + +def getcrx(extension_id, auto_overwrite=None): + url = CRX_URL.format(extension_id=extension_id) + response = requests.get(url) + response.raise_for_status() + + (name, version) = get_webstore_name_version(extension_id) + if name is None or version is None: + (crx_name, crx_ver) = get_crx_name_version(response.content) + name = name or crx_name + version = version or crx_version + + name = name or extension_id + version = version or time.strftime('%Y%m%d') + + version = version or response.url.split('/')[-1] + + crx_filename = '{name} ({id}) [{version}]' + crx_filename = crx_filename.format( + name=name, + id=extension_id, + version=version, + ) + + if not crx_filename.endswith('.crx'): + crx_filename += '.crx' + + crx_filename = sanitize_filename(crx_filename) + if os.path.isfile(crx_filename): + if auto_overwrite is None: + message = '"%s" already exists. Overwrite?' % crx_filename + permission = prompt_permission(message) + else: + permission = False + else: + permission = True + + if permission: + crx_handle = open(crx_filename, 'wb') + crx_handle.write(response.content) + print('Downloaded "%s".' % crx_filename) + +def getcrx_argparse(args): + extension_ids = [] + + if len(args.extension_ids) == 1: + extension_ids.extend(clipext.resolve(args.extension_ids[0], split_lines=True)) + + elif args.extension_ids: + extension_ids.extend(args.extension_ids) + + if args.file: + with open(args.file, 'r') as handle: + lines = handle.readlines() + extension_ids.extend(lines) + + extension_ids = [x.split('/')[-1].strip() for x in extension_ids] + + if args.overwrite and not args.dont_overwrite: + auto_overwrite = True + elif args.dont_overwrite and not args.overwrite: + auto_overwrite = False + else: + auto_overwrite = None + + for extension_id in extension_ids: + try: + getcrx(extension_id, auto_overwrite=auto_overwrite) + except Exception: + if args.fail_early: + raise + else: + traceback.print_exc() + print('Resuming...') + +def main(argv): + parser = argparse.ArgumentParser() + + parser.add_argument('extension_ids', nargs='*', default=None) + parser.add_argument('--file', dest='file', default=None) + parser.add_argument('--fail_early', dest='fail_early', action='store_true') + parser.add_argument('--overwrite', dest='overwrite', action='store_true') + parser.add_argument('--dont_overwrite', dest='dont_overwrite', action='store_true') + parser.set_defaults(func=getcrx_argparse) + + args = parser.parse_args(argv) + args.func(args) + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/gif_mp4.bat b/gif_mp4.bat new file mode 100644 index 0000000..f26956d --- /dev/null +++ b/gif_mp4.bat @@ -0,0 +1,3 @@ +set basename=%~n1 +set newname="%basename%.mp4" +ffmpeg -i %1 -pix_fmt yuv420p -preset placebo -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" %newname% diff --git a/head.py b/head.py new file mode 100644 index 0000000..cb747c7 --- /dev/null +++ b/head.py @@ -0,0 +1,16 @@ +''' +Perform a HEAD request and print the results. +''' +import sys +import json +import requests + +from voussoirkit import clipext + +urls = clipext.resolve(sys.argv[1], split_lines=True) +for url in urls: + page = requests.head(url) + headers = dict(page.headers) + headers = json.dumps(headers, indent=4, sort_keys=True) + print(page) + print(headers) diff --git a/hexdump.py b/hexdump.py new file mode 100644 index 0000000..afa5451 --- /dev/null +++ b/hexdump.py @@ -0,0 +1,63 @@ +import argparse +import sys + +DEFAULT_WIDTH = 16 + +def hexy(i, width=0): + return hex(i)[2:].upper().rjust(width, '0') + +def hexdump(handle, width=DEFAULT_WIDTH, ellipse=False, start=None, end=None): + if start is not None: + start = int(start, 16) + handle.seek(start) + address = start + else: + address = 0 + if end is not None: + end = int(end, 16) + + did_ellipse = False + previous_line = None + while True: + if end is not None: + if address > end: + break + this_width = min(width, end - address) + else: + this_width = width + line = handle.read(this_width) + if not line: + break + line = [hexy(x, 2) for x in line] + line = ' '.join(line) + if ellipse: + if line == previous_line: + if not did_ellipse: + print('...') + did_ellipse = True + address += width + continue + previous_line = line + print('%s | ' % hexy(address, 8), end='', flush=False) + print(line) + address += width + +def hexdump_argparse(args): + handle = open(args.filename, 'rb') + return hexdump(handle, width=args.width, ellipse=args.ellipse, start=args.start, end=args.end) + +def main(argv): + parser = argparse.ArgumentParser() + + parser.add_argument('filename') + parser.add_argument('--width', dest='width', default=DEFAULT_WIDTH, type=int) + parser.add_argument('--start', dest='start', default=None) + parser.add_argument('--end', dest='end', default=None) + parser.add_argument('--ellipse', dest='ellipse', action='store_true') + parser.set_defaults(func=hexdump_argparse) + + args = parser.parse_args(argv) + args.func(args) + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/hexpng.py b/hexpng.py new file mode 100644 index 0000000..f7993ed --- /dev/null +++ b/hexpng.py @@ -0,0 +1,46 @@ +''' +Generate a png file of a solid color, specified by a hex code. +''' + +import argparse +import os +import PIL.Image +import sys + + +def full_hex(h): + h = h.replace('#', '') + if len(h) in [3, 4]: + h = ''.join([c * 2 for c in h]) + if len(h) == 6: + h += 'ff' + return h + +def hex_to_rgb(h): + rgb = [int(h[(2*i):(2*i)+2], 16) for i in range(len(h)//2)] + return tuple(rgb) + +def make_hexpng(h, width=1, height=1): + h = full_hex(h) + rgb = hex_to_rgb(h) + filename = f'{h}.png' + i = PIL.Image.new('RGBA', size=[width, height], color=rgb) + print(filename) + i.save(filename) + +def hexpng_argparse(args): + make_hexpng(args.hex_value, width=args.width, height=args.height) + +def main(argv): + parser = argparse.ArgumentParser(description=__doc__) + + parser.add_argument('hex_value') + parser.add_argument('--width', dest='width', type=int, default=1) + parser.add_argument('--height', dest='height', type=int, default=1) + parser.set_defaults(func=hexpng_argparse) + + args = parser.parse_args(argv) + args.func(args) + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) diff --git a/hms_s.py b/hms_s.py new file mode 100644 index 0000000..fc3d745 --- /dev/null +++ b/hms_s.py @@ -0,0 +1,57 @@ +import math +import sys + +from voussoirkit import pipeable + + +def hms_to_seconds(hms): + ''' + Convert hh:mm:ss string to an integer seconds. + ''' + hms = hms.split(':') + seconds = 0 + if len(hms) == 3: + seconds += int(hms[0]) * 3600 + hms.pop(0) + if len(hms) == 2: + seconds += int(hms[0]) * 60 + hms.pop(0) + if len(hms) == 1: + seconds += float(hms[0]) + return seconds + +def seconds_to_hms(seconds): + ''' + Convert integer number of seconds to an hh:mm:ss string. + Only the necessary fields are used. + ''' + (minutes, seconds) = divmod(seconds, 60) + (hours, minutes) = divmod(minutes, 60) + + parts = [] + if hours: + parts.append(f'{int(hours):02d}') + if minutes: + parts.append(f'{int(minutes):02d}') + if seconds == int(seconds): + parts.append(f'{int(seconds):02d}') + else: + parts.append(f'{seconds:0.3f}') + hms = ':'.join(parts) + + return hms + +def main(args): + for line in pipeable.go(args, strip=True, skip_blank=True): + if ':' in line: + line = hms_to_seconds(line) + else: + line = float(line) + if line > 60: + line = seconds_to_hms(line) + + pipeable.output(f'{line}') + + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) diff --git a/kbps.py b/kbps.py new file mode 100644 index 0000000..2edbda1 --- /dev/null +++ b/kbps.py @@ -0,0 +1,75 @@ +''' +Find time, filesize, or bitrate, given two of the three. + +For example: + +kbps.py --time 1:00:00 --size 2g +kbps.py --time 1:00:00 --kbps 4660 +kbps.py --size 2g --kpbps 4660 +''' +import argparse +import sys + +from voussoirkit import bytestring + +def hms_s(hms): + hms = hms.split(':') + seconds = 0 + if len(hms) == 3: + seconds += int(hms[0])*3600 + hms.pop(0) + if len(hms) == 2: + seconds += int(hms[0])*60 + hms.pop(0) + if len(hms) == 1: + seconds += int(hms[0]) + return seconds + +def s_hms(s): + (minutes, seconds) = divmod(s, 60) + (hours, minutes) = divmod(minutes, 60) + return '%02d:%02d:%02d' % (hours, minutes, seconds) + +def kbps(time=None, size=None, kbps=None): + if [time, size, kbps].count(None) != 1: + raise ValueError('Incorrect number of unknowns') + + if size is None: + seconds = hms_s(time) + kibs = int(kbps) / 8 + size = kibs * 1024 + size *= seconds + out = bytestring.bytestring(size) + return out + + if time is None: + size = bytestring.parsebytes(size) + kilobits = size / 128 + time = kilobits / int(kbps) + return s_hms(time) + + if kbps is None: + seconds = hms_s(time) + size = bytestring.parsebytes(size) + kibs = size / 1024 + kilobits = kibs * 8 + kbps = kilobits / seconds + kbps = '%d kbps' % int(round(kbps)) + return kbps + +def example_argparse(args): + print(kbps(time=args.time, size=args.size, kbps=args.kbps)) + +def main(argv): + parser = argparse.ArgumentParser() + + parser.add_argument('-t', '--time', dest='time', default=None) + parser.add_argument('-s', '--size', dest='size', default=None) + parser.add_argument('-k', '--kbps', dest='kbps', default=None) + parser.set_defaults(func=example_argparse) + + args = parser.parse_args(argv) + args.func(args) + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/linenumbers.py b/linenumbers.py new file mode 100644 index 0000000..5715723 --- /dev/null +++ b/linenumbers.py @@ -0,0 +1,12 @@ +import sys + +from voussoirkit import clipext + +if len(sys.argv) == 1: + sys.argv.append('!i') +text = clipext.resolve(sys.argv[1]) +lines = text.splitlines() +digits = len(str(len(lines))) +form = '{no:>0%d} | {line}' % digits +for (index, line) in enumerate(lines): + print(form.format(no=index+1, line=line)) diff --git a/lowercase.py b/lowercase.py new file mode 100644 index 0000000..032c7ac --- /dev/null +++ b/lowercase.py @@ -0,0 +1,7 @@ +import sys + +from voussoirkit import pipeable + + +for line in pipeable.go(): + pipeable.output(line.lower()) diff --git a/mp3slice.py b/mp3slice.py new file mode 100644 index 0000000..bcf7a11 --- /dev/null +++ b/mp3slice.py @@ -0,0 +1,171 @@ +''' +This script cuts an audio file into multiple files when you provide the +timestamps and titles for each. + +> mp3slice bigfile.mp3 00:00-01:00 part1.mp3 01:00-02:00 part2.mp3 +''' + +import argparse +import os +import sys + +from voussoirkit import bytestring + + +def parse_rules(lines): + rules = [] + for (times, title) in lines[::-1]: + rule = {'title': title} + (start, end) = hyphen_range(times) + if start is None: + raise ValueError('Null start') + rule['start'] = start + if end is None and len(rules) > 0: + end = rules[-1]['start'] + rule['end'] = end + rules.append(rule) + rules.sort(key=lambda x: x.get('start')) + return rules + +def read_rulefile(filename): + text = None + for encoding in [None, 'utf-8']: + try: + with open(filename, 'r', encoding=encoding) as handle: + text = handle.read() + break + except UnicodeError: + pass + else: + raise UnicodeError() + + lines = [l.strip() for l in text.strip().splitlines()] + lines = [l for l in lines if l] + rules = [l.split(maxsplit=1) for l in lines] + return parse_rules(rules) + +def chunk_sequence(sequence, chunk_length, allow_incomplete=True): + ''' + Given a sequence, divide it into sequences of length `chunk_length`. + + allow_incomplete: + If True, allow the final chunk to be shorter if the + given sequence is not an exact multiple of `chunk_length`. + If False, the incomplete chunk will be discarded. + ''' + (complete, leftover) = divmod(len(sequence), chunk_length) + if not allow_incomplete: + leftover = 0 + + chunk_count = complete + min(leftover, 1) + + chunks = [] + for x in range(chunk_count): + left = chunk_length * x + right = left + chunk_length + chunks.append(sequence[left:right]) + + return chunks + +def hyphen_range(s): + ''' + Given a string like '1-3', return numbers (1, 3) representing lower + and upper bounds. + + Supports bytestring.parsebytes and hh:mm:ss format, for example + '1k-2k', '10:00-20:00', '4gib-' + ''' + s = s.strip() + s = s.replace(' ', '') + if not s: + return (None, None) + parts = s.split('-') + parts = [part.strip() or None for part in parts] + if len(parts) == 1: + low = parts[0] + high = None + elif len(parts) == 2: + (low, high) = parts + else: + raise ValueError('Too many hyphens') + + low = _unitconvert(low) + high = _unitconvert(high) + if low is not None and high is not None and low > high: + raise exceptions.OutOfOrder(range=s, min=low, max=high) + return low, high + +def hms_to_seconds(hms): + ''' + Convert hh:mm:ss string to an integer seconds. + ''' + hms = hms.split(':') + seconds = 0 + if len(hms) == 3: + seconds += int(hms[0])*3600 + hms.pop(0) + if len(hms) == 2: + seconds += int(hms[0])*60 + hms.pop(0) + if len(hms) == 1: + seconds += float(hms[0]) + return seconds + +def _unitconvert(value): + ''' + When parsing hyphenated ranges, this function is used to convert + strings like "1k" to 1024 and "1:00" to 60. + ''' + if value is None: + return None + if ':' in value: + return hms_to_seconds(value) + elif all(c in '0123456789.' for c in value): + return float(value) + else: + return bytestring.parsebytes(value) + +def example_argparse(args): + if len(args.rules) == 1 and os.path.isfile(args.rules[0]): + rules = read_rulefile(args.rules[0]) + else: + rules = args.rules + rules = chunk_sequence(rules, 2) + if len(rules[-1]) != 2: + raise ValueError('Odd-number parameters') + rules = parse_rules(rules) + + extension = os.path.splitext(args.input_filename)[1] + outputters = [] + for rule in rules: + outputter = [] + if not rule['title'].endswith(extension): + rule['title'] += extension + outputter.append('-ss') + outputter.append(str(rule['start'])) + if rule['end'] is not None: + outputter.append('-to') + outputter.append(str(rule['end'])) + outputter.append(' -c copy') + outputter.append('"%s"' % rule['title']) + print(outputter) + outputter = ' '.join(outputter) + outputters.append(outputter) + outputters = ' '.join(outputters) + command = 'ffmpeg -i "%s" %s' % (args.input_filename, outputters) + print(command) + os.system(command) + + +def main(argv): + parser = argparse.ArgumentParser() + + parser.add_argument('input_filename') + parser.add_argument('rules', nargs='+', default=None) + parser.set_defaults(func=example_argparse) + + args = parser.parse_args(argv) + args.func(args) + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/randomfile.py b/randomfile.py new file mode 100644 index 0000000..c9f473b --- /dev/null +++ b/randomfile.py @@ -0,0 +1,46 @@ +import math +import random +import sys +from voussoirkit import bytestring +CHUNK_SIZE = 512 * (2 ** 10) +def listget(li, index, fallback=None): + try: + return li[index] + except IndexError: + return fallback + +def rid(length=8): + import random + bits = length * 4 + bits = random.getrandbits(bits) + identifier = '{:02x}'.format(bits).rjust(length, '0') + return identifier + +def make_randomfile(length, filename=None): + if filename is None: + filename = rid(8) + '.txt' + chunks = math.ceil(length / CHUNK_SIZE) + written = 0 + f = open(filename, 'w') + for x in range(chunks): + b = min(CHUNK_SIZE, length-written) + f.write(rid(b)) + written += b + f.close() + print('Created %s' % filename) + + +bytes = listget(sys.argv, 1, None) +if bytes is None: + bytes = 2 ** 10 +else: + bytes = bytestring.parsebytes(bytes) + +filecount = 1 +filename = listget(sys.argv, 2, None) +if filename is not None and filename.isdigit(): + filecount = int(filename) + filename = None + +for x in range(filecount): + make_randomfile(bytes, filename) diff --git a/rejpg.py b/rejpg.py new file mode 100644 index 0000000..c4bb8af --- /dev/null +++ b/rejpg.py @@ -0,0 +1,48 @@ +''' +Recompress all jpg images in the current directory. +Add /r to do nested directories as well. +''' + +from voussoirkit import bytestring +import io +import os +import PIL.Image +import PIL.ImageFile +import string +import sys + +PIL.ImageFile.LOAD_TRUNCATED_IMAGES = True + +if '/r' in sys.argv: + from voussoirkit import spinal + walker = spinal.walk_generator() + files = list(walker) + files = [f.absolute_path for f in files] + +else: + files = os.listdir() + files = [f for f in files if os.path.isfile(f)] + +files = [f for f in files if any(ext in f.lower() for ext in ['.jpg', '.jpeg'])] + +bytes_saved = 0 +remaining_size = 0 +for filename in files: + print(''.join(c for c in filename if c in string.printable)) + bytesio = io.BytesIO() + i = PIL.Image.open(filename) + i.save(bytesio, format='jpeg', quality=80) + + bytesio.seek(0) + new_bytes = bytesio.read() + old_size = os.path.getsize(filename) + new_size = len(new_bytes) + remaining_size += new_size + if new_size < old_size: + bytes_saved += (old_size - new_size) + f = open(filename, 'wb') + f.write(new_bytes) + f.close() + +print('Saved', bytestring.bytestring(bytes_saved)) +print('Remaining are', bytestring.bytestring(remaining_size)) diff --git a/repeat.py b/repeat.py new file mode 100644 index 0000000..341bc33 --- /dev/null +++ b/repeat.py @@ -0,0 +1,16 @@ +''' +Repeat the input as many times as you want + +> repeat "hello" 8 +> echo hi | repeat !i 4 +''' + +import sys + +from voussoirkit import clipext + +text = clipext.resolve(sys.argv[1]) +repeat_times = int(sys.argv[2]) + +for t in range(repeat_times): + print(text) diff --git a/replace.py b/replace.py new file mode 100644 index 0000000..e547daa --- /dev/null +++ b/replace.py @@ -0,0 +1,12 @@ +import glob +import sys + +from voussoirkit import pipeable + + +lines = pipeable.input(sys.argv[1]) +replace_from = sys.argv[2] +replace_to = sys.argv[3] + +for line in lines: + pipeable.output(line.replace(replace_from, replace_to)) diff --git a/repr.py b/repr.py new file mode 100644 index 0000000..75e5e4b --- /dev/null +++ b/repr.py @@ -0,0 +1,5 @@ +from voussoirkit import pipeable + + +for line in pipeable.go(): + print(repr(line)) diff --git a/resize.py b/resize.py new file mode 100644 index 0000000..5c28f84 --- /dev/null +++ b/resize.py @@ -0,0 +1,52 @@ +import glob +import os +from PIL import Image +import sys + +def fit_into_bounds(image_width, image_height, frame_width, frame_height): + ''' + Given the w+h of the image and the w+h of the frame, + return new w+h that fits the image into the frame + while maintaining the aspect ratio. + + (1920, 1080, 400, 400) -> (400, 225) + ''' + width_ratio = frame_width / image_width + height_ratio = frame_height / image_height + ratio = min(width_ratio, height_ratio) + + new_width = int(image_width * ratio) + new_height = int(image_height * ratio) + + return (new_width, new_height) + +filenames = sys.argv[1] + +filenames = glob.glob(filenames) +for filename in filenames: + i = Image.open(filename) + if all(x.isdigit() for x in sys.argv[2:3]): + new_x = int(sys.argv[2]) + new_y = int(sys.argv[3]) + else: + try: + ratio = float(sys.argv[2]) + new_x = int(i.size[0] * ratio) + new_y = int(i.size[1] * ratio) + except ValueError: + print('you did it wrong') + quit() + + (image_width, image_height) = i.size + + if new_x == 0: + (new_x, new_y) = fit_into_bounds(image_width, image_height, 10000000, new_y) + if new_y == 0: + (new_x, new_y) = fit_into_bounds(image_width, image_height, new_x, 10000000) + + print(i.size, new_x, new_y) + i = i.resize( (new_x, new_y), Image.ANTIALIAS) + suffix = '_{width}x{height}'.format(width=new_x, height=new_y) + (base, extension) = os.path.splitext(filename) + newname = base + suffix + extension + i.save(newname, quality=100) diff --git a/reverse.py b/reverse.py new file mode 100644 index 0000000..bef9c56 --- /dev/null +++ b/reverse.py @@ -0,0 +1,7 @@ +import sys + +from voussoirkit import clipext + +arg = clipext.resolve(sys.argv[1]) +arg = ''.join(reversed(arg)) +print(arg) diff --git a/sdate.py b/sdate.py new file mode 100644 index 0000000..f4d9fc3 --- /dev/null +++ b/sdate.py @@ -0,0 +1,25 @@ +import datetime +import time + +EPOCH = datetime.datetime( + year=1993, + month=9, + day=1, + tzinfo=datetime.timezone.utc, +) + +def sdate(): + (day, hms) = sdate_tuple() + return f'1993 September {day} {hms}' + +def sdate_tuple(): + now = datetime.datetime.now(datetime.timezone.utc) + diff = now - EPOCH + day = diff.days + 1 + (minutes, seconds) = divmod(diff.seconds, 60) + (hours, minutes) = divmod(minutes, 60) + hms = f'{hours:02}:{minutes:02}:{seconds:02}' + return (day, hms) + +if __name__ == '__main__': + print(sdate()) diff --git a/search.py b/search.py new file mode 100644 index 0000000..1cce33f --- /dev/null +++ b/search.py @@ -0,0 +1,224 @@ +import argparse +import fnmatch +import itertools +import os +import re +import stat +import sys +import traceback + +from voussoirkit import clipext +from voussoirkit import expressionmatch +from voussoirkit import pathclass +from voussoirkit import safeprint +from voussoirkit import spinal + +# Thanks georg +# http://stackoverflow.com/a/13443424 +STDIN_MODE = os.fstat(sys.stdin.fileno()).st_mode +if stat.S_ISFIFO(STDIN_MODE): + STDIN_MODE = 'pipe' +else: + STDIN_MODE = 'terminal' + +def all_terms_match(search_text, terms, match_function): + matches = ( + (not terms['yes_all'] or all(match_function(search_text, term) for term in terms['yes_all'])) and + (not terms['yes_any'] or any(match_function(search_text, term) for term in terms['yes_any'])) and + (not terms['not_all'] or not all(match_function(search_text, term) for term in terms['not_all'])) and + (not terms['not_any'] or not any(match_function(search_text, term) for term in terms['not_any'])) + ) + return matches + +def search( + *, + yes_all=None, + yes_any=None, + not_all=None, + not_any=None, + case_sensitive=False, + content_args=None, + do_expression=False, + do_glob=False, + do_regex=False, + line_numbers=False, + local_only=False, + text=None, + ): + terms = { + 'yes_all': yes_all, + 'yes_any': yes_any, + 'not_all': not_all, + 'not_any': not_any + } + terms = {k: ([v] if isinstance(v, str) else v or []) for (k, v) in terms.items()} + #print(terms, content_args) + + if all(v == [] for v in terms.values()) and not content_args: + raise ValueError('No terms supplied') + + def term_matches(line, term): + if not case_sensitive: + line = line.lower() + + if do_expression: + return term.evaluate(line) + + return ( + (term in line) or + (do_regex and re.search(term, line)) or + (do_glob and fnmatch.fnmatch(line, term)) + ) + + if do_expression: + # The value still needs to be a list so the upcoming any() / all() + # receives an iterable as it expects. It just happens to be 1 tree. + trees = {} + for (key, value) in terms.items(): + if value == []: + trees[key] = [] + continue + tree = ' '.join(value) + tree = expressionmatch.ExpressionTree.parse(tree) + if not case_sensitive: + tree.map(str.lower) + trees[key] = [tree] + terms = trees + + elif not case_sensitive: + terms = {k: [x.lower() for x in v] for (k, v) in terms.items()} + + if text is None: + search_objects = spinal.walk_generator( + depth_first=False, + recurse=not local_only, + yield_directories=True, + ) + else: + search_objects = text.splitlines() + + for (index, search_object) in enumerate(search_objects): + if index % 10 == 0: + #print(index, end='\r', flush=True) + pass + if isinstance(search_object, pathclass.Path): + search_text = search_object.basename + result_text = search_object.absolute_path + else: + search_text = search_object + result_text = search_object + if line_numbers: + result_text = '%4d | %s' % (index+1, result_text) + + if all_terms_match(search_text, terms, term_matches): + if not content_args: + yield result_text + else: + filepath = pathclass.Path(search_object) + if not filepath.is_file: + continue + try: + with open(filepath.absolute_path, 'r') as handle: + text = handle.read() + except UnicodeDecodeError: + try: + with open(filepath.absolute_path, 'r', encoding='utf-8') as handle: + text = handle.read() + except UnicodeDecodeError: + #safeprint.safeprint(filepath.absolute_path) + #traceback.print_exc() + continue + except Exception: + safeprint.safeprint(filepath.absolute_path) + traceback.print_exc() + continue + + content_args['text'] = text + content_args['line_numbers'] = True + results = search(**content_args) + results = list(results) + if not results: + continue + + yield filepath.absolute_path + yield from results + yield '' + +def argparse_to_dict(args): + text = args.text + if text is not None: + text = clipext.resolve(text) + elif STDIN_MODE == 'pipe': + text = clipext.resolve('!i') + + if hasattr(args, 'content_args') and args.content_args is not None: + content_args = argparse_to_dict(args.content_args) + else: + content_args = None + + return { + 'yes_all': args.yes_all, + 'yes_any': args.yes_any, + 'not_all': args.not_all, + 'not_any': args.not_any, + 'case_sensitive': args.case_sensitive, + 'content_args': content_args, + 'do_expression': args.do_expression, + 'do_glob': args.do_glob, + 'do_regex': args.do_regex, + 'local_only': args.local_only, + 'line_numbers': args.line_numbers, + 'text': text, + } + +def search_argparse(args): + generator = search(**argparse_to_dict(args)) + result_count = 0 + for result in generator: + safeprint.safeprint(result) + result_count += 1 + if args.show_count: + print('%d items.' % result_count) + +def main(argv): + parser = argparse.ArgumentParser() + + # The padding is inserted to guarantee that --content is not the first + # argument. Because if it were, we wouldn't know if we have + # [pre, '--content'] or ['--content', post], etc. and I don't want to + # actually check the values. + argv.insert(0, 'padding') + grouper = itertools.groupby(argv, lambda x: x == '--content') + halves = [list(group) for (key, group) in grouper] + # halves looks like [pre, '--content', post] + name_args = halves[0] + # Pop the padding + name_args.pop(0) + content_args = [item for chunk in halves[2:] for item in chunk] + + parser.add_argument('yes_all', nargs='*', default=None) + parser.add_argument('--all', dest='yes_all', nargs='+') + parser.add_argument('--any', dest='yes_any', nargs='+') + parser.add_argument('--not_all', dest='not_all', nargs='+') + parser.add_argument('--not_any', dest='not_any', nargs='+') + + parser.add_argument('--case', dest='case_sensitive', action='store_true') + parser.add_argument('--content', dest='do_content', action='store_true') + parser.add_argument('--count', dest='show_count', action='store_true') + parser.add_argument('--expression', dest='do_expression', action='store_true') + parser.add_argument('--glob', dest='do_glob', action='store_true') + parser.add_argument('--line_numbers', dest='line_numbers', action='store_true') + parser.add_argument('--local', dest='local_only', action='store_true') + parser.add_argument('--regex', dest='do_regex', action='store_true') + parser.add_argument('--text', dest='text', default=None) + parser.set_defaults(func=search_argparse) + + args = parser.parse_args(name_args) + if content_args: + args.content_args = parser.parse_args(content_args) + else: + args.content_args = None + args.func(args) + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) diff --git a/sleep.py b/sleep.py new file mode 100644 index 0000000..42b2436 --- /dev/null +++ b/sleep.py @@ -0,0 +1,6 @@ +import time +import sys + +seconds = sys.argv[1] +seconds = float(seconds) +time.sleep(seconds) diff --git a/sorted.py b/sorted.py new file mode 100644 index 0000000..02a91bf --- /dev/null +++ b/sorted.py @@ -0,0 +1,19 @@ +''' +Sort the lines coming from stdin and print them. +''' +from voussoirkit import clipext +import sys + +if len(sys.argv) > 1: + text = clipext.resolve(sys.argv[1]) +else: + text = clipext.resolve('!input') + +text = text.split('\n') +if '-l' in sys.argv: + text.sort(key=lambda x: x.lower()) +else: + text.sort() + +new_text = '\n'.join(text) +print(new_text) diff --git a/subtitle_shift.py b/subtitle_shift.py new file mode 100644 index 0000000..20bf6ec --- /dev/null +++ b/subtitle_shift.py @@ -0,0 +1,67 @@ +''' +Usage: + +Shift all subtitles 10 seconds forward: +> subtitle_shift file.srt +10 + +Shift all subtitles 10 seconds backward: +> subtitle_shift file.srt -10 + +This will produce "file_correct.srt" with the new timestamps. +''' + +import os +import sys +filename = sys.argv[1] +offset = float(sys.argv[2]) +f = open(filename, 'r') + +lines = [l.strip() for l in f.readlines()] +for (lineindex, line) in enumerate(lines): + changed = False + + if '-->' not in line: + continue + + words = line.split(' ') + for (wordindex, word) in enumerate(words): + word = word.replace('.', ',') + if not (':' in word and ',' in word): + continue + + if not word.replace(':', '').replace(',', '').isdigit(): + continue + + # 1.) 01:23:45,678 --> 02:34:56,789 | our input + # 2.) 01:23:45:678 --> 02:34:56:789 | comma to colon + # 3.) 5025.678 --> 9296.789 | split by colon and sum + # 4.) 5035.678 --> 9306.789 | add offset + # 5.) 01:23:55.678 --> 02:35:06.789 | reformat + # 6.) 01:23:55,678 --> 02:35:06,789 | period to comma + word = word.replace(',', ':') + (hours, minutes, seconds, mili) = [int(x) for x in word.split(':')] + seconds = (3600 * hours) + (60 * minutes) + (seconds) + (mili / 1000) + + seconds += offset + (hours, seconds) = divmod(seconds, 3600) + (minutes, seconds) = divmod(seconds, 60) + + if hours < 0: + raise Exception('Negative time') + + word = '%02d:%02d:%06.3f' % (hours, minutes, seconds) + word = word.replace('.', ',') + changed = True + words[wordindex] = word + + if changed: + line = ' '.join(words) + print(line) + lines[lineindex] = line + +lines = '\n'.join(lines) +(name, extension) = os.path.splitext(filename) +newname = name + '_correct' + extension +x = open(newname, 'w') +x.write(lines) +x.close() diff --git a/subtitle_shift_pointsync.py b/subtitle_shift_pointsync.py new file mode 100644 index 0000000..3a243ae --- /dev/null +++ b/subtitle_shift_pointsync.py @@ -0,0 +1,197 @@ +import argparse +import sys + +inf = float('inf') + +class Subtitles: + def __init__(self, lines): + self.lines = sorted(lines) + + @classmethod + def from_text(cls, text): + text = text.strip() + while '\n\n\n' in text: + text = text.replace('\n\n\n', '\n\n') + lines = text.split('\n\n') + lines = [SubtitleLine.from_text(line) for line in lines] + return cls(lines) + + def __getitem__(self, index): + return self.lines[index] + + def __len__(self): + return len(self.lines) + + def __repr__(self): + return f'Subtitles with {len(self.lines)} lines.' + + def as_srt(self): + lines = sorted(self.lines) + lines = [f'{index+1}\n{line.as_srt()}' for (index, line) in enumerate(lines)] + return '\n\n'.join(lines) + +class SubtitleLine: + def __init__(self, start, end, text): + self.start = start + self.end = end + self.text = text + + @classmethod + def from_text(cls, text): + (index, timestamps, text) = text.split('\n', 2) + timestamps = timestamps.replace(',', '.') + (start, end) = timestamps.split('-->') + start = hms_to_seconds(start) + end = hms_to_seconds(end) + return cls(start, end, text) + + def __lt__(self, other): + return (self.start, self.end) < (other.start, other.end) + + def __repr__(self): + return repr(self.as_srt()) + + def as_srt(self): + start = seconds_to_hms(self.start, force_hours=True, force_milliseconds=True) + end = seconds_to_hms(self.end, force_hours=True, force_milliseconds=True) + srt = f'{start} --> {end}'.replace('.', ',') + srt += '\n' + self.text + return srt + + +class Point: + def __init__(self, x, y=None): + self.x = x + self.y = y + + def __lt__(self, other): + return (self.x, self.y) < (other.x, other.y) + + def __repr__(self): + return f'Point({self.x}, {self.y})' + + +def hms_to_seconds(hms): + ''' + Convert hh:mm:ss string to an integer seconds. + ''' + hms = hms.split(':') + seconds = 0 + if len(hms) == 3: + seconds += int(hms.pop(0)) * 3600 + if len(hms) == 2: + seconds += int(hms.pop(0)) * 60 + if len(hms) == 1: + seconds += float(hms.pop(0).replace(',', '.')) + return seconds + +def seconds_to_hms(seconds, force_hours=False, force_milliseconds=False): + milliseconds = seconds % 1 + if milliseconds >= 0.999: + milliseconds = 0 + seconds += 1 + seconds = int(seconds) + (minutes, seconds) = divmod(seconds, 60) + (hours, minutes) = divmod(minutes, 60) + + parts = [] + if hours or force_hours: + parts.append(hours) + + if minutes or force_hours: + parts.append(minutes) + + parts.append(seconds) + + hms = ':'.join(f'{part:02d}' for part in parts) + if milliseconds or force_milliseconds: + milliseconds = f'.{milliseconds:.03f}'.split('.')[-1] + hms += '.' + milliseconds + return hms + +def linear(slope, intercept): + ''' + Given slope m and intercept b, return a function f such that f(x) = mx + b. + ''' + def f(x): + y = (slope * x) + intercept + print(x, y, f'{y:.03f}', seconds_to_hms(y)) + return y + return f + +def slope_intercept(p1, p2): + ''' + Given two Points, return the slope and intercept describing a line + between them. + ''' + slope = (p2.y - p1.y) / (p2.x - p1.x) + intercept = p1.y - (slope * p1.x) + return (slope, intercept) + +def pointsync(input_filename, output_filename, input_landmarks): + landmarks = [] + used_olds = set() + for landmark in input_landmarks: + (old, new) = landmark.split('=') + (old, new) = (hms_to_seconds(old), hms_to_seconds(new)) + if old < 0 or new < 0: + raise ValueError('No negative numbers!') + if old in used_olds: + raise ValueError(f'Cant use the same old value {seconds_to_hms(old, force_hours=True)} twice.') + used_olds.add(old) + landmarks.append(Point(old, new)) + landmarks.sort() + if landmarks[0].x != 0: + landmarks.insert(0, Point(0, 0)) + + # print(landmarks) + if len(landmarks) < 2: + raise ValueError('Not enough landmarks') + + landmark_functions = [] + for (land1, land2) in zip(landmarks, landmarks[1:]): + (slope, intercept) = slope_intercept(land1, land2) + if slope < 0: + raise ValueError(f'Negative slope between {land1} and {land2}.') + f = linear(slope, intercept) + landmark_functions.append((land1.x, f)) + landmark_functions.append((inf, None)) + old_srt = Subtitles.from_text(open(input_filename, encoding='utf-8').read()) + + pointer = 0 + new_srt = Subtitles([]) + for old_line in old_srt: + if old_line.start >= landmark_functions[pointer+1][0]: + pointer += 1 + new_start = landmark_functions[pointer][1](old_line.start) + + if old_line.end >= landmark_functions[pointer+1][0]: + pointer += 1 + new_end = landmark_functions[pointer][1](old_line.end) + new_line = SubtitleLine(new_start, new_end, old_line.text) + new_srt.lines.append(new_line) + new_file = open(output_filename, 'w', encoding='utf-8') + new_file.write(new_srt.as_srt()) + +def pointsync_argparse(args): + input_filename = args.input_filename + output_filename = args.output_filename + if '.srt' not in output_filename: + raise ValueError('Output filename', output_filename) + + input_landmarks = args.landmarks + return pointsync(input_filename, output_filename, input_landmarks) + +def main(argv): + parser = argparse.ArgumentParser(description=__doc__) + + parser.add_argument('input_filename') + parser.add_argument('output_filename') + parser.add_argument('landmarks', nargs='*', default=None) + parser.set_defaults(func=pointsync_argparse) + + args = parser.parse_args(argv) + args.func(args) + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) diff --git a/timestampfilename.pyw b/timestampfilename.pyw new file mode 100644 index 0000000..43f56c3 --- /dev/null +++ b/timestampfilename.pyw @@ -0,0 +1,25 @@ +''' +Drag a file on top of this .py file, and it will +be renamed to the current timestamp. +''' + +import datetime +import os +import sys + +STRFTIME = '%Y%m%d %H%M%S' +UTC = True + +filename = sys.argv[1] +folder = os.path.dirname(filename) +if folder == '': + folder = os.getcwd() +basename = os.path.basename(filename) +extension = os.path.splitext(basename)[1] + +now = datetime.datetime.now(datetime.timezone.utc if UTC else None) +newname = now.strftime(STRFTIME) + +newname = '%s\\%s%s' % (folder, newname, extension) +print(filename, '-->', newname) +os.rename(filename, newname) diff --git a/touch.py b/touch.py new file mode 100644 index 0000000..bf94858 --- /dev/null +++ b/touch.py @@ -0,0 +1,24 @@ +''' +Create the file, or update the last modified timestamp. +''' +import glob +import os +import sys + +from voussoirkit import clipext +from voussoirkit import safeprint + +def touch(glob_pattern): + filenames = glob.glob(glob_pattern) + if len(filenames) == 0: + safeprint.safeprint(glob_pattern) + open(glob_pattern, 'a').close() + else: + for filename in filenames: + safeprint.safeprint(filename) + os.utime(filename) + +if __name__ == '__main__': + glob_patterns = [clipext.resolve(x).strip() for x in sys.argv[1:]] + for glob_pattern in glob_patterns: + touch(glob_pattern) diff --git a/unique.py b/unique.py new file mode 100644 index 0000000..35e6982 --- /dev/null +++ b/unique.py @@ -0,0 +1,20 @@ +''' +Keep the unique lines coming from stdin and print them. +''' +from voussoirkit import clipext +import sys + +if len(sys.argv) > 1: + source = sys.argv[1] +else: + source = '!input' +lines = clipext.resolve(source, split_lines=True) + +new_text = [] +seen = set() +for line in lines: + if line not in seen: + #new_text.append(line) + seen.add(line) + print(line) + diff --git a/zerofile.py b/zerofile.py new file mode 100644 index 0000000..82957d5 --- /dev/null +++ b/zerofile.py @@ -0,0 +1,36 @@ +import argparse +import os +import sys + +from voussoirkit import bytestring + + +filename = os.path.abspath(sys.argv[1]) + + +def zerofile(filename, length): + if os.path.exists(filename): + raise ValueError(f'{filename} already exists.') + + with open(filename, 'wb') as handle: + handle.seek(length - 1) + handle.write(bytes([0])) + +def zerofile_argparse(args): + return zerofile( + filename=args.filename, + length=bytestring.parsebytes(args.length), + ) + +def main(argv): + parser = argparse.ArgumentParser(description=__doc__) + + parser.add_argument('filename') + parser.add_argument('length') + parser.set_defaults(func=zerofile_argparse) + + args = parser.parse_args(argv) + args.func(args) + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:]))