From 6a3fde461e5f51bd7af10a69030c9325133560ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ben=C3=AD=C4=8Dek?= Date: Sat, 6 Jul 2024 20:33:07 +0200 Subject: [PATCH] first build --- .env.example | 5 ++ .pdm-python | 1 + pdm.lock | 113 ++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 21 +++++++++ src/config.py | 18 ++++++++ src/ffmpegcmd.py | 24 ++++++++++ src/main.py | 92 +++++++++++++++++++++++++++++++++++++ tests/__init__.py | 0 8 files changed, 274 insertions(+) create mode 100644 .env.example create mode 100644 .pdm-python create mode 100644 pdm.lock create mode 100644 pyproject.toml create mode 100644 src/config.py create mode 100644 src/ffmpegcmd.py create mode 100644 src/main.py create mode 100644 tests/__init__.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f0ecfee --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +FFMPEG=C://path +FFMPEG_PARAMS='-c:v av1_nvenc -c:a copy -c:s copy -map 0 -strict experimental -preset p7 -tune hq -multipass fullres -rc vbr' +WORKDIR=D://path +SOURCEDIR=E://path +OUTPUTDIR=F://path \ No newline at end of file diff --git a/.pdm-python b/.pdm-python new file mode 100644 index 0000000..4582987 --- /dev/null +++ b/.pdm-python @@ -0,0 +1 @@ +C:/Users/jan00/Documents/Programovani/Home/Automatic_Video_Transcoder/.venv/Scripts/python.exe \ No newline at end of file diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..bb5da36 --- /dev/null +++ b/pdm.lock @@ -0,0 +1,113 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default"] +strategy = ["cross_platform", "inherit_metadata"] +lock_version = "4.4.1" +content_hash = "sha256:c7db77dad7558ae5efaaa39b8bd0c5c77303c3a1a73af2a402a658cd9d3fabb5" + +[[package]] +name = "annotated-types" +version = "0.7.0" +requires_python = ">=3.8" +summary = "Reusable constraint types to use with typing.Annotated" +groups = ["default"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "pydantic" +version = "2.8.2" +requires_python = ">=3.8" +summary = "Data validation using Python type hints" +groups = ["default"] +dependencies = [ + "annotated-types>=0.4.0", + "pydantic-core==2.20.1", + "typing-extensions>=4.6.1; python_version < \"3.13\"", +] +files = [ + {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, + {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, +] + +[[package]] +name = "pydantic-core" +version = "2.20.1" +requires_python = ">=3.8" +summary = "Core functionality for Pydantic validation and serialization" +groups = ["default"] +dependencies = [ + "typing-extensions!=4.7.0,>=4.6.0", +] +files = [ + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, + {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, + {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, + {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, +] + +[[package]] +name = "pydantic-settings" +version = "2.3.4" +requires_python = ">=3.8" +summary = "Settings management using Pydantic" +groups = ["default"] +dependencies = [ + "pydantic>=2.7.0", + "python-dotenv>=0.21.0", +] +files = [ + {file = "pydantic_settings-2.3.4-py3-none-any.whl", hash = "sha256:11ad8bacb68a045f00e4f862c7a718c8a9ec766aa8fd4c32e39a0594b207b53a"}, + {file = "pydantic_settings-2.3.4.tar.gz", hash = "sha256:c5802e3d62b78e82522319bbc9b8f8ffb28ad1c988a99311d04f2a6051fca0a7"}, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +requires_python = ">=3.8" +summary = "Read key-value pairs from a .env file and set them as environment variables" +groups = ["default"] +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +requires_python = ">=3.8" +summary = "Backported and Experimental Type Hints for Python 3.8+" +groups = ["default"] +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1dd4054 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "Automatic_Video_Transcoder" +version = "0.1.0" +description = "Default template for PDM package" +authors = [ + {name = "Jan Beníček", email = "jan00@benicek.xyz"}, +] +dependencies = [ + "pydantic-settings>=2.3.4", + "pydantic>=2.8.2", +] +requires-python = "==3.12.*" +readme = "README.md" +license = {text = "MIT"} + + +[tool.pdm] +distribution = false + +[tool.pdm.scripts] +start = "python src/main.py" \ No newline at end of file diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..4985c65 --- /dev/null +++ b/src/config.py @@ -0,0 +1,18 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Config(BaseSettings): + model_config = SettingsConfigDict(env_file=".env") + + ffmpeg: str + ffmpeg_params: str + workdir: str + sourcedir: str + outputdir: str + + +cfg = Config() + + +def get_config() -> Config: + return Config() diff --git a/src/ffmpegcmd.py b/src/ffmpegcmd.py new file mode 100644 index 0000000..35a3c13 --- /dev/null +++ b/src/ffmpegcmd.py @@ -0,0 +1,24 @@ +import subprocess + + +def generate_command(ffmpeg: str, params: str, source_file: str, output_file: str): + command = [ffmpeg, "-y", "-i", source_file] + command.extend(params.split()) + command.append(output_file) + return command + + +def transcode(command) -> None: + print(command) + subprocess.run(command, check=True) + + +def ffmpeg(ffmpeg: str, params: str, source_file: str, output_file: str) -> None: + transcode( + command=generate_command( + ffmpeg=ffmpeg, + params=params, + source_file=source_file, + output_file=output_file, + ) + ) diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..a7039b1 --- /dev/null +++ b/src/main.py @@ -0,0 +1,92 @@ +from config import get_config +from ffmpegcmd import ffmpeg +import os, shutil, time + + +conf = get_config() + + +def reset_workdir(workdir: str): + try: + shutil.rmtree(workdir) + except: + print("workdir remove error") + + try: + os.mkdir(workdir) + except: + print("workdir create error") + + print("Workdir Cleared") + + +def videofile_test(file: str) -> bool: + if file.endswith(".mkv") or file.endswith(".avi") or file.endswith(".mp4"): + return True + else: + return False + + +def copy_file(source: str, destination: str): + if os.path.exists(f"{destination}.filepart"): + os.remove(f"{destination}.filepart") + + try: + shutil.copy(source, f"{destination}.filepart") + os.rename(f"{destination}.filepart", destination) + except: + time.sleep(10) + copy_file(source=source, destination=destination) + + +def process_file(source_dir: str, file: str): + outputdir = source_dir.replace(conf.sourcedir, conf.outputdir) + av1file = f"AV1_{file}" + + if not os.path.exists(outputdir): + os.makedirs(outputdir) # create dir if not exist + + if os.path.exists(os.path.join(outputdir, file)): + print("Output File Exist --> Skipping") + return False + + if videofile_test(file=file): + print( + f"Move video file: {os.path.join(source_dir,file)} --> {os.path.join(conf.workdir, file)}" + ) + copy_file(os.path.join(source_dir, file), os.path.join(conf.workdir, file)) + + print(f"Start transcode File: {file} --> {av1file}") + ffmpeg( + ffmpeg=conf.ffmpeg, + params=conf.ffmpeg_params, + source_file=os.path.join(conf.workdir, file), + output_file=os.path.join(conf.workdir, av1file), + ) + + print( + f"Move video file: {os.path.join(conf.workdir, av1file)} --> {os.path.join(outputdir, file)}" + ) + copy_file(os.path.join(conf.workdir, av1file), os.path.join(outputdir, file)) + + os.remove(os.path.join(conf.workdir, av1file)) + os.remove(os.path.join(conf.workdir, file)) + return True + else: + print( + f"Move non video file: {os.path.join(source_dir, file)} --> {os.path.join(outputdir, file)}" + ) + copy_file(os.path.join(source_dir, file), os.path.join(outputdir, file)) + return False + + +reset_workdir(workdir=conf.workdir) + +for root, _, files in os.walk(conf.sourcedir): + for file in files: + print("--------------------") + print(f"Process file: {os.path.join(root, file)}") + if process_file(root, file): + print("Waiting for terminate script") + time.sleep(10) # testing sleep for stop script time + print("End of waiting for terminating script --> Continuing") diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29