From 6e64483523b7b013f7e2c77e464c99a86869c5d8 Mon Sep 17 00:00:00 2001 From: Ethan Dalool Date: Mon, 12 Dec 2016 19:53:21 -0800 Subject: [PATCH] else --- Bencode/bencode.py | 147 +++++++++++++++++++++++++++++++++++ ICO/icoconvert.py | 2 +- OpenDirDL/opendirdl.py | 1 + SpinalTap/README.md | 3 + SpinalTap/spinal.py | 33 ++++++-- ThreadedDL/threaded_dl.py | 10 +-- Toolbox/brename.py | 2 +- Toolbox/eval.py | 18 +++++ Toolbox/ffrename.py | 15 ++-- Toolbox/filepull.py | 20 ++++- Toolbox/fileswith.py | 2 +- Toolbox/touch.py | 13 ++-- _voussoirkit/voussoirkit.py | 3 +- _voussoirkit/voussoirkit.zip | Bin 15978 -> 16921 bytes 14 files changed, 240 insertions(+), 29 deletions(-) create mode 100644 Bencode/bencode.py create mode 100644 Toolbox/eval.py diff --git a/Bencode/bencode.py b/Bencode/bencode.py new file mode 100644 index 0000000..5f544b5 --- /dev/null +++ b/Bencode/bencode.py @@ -0,0 +1,147 @@ +''' +A Python 3 translation of bcode.py +https://pypi.python.org/pypi/bcode/0.5 +''' + +def bencode(data): + ''' + Encode python types to bencode. + ''' + if data is None: + return None + + data_type = type(data) + + encoders = { + bytes: encode_bytes, + str: encode_string, + float: encode_float, + int: encode_int, + dict: encode_dict, + } + + encoder = encoders.get(data_type, None) + if encoder is None: + try: + return encode_iterator(iter(data)) + except TypeError: + raise ValueError('Invalid field type %s' % data_type) + return encoder(data) + +def encode_bytes(data): + return '%d:%s' % (len(data), data) + +def encode_dict(data): + result = [] + keys = list(data.keys()) + keys.sort() + for key in keys: + result.append(bencode(key)) + result.append(bencode(data[key])) + result = ''.join(result) + return 'd%se' % result + +def encode_float(data): + return encode_string(str(data)) + +def encode_int(data): + return 'i%de' % data + +def encode_iterator(data): + result = [] + for item in data: + result.append(bencode(item)) + result = ''.join(result) + return 'l%se' % result + +def encode_string(data): + return encode_bytes(data) + + +# ============================================================================= + + +def bdecode(data): + ''' + Decode bencode to python types. + + Returns a dictionary + { + 'result': the decoded item + 'remainder': what's left of the input text + } + ''' + if data is None: + return None + + data = data.strip() + if isinstance(data, bytes): + data = data.decode('utf-8') + + if data[0] == 'i': + return decode_int(data) + + if data[0].isdigit(): + return decode_string(data) + + if data[0] == 'l': + return decode_list(data) + + if data[0] == 'd': + return decode_dict(data) + + raise ValueError('Invalid initial delimiter "%s"' % data[0]) + +def decode_dict(data): + result = {} + + # slice leading d + remainder = data[1:] + while remainder[0] != 'e': + temp = bdecode(remainder) + key = temp['result'] + remainder = temp['remainder'] + + temp = bdecode(remainder) + value = temp['result'] + remainder = temp['remainder'] + + result[key] = value + + # slice ending 3 + remainder = remainder[1:] + return {'result': result, 'remainder': remainder} + +def decode_int(data): + end = data.find('e') + if end == -1: + raise ValueError('Missing end delimiter "e"') + + # slice leading i and closing e + result = data[1:end] + remainder = data[end+1:] + return {'result': result, 'remainder': remainder} + +def decode_list(data): + result = [] + + # slice leading l + remainder = data[1:] + while remainder[0] != 'e': + item = bdecode(data) + result.append(item['result']) + reaminder = item['remainder'] + + # slice ending e + remainder = remainder[1:] + return {'result': result, 'remainder': remainder} + +def decode_string(data): + start = data.find(':') + 1 + size = int(data[:start-1]) + end = start + size + text = data[start:end] + if len(text) < size: + raise ValueError('Actual length %d is less than declared length %d' % len(text), size) + remainder = data[end:] + return {'result': text, 'remainder': remainder} diff --git a/ICO/icoconvert.py b/ICO/icoconvert.py index fefdb61..d115dc0 100644 --- a/ICO/icoconvert.py +++ b/ICO/icoconvert.py @@ -100,7 +100,7 @@ def image_to_ico(filename): if min(image.size) > 256: w = image.size[0] h = image.size[1] - image = image.resize((256, 256)) + image = image.resize((256, 256), resample=Image.ANTIALIAS) image = image.convert('RGBA') print('Building ico header') diff --git a/OpenDirDL/opendirdl.py b/OpenDirDL/opendirdl.py index b595c13..9de6f86 100644 --- a/OpenDirDL/opendirdl.py +++ b/OpenDirDL/opendirdl.py @@ -189,6 +189,7 @@ SKIPPABLE_FILETYPES = [ '.wav', '.webm', '.wma', + '.xml', '.zip', ] SKIPPABLE_FILETYPES = set(x.lower() for x in SKIPPABLE_FILETYPES) diff --git a/SpinalTap/README.md b/SpinalTap/README.md index c4b8589..ef7a830 100644 --- a/SpinalTap/README.md +++ b/SpinalTap/README.md @@ -3,6 +3,9 @@ Spinal A couple of tools for copying files and directories. +- 2016 12 06 + - Fixed bug where dry runs would still create directories + - 2016 11 27 - Renamed the `copy_file` parameter `callback` to `callback_progress` for clarity. diff --git a/SpinalTap/spinal.py b/SpinalTap/spinal.py index 1dbb329..81f2598 100644 --- a/SpinalTap/spinal.py +++ b/SpinalTap/spinal.py @@ -248,7 +248,8 @@ def copy_dir( raise DestinationIsDirectory(destination_abspath) destination_location = os.path.split(destination_abspath.absolute_path)[0] - os.makedirs(destination_location, exist_ok=True) + if not dry_run: + os.makedirs(destination_location, exist_ok=True) copied = copy_file( source_abspath, @@ -391,7 +392,6 @@ def copy_file( source_bytes = source.size destination_location = os.path.split(destination.absolute_path)[0] os.makedirs(destination_location, exist_ok=True) - written_bytes = 0 try: log.debug('Opening handles.') @@ -407,6 +407,7 @@ def copy_file( if validate_hash: hasher = HASH_CLASS() + written_bytes = 0 while True: data_chunk = source_handle.read(CHUNK_SIZE) data_bytes = len(data_chunk) @@ -424,6 +425,10 @@ def copy_file( callback_progress(destination, written_bytes, source_bytes) + if written_bytes == 0: + # For zero-length files, we want to get at least one call in there. + callback_progress(destination, written_bytes, source_bytes) + # Fin log.debug('Closing source handle.') source_handle.close() @@ -556,7 +561,9 @@ def walk_generator( exclude_directories=None, exclude_filenames=None, recurse=True, - yield_style='flat' + yield_directories=False, + yield_files=True, + yield_style='flat', ): ''' Yield Path objects for files in the file tree, similar to os.walk. @@ -586,11 +593,20 @@ def walk_generator( recurse: Yield from subdirectories. If False, only immediate files are returned. + yield_directories: + Should the generator produce directories? Has no effect in nested yield style. + + yield_files: + Should the generator produce files? Has no effect in nested yield style. + yield_style: If 'flat', yield individual files one by one in a constant stream. If 'nested', yield tuple(root, directories, files) like os.walk does, except I use Path objects with absolute paths for everything. ''' + if not yield_directories and not yield_files: + raise ValueError('yield_directories and yield_files cannot both be False') + if yield_style not in ['flat', 'nested']: raise ValueError('Invalid yield_style %s. Either "flat" or "nested".' % repr(yield_style)) @@ -607,6 +623,7 @@ def walk_generator( exclude_directories = {normalize(f) for f in exclude_directories} path = str_to_fp(path) + path.correct_case() # Considering full paths if normalize(path.absolute_path) in exclude_directories: @@ -631,9 +648,11 @@ def walk_generator( except PermissionError as exception: callback_permission_denied(current_location, exception) continue - log.debug('received %d items', len(contents)) + if yield_style == 'flat' and yield_directories: + yield current_location + directories = [] files = [] for base_name in contents: @@ -646,7 +665,11 @@ def walk_generator( callback_exclusion(absolute_name, 'directory') continue - directories.append(str_to_fp(absolute_name)) + directory = str_to_fp(absolute_name) + directories.append(directory) + + elif yield_style == 'flat' and not yield_files: + continue else: exclude = normalize(absolute_name) in exclude_filenames diff --git a/ThreadedDL/threaded_dl.py b/ThreadedDL/threaded_dl.py index d069992..a091f4a 100644 --- a/ThreadedDL/threaded_dl.py +++ b/ThreadedDL/threaded_dl.py @@ -61,8 +61,8 @@ def threaded_dl(urls, thread_count, filename_format=None): print('%d threads remaining\r' % len(threads), end='', flush=True) time.sleep(0.1) -def main(): - filename = sys.argv[1] +def main(argv): + filename = argv[0] if os.path.isfile(filename): f = open(filename, 'r') with f: @@ -70,9 +70,9 @@ def main(): else: urls = clipext.resolve(filename) urls = urls.replace('\r', '').split('\n') - thread_count = int(listget(sys.argv, 2, 4)) - filename_format = listget(sys.argv, 3, None) + thread_count = int(listget(argv, 1, 4)) + filename_format = listget(argv, 2, None) threaded_dl(urls, thread_count=thread_count, filename_format=filename_format) if __name__ == '__main__': - main() + main(sys.argv[1:]) diff --git a/Toolbox/brename.py b/Toolbox/brename.py index fa417c5..9172002 100644 --- a/Toolbox/brename.py +++ b/Toolbox/brename.py @@ -46,7 +46,7 @@ def loop(pairs, dry=False): line = '{old}\n{new}\n' line = line.format(old=x, new=y) #print(line.encode('utf-8')) - print(line) + print(line.encode('ascii', 'replace').decode()) has_content = True else: os.rename(x, y) diff --git a/Toolbox/eval.py b/Toolbox/eval.py new file mode 100644 index 0000000..8e04c17 --- /dev/null +++ b/Toolbox/eval.py @@ -0,0 +1,18 @@ +''' +Great for applying Python post-processing to the output of some other command. +Provide an input string (!i for stdin) and an eval string using `x` as the +variable name of the input. +''' +from voussoirkit import clipext +import math +import os +import random +import string +import sys +import time + +x = clipext.resolve(sys.argv[1]) +transformation = ' '.join(sys.argv[2:]) + +result = eval(transformation) +print(result) diff --git a/Toolbox/ffrename.py b/Toolbox/ffrename.py index 900d296..1e4d9ae 100644 --- a/Toolbox/ffrename.py +++ b/Toolbox/ffrename.py @@ -1,4 +1,5 @@ import converter +import glob import os import re import subprocess @@ -6,7 +7,6 @@ import sys import time def main(filename): - assert os.path.isfile(filename) ffmpeg = converter.Converter() probe = ffmpeg.probe(filename) new_name = filename @@ -16,10 +16,12 @@ def main(filename): if '___' in filename: video_codec = probe.video.codec - audios = [stream for stream in probe.streams if stream.type == 'audio'] - audio = max(audios, key=lambda x: x.bitrate) - - audio_codec = probe.audio.codec + audios = [stream for stream in probe.streams if stream.type == 'audio' and stream.bitrate] + if audios: + audio = max(audios, key=lambda x: x.bitrate) + audio_codec = probe.audio.codec + else: + audio_codec = None if any(not x for x in [video_codec, probe.video.bitrate, audio_codec, probe.audio.bitrate]): print('Could not identify media info') @@ -40,4 +42,5 @@ def main(filename): os.rename(filename, new_name) if __name__ == '__main__': - main(sys.argv[1]) \ No newline at end of file + for filename in glob.glob(sys.argv[1]): + main(filename) diff --git a/Toolbox/filepull.py b/Toolbox/filepull.py index 839d9f4..2344589 100644 --- a/Toolbox/filepull.py +++ b/Toolbox/filepull.py @@ -1,14 +1,14 @@ ''' Pull all of the files in nested directories into the current directory. ''' - +import argparse import os import sys from voussoirkit import spinal -def main(): - files = list(spinal.walk_generator()) +def filepull(pull_from='.'): + 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] @@ -36,5 +36,17 @@ def main(): local = os.path.join('.', f.basename) os.rename(f.absolute_path, local) +def filepull_argparse(args): + filepull(pull_from=args.pull_from) + +def main(argv): + parser = argparse.ArgumentParser() + + parser.add_argument('pull_from', nargs='?', default='.') + parser.set_defaults(func=filepull_argparse) + + args = parser.parse_args(argv) + args.func(args) + if __name__ == '__main__': - main() + main(sys.argv[1:]) diff --git a/Toolbox/fileswith.py b/Toolbox/fileswith.py index 700e72f..2d2011a 100644 --- a/Toolbox/fileswith.py +++ b/Toolbox/fileswith.py @@ -31,5 +31,5 @@ for filename in spinal.walk_generator(): pass if matches: print(filename) - print('\n'.join(matches)) + print('\n'.join(matches).encode('ascii', 'replace').decode()) print() diff --git a/Toolbox/touch.py b/Toolbox/touch.py index 2ae87e7..a7c6db2 100644 --- a/Toolbox/touch.py +++ b/Toolbox/touch.py @@ -5,14 +5,17 @@ import glob import os import sys - -glob_patterns = sys.argv[1:] -for glob_pattern in glob_patterns: +def touch(glob_pattern): filenames = glob.glob(glob_pattern) if len(filenames) == 0: - print(glob_pattern) + print(glob_pattern.encode('ascii', 'replace').decode()) open(glob_pattern, 'a').close() else: for filename in filenames: - print(filename) + print(filename.encode('ascii', 'replace').decode()) os.utime(filename) + +if __name__ == '__main__': + glob_patterns = sys.argv[1:] + for glob_pattern in glob_patterns: + touch(glob_pattern) diff --git a/_voussoirkit/voussoirkit.py b/_voussoirkit/voussoirkit.py index fd1c306..184afb9 100644 --- a/_voussoirkit/voussoirkit.py +++ b/_voussoirkit/voussoirkit.py @@ -11,6 +11,7 @@ PATHS = [ 'C:\\git\\else\\Ratelimiter\\ratelimiter.py', 'C:\\git\\else\\RateMeter\\ratemeter.py', 'C:\\git\\else\\SpinalTap\\spinal.py', +'C:\\git\\else\\WebstreamZip\\webstreamzip.py', ] os.makedirs(PACKAGE, exist_ok=True) @@ -39,7 +40,7 @@ import setuptools setuptools.setup( author='voussoir', name='{package}', - version='0.0.2', + version='0.0.3', description='', py_modules=[{py_modules}], ) diff --git a/_voussoirkit/voussoirkit.zip b/_voussoirkit/voussoirkit.zip index 6213275327cbd7695e4bf5251618751eed481d7f..dad85249147cd86ca509d172de97d05d96d57249 100644 GIT binary patch delta 7557 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 D74P$|