├── tests ├── media │ ├── bad.jpeg │ ├── bad.mp3 │ ├── bad.mp4 │ ├── unknown.ext │ ├── bad2.mp3 │ ├── good.jpg │ ├── good.mp3 │ ├── good.pdf │ └── good.png ├── script-exit-1 ├── script-sleep ├── verify-test-1 │ └── good.mp3 ├── verify-test-2 │ ├── good.jpg │ ├── good.mp3 │ ├── good.pdf │ └── good.png ├── verify-test-bad-1 │ └── bad.mp3 ├── verify-test-bad-2 │ └── bad.mp4 ├── transcode-test-1 │ ├── good-1.mp3 │ └── good-2.mp3 ├── transcode-test-2 │ ├── good-1.jpg │ └── good-1.mp3 ├── transcode-test-bad-1 │ ├── bad-1.mp3 │ └── good-1.mp3 ├── verify-test-bad-3 │ ├── bad.jpeg │ └── good.mp3 ├── script-print-error ├── test_verify.py └── test_transcode.py ├── docs ├── render.gif └── recording.yml ├── .gitignore ├── test ├── automedia ├── automedia-docker ├── Dockerfile ├── src └── automedia │ ├── operation.py │ ├── docker.py │ ├── ffmpeg.py │ ├── ffmpeg_validator.py │ ├── jobqueue.py │ ├── ffmpeg_transcoder.py │ ├── path_scan.py │ ├── forward_progress.py │ ├── par2.py │ └── main.py ├── pyproject.toml ├── make.sh ├── README.md └── LICENSE /tests/media/bad.jpeg: -------------------------------------------------------------------------------- 1 | bad jpeg 2 | -------------------------------------------------------------------------------- /tests/media/bad.mp3: -------------------------------------------------------------------------------- 1 | bad mp3 2 | -------------------------------------------------------------------------------- /tests/media/bad.mp4: -------------------------------------------------------------------------------- 1 | bad mp4 2 | -------------------------------------------------------------------------------- /tests/media/unknown.ext: -------------------------------------------------------------------------------- 1 | hi 2 | -------------------------------------------------------------------------------- /tests/script-exit-1: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exit 1 3 | -------------------------------------------------------------------------------- /tests/script-sleep: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sleep 60 3 | -------------------------------------------------------------------------------- /tests/verify-test-1/good.mp3: -------------------------------------------------------------------------------- 1 | ../media/good.mp3 -------------------------------------------------------------------------------- /tests/verify-test-2/good.jpg: -------------------------------------------------------------------------------- 1 | ../media/good.jpg -------------------------------------------------------------------------------- /tests/verify-test-2/good.mp3: -------------------------------------------------------------------------------- 1 | ../media/good.mp3 -------------------------------------------------------------------------------- /tests/verify-test-2/good.pdf: -------------------------------------------------------------------------------- 1 | ../media/good.pdf -------------------------------------------------------------------------------- /tests/verify-test-2/good.png: -------------------------------------------------------------------------------- 1 | ../media/good.png -------------------------------------------------------------------------------- /tests/verify-test-bad-1/bad.mp3: -------------------------------------------------------------------------------- 1 | ../media/bad.mp3 -------------------------------------------------------------------------------- /tests/verify-test-bad-2/bad.mp4: -------------------------------------------------------------------------------- 1 | ../media/bad.mp4 -------------------------------------------------------------------------------- /tests/transcode-test-1/good-1.mp3: -------------------------------------------------------------------------------- 1 | ../media/good.mp3 -------------------------------------------------------------------------------- /tests/transcode-test-1/good-2.mp3: -------------------------------------------------------------------------------- 1 | ../media/good.mp3 -------------------------------------------------------------------------------- /tests/transcode-test-2/good-1.jpg: -------------------------------------------------------------------------------- 1 | ../media/good.jpg -------------------------------------------------------------------------------- /tests/transcode-test-2/good-1.mp3: -------------------------------------------------------------------------------- 1 | ../media/good.mp3 -------------------------------------------------------------------------------- /tests/transcode-test-bad-1/bad-1.mp3: -------------------------------------------------------------------------------- 1 | ../media/bad.mp3 -------------------------------------------------------------------------------- /tests/verify-test-bad-3/bad.jpeg: -------------------------------------------------------------------------------- 1 | ../media/bad.jpeg -------------------------------------------------------------------------------- /tests/verify-test-bad-3/good.mp3: -------------------------------------------------------------------------------- 1 | ../media/good.mp3 -------------------------------------------------------------------------------- /tests/transcode-test-bad-1/good-1.mp3: -------------------------------------------------------------------------------- 1 | ../media/good.mp3 -------------------------------------------------------------------------------- /docs/render.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmastrac/automedia/HEAD/docs/render.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | .DS_Store 4 | 5 | *.egg-info 6 | build 7 | dist 8 | -------------------------------------------------------------------------------- /tests/script-print-error: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "ERROR" 1>&2 3 | cat - | wc -c > /dev/null 4 | -------------------------------------------------------------------------------- /tests/media/bad2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmastrac/automedia/HEAD/tests/media/bad2.mp3 -------------------------------------------------------------------------------- /tests/media/good.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmastrac/automedia/HEAD/tests/media/good.jpg -------------------------------------------------------------------------------- /tests/media/good.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmastrac/automedia/HEAD/tests/media/good.mp3 -------------------------------------------------------------------------------- /tests/media/good.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmastrac/automedia/HEAD/tests/media/good.pdf -------------------------------------------------------------------------------- /tests/media/good.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmastrac/automedia/HEAD/tests/media/good.png -------------------------------------------------------------------------------- /test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os, sys 3 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src")) 4 | import pytest 5 | pytest.main() 6 | -------------------------------------------------------------------------------- /automedia: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os, sys 3 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src")) 4 | import automedia.main 5 | automedia.main.main() 6 | -------------------------------------------------------------------------------- /automedia-docker: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker run --rm -v/:/__root__/ mmastrac/automedia --hidden-container-pwd="$(pwd)" --hidden-container-prefix=/__root__/host_mnt/,/__root__/ "$@" 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | ARG AUTOMEDIA_VERSION 4 | ADD dist/automedia-${AUTOMEDIA_VERSION}-py3-none-any.whl /tmp/ 5 | 6 | RUN apt-get update \ 7 | && apt-get install --no-install-recommends --yes python3 pip par2 \ 8 | && apt-get install --yes ffmpeg \ 9 | && rm -rf /var/lib/apt/lists/* \ 10 | && pip3 install /tmp/automedia-${AUTOMEDIA_VERSION}-py3-none-any.whl \ 11 | && rm /tmp/*.whl 12 | 13 | ENTRYPOINT [ "automedia" ] 14 | -------------------------------------------------------------------------------- /src/automedia/operation.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | class Operation: 4 | @abstractmethod 5 | def operate(self, q, dir, files): 6 | pass 7 | 8 | @abstractmethod 9 | def initialize(self, q, dir): 10 | pass 11 | 12 | class PrintFilesOperation(Operation): 13 | def operate(self, q, _, files): 14 | q.info(f"{len(files)} file(s)") 15 | for file in files: 16 | q.info(f" {file.name}") 17 | pass 18 | -------------------------------------------------------------------------------- /tests/test_verify.py: -------------------------------------------------------------------------------- 1 | from automedia import main 2 | import pytest 3 | 4 | @pytest.mark.parametrize("dir", [1, 2]) 5 | def test_verify_good(dir): 6 | result = main.do_main(['', '--symlinks=allowfile', '--root', f'tests/verify-test-{dir}', 'verify']) 7 | assert result == 0 8 | 9 | @pytest.mark.parametrize("dir", [1, 2, 3]) 10 | def test_verify_bad(dir): 11 | result = main.do_main(['', '--symlinks=allowfile', '--root', f'tests/verify-test-bad-{dir}', 'verify']) 12 | assert result == 1 13 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "twine", "build"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "automedia" 7 | version = "0.9" 8 | description = "Automedia is a tool to manage large media libraries, whether it be audio or video." 9 | license = { text = "GNU General Public License v3 (GPLv3)" } 10 | readme = "README.md" 11 | authors = [ 12 | { name="Matt Mastracci", email="matthew@mastracci.com" }, 13 | { name="Estefania Barreto-Ojeda", email="estefania@ojeda-e.com" }, 14 | ] 15 | 16 | [project.scripts] 17 | automedia = "automedia.main:main" 18 | -------------------------------------------------------------------------------- /tests/test_transcode.py: -------------------------------------------------------------------------------- 1 | from automedia import main 2 | import pytest 3 | 4 | @pytest.mark.parametrize("dir", [1, 2]) 5 | def test_transcode_good(tmp_path, dir): 6 | print(dir) 7 | result = main.do_main(['', '--symlinks=allowfile', '--root', f'tests/transcode-test-{dir}', 'transcode', '--preset', 'aac-64k', '--output', str(tmp_path)]) 8 | assert result == 0 9 | 10 | @pytest.mark.parametrize("dir", [1]) 11 | def test_transcode_bad(tmp_path, dir): 12 | print(dir) 13 | result = main.do_main(['', '--symlinks=allowfile', '--root', f'tests/transcode-test-bad-{dir}', 'transcode', '--preset', 'aac-64k', '--output', str(tmp_path)]) 14 | assert result == 1 15 | -------------------------------------------------------------------------------- /src/automedia/docker.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | class Docker: 4 | def __init__(self, pwd, prefixes) -> None: 5 | if pwd == None or prefixes == None: 6 | self.docker_prefix = None 7 | return 8 | 9 | # Test each candidate to see if we can find the pwd 10 | pwd = Path(pwd) 11 | prefixes = [Path(x) for x in prefixes] 12 | for candidate in prefixes: 13 | if (candidate / pwd.relative_to('/')).exists(): 14 | self.docker_prefix = candidate 15 | self.pwd = candidate / pwd.relative_to('/') 16 | return 17 | 18 | raise BaseException("Unable to locate docker root") 19 | 20 | def none(): 21 | return Docker(None, None) 22 | 23 | def dockerize_path(self, path): 24 | path = Path(path) 25 | if self.docker_prefix is None: 26 | return path 27 | if path.is_absolute(): 28 | return self.docker_prefix / Path(path).relative_to('/') 29 | return self.pwd / path 30 | -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu -o pipefail 3 | 4 | if [[ "$#" != "1" || "$1" == "--help" ]]; then 5 | echo usage: "$0" [version] 6 | echo ' version numeric version plus hyphen/period only (ie: 0.1, 1.0-beta, 2.1, etc)' 7 | exit 1 8 | fi 9 | 10 | git diff --exit-code >/dev/null || (echo 'Uncommitted changes, aborting!'; exit 2) 11 | git diff --cached --exit-code >/dev/null || (echo 'Uncommitted changes, aborting!'; exit 2) 12 | 13 | VERSION=v"$1" 14 | RAW_VERSION="$1" 15 | echo Building version "$VERSION"... 16 | 17 | sed -I.bak 's/version =.*/version = "'$RAW_VERSION'"/g' pyproject.toml && rm pyproject.toml.bak 18 | git diff --exit-code >/dev/null || (git add pyproject.toml && git commit -m "Bump version to $VERSION") 19 | git tag -d "$VERSION" >/dev/null 2>&1 || true 20 | git tag "$VERSION" 21 | 22 | python3 -m build 23 | twine check dist/* 24 | docker build . --build-arg AUTOMEDIA_VERSION="$RAW_VERSION" -t mmastrac/automedia:$RAW_VERSION -t mmastrac/automedia:latest 25 | 26 | echo 'Now publish the package:' 27 | echo 28 | echo 'docker push mmastrac/automedia:latest' 29 | echo 'docker push mmastrac/automedia:'$RAW_VERSION 30 | echo 'twine upload dist/automedia-'$RAW_VERSION'*' 31 | echo 'git push --tags' 32 | -------------------------------------------------------------------------------- /src/automedia/ffmpeg.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | import re 4 | 5 | class MediaType(Enum): 6 | Video = 0, 7 | Audio = 1, 8 | Image = 2, 9 | 10 | def compile_extension_regex(*extensions): 11 | return re.compile('|'.join([f'\\.{e}' for e in extensions]), flags=re.IGNORECASE) 12 | 13 | FFMPEG_SUPPORTED_EXTENSIONS_BY_TYPE = { 14 | MediaType.Video: ["mp4", "m4v", "mov", "avi", "mkv", "mpeg", "mpg"], 15 | MediaType.Audio: ["aac", "mp3", "flac", "ogg", "m4a", "wav", "wma"], 16 | MediaType.Image: ["jpg", "jpeg", "png", "gif"] 17 | } 18 | FFMPEG_SUPPORTED_EXTENSIONS = sum(FFMPEG_SUPPORTED_EXTENSIONS_BY_TYPE.values(), []) 19 | FFMPEG_SUPPORTED_EXTENSIONS_REGEX = {k: compile_extension_regex(*v) for k, v in FFMPEG_SUPPORTED_EXTENSIONS_BY_TYPE.items()} 20 | 21 | def ffmpeg_supports(filename): 22 | for type in FFMPEG_SUPPORTED_EXTENSIONS_BY_TYPE.keys(): 23 | if ffmpeg_supports_type(type, filename): 24 | return True 25 | return False 26 | 27 | def ffmpeg_supports_types(types, filename): 28 | for type in types: 29 | if ffmpeg_supports_type(type, filename): 30 | return True 31 | return False 32 | 33 | def ffmpeg_supports_type(type, filename): 34 | return FFMPEG_SUPPORTED_EXTENSIONS_REGEX[type].match(filename.suffix) 35 | -------------------------------------------------------------------------------- /src/automedia/ffmpeg_validator.py: -------------------------------------------------------------------------------- 1 | from .ffmpeg import ffmpeg_supports 2 | from .jobqueue import JobQueue 3 | from .forward_progress import subprocess_forward_progress 4 | from .operation import Operation 5 | 6 | FFMPEG_VERIFY_ARGS = [ 7 | "-xerror", 8 | "-v", "error", 9 | "-i", "-", 10 | "-f", "null", 11 | "-" 12 | ] 13 | 14 | def ffmpeg_validate(input, timeout=10, executable="ffmpeg", progress_callback=None): 15 | return subprocess_forward_progress(input, FFMPEG_VERIFY_ARGS, executable, timeout=timeout, progress_callback=progress_callback) 16 | 17 | class FFMPEGValidateOperation(Operation): 18 | def initialize(self, q, dir): 19 | q.info(f"Verifying internal consistency of media files: ffmpeg {' '.join(FFMPEG_VERIFY_ARGS)} < [file] > /dev/null") 20 | 21 | def operate(self, q: JobQueue, dir, files): 22 | stats = { 'good': 0, 'bad': 0, 'ignored': 0 } 23 | for file in files: 24 | q.submit(file.name, lambda q: self._job(q, stats, file)) 25 | q.wait() 26 | if stats['ignored']: 27 | q.info(f"{stats['good']} good file(s), {stats['bad']} bad file(s), {stats['ignored']} ignored file(s)") 28 | elif stats['bad']: 29 | q.info(f"{stats['good']} good file(s), {stats['bad']} bad file(s)") 30 | else: 31 | q.info(f"{stats['good']} good file(s)") 32 | 33 | def _job(self, q, stats, file): 34 | if ffmpeg_supports(file): 35 | errors = ffmpeg_validate(file) 36 | if errors: 37 | stats['bad'] += 1 38 | q.error(errors) 39 | else: 40 | stats['good'] += 1 41 | else: 42 | stats['ignored'] += 1 43 | 44 | if __name__ == '__main__': 45 | import sys 46 | errors = ffmpeg_validate(sys.argv[1], timeout=1, executable="ffmpeg") 47 | if errors: 48 | print(errors) 49 | else: 50 | print("Ok!") 51 | -------------------------------------------------------------------------------- /src/automedia/jobqueue.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | @dataclass 4 | class JobResults: 5 | errors: int = 0 6 | 7 | """ 8 | Single-threaded job queue. This should be turned into a parallel job queue at some point. 9 | """ 10 | class JobQueue: 11 | def __init__(self, name=None, parent=None) -> None: 12 | self.name = name 13 | self.parent = parent 14 | self.waited = False 15 | self.subs = [] 16 | self.logs = [] 17 | self.results = parent.results if parent else JobResults() 18 | 19 | def __del__(self): 20 | if not self.waited and len(self.subs) > 0: 21 | print(f'X[{self._name()}]: Failed to wait for subordinate jobs!', flush=True) 22 | if self.logs: 23 | print(f'X[{self._name()}]: Failed to print logs!', flush=True) 24 | 25 | def submit(self, name, job): 26 | if name is None and self.name is not None: 27 | raise Exception('Queue must have a name if parent queue has a name') 28 | sub = JobQueue(name=name, parent=self) 29 | self.subs.append(sub) 30 | job(sub) 31 | sub.flush_logs() 32 | 33 | def flush_logs(self): 34 | for level, msg in self.logs: 35 | print(f'{level}[{self._name()}]: {msg}', flush=True) 36 | self.logs = [] 37 | 38 | def error(self, msg): 39 | self.results.errors += 1 40 | self._log("E", msg) 41 | 42 | def info(self, msg): 43 | self._log("I", msg) 44 | 45 | def warning(self, msg): 46 | self._log("W", msg) 47 | 48 | def _log(self, level, msg): 49 | self.logs.append((level, msg)) 50 | 51 | def wait(self): 52 | self.waited = True 53 | return self.results 54 | 55 | def is_root(self): 56 | return self.parent is None or self.name is None 57 | 58 | def _name(self): 59 | if self.is_root(): 60 | return '(root)' 61 | else: 62 | return f'{self.parent._name()}/{self.name}' 63 | -------------------------------------------------------------------------------- /src/automedia/ffmpeg_transcoder.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from pathlib import Path 3 | from typing import List 4 | 5 | from .ffmpeg import MediaType, ffmpeg_supports_types 6 | from .forward_progress import subprocess_forward_progress 7 | from .operation import Operation 8 | 9 | @dataclass 10 | class FFMPEGPreset: 11 | ext: str 12 | args: List[str] 13 | 14 | FFMPEG_TRANSCODE_BASE_ARGS = [ 15 | '-xerror', 16 | '-v', 'error', 17 | '-y', 18 | '-i', '-'] 19 | FFMPEG_PRESETS = { 20 | 'aac-64k': FFMPEGPreset(ext='m4a', args=['-vn', '-c:a', 'aac', '-b:a', '64k', '-f', 'mp4']), 21 | 'aac-128k': FFMPEGPreset(ext='m4a', args=['-vn', '-c:a', 'aac', '-b:a', '128k', '-f', 'mp4']), 22 | 'mp3-128k': FFMPEGPreset(ext='mp3', args=['-vn', '-c:a', 'mp3', '-b:a', '128k', '-f', 'mp3']), 23 | 'mp3-320k': FFMPEGPreset(ext='mp3', args=['-vn', '-c:a', 'mp3', '-b:a', '320k', '-f', 'mp3']), 24 | 'flac': FFMPEGPreset(ext='flac', args=['-vn', '-c:a', 'flac', '-f', 'flac']), 25 | } 26 | 27 | class FFMPEGTranscoderOperation(Operation): 28 | def __init__(self, output_dir: Path, transcode_args: List[str], extension: str) -> None: 29 | self.output_dir = output_dir 30 | self.transcode_args = FFMPEG_TRANSCODE_BASE_ARGS + transcode_args 31 | self.extension = f'.{extension}' 32 | 33 | def initialize(self, q, dir): 34 | q.info(f"Transcoding files: ffmpeg {' '.join(self.transcode_args)} [output-file] < [input-file]") 35 | self.root = dir 36 | self.output_dir.mkdir(parents=True, exist_ok=True) 37 | 38 | def operate(self, q, dir, files: List[Path]): 39 | for file in files: 40 | q.submit(file.name, lambda q: self._job(q, file)) 41 | q.wait() 42 | 43 | def _job(self, q, file: Path): 44 | if ffmpeg_supports_types([MediaType.Video, MediaType.Audio], file): 45 | q.info(f"Transcoding...") 46 | out = self.output_dir / file.with_suffix(self.extension).relative_to(self.root) 47 | out.parent.mkdir(parents=True, exist_ok=True) 48 | args = self.transcode_args + [str(out)] 49 | errors = subprocess_forward_progress(file, args, "ffmpeg") 50 | if errors: 51 | q.error(errors) 52 | if out.exists(): 53 | q.info(f"Size: {file.stat().st_size // 1024}k -> {out.stat().st_size // 1024}k") 54 | else: 55 | q.error("Failed to transcode file") 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Automedia 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/automedia)](https://pypi.org/project/automedia/) 4 | 5 | Automedia is a tool to manage bitrot and formats in large media libraries, whether it be audio or video. 6 | 7 | The tool currently supports the following operations: 8 | 9 | * **Printing/logging to list media files**: view your media files at a glance 10 | * **Verification of media correctness via `ffmpeg`**: test-decoding of supported files to find bitrot 11 | * **Transcoding media libraries to other formats via `ffmpeg`**: archive your media in a lossless/high-quality format, transcode for daily use or for older devices 12 | * **PAR2 creation and verification**: prevent future bitrot of data at rest 13 | 14 | ## But why? 15 | 16 | You can replace much of what this tool does using `find`, `xargs`, `grep` and a dash of scripting, but `automedia` deals with all of 17 | the incantations itself and has some intelligence to handle interaction with complex tools like as `ffmpeg`. 18 | 19 | ## Requirements 20 | 21 | `automedia` requires the `ffmpeg` and `par2` executables to be fully operational. These must exist on the path for the application to function. It is 22 | recommended to use the dockerized version of this application as these requirements will be packaged up with it. 23 | 24 | ## Installation 25 | 26 | `automedia` can be installed via pip, or can be run with all binary dependencies in a Docker container. 27 | 28 | ### Via pip 29 | 30 | Automedia is available as a `pip` package. You can download it with: 31 | 32 | ```bash 33 | pip install automedia 34 | automedia --help 35 | ``` 36 | 37 | ### Via Docker 38 | 39 | If you wish to run Automedia via Docker, a script has been provided that transparently runs Automedia on your machine as 40 | if it were not running within a container (by mounting the entire root of your drive within the container). 41 | 42 | This script may be copied to a directory on your local `$PATH` and will automatically invoke the appropriate Docker container. 43 | 44 | ```bash 45 | cp automedia-docker /usr/local/bin/automedia 46 | automedia --help 47 | ``` 48 | 49 | ## Usage 50 | 51 | Print a list of media files we find: 52 | 53 | `automedia --root /media print` 54 | 55 | Verify the media files we find using `ffmpeg`: 56 | 57 | `automedia --root /media verify` 58 | 59 | Transcode the media files from `/media` to `/mnt/usb_stick` to 64k AAC format: 60 | 61 | `automedia --root /media transcode --preset aac-64k --output=/mnt/usb_stick` 62 | 63 | Transcode the media files from `/media` to `/mnt/usb_stick` to FLAC format: 64 | 65 | `automedia --root /media transcode --preset flac --output=/mnt/usb_stick` 66 | 67 | Create PAR2 files for the media files we find: 68 | 69 | `automedia --root /media par2-create` 70 | 71 | Verify PAR2 files for the media files we find: 72 | 73 | `automedia --root /media par2-verify` 74 | 75 | ## Screenshots 76 | 77 | ![An animated GIF showing automedia running a verify operation](docs/render.gif) 78 | -------------------------------------------------------------------------------- /src/automedia/path_scan.py: -------------------------------------------------------------------------------- 1 | from ntpath import realpath 2 | import os 3 | 4 | from dataclasses import dataclass 5 | from enum import Enum 6 | from pathlib import Path 7 | from stat import S_ISDIR, S_ISREG, S_ISLNK 8 | from typing import Callable, List 9 | 10 | class EntryType(Enum): 11 | DIR = 0 12 | FILE_MEDIA = 1 13 | FILE_IGNORE = 2 14 | FILE_SPAM = 3 15 | 16 | class SymlinkMode(Enum): 17 | Ignore = "ignore" 18 | Error = "error" 19 | Warn = "warn" 20 | Allow = "allow" 21 | AllowFile = "allowfile" 22 | AllowDir = "allowdir" 23 | 24 | @dataclass 25 | class PathScanResults: 26 | directory_list: List[Path] 27 | media_list: List[Path] 28 | unknown_extensions: List[str] 29 | 30 | class PathScanner: 31 | def __init__(self, 32 | symlink_mode = SymlinkMode.Warn, 33 | supported_extension_matcher: Callable[[Path], bool] = None, 34 | ignored_pattern_matcher: Callable[[Path], bool] = None, 35 | spam_files_matcher: Callable[[Path], bool] = None) -> None: 36 | 37 | self.symlink_warned = False 38 | self.symlink_mode = symlink_mode 39 | self.supported_extension_matcher = supported_extension_matcher 40 | self.ignored_pattern_matcher = ignored_pattern_matcher 41 | self.spam_files_matcher = spam_files_matcher 42 | 43 | def scan(self, q, dir): 44 | files = [] 45 | dirs = [] 46 | unknown_extensions = set() 47 | for f in os.listdir(dir): 48 | filename = dir / f 49 | if self.ignored_pattern_matcher(filename): 50 | continue 51 | try: 52 | st = os.lstat(filename) 53 | except Exception as e: 54 | q.error(f"Unrecoverable filesystem error while trying to read {f} ({e})") 55 | continue 56 | 57 | if S_ISLNK(st.st_mode): 58 | if self.symlink_mode == SymlinkMode.Error: 59 | q.error(f"Found unexpected symlink: {filename}") 60 | continue 61 | elif self.symlink_mode == SymlinkMode.Warn: 62 | if not self.symlink_warned: 63 | q.warning(f"Ignoring symlinks, pass --symlinks=allow to allow this behavior") 64 | self.symlink_warned = True 65 | continue 66 | elif self.symlink_mode in [SymlinkMode.Allow, SymlinkMode.AllowDir, SymlinkMode.AllowFile]: 67 | pass 68 | elif self.symlink_mode == SymlinkMode.Ignore: 69 | continue 70 | else: 71 | raise Exception(f"Unexpected symlink mode: {self.symlink_mode}") 72 | # Stat the underlying file or directory 73 | st = os.stat(filename) 74 | # Assume that the user only wanted the symlink type specified 75 | if S_ISREG(st.st_mode) and self.symlink_mode == SymlinkMode.AllowDir: 76 | q.error(f"File symlink found and only directories are allowed: {filename}") 77 | continue 78 | elif S_ISDIR(st.st_mode) and self.symlink_mode == SymlinkMode.AllowFile: 79 | q.error(f"Directory symlink found and only files are allowed: {filename}") 80 | continue 81 | 82 | # Ignore zero-length files 83 | if st.st_size == 0: 84 | continue 85 | if S_ISREG(st.st_mode): 86 | if self.supported_extension_matcher(filename): 87 | files.append(filename) 88 | else: 89 | if filename.suffix: 90 | unknown_extensions.add(filename.suffix) 91 | elif S_ISDIR(st.st_mode): 92 | dirs.append(filename) 93 | pass 94 | unknown_extensions = list(unknown_extensions) 95 | unknown_extensions.sort() 96 | files.sort() 97 | dirs.sort() 98 | 99 | return PathScanResults(directory_list=dirs, media_list=files, unknown_extensions=unknown_extensions) 100 | -------------------------------------------------------------------------------- /src/automedia/forward_progress.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import subprocess 4 | from pathlib import Path 5 | from subprocess import Popen, TimeoutExpired 6 | from threading import Thread 7 | from typing import List 8 | 9 | BUFFER_SIZE = 128 * 1024 10 | 11 | class ClosedException(BaseException): 12 | pass 13 | 14 | """ 15 | We don't need the full complexity of the python streams, so let's work directly off OS file descriptors. 16 | """ 17 | class BasicStream: 18 | def __init__(self, fd) -> None: 19 | self.fd = fd 20 | def read(self, n): 21 | if self.fd == -1: 22 | raise ClosedException("Read on closed stream") 23 | return os.read(self.fd, n) 24 | def write(self, data): 25 | if self.fd == -1: 26 | raise ClosedException("Write on closed stream") 27 | return os.write(self.fd, data) 28 | def close(self): 29 | if self.fd != -1: 30 | try: 31 | os.close(self.fd) 32 | except OSError: 33 | pass 34 | self.fd = -1 35 | 36 | """ 37 | Create a subprocess and ensure that it's always making forward progress by consuming stdin. 38 | """ 39 | def subprocess_forward_progress(input: Path, args: List[str], executable: str, timeout=10, progress_callback=None) -> List[str]: 40 | process = Popen(args=args, executable=executable, stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) 41 | stdin = BasicStream(process.stdin.fileno()) 42 | stderr = BasicStream(process.stderr.fileno()) 43 | errors = [] 44 | 45 | stderr_buffer = [b''] 46 | def stderr_reader(stderr, stderr_buffer): 47 | try: 48 | while True: 49 | r = stderr.read(128) 50 | stderr_buffer[0] += r 51 | except ClosedException: 52 | pass 53 | except: 54 | stderr.close() 55 | t1 = Thread(target=stderr_reader, args=(stderr, stderr_buffer,)) 56 | t1.start() 57 | 58 | last_write = [time.monotonic()] 59 | progress = [0] 60 | def stdin_writer(stdin, progress, last_write): 61 | try: 62 | with open(input, 'rb') as f: 63 | fl = f.seek(0, 2) 64 | f.tell() 65 | f.seek(0, 0) 66 | w = 0 67 | while True: 68 | bytes = f.read(BUFFER_SIZE) 69 | if not bytes: 70 | break 71 | w += len(bytes) 72 | stdin.write(bytes) 73 | last_write[0] = time.monotonic() 74 | progress[0] = w / fl 75 | except BrokenPipeError: 76 | errors.append("Process failed to read the entire input") 77 | except ClosedException: 78 | pass 79 | finally: 80 | stdin.close() 81 | t2 = Thread(target=stdin_writer, args=(stdin, progress, last_write,)) 82 | t2.start() 83 | 84 | try: 85 | while True: 86 | if progress_callback: 87 | try: 88 | progress_callback(progress[0]) 89 | except: 90 | pass 91 | if time.monotonic() - last_write[0] > timeout: 92 | errors.append("Process timed out reading from input stream") 93 | break 94 | if t1: 95 | t1.join(timeout=0) 96 | if not t1.is_alive(): 97 | t1 = None 98 | if t2: 99 | t2.join(timeout=0) 100 | if not t2.is_alive(): 101 | t2 = None 102 | try: 103 | ret = process.wait(timeout=0.01) 104 | if ret != 0: 105 | errors.append(f"Process failed with exit code {ret}") 106 | break 107 | except TimeoutExpired: 108 | pass 109 | except KeyboardInterrupt as e: 110 | errors.append("Interrupted by user") 111 | raise e 112 | except: 113 | errors.append("Failed for unknown reason") 114 | break 115 | finally: 116 | stdin.close() 117 | stderr.close() 118 | Thread(target=lambda: process.kill()).start() 119 | if t1: 120 | t1.join() 121 | if t2: 122 | t2.join() 123 | if progress_callback: 124 | try: 125 | progress_callback(progress[1]) 126 | except: 127 | pass 128 | if len(stderr_buffer[0]) > 0: 129 | errors.append("Process wrote to error stream: " + str(stderr_buffer[0], encoding='utf8', errors='replace')) 130 | 131 | return errors 132 | -------------------------------------------------------------------------------- /src/automedia/par2.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | from enum import Enum 5 | from pathlib import Path 6 | from typing import List 7 | from subprocess import Popen 8 | 9 | from .operation import Operation 10 | 11 | DEFAULT_PAR2_CREATE_ARGS = ' '.join(['-u', '-n3', '-r10']) 12 | DEFAULT_PAR2_VERIFY_ARGS = ' '.join(['-N']) 13 | 14 | RECOVERY_LIST_HEADER = "[media-tools-v1]" 15 | 16 | class RecoveryList: 17 | def __init__(self, files) -> None: 18 | self.files = files 19 | 20 | def read(file: Path): 21 | with open(file, 'rt') as f: 22 | header = f.readline() 23 | if header.rstrip() != RECOVERY_LIST_HEADER: 24 | raise Exception(f"Invalid {file.name} found (header was {header}), cannot create parity files") 25 | old_files = [x.rstrip() for x in f.readlines()] 26 | old_files.sort() 27 | return RecoveryList(old_files) 28 | 29 | def write(self, file: Path): 30 | with open(file, 'wt') as f: 31 | f.write("[media-tools-v1]\n") 32 | f.write('\n'.join([x.name for x in self.files])) 33 | 34 | class RecoveryListState(Enum): 35 | MISSING = 0 36 | UP_TO_DATE = 1 37 | ERROR = 2 38 | 39 | class Par2Operation(Operation): 40 | def __init__(self, args, recovery_name) -> None: 41 | self.args = list(args) 42 | self.recovery_name = recovery_name 43 | 44 | def recovery_list(self, dir): 45 | return dir / f'{self.recovery_name}.filelist' 46 | 47 | def par2_index(self, dir): 48 | return dir / f'{self.recovery_name}.par2' 49 | 50 | def validate_recovery_list(self, q, dir: Path, files: List[Path]) -> RecoveryListState: 51 | if not self.par2_index(dir).exists(): 52 | if self.recovery_list(dir).exists(): 53 | q.warning("File list exists, but PAR2 does not exist") 54 | return RecoveryListState.MISSING 55 | if self.recovery_list(dir).exists(): 56 | try: 57 | list = RecoveryList.read(self.recovery_list(dir)) 58 | if list.files != [x.name for x in files]: 59 | q.warning("PAR2 exists, but is out-of-date") 60 | q.warning(list.files) 61 | q.warning([x.name for x in files]) 62 | else: 63 | q.info("PAR2 exists, and is up-to-date") 64 | return RecoveryListState.UP_TO_DATE 65 | except Exception as e: 66 | q.error(str(e)) 67 | return RecoveryListState.ERROR 68 | return RecoveryListState.MISSING 69 | 70 | def run_par2(self, q, dir, args): 71 | # q.info(args) 72 | cmd = Popen(args, executable="par2", encoding="utf8", errors="", cwd=dir, stdout=subprocess.PIPE, stdin=subprocess.DEVNULL, stderr=subprocess.PIPE) 73 | stdout, stderr = cmd.communicate() 74 | if stderr: 75 | q.error(stderr) 76 | elif not stdout.find("\nDone"): 77 | q.error(stdout) 78 | elif cmd.returncode != 0: 79 | q.error(f"PAR2 returned a non-zero errorcode: {cmd.returncode}") 80 | else: 81 | return True 82 | return False 83 | 84 | def initialize(self, q, dir): 85 | self.root_args = args = ['par2', self.COMMAND] + self.args + ['--', f'{self.recovery_name}'] 86 | q.info(f"{self.VERB} par2 files: {' '.join(self.root_args + ['[files...]'])}") 87 | 88 | class CreatePar2Operation(Par2Operation): 89 | COMMAND = 'create' 90 | VERB = 'Creating' 91 | def operate(self, q, dir, files): 92 | if self.validate_recovery_list(q, dir, files) != RecoveryListState.MISSING: 93 | return 94 | args = self.root_args + [x.name for x in files] 95 | if self.run_par2(q, dir, args): 96 | if self.par2_index(dir).exists(): 97 | RecoveryList(files).write(self.recovery_list(dir)) 98 | q.info("Done") 99 | else: 100 | q.warning("No PAR2 files were generated") 101 | 102 | class VerifyPar2Operation(Par2Operation): 103 | COMMAND = 'verify' 104 | VERB = 'Verifying' 105 | def operate(self, q, dir, files): 106 | if self.validate_recovery_list(q, dir, files) != RecoveryListState.UP_TO_DATE: 107 | q.warning("Unable to verify directory") 108 | return 109 | args = self.root_args + [x.name for x in files] 110 | if self.run_par2(q, dir, args): 111 | # If the user requested removal of PAR2 files, we should unlink our recovery list too 112 | if not self.par2_index(dir).exists(): 113 | q.warning("PAR2 files were removed after verification") 114 | os.unlink(self.recovery_list(dir)) 115 | -------------------------------------------------------------------------------- /src/automedia/main.py: -------------------------------------------------------------------------------- 1 | from concurrent.futures import process 2 | from pathlib import Path 3 | import argparse 4 | import re 5 | import shlex 6 | import sys 7 | import importlib.metadata 8 | 9 | from .docker import Docker 10 | from .path_scan import PathScanner, SymlinkMode 11 | from .jobqueue import JobQueue 12 | from .ffmpeg import FFMPEG_SUPPORTED_EXTENSIONS 13 | from .ffmpeg_validator import FFMPEGValidateOperation 14 | from .ffmpeg_transcoder import FFMPEG_PRESETS, FFMPEGTranscoderOperation 15 | from .par2 import CreatePar2Operation, VerifyPar2Operation, DEFAULT_PAR2_CREATE_ARGS, DEFAULT_PAR2_VERIFY_ARGS 16 | from .operation import Operation, PrintFilesOperation 17 | 18 | """ 19 | Default precious extensions that we want to preserve w/PAR2. 20 | """ 21 | DEFAULT_EXTENSIONS = ','.join(FFMPEG_SUPPORTED_EXTENSIONS + [ 22 | # Docs 23 | "pdf", 24 | # Subtitles 25 | "srt", "idx", "sub", 26 | # Chiptunes 27 | "d64", "mod", "s3m"]) 28 | DEFAULT_IGNORE_FILES = ','.join([r"\.DS_Store", r"Thumbs\.db", r"\._.*", r".*\.par2", r".*\.filelist"]) 29 | DEFAULT_SPAM_FILES =','.join(["RARBG.txt", "RARBG_DO_NOT_MIRROR.exe", "WWW.YIFY-TORRENTS.COM.jpg", "www.YTS.AM.jpg", "WWW.YTS.TO.jpg", "www.YTS.LT.jpg"]) 30 | 31 | def process_dir(q: JobQueue, scanner: PathScanner, dir: Path, op: Operation): 32 | results = scanner.scan(q, dir) 33 | if results.unknown_extensions: 34 | q.warning(f"Unknown extensions found in path: {' '.join(results.unknown_extensions)}") 35 | if results.media_list: 36 | op.operate(q, dir, results.media_list) 37 | for dir in results.directory_list: 38 | q.submit(dir.name, lambda q: process_dir(q, scanner, dir, op)) 39 | q.wait() 40 | 41 | def compile_extension_regex(extensions): 42 | return re.compile('|'.join([f'\\.{e}' for e in extensions.split(',')]), flags=re.IGNORECASE) 43 | 44 | def compile_ignore_regex(files): 45 | return re.compile('|'.join([f'({f})' for f in files.split(',')])) 46 | 47 | def do_main(args): 48 | try: 49 | __version__ = importlib.metadata.version(__package__ or __name__) 50 | except: 51 | __version__ = "(dev)" 52 | parser = argparse.ArgumentParser(description=f'automedia {__version__}: Process media directories to validate and add parity files') 53 | parser.add_argument("--hidden-container-prefix", dest="container_prefix", action="store", help=argparse.SUPPRESS) 54 | parser.add_argument("--hidden-container-pwd", dest="container_pwd", action="store", help=argparse.SUPPRESS) 55 | parser.add_argument("--root", required=True, dest="root_dir", action="store", help="root directory for media") 56 | parser.add_argument("--symlinks", dest="symlink_mode", default=SymlinkMode.Warn.value, choices=[e.value for e in SymlinkMode], action="store", help="sets the symlink-following behavior (silently ignore, allow in all or some cases, or error)") 57 | parser.add_argument("--extensions", default=DEFAULT_EXTENSIONS, help=f"file extensions to include in processing (default {DEFAULT_EXTENSIONS})") 58 | parser.add_argument("--ignore", default=DEFAULT_IGNORE_FILES, help=f"file regular expressions to completely exclude in processing (default {DEFAULT_IGNORE_FILES})") 59 | commands = parser.add_subparsers(dest="command", required=True, help="sub-command help (use sub-command --help for more info)") 60 | verify_cmd = commands.add_parser("verify", help="verify media files are corruption-free with FFMPEG") 61 | transcode_cmd = commands.add_parser("transcode", help="transcode media files with FFMPEG") 62 | transcode_cmd.add_argument("--preset", required=True, choices=FFMPEG_PRESETS.keys(), help=f"output format preset (one of {' '.join(FFMPEG_PRESETS.keys())})") 63 | transcode_cmd.add_argument("--output", required=True, help=f"output directory") 64 | print_cmd = commands.add_parser("print", help="print all media files") 65 | par2_create_cmd = commands.add_parser("par2-create", help="create a PAR2 archive in each directory") 66 | par2_create_cmd.add_argument("--par2-args", default=DEFAULT_PAR2_CREATE_ARGS, help=f"arguments to pass to PAR2 (default {DEFAULT_PAR2_CREATE_ARGS})") 67 | par2_create_cmd.add_argument("--name", dest="par2_name", default="recovery", help="recovery filename (for .par2 and .filelist files)") 68 | par2_verify_cmd = commands.add_parser("par2-verify", help="verify the PAR2 archive in each directory") 69 | par2_verify_cmd.add_argument("--par2-args", default=DEFAULT_PAR2_VERIFY_ARGS, help=f"arguments to pass to PAR2 (default {DEFAULT_PAR2_VERIFY_ARGS})") 70 | par2_verify_cmd.add_argument("--name", dest="par2_name", default="recovery", help="recovery filename (for .par2 and .filelist files)") 71 | 72 | args = parser.parse_args(args[1:]) 73 | if args.container_pwd and args.container_prefix: 74 | docker = Docker(args.container_pwd, args.container_prefix.split(',')) 75 | else: 76 | docker = Docker.none() 77 | 78 | root = docker.dockerize_path(args.root_dir) 79 | if not root.is_dir(): 80 | print(f"Root must be a directory: {args.root_dir}") 81 | sys.exit(1) 82 | 83 | extension_regex = compile_extension_regex(args.extensions) 84 | ignore_regex = compile_ignore_regex(args.ignore) 85 | if args.command == 'verify': 86 | operation = FFMPEGValidateOperation() 87 | elif args.command == 'transcode': 88 | preset = FFMPEG_PRESETS[args.preset] 89 | operation = FFMPEGTranscoderOperation(docker.dockerize_path(args.output), preset.args, preset.ext) 90 | elif args.command == 'print': 91 | operation = PrintFilesOperation() 92 | elif args.command == 'par2-create': 93 | operation = CreatePar2Operation(shlex.split(args.par2_args), args.par2_name) 94 | elif args.command == 'par2-verify': 95 | operation = VerifyPar2Operation(shlex.split(args.par2_args), args.par2_name) 96 | else: 97 | print("Unexpected operation") 98 | sys.exit(1) 99 | q = JobQueue() 100 | 101 | scanner = PathScanner( 102 | symlink_mode=SymlinkMode(args.symlink_mode), 103 | supported_extension_matcher=lambda p: p.suffix and extension_regex.fullmatch(p.suffix), 104 | ignored_pattern_matcher=lambda p: ignore_regex.fullmatch(p.name), 105 | spam_files_matcher=lambda _: False) 106 | 107 | # Allow the operation to initalize and log if needed 108 | operation.initialize(q, root) 109 | q.flush_logs() 110 | 111 | q.submit(None, lambda q: process_dir(q, scanner, root, operation)) 112 | results = q.wait() 113 | if results.errors: 114 | return 1 115 | return 0 116 | 117 | def main(args=sys.argv): 118 | try: 119 | sys.exit(do_main(args)) 120 | except KeyboardInterrupt: 121 | print("Interrupted by user!") 122 | -------------------------------------------------------------------------------- /docs/recording.yml: -------------------------------------------------------------------------------- 1 | # The configurations that used for the recording, feel free to edit them 2 | config: 3 | 4 | # Specify a command to be executed 5 | # like `/bin/bash -l`, `ls`, or any other commands 6 | # the default is bash for Linux 7 | # or powershell.exe for Windows 8 | command: bash --noprofile 9 | 10 | # Specify the current working directory path 11 | # the default is the current working directory path 12 | cwd: /Users/matt/Documents/github/automedia 13 | 14 | # Export additional ENV variables 15 | env: 16 | recording: true 17 | PS1: "$ " 18 | BASH_SILENCE_DEPRECATION_WARNING: 1 19 | 20 | # Explicitly set the number of columns 21 | # or use `auto` to take the current 22 | # number of columns of your shell 23 | cols: 80 24 | 25 | # Explicitly set the number of rows 26 | # or use `auto` to take the current 27 | # number of rows of your shell 28 | rows: 20 29 | 30 | # Amount of times to repeat GIF 31 | # If value is -1, play once 32 | # If value is 0, loop indefinitely 33 | # If value is a positive number, loop n times 34 | repeat: 0 35 | 36 | # Quality 37 | # 1 - 100 38 | quality: 100 39 | 40 | # Delay between frames in ms 41 | # If the value is `auto` use the actual recording delays 42 | frameDelay: auto 43 | 44 | # Maximum delay between frames in ms 45 | # Ignored if the `frameDelay` isn't set to `auto` 46 | # Set to `auto` to prevent limiting the max idle time 47 | maxIdleTime: 2000 48 | 49 | # The surrounding frame box 50 | # The `type` can be null, window, floating, or solid` 51 | # To hide the title use the value null 52 | # Don't forget to add a backgroundColor style with a null as type 53 | frameBox: 54 | type: floating 55 | title: Shell 56 | style: 57 | border: 0px black solid 58 | # boxShadow: none 59 | # margin: 0px 60 | 61 | # Add a watermark image to the rendered gif 62 | # You need to specify an absolute path for 63 | # the image on your machine or a URL, and you can also 64 | # add your own CSS styles 65 | watermark: 66 | imagePath: null 67 | style: 68 | position: absolute 69 | right: 15px 70 | bottom: 15px 71 | width: 100px 72 | opacity: 0.9 73 | 74 | # Cursor style can be one of 75 | # `block`, `underline`, or `bar` 76 | cursorStyle: block 77 | 78 | # Font family 79 | # You can use any font that is installed on your machine 80 | # in CSS-like syntax 81 | fontFamily: "Monaco, Lucida Console, Ubuntu Mono, Monospace" 82 | 83 | # The size of the font 84 | fontSize: 12 85 | 86 | # The height of lines 87 | lineHeight: 1 88 | 89 | # The spacing between letters 90 | letterSpacing: 0 91 | 92 | # Theme 93 | theme: 94 | background: "transparent" 95 | foreground: "#afafaf" 96 | cursor: "#c7c7c7" 97 | black: "#232628" 98 | red: "#fc4384" 99 | green: "#b3e33b" 100 | yellow: "#ffa727" 101 | blue: "#75dff2" 102 | magenta: "#ae89fe" 103 | cyan: "#708387" 104 | white: "#d5d5d0" 105 | brightBlack: "#626566" 106 | brightRed: "#ff7fac" 107 | brightGreen: "#c8ed71" 108 | brightYellow: "#ebdf86" 109 | brightBlue: "#75dff2" 110 | brightMagenta: "#ae89fe" 111 | brightCyan: "#b1c6ca" 112 | brightWhite: "#f9f9f4" 113 | 114 | # Records, feel free to edit them 115 | records: 116 | - delay: 518 117 | content: "\e[?1034h" 118 | - delay: 762 119 | content: "$ p" 120 | - delay: 179 121 | content: i 122 | - delay: 45 123 | content: p 124 | - delay: 156 125 | content: ' ' 126 | - delay: 191 127 | content: i 128 | - delay: 74 129 | content: 'n' 130 | - delay: 42 131 | content: s 132 | - delay: 60 133 | content: t 134 | - delay: 56 135 | content: a 136 | - delay: 143 137 | content: l 138 | - delay: 131 139 | content: l 140 | - delay: 79 141 | content: ' ' 142 | - delay: 1193 143 | content: a 144 | - delay: 61 145 | content: u 146 | - delay: 118 147 | content: t 148 | - delay: 98 149 | content: o 150 | - delay: 207 151 | content: m 152 | - delay: 118 153 | content: e 154 | - delay: 51 155 | content: d 156 | - delay: 97 157 | content: i 158 | - delay: 75 159 | content: a 160 | - delay: 560 161 | content: "\r\n" 162 | - delay: 793 163 | content: "Collecting automedia\r\n" 164 | - delay: 283 165 | content: " Downloading automedia-0.6-py3-none-any.whl (23 kB)\r\n" 166 | - delay: 91 167 | content: "Installing collected packages: automedia\r\n" 168 | - delay: 24 169 | content: "Successfully installed automedia-0.6\r\n" 170 | - delay: 558 171 | content: '$ ' 172 | - delay: 6313 173 | content: a 174 | - delay: 95 175 | content: u 176 | - delay: 86 177 | content: t 178 | - delay: 99 179 | content: o 180 | - delay: 110 181 | content: "\a" 182 | - delay: 344 183 | content: m 184 | - delay: 152 185 | content: e 186 | - delay: 84 187 | content: 'dia ' 188 | - delay: 640 189 | content: '-' 190 | - delay: 125 191 | content: '-' 192 | - delay: 114 193 | content: r 194 | - delay: 208 195 | content: o 196 | - delay: 171 197 | content: o 198 | - delay: 677 199 | content: t 200 | - delay: 56 201 | content: ' ' 202 | - delay: 363 203 | content: / 204 | - delay: 251 205 | content: 'media' 206 | - delay: 64 207 | content: / 208 | - delay: 951 209 | content: J 210 | - delay: 154 211 | content: o 212 | - delay: 132 213 | content: hn\ Williams/ 214 | - delay: 1421 215 | content: ' ' 216 | - delay: 552 217 | content: v 218 | - delay: 55 219 | content: e 220 | - delay: 71 221 | content: r 222 | - delay: 155 223 | content: i 224 | - delay: 93 225 | content: f 226 | - delay: 133 227 | content: 'y' 228 | - delay: 642 229 | content: "\r\n" 230 | - delay: 667 231 | content: "I[(root)]: Verifying internal consistency of media files: ffmpeg -xerror -v error -i - -f null - < [file] > /dev/null\r\n" 232 | - delay: 4999 233 | content: "I[(root)/Star Wars, Episode III_ Revenge of the Sith_ Original Motion Picture Soundtrack]: 15 good file(s)\r\n" 234 | - delay: 5186 235 | content: "I[(root)/Star Wars, Episode II_ Attack of the Clones_ Original Motion Picture Soundtrack]: 13 good file(s)\r\n" 236 | - delay: 5797 237 | content: "I[(root)/Star Wars, Episode I_ The Phantom Menace_ Original Motion Picture Soundtrack]: 18 good file(s)\r\n" 238 | - delay: 10701 239 | content: "I[(root)/Star Wars, Episode VI_ Return of the Jedi_ The Original Motion Picture Soundtrack]: 27 good file(s)\r\n" 240 | - delay: 8616 241 | content: "I[(root)/Star Wars, Episode V_ The Empire Strikes Back_ The Original Motion Picture Soundtrack]: 23 good file(s)\r\n" 242 | - delay: 7720 243 | content: "I[(root)/Star Wars_ A New Hope]: 24 good file(s)\r\n" 244 | - delay: 3980 245 | content: "I[(root)/Star Wars_ The Corellian Edition]: 13 good file(s)\r\n" 246 | - delay: 6103 247 | content: "I[(root)/Star Wars_ The Force Awakens_ Original Motion Picture Soundtrack]: 23 good file(s)\r\n" 248 | - delay: 12939 249 | content: "I[(root)/Star Wars_ The Last Jedi_ Original Motion Picture Soundtrack]: 20 good file(s)\r\n" 250 | - delay: 31648 251 | content: "I[(root)/Star Wars_ The Ultimate Digital Collection]: 89 good file(s)\r\n" 252 | - delay: 14 253 | content: '$ ' 254 | - delay: 20000 255 | content: "\r\n" 256 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright © 2007 Free Software Foundation, Inc. 5 | 6 | Everyone is permitted to copy and distribute verbatim copies of this license 7 | document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The GNU General Public License is a free, copyleft license for software 12 | and other kinds of works. 13 | 14 | The licenses for most software and other practical works are designed to take 15 | away your freedom to share and change the works. By contrast, the GNU General 16 | Public License is intended to guarantee your freedom to share and change all 17 | versions of a program--to make sure it remains free software for all its users. 18 | We, the Free Software Foundation, use the GNU General Public License for most of 19 | our software; it applies also to any other work released this way by its 20 | authors. You can apply it to your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not price. Our 23 | General Public Licenses are designed to make sure that you have the freedom to 24 | distribute copies of free software (and charge for them if you wish), that you 25 | receive source code or can get it if you want it, that you can change the 26 | software or use pieces of it in new free programs, and that you know you can do 27 | these things. 28 | 29 | To protect your rights, we need to prevent others from denying you these rights 30 | or asking you to surrender the rights. Therefore, you have certain 31 | responsibilities if you distribute copies of the software, or if you modify it: 32 | responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether gratis or for a 35 | fee, you must pass on to the recipients the same freedoms that you received. You 36 | must make sure that they, too, receive or can get the source code. And you must 37 | show them these terms so they know their rights. 38 | 39 | Developers that use the GNU GPL protect your rights with two steps: (1) assert 40 | copyright on the software, and (2) offer you this License giving you legal 41 | permission to copy, distribute and/or modify it. 42 | 43 | For the developers' and authors' protection, the GPL clearly explains that there 44 | is no warranty for this free software. For both users' and authors' sake, the 45 | GPL requires that modified versions be marked as changed, so that their problems 46 | will not be attributed erroneously to authors of previous versions. 47 | 48 | Some devices are designed to deny users access to install or run modified 49 | versions of the software inside them, although the manufacturer can do so. This 50 | is fundamentally incompatible with the aim of protecting users' freedom to 51 | change the software. The systematic pattern of such abuse occurs in the area of 52 | products for individuals to use, which is precisely where it is most 53 | unacceptable. Therefore, we have designed this version of the GPL to prohibit 54 | the practice for those products. If such problems arise substantially in other 55 | domains, we stand ready to extend this provision to those domains in future 56 | versions of the GPL, as needed to protect the freedom of users. 57 | 58 | Finally, every program is threatened constantly by software patents. States 59 | should not allow patents to restrict development and use of software on 60 | general-purpose computers, but in those that do, we wish to avoid the special 61 | danger that patents applied to a free program could make it effectively 62 | proprietary. To prevent this, the GPL assures that patents cannot be used to 63 | render the program non-free. 64 | 65 | The precise terms and conditions for copying, distribution and modification 66 | follow. 67 | 68 | TERMS AND CONDITIONS 69 | 70 | 0. Definitions.“This License” refers to version 3 of the GNU General Public 71 | License. 72 | 73 | “Copyright” also means copyright-like laws that apply to other kinds of works, 74 | such as semiconductor masks. 75 | 76 | “The Program” refers to any copyrightable work licensed under this License. Each 77 | licensee is addressed as “you”. “Licensees” and “recipients” may be individuals 78 | or organizations. 79 | 80 | To “modify” a work means to copy from or adapt all or part of the work in a 81 | fashion requiring copyright permission, other than the making of an exact copy. 82 | The resulting work is called a “modified version” of the earlier work or a work 83 | “based on” the earlier work. 84 | 85 | A “covered work” means either the unmodified Program or a work based on the 86 | Program. 87 | 88 | To “propagate” a work means to do anything with it that, without permission, 89 | would make you directly or secondarily liable for infringement under applicable 90 | copyright law, except executing it on a computer or modifying a private copy. 91 | Propagation includes copying, distribution (with or without modification), 92 | making available to the public, and in some countries other activities as well. 93 | 94 | To “convey” a work means any kind of propagation that enables other parties to 95 | make or receive copies. Mere interaction with a user through a computer network, 96 | with no transfer of a copy, is not conveying. 97 | 98 | An interactive user interface displays “Appropriate Legal Notices” to the extent 99 | that it includes a convenient and prominently visible feature that (1) displays 100 | an appropriate copyright notice, and (2) tells the user that there is no 101 | warranty for the work (except to the extent that warranties are provided), that 102 | licensees may convey the work under this License, and how to view a copy of this 103 | License. If the interface presents a list of user commands or options, such as a 104 | menu, a prominent item in the list meets this criterion. 105 | 106 | 1. Source Code. The “source code” for a work means the preferred form of the 107 | work for making modifications to it. “Object code” means any non-source form of 108 | a work. 109 | 110 | A “Standard Interface” means an interface that either is an official standard 111 | defined by a recognized standards body, or, in the case of interfaces specified 112 | for a particular programming language, one that is widely used among developers 113 | working in that language. 114 | 115 | The “System Libraries” of an executable work include anything, other than the 116 | work as a whole, that (a) is included in the normal form of packaging a Major 117 | Component, but which is not part of that Major Component, and (b) serves only to 118 | enable use of the work with that Major Component, or to implement a Standard 119 | Interface for which an implementation is available to the public in source code 120 | form. A “Major Component”, in this context, means a major essential component 121 | (kernel, window system, and so on) of the specific operating system (if any) on 122 | which the executable work runs, or a compiler used to produce the work, or an 123 | object code interpreter used to run it. 124 | 125 | The “Corresponding Source” for a work in object code form means all the source 126 | code needed to generate, install, and (for an executable work) run the object 127 | code and to modify the work, including scripts to control those activities. 128 | However, it does not include the work's System Libraries, or general-purpose 129 | tools or generally available free programs which are used unmodified in 130 | performing those activities but which are not part of the work. For example, 131 | Corresponding Source includes interface definition files associated with source 132 | files for the work, and the source code for shared libraries and dynamically 133 | linked subprograms that the work is specifically designed to require, such as by 134 | intimate data communication or control flow between those subprograms and other 135 | parts of the work. 136 | 137 | The Corresponding Source need not include anything that users can regenerate 138 | automatically from other parts of the Corresponding Source. 139 | 140 | The Corresponding Source for a work in source code form is that same work. 141 | 142 | 2. Basic Permissions. All rights granted under this License are granted for the 143 | term of copyright on the Program, and are irrevocable provided the stated 144 | conditions are met. This License explicitly affirms your unlimited permission to 145 | run the unmodified Program. The output from running a covered work is covered by 146 | this License only if the output, given its content, constitutes a covered work. 147 | This License acknowledges your rights of fair use or other equivalent, as 148 | provided by copyright law. 149 | 150 | You may make, run and propagate covered works that you do not convey, without 151 | conditions so long as your license otherwise remains in force. You may convey 152 | covered works to others for the sole purpose of having them make modifications 153 | exclusively for you, or provide you with facilities for running those works, 154 | provided that you comply with the terms of this License in conveying all 155 | material for which you do not control copyright. Those thus making or running 156 | the covered works for you must do so exclusively on your behalf, under your 157 | direction and control, on terms that prohibit them from making any copies of 158 | your copyrighted material outside their relationship with you. 159 | 160 | Conveying under any other circumstances is permitted solely under the conditions 161 | stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 162 | 163 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work 164 | shall be deemed part of an effective technological measure under any applicable 165 | law fulfilling obligations under article 11 of the WIPO copyright treaty adopted 166 | on 20 December 1996, or similar laws prohibiting or restricting circumvention of 167 | such measures. 168 | 169 | When you convey a covered work, you waive any legal power to forbid 170 | circumvention of technological measures to the extent such circumvention is 171 | effected by exercising rights under this License with respect to the covered 172 | work, and you disclaim any intention to limit operation or modification of the 173 | work as a means of enforcing, against the work's users, your or third parties' 174 | legal rights to forbid circumvention of technological measures. 175 | 176 | 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's 177 | source code as you receive it, in any medium, provided that you conspicuously 178 | and appropriately publish on each copy an appropriate copyright notice; keep 179 | intact all notices stating that this License and any non-permissive terms added 180 | in accord with section 7 apply to the code; keep intact all notices of the 181 | absence of any warranty; and give all recipients a copy of this License along 182 | with the Program. 183 | 184 | You may charge any price or no price for each copy that you convey, and you may 185 | offer support or warranty protection for a fee. 186 | 187 | 5. Conveying Modified Source Versions. You may convey a work based on the 188 | Program, or the modifications to produce it from the Program, in the form of 189 | source code under the terms of section 4, provided that you also meet all of 190 | these conditions: 191 | 192 | a) The work must carry prominent notices stating that you modified it, and 193 | giving a relevant date. b) The work must carry prominent notices stating that it 194 | is released under this License and any conditions added under section 7. This 195 | requirement modifies the requirement in section 4 to “keep intact all notices”. 196 | c) You must license the entire work, as a whole, under this License to anyone 197 | who comes into possession of a copy. This License will therefore apply, along 198 | with any applicable section 7 additional terms, to the whole of the work, and 199 | all its parts, regardless of how they are packaged. This License gives no 200 | permission to license the work in any other way, but it does not invalidate such 201 | permission if you have separately received it. d) If the work has interactive 202 | user interfaces, each must display Appropriate Legal Notices; however, if the 203 | Program has interactive interfaces that do not display Appropriate Legal 204 | Notices, your work need not make them do so. A compilation of a covered work 205 | with other separate and independent works, which are not by their nature 206 | extensions of the covered work, and which are not combined with it such as to 207 | form a larger program, in or on a volume of a storage or distribution medium, is 208 | called an “aggregate” if the compilation and its resulting copyright are not 209 | used to limit the access or legal rights of the compilation's users beyond what 210 | the individual works permit. Inclusion of a covered work in an aggregate does 211 | not cause this License to apply to the other parts of the aggregate. 212 | 213 | 6. Conveying Non-Source Forms. You may convey a covered work in object code form 214 | under the terms of sections 4 and 5, provided that you also convey the 215 | machine-readable Corresponding Source under the terms of this License, in one of 216 | these ways: 217 | 218 | a) Convey the object code in, or embodied in, a physical product (including a 219 | physical distribution medium), accompanied by the Corresponding Source fixed on 220 | a durable physical medium customarily used for software interchange. b) Convey 221 | the object code in, or embodied in, a physical product (including a physical 222 | distribution medium), accompanied by a written offer, valid for at least three 223 | years and valid for as long as you offer spare parts or customer support for 224 | that product model, to give anyone who possesses the object code either (1) a 225 | copy of the Corresponding Source for all the software in the product that is 226 | covered by this License, on a durable physical medium customarily used for 227 | software interchange, for a price no more than your reasonable cost of 228 | physically performing this conveying of source, or (2) access to copy the 229 | Corresponding Source from a network server at no charge. c) Convey individual 230 | copies of the object code with a copy of the written offer to provide the 231 | Corresponding Source. This alternative is allowed only occasionally and 232 | noncommercially, and only if you received the object code with such an offer, in 233 | accord with subsection 6b. d) Convey the object code by offering access from a 234 | designated place (gratis or for a charge), and offer equivalent access to the 235 | Corresponding Source in the same way through the same place at no further 236 | charge. You need not require recipients to copy the Corresponding Source along 237 | with the object code. If the place to copy the object code is a network server, 238 | the Corresponding Source may be on a different server (operated by you or a 239 | third party) that supports equivalent copying facilities, provided you maintain 240 | clear directions next to the object code saying where to find the Corresponding 241 | Source. Regardless of what server hosts the Corresponding Source, you remain 242 | obligated to ensure that it is available for as long as needed to satisfy these 243 | requirements. e) Convey the object code using peer-to-peer transmission, 244 | provided you inform other peers where the object code and Corresponding Source 245 | of the work are being offered to the general public at no charge under 246 | subsection 6d. A separable portion of the object code, whose source code is 247 | excluded from the Corresponding Source as a System Library, need not be included 248 | in conveying the object code work. 249 | 250 | A “User Product” is either (1) a “consumer product”, which means any tangible 251 | personal property which is normally used for personal, family, or household 252 | purposes, or (2) anything designed or sold for incorporation into a dwelling. In 253 | determining whether a product is a consumer product, doubtful cases shall be 254 | resolved in favor of coverage. For a particular product received by a particular 255 | user, “normally used” refers to a typical or common use of that class of 256 | product, regardless of the status of the particular user or of the way in which 257 | the particular user actually uses, or expects or is expected to use, the 258 | product. A product is a consumer product regardless of whether the product has 259 | substantial commercial, industrial or non-consumer uses, unless such uses 260 | represent the only significant mode of use of the product. 261 | 262 | “Installation Information” for a User Product means any methods, procedures, 263 | authorization keys, or other information required to install and execute 264 | modified versions of a covered work in that User Product from a modified version 265 | of its Corresponding Source. The information must suffice to ensure that the 266 | continued functioning of the modified object code is in no case prevented or 267 | interfered with solely because modification has been made. 268 | 269 | If you convey an object code work under this section in, or with, or 270 | specifically for use in, a User Product, and the conveying occurs as part of a 271 | transaction in which the right of possession and use of the User Product is 272 | transferred to the recipient in perpetuity or for a fixed term (regardless of 273 | how the transaction is characterized), the Corresponding Source conveyed under 274 | this section must be accompanied by the Installation Information. But this 275 | requirement does not apply if neither you nor any third party retains the 276 | ability to install modified object code on the User Product (for example, the 277 | work has been installed in ROM). 278 | 279 | The requirement to provide Installation Information does not include a 280 | requirement to continue to provide support service, warranty, or updates for a 281 | work that has been modified or installed by the recipient, or for the User 282 | Product in which it has been modified or installed. Access to a network may be 283 | denied when the modification itself materially and adversely affects the 284 | operation of the network or violates the rules and protocols for communication 285 | across the network. 286 | 287 | Corresponding Source conveyed, and Installation Information provided, in accord 288 | with this section must be in a format that is publicly documented (and with an 289 | implementation available to the public in source code form), and must require no 290 | special password or key for unpacking, reading or copying. 291 | 292 | 7. Additional Terms.“Additional permissions” are terms that supplement the terms 293 | of this License by making exceptions from one or more of its conditions. 294 | Additional permissions that are applicable to the entire Program shall be 295 | treated as though they were included in this License, to the extent that they 296 | are valid under applicable law. If additional permissions apply only to part of 297 | the Program, that part may be used separately under those permissions, but the 298 | entire Program remains governed by this License without regard to the additional 299 | permissions. 300 | 301 | When you convey a copy of a covered work, you may at your option remove any 302 | additional permissions from that copy, or from any part of it. (Additional 303 | permissions may be written to require their own removal in certain cases when 304 | you modify the work.) You may place additional permissions on material, added by 305 | you to a covered work, for which you have or can give appropriate copyright 306 | permission. 307 | 308 | Notwithstanding any other provision of this License, for material you add to a 309 | covered work, you may (if authorized by the copyright holders of that material) 310 | supplement the terms of this License with terms: 311 | 312 | a) Disclaiming warranty or limiting liability differently from the terms of 313 | sections 15 and 16 of this License; or b) Requiring preservation of specified 314 | reasonable legal notices or author attributions in that material or in the 315 | Appropriate Legal Notices displayed by works containing it; or c) Prohibiting 316 | misrepresentation of the origin of that material, or requiring that modified 317 | versions of such material be marked in reasonable ways as different from the 318 | original version; or d) Limiting the use for publicity purposes of names of 319 | licensors or authors of the material; or e) Declining to grant rights under 320 | trademark law for use of some trade names, trademarks, or service marks; or f) 321 | Requiring indemnification of licensors and authors of that material by anyone 322 | who conveys the material (or modified versions of it) with contractual 323 | assumptions of liability to the recipient, for any liability that these 324 | contractual assumptions directly impose on those licensors and authors. All 325 | other non-permissive additional terms are considered “further restrictions” 326 | within the meaning of section 10. If the Program as you received it, or any part 327 | of it, contains a notice stating that it is governed by this License along with 328 | a term that is a further restriction, you may remove that term. If a license 329 | document contains a further restriction but permits relicensing or conveying 330 | under this License, you may add to a covered work material governed by the terms 331 | of that license document, provided that the further restriction does not survive 332 | such relicensing or conveying. 333 | 334 | If you add terms to a covered work in accord with this section, you must place, 335 | in the relevant source files, a statement of the additional terms that apply to 336 | those files, or a notice indicating where to find the applicable terms. 337 | 338 | Additional terms, permissive or non-permissive, may be stated in the form of a 339 | separately written license, or stated as exceptions; the above requirements 340 | apply either way. 341 | 342 | 8. Termination. You may not propagate or modify a covered work except as 343 | expressly provided under this License. Any attempt otherwise to propagate or 344 | modify it is void, and will automatically terminate your rights under this 345 | License (including any patent licenses granted under the third paragraph of 346 | section 11). 347 | 348 | However, if you cease all violation of this License, then your license from a 349 | particular copyright holder is reinstated (a) provisionally, unless and until 350 | the copyright holder explicitly and finally terminates your license, and (b) 351 | permanently, if the copyright holder fails to notify you of the violation by 352 | some reasonable means prior to 60 days after the cessation. 353 | 354 | Moreover, your license from a particular copyright holder is reinstated 355 | permanently if the copyright holder notifies you of the violation by some 356 | reasonable means, this is the first time you have received notice of violation 357 | of this License (for any work) from that copyright holder, and you cure the 358 | violation prior to 30 days after your receipt of the notice. 359 | 360 | Termination of your rights under this section does not terminate the licenses of 361 | parties who have received copies or rights from you under this License. If your 362 | rights have been terminated and not permanently reinstated, you do not qualify 363 | to receive new licenses for the same material under section 10. 364 | 365 | 9. Acceptance Not Required for Having Copies. You are not required to accept 366 | this License in order to receive or run a copy of the Program. Ancillary 367 | propagation of a covered work occurring solely as a consequence of using 368 | peer-to-peer transmission to receive a copy likewise does not require 369 | acceptance. However, nothing other than this License grants you permission to 370 | propagate or modify any covered work. These actions infringe copyright if you do 371 | not accept this License. Therefore, by modifying or propagating a covered work, 372 | you indicate your acceptance of this License to do so. 373 | 374 | 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered 375 | work, the recipient automatically receives a license from the original 376 | licensors, to run, modify and propagate that work, subject to this License. You 377 | are not responsible for enforcing compliance by third parties with this License. 378 | 379 | An “entity transaction” is a transaction transferring control of an 380 | organization, or substantially all assets of one, or subdividing an 381 | organization, or merging organizations. If propagation of a covered work results 382 | from an entity transaction, each party to that transaction who receives a copy 383 | of the work also receives whatever licenses to the work the party's predecessor 384 | in interest had or could give under the previous paragraph, plus a right to 385 | possession of the Corresponding Source of the work from the predecessor in 386 | interest, if the predecessor has it or can get it with reasonable efforts. 387 | 388 | You may not impose any further restrictions on the exercise of the rights 389 | granted or affirmed under this License. For example, you may not impose a 390 | license fee, royalty, or other charge for exercise of rights granted under this 391 | License, and you may not initiate litigation (including a cross-claim or 392 | counterclaim in a lawsuit) alleging that any patent claim is infringed by 393 | making, using, selling, offering for sale, or importing the Program or any 394 | portion of it. 395 | 396 | 11. Patents. A “contributor” is a copyright holder who authorizes use under this 397 | License of the Program or a work on which the Program is based. The work thus 398 | licensed is called the contributor's “contributor version”. 399 | 400 | A contributor's “essential patent claims” are all patent claims owned or 401 | controlled by the contributor, whether already acquired or hereafter acquired, 402 | that would be infringed by some manner, permitted by this License, of making, 403 | using, or selling its contributor version, but do not include claims that would 404 | be infringed only as a consequence of further modification of the contributor 405 | version. For purposes of this definition, “control” includes the right to grant 406 | patent sublicenses in a manner consistent with the requirements of this License. 407 | 408 | Each contributor grants you a non-exclusive, worldwide, royalty-free patent 409 | license under the contributor's essential patent claims, to make, use, sell, 410 | offer for sale, import and otherwise run, modify and propagate the contents of 411 | its contributor version. 412 | 413 | In the following three paragraphs, a “patent license” is any express agreement 414 | or commitment, however denominated, not to enforce a patent (such as an express 415 | permission to practice a patent or covenant not to sue for patent infringement). 416 | To “grant” such a patent license to a party means to make such an agreement or 417 | commitment not to enforce a patent against the party. 418 | 419 | If you convey a covered work, knowingly relying on a patent license, and the 420 | Corresponding Source of the work is not available for anyone to copy, free of 421 | charge and under the terms of this License, through a publicly available network 422 | server or other readily accessible means, then you must either (1) cause the 423 | Corresponding Source to be so available, or (2) arrange to deprive yourself of 424 | the benefit of the patent license for this particular work, or (3) arrange, in a 425 | manner consistent with the requirements of this License, to extend the patent 426 | license to downstream recipients. “Knowingly relying” means you have actual 427 | knowledge that, but for the patent license, your conveying the covered work in a 428 | country, or your recipient's use of the covered work in a country, would 429 | infringe one or more identifiable patents in that country that you have reason 430 | to believe are valid. 431 | 432 | If, pursuant to or in connection with a single transaction or arrangement, you 433 | convey, or propagate by procuring conveyance of, a covered work, and grant a 434 | patent license to some of the parties receiving the covered work authorizing 435 | them to use, propagate, modify or convey a specific copy of the covered work, 436 | then the patent license you grant is automatically extended to all recipients of 437 | the covered work and works based on it. 438 | 439 | A patent license is “discriminatory” if it does not include within the scope of 440 | its coverage, prohibits the exercise of, or is conditioned on the non-exercise 441 | of one or more of the rights that are specifically granted under this License. 442 | You may not convey a covered work if you are a party to an arrangement with a 443 | third party that is in the business of distributing software, under which you 444 | make payment to the third party based on the extent of your activity of 445 | conveying the work, and under which the third party grants, to any of the 446 | parties who would receive the covered work from you, a discriminatory patent 447 | license (a) in connection with copies of the covered work conveyed by you (or 448 | copies made from those copies), or (b) primarily for and in connection with 449 | specific products or compilations that contain the covered work, unless you 450 | entered into that arrangement, or that patent license was granted, prior to 28 451 | March 2007. 452 | 453 | Nothing in this License shall be construed as excluding or limiting any implied 454 | license or other defenses to infringement that may otherwise be available to you 455 | under applicable patent law. 456 | 457 | 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether 458 | by court order, agreement or otherwise) that contradict the conditions of this 459 | License, they do not excuse you from the conditions of this License. If you 460 | cannot convey a covered work so as to satisfy simultaneously your obligations 461 | under this License and any other pertinent obligations, then as a consequence 462 | you may not convey it at all. For example, if you agree to terms that obligate 463 | you to collect a royalty for further conveying from those to whom you convey the 464 | Program, the only way you could satisfy both those terms and this License would 465 | be to refrain entirely from conveying the Program. 466 | 467 | 13. Use with the GNU Affero General Public License. Notwithstanding any other 468 | provision of this License, you have permission to link or combine any covered 469 | work with a work licensed under version 3 of the GNU Affero General Public 470 | License into a single combined work, and to convey the resulting work. The terms 471 | of this License will continue to apply to the part which is the covered work, 472 | but the special requirements of the GNU Affero General Public License, section 473 | 13, concerning interaction through a network will apply to the combination as 474 | such. 475 | 476 | 14. Revised Versions of this License. The Free Software Foundation may publish 477 | revised and/or new versions of the GNU General Public License from time to time. 478 | Such new versions will be similar in spirit to the present version, but may 479 | differ in detail to address new problems or concerns. 480 | 481 | Each version is given a distinguishing version number. If the Program specifies 482 | that a certain numbered version of the GNU General Public License “or any later 483 | version” applies to it, you have the option of following the terms and 484 | conditions either of that numbered version or of any later version published by 485 | the Free Software Foundation. If the Program does not specify a version number 486 | of the GNU General Public License, you may choose any version ever published by 487 | the Free Software Foundation. 488 | 489 | If the Program specifies that a proxy can decide which future versions of the 490 | GNU General Public License can be used, that proxy's public statement of 491 | acceptance of a version permanently authorizes you to choose that version for 492 | the Program. 493 | 494 | Later license versions may give you additional or different permissions. 495 | However, no additional obligations are imposed on any author or copyright holder 496 | as a result of your choosing to follow a later version. 497 | 498 | 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT 499 | PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE 500 | COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT 501 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED 502 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 503 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS 504 | WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL 505 | NECESSARY SERVICING, REPAIR OR CORRECTION. 506 | 507 | 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR 508 | AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES 509 | AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 510 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT 511 | OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 512 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 513 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF 514 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 515 | 516 | 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and 517 | limitation of liability provided above cannot be given local legal effect 518 | according to their terms, reviewing courts shall apply local law that most 519 | closely approximates an absolute waiver of all civil liability in connection 520 | with the Program, unless a warranty or assumption of liability accompanies a 521 | copy of the Program in return for a fee. 522 | 523 | END OF TERMS AND CONDITIONS 524 | 525 | How to Apply These Terms to Your New Programs If you develop a new program, and 526 | you want it to be of the greatest possible use to the public, the best way to 527 | achieve this is to make it free software which everyone can redistribute and 528 | change under these terms. 529 | 530 | To do so, attach the following notices to the program. It is safest to attach 531 | them to the start of each source file to most effectively state the exclusion of 532 | warranty; and each file should have at least the “copyright” line and a pointer 533 | to where the full notice is found. 534 | 535 | 536 | Copyright (C) 537 | 538 | This program is free software: you can redistribute it and/or modify it 539 | under the terms of the GNU General Public License as published by the Free 540 | Software Foundation, either version 3 of the License, or (at your option) 541 | any later version. 542 | 543 | This program is distributed in the hope that it will be useful, but WITHOUT 544 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 545 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 546 | more details. 547 | 548 | You should have received a copy of the GNU General Public License along with 549 | this program. If not, see . 550 | Also add information on how to contact you by electronic and paper mail. 551 | 552 | If the program does terminal interaction, make it output a short notice like 553 | this when it starts in an interactive mode: 554 | 555 | Copyright (C) This program comes with 556 | ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, 557 | and you are welcome to redistribute it under certain conditions; type `show 558 | c' for details. 559 | 560 | The hypothetical commands `show w' and `show c' should show the appropriate 561 | parts of the General Public License. Of course, your program's commands might be 562 | different; for a GUI interface, you would use an “about box”. 563 | 564 | You should also get your employer (if you work as a programmer) or school, if 565 | any, to sign a “copyright disclaimer” for the program, if necessary. For more 566 | information on this, and how to apply and follow the GNU GPL, see 567 | . 568 | 569 | The GNU General Public License does not permit incorporating your program into 570 | proprietary programs. If your program is a subroutine library, you may consider 571 | it more useful to permit linking proprietary applications with the library. If 572 | this is what you want to do, use the GNU Lesser General Public License instead 573 | of this License. But first, please read 574 | . --------------------------------------------------------------------------------