commit f44e46aab587d5ea23dd4048dff177867c2cc672 Author: Ethan Dalool Date: Tue Jun 11 22:41:31 2019 -0700 Initial commit. 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:]))