From 12c3ac9c051f2ae4e8d68465eec5dfbb1b4ea13b Mon Sep 17 00:00:00 2001 From: MasterofJOKers Date: Tue, 25 Jan 2022 23:33:53 +0100 Subject: [PATCH] Add sorted-co.py This is a script to copy files onto an USB stick or an MP3 player in sorted order so stupd mp3 players or radios that play files in the order they were added to the directory instead of by sorted filename still play the files in the right order. --- sorted-copy/setup.cfg | 15 ++++++ sorted-copy/setup.py | 4 ++ sorted-copy/sorted-co.py | 111 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 sorted-copy/setup.cfg create mode 100644 sorted-copy/setup.py create mode 100755 sorted-copy/sorted-co.py diff --git a/sorted-copy/setup.cfg b/sorted-copy/setup.cfg new file mode 100644 index 0000000..94b2dca --- /dev/null +++ b/sorted-copy/setup.cfg @@ -0,0 +1,15 @@ +[metadata] +name = sorted-copy +version = 0.1 +description = Copy files and directories recursively sorted and one by one to the destination. Mainly useful for USB sticks/MP3 players. + +[options] +scripts = sorted-co.py +install_requires = + click + + +[flake8] +max-line-length = 120 +exclude = .git,__pycache__,*.egg-info,*lib/python* +ignore = E241,E741,W503,W504 diff --git a/sorted-copy/setup.py b/sorted-copy/setup.py new file mode 100644 index 0000000..056ba45 --- /dev/null +++ b/sorted-copy/setup.py @@ -0,0 +1,4 @@ +import setuptools + + +setuptools.setup() diff --git a/sorted-copy/sorted-co.py b/sorted-copy/sorted-co.py new file mode 100755 index 0000000..95cc758 --- /dev/null +++ b/sorted-copy/sorted-co.py @@ -0,0 +1,111 @@ +#!/usr/bin/python3 +from pathlib import Path +import shutil +from typing import Any, Generator, Tuple, Union + +import click + + +def _to_pathlib_path(ctx: click.Context, param: Any, value: str) \ + -> Union[Path, Tuple[Path]]: + if isinstance(value, tuple): + return tuple(Path(v) for v in value) + else: + return Path(value) + + +def get_directories(directory: Path) -> Generator[Path, None, None]: + """Return all directories below given directory in sorted order + + This returns first all directories of a directory before returning + subdirectories of the sorted subdirectories. + """ + directories = [directory] + for d in directories: + for p in sorted(d.iterdir()): + if p.is_dir(): + directories.append(p) + yield p + + +def get_files(directory: Path) -> Generator[Path, None, None]: + """Traverse the given directory and its sorted subdirectory and yield files + + See `get_directories()` for a hint to the order of directories traversed. + """ + if not directory.is_dir(): + msg = f"get_files() needs a directory but got {directory}" + raise RuntimeError(msg) + + directories = [directory] + for d in directories: + for p in sorted(d.iterdir()): + if p.is_dir(): + directories.append(p) + elif p.is_file(): + yield p + + +def relative_path(base: Path, p: Path) -> Path: + """Return the part of `p` relative to `base` as relative Path""" + for a, b in zip(base.parts, p.parts): + if a != b: + raise ValueError('Base {} is not the base of {}'.format(a, b)) + + return Path('/'.join(p.parts[len(base.parts):])) + + +@click.command() +@click.argument('srces', nargs=-1, type=click.Path(exists=True, file_okay=True, + dir_okay=True, resolve_path=True), callback=_to_pathlib_path, + required=True) +@click.argument('dest', type=click.Path(exists=True, dir_okay=True, + file_okay=False, resolve_path=True), callback=_to_pathlib_path, + required=True) +@click.option('--verbose', is_flag=True) +def main(srces: Tuple[Path], dest: Path, verbose: bool) -> None: + """Copy `srces` recursively and sorted one by one to `dest + + This is mainly useful for USB sticks and MP3 players that take the order of + files created in the filesystem instead of the order of the sorted names to + play files. + """ + for src in srces: + if src.is_file(): + # copy this file over directly + rel_path = Path(src.name) + dest_path = dest / rel_path + if verbose: + click.echo('copy {} -> {}'.format(src, dest_path)) + shutil.copy(src, dest_path) + continue + + if src.is_dir(): + dest_path = dest / src.name + if verbose: + click.echo('mkdir {}'.format(dest_path)) + dest_path.mkdir() + + # create all directories first + for p in get_directories(src): + rel_path = Path(src.name) / relative_path(src, p) + dest_path = dest / rel_path + if verbose: + click.echo('mkdir {}'.format(dest_path)) + dest_path.mkdir() + + # copy all files over + for p in get_files(src): + rel_path = Path(src.name) / relative_path(src, p) + dest_path = dest / rel_path + if verbose: + click.echo('copy {} -> {}'.format(p, dest_path)) + shutil.copy(p, dest_path) + continue + + click.echo(f"Warning: Not copying {src}. Neither file nor directory") + + +if __name__ == '__main__': + import sys + sys.exit(main())