diff --git a/CHANGELOG.md b/CHANGELOG.md index f02feed..6f6de9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## v0.10.0 + +## Features + - Add the possibility to specify strict checks option to never forgot parameters when uploading (see #36) + - Improve logging system, add options for batch upload and print url-only in the stdout (see #29) + - --debug option is now deprecated in favor of --log=debug + +## Fixes + - Workaround against the Youtube API breakdown while adding video in playlist. See #47 for details. Should be removed once Google fix their bugs. + + ## v0.9.1 ### Features diff --git a/README.md b/README.md index 1450bc4..02269d0 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Scripting your way to upload videos to peertube and youtube. Works with Python 3 [TOC]: # ## Table of Contents -- [Installation](#installation) +- [Installation](#installation-and-upgrade) - [From pip](#from-pip) - [From source](#from-source) - [Configuration](#configuration) @@ -13,12 +13,13 @@ Scripting your way to upload videos to peertube and youtube. Works with Python 3 - [Youtube](#youtube) - [Usage](#usage) - [Enhanced use of NFO](#enhanced-use-of-nfo) +- [Strict check options](#strict-check-options) - [Features](#features) - [Compatibility](#compatibility) -- [Sources](#sources) +- [Inspirations](#inspirations) - [Contributors](#contributors) -## Installation an upgrade +## Installation and upgrade ### From pip @@ -121,9 +122,8 @@ Use --help to get all available options: ``` Options: - -f, --file=STRING Path to the video file to upload in mp4 + -f, --file=STRING Path to the video file to upload in mp4. This is the only mandatory option. --name=NAME Name of the video to upload. (default to video filename) - --debug Trigger some debug information like options used (default: no) -d, --description=STRING Description of the video. (default: default description) -t, --tags=STRING Tags for the video. comma separated. WARN: tags with punctuation (!, ', ", ?, ...) @@ -159,6 +159,34 @@ Options: -h --help Show this help. --version Show version. +Logging options + -q --quiet Suppress any log except Critical (alias for --log=critical). + --log=STRING Log level, between debug, info, warning, error, critical. Ignored if --quiet is set (default to info) + -u --url-only Display generated URL after upload directly on stdout, implies --quiet + --batch Display generated URL after upload with platform information for easier parsing. Implies --quiet + Be careful --batch and --url-only are mutually exclusives. + --debug (Deprecated) Alias for --log=debug. Ignored if --log is set + +Strict options: + Strict options allow you to force some option to be present when uploading a video. It's useful to be sure you do not + forget something when uploading a video, for example if you use multiples NFO. You may force the presence of description, + tags, thumbnail, ... + All strict option are optionals and are provided only to avoid errors when uploading :-) + All strict options can be specified in NFO directly, the only strict option mandatory on cli is --withNFO + All strict options are off by default + + --withNFO Prevent the upload without a NFO, either specified via cli or found in the directory + --withThumbnail Prevent the upload without a thumbnail + --withName Prevent the upload if no name are found + --withDescription Prevent the upload without description + --withTags Prevent the upload without tags + --withPlaylist Prevent the upload if no playlist + --withPublishAt Prevent the upload if no schedule + --withPlatform Prevent the upload if at least one platform is not specified + --withCategory Prevent the upload if no category + --withLanguage Prevent upload if no language + --withChannel Prevent upload if no channel + Categories: Category is the type of video you upload. Default is films. Here are available categories from Peertube and Youtube: @@ -173,6 +201,7 @@ Languages: Here are available languages from Peertube and Youtube: Arabic, English, French, German, Hindi, Italian, Japanese, Korean, Mandarin, Portuguese, Punjabi, Russian, Spanish + ``` ## Enhanced use of NFO @@ -211,10 +240,32 @@ Prismedia will: - erase any previous option regarding CCA as it's specified in cli with `--cca` - take `yourvideo1.jpg` as thumbnail if no other files has been specified in previous NFO -In other word, Prismedia will now use option given in cli, then look for option in cli_nfo.txt, then complete with video_name.txt, then directory_name.txt, and finally complete with nfo.txt +In other word, Prismedia will use option given in cli, then look for option in cli_nfo.txt, then complete with video_name.txt, then directory_name.txt, and finally complete with nfo.txt It allows to specify more easily default options for an entire set of video, directory, playlist and so on. +## Strict check options +Since prismedia v0.10.0, a bunch of special options have been added to force the presence of parameters before uploading. +Strict options allow you to force some option to be present when uploading a video. It's useful to be sure you do not +forget something when uploading a video, for example if you use multiples NFO. You may force the presence of description, +tags, thumbnail, ... +All strict option are optionals and are provided only to avoid errors when uploading :-) +All strict options can be specified in NFO directly, the only strict option mandatory on cli is --withNFO +All strict options are off by default. + +Available strict options: + - --withNFO Prevent the upload without a NFO, either specified via cli or found in the directory + - --withThumbnail Prevent the upload without a thumbnail + - --withName Prevent the upload if no name are found + - --withDescription Prevent the upload without description + - --withTags Prevent the upload without tags + - --withPlaylist Prevent the upload if no playlist + - --withPublishAt Prevent the upload if no schedule + - --withPlatform Prevent the upload if at least one platform is not specified + - --withCategory Prevent the upload if no category + - --withLanguage Prevent upload if no language + - --withChannel Prevent upload if no channel + ## Features - [x] Youtube upload @@ -235,9 +286,11 @@ It allows to specify more easily default options for an entire set of video, dir - [x] schedule your video with publishAt - [x] combine channel and playlist (Peertube only as channel is Peertube feature). See [issue 40](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/40) for detailed usage. - [x] Use a config file (NFO) file to retrieve videos arguments -- [x] Allow to choose peertube or youtube upload (to resume failed upload for example) +- [x] Allow choosing peertube or youtube upload (to retry a failed upload for example) - [x] Usable on Desktop (Linux and/or Windows and/or MacOS) - [x] Different schedules on platforms to prepare preview +- [x] Possibility to force the presence of upload options +- [ ] Copy and forget, eg possibility to copy video in a directory, and prismedia uploads itself: [Work in progress](https://git.lecygnenoir.info/Zykino/prismedia-autoupload) thanks to @Zykino 🎉 (Discussions in [issue 27](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/27)) - [ ] A usable graphical interface ## Compatibility @@ -245,8 +298,8 @@ It allows to specify more easily default options for an entire set of video, dir - If you still use python2, use the version 0.7.1 (no more updated) - If you use peertube before 1.0.0-beta4, use the version inside tag 1.0.0-beta3 -## Sources -inspired by [peeror](https://git.rigelk.eu/rigelk/peeror) and [youtube-upload](https://github.com/tokland/youtube-upload) +## Inspirations +Inspired by [peeror](https://git.rigelk.eu/rigelk/peeror) and [youtube-upload](https://github.com/tokland/youtube-upload) ## Contributors Thanks to: @Zykino, @meewan, @rigelk 😘 \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index d69caf7..ebaf6e1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,7 +12,7 @@ description = "Python package for providing Mozilla's CA Bundle." name = "certifi" optional = false python-versions = "*" -version = "2020.4.5.1" +version = "2020.6.20" [[package]] category = "main" @@ -34,6 +34,14 @@ version = "3.8.1" docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2)", "pytest-flake8"] +[[package]] +category = "main" +description = "Backports and enhancements for the contextlib module" +name = "contextlib2" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.6.0.post1" + [[package]] category = "main" description = "Pythonic argument parser, that will make you smile" @@ -56,19 +64,19 @@ description = "Google API client core library" name = "google-api-core" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.17.0" +version = "1.22.2" [package.dependencies] -google-auth = ">=1.14.0,<2.0dev" +google-auth = ">=1.21.1,<2.0dev" googleapis-common-protos = ">=1.6.0,<2.0dev" -protobuf = ">=3.4.0" +protobuf = ">=3.12.0" pytz = "*" requests = ">=2.18.0,<3.0.0dev" setuptools = ">=34.0.0" six = ">=1.10.0" [package.extras] -grpc = ["grpcio (>=1.8.2,<2.0dev)"] +grpc = ["grpcio (>=1.29.0,<2.0dev)"] grpcgcp = ["grpcio-gcp (>=0.2.2)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2)"] @@ -78,14 +86,14 @@ description = "Google API Client Library for Python" name = "google-api-python-client" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.8.2" +version = "1.12.1" [package.dependencies] -google-api-core = ">=1.13.0,<2dev" -google-auth = ">=1.4.1" +google-api-core = ">=1.21.0,<2dev" +google-auth = ">=1.16.0" google-auth-httplib2 = ">=0.0.3" httplib2 = ">=0.9.2,<1dev" -six = ">=1.6.1,<2dev" +six = ">=1.13.0,<2dev" uritemplate = ">=3.0.0,<4dev" [[package]] @@ -94,26 +102,30 @@ description = "Google Authentication Library" name = "google-auth" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.14.1" +version = "1.21.1" [package.dependencies] cachetools = ">=2.0.0,<5.0" pyasn1-modules = ">=0.2.1" -rsa = ">=3.1.4,<4.1" setuptools = ">=40.3.0" six = ">=1.9.0" +[package.dependencies.rsa] +python = ">=3.5" +version = ">=3.1.4,<5" + [[package]] category = "main" description = "Google Authentication Library: httplib2 transport" name = "google-auth-httplib2" optional = false python-versions = "*" -version = "0.0.3" +version = "0.0.4" [package.dependencies] google-auth = "*" httplib2 = ">=0.9.1" +six = "*" [[package]] category = "main" @@ -121,7 +133,7 @@ description = "Google Authentication Library" name = "google-auth-oauthlib" optional = false python-versions = "*" -version = "0.2.0" +version = "0.4.1" [package.dependencies] google-auth = "*" @@ -135,8 +147,8 @@ category = "main" description = "Common protobufs used in Google APIs" name = "googleapis-common-protos" optional = false -python-versions = "*" -version = "1.51.0" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.52.0" [package.dependencies] protobuf = ">=3.6.0" @@ -158,7 +170,7 @@ description = "Internationalized Domain Names in Applications (IDNA)" name = "idna" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.9" +version = "2.10" [[package]] category = "main" @@ -180,7 +192,7 @@ description = "Protocol Buffers" name = "protobuf" optional = false python-versions = "*" -version = "3.11.3" +version = "3.13.0" [package.dependencies] setuptools = "*" @@ -210,8 +222,8 @@ category = "main" description = "File type identification using libmagic" name = "python-magic" optional = false -python-versions = "*" -version = "0.4.15" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.18" [[package]] category = "main" @@ -228,7 +240,7 @@ description = "World timezone definitions, modern and historical" name = "pytz" optional = false python-versions = "*" -version = "2019.3" +version = "2020.1" [[package]] category = "main" @@ -236,7 +248,7 @@ description = "Python HTTP for Humans." name = "requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.23.0" +version = "2.24.0" [package.dependencies] certifi = ">=2017.4.17" @@ -277,10 +289,11 @@ requests = ">=2.0.1,<3.0.0" [[package]] category = "main" description = "Pure-Python RSA implementation" +marker = "python_version >= \"3.5\"" name = "rsa" optional = false python-versions = "*" -version = "4.0" +version = "4.4" [package.dependencies] pyasn1 = ">=0.1.3" @@ -291,7 +304,10 @@ description = "Simple data validation library" name = "schema" optional = false python-versions = "*" -version = "0.6.8" +version = "0.7.3" + +[package.dependencies] +contextlib2 = ">=0.5.5" [[package]] category = "main" @@ -299,7 +315,7 @@ description = "Python 2 and 3 compatibility utilities" name = "six" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "1.14.0" +version = "1.15.0" [[package]] category = "main" @@ -341,7 +357,7 @@ secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "cer socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] [metadata] -content-hash = "b3063876dbcd6443d0459a9ef376ccdba2a21adc2e7a49d75c9450904b40615f" +content-hash = "5f912013e1ff1f79bdecd9626157960bd0ccc41f441370419888238adc32b385" python-versions = ">=3.5" [metadata.files] @@ -350,8 +366,8 @@ cachetools = [ {file = "cachetools-3.1.1.tar.gz", hash = "sha256:8ea2d3ce97850f31e4a08b0e2b5e6c34997d7216a9d2c98e0f3978630d4da69a"}, ] certifi = [ - {file = "certifi-2020.4.5.1-py2.py3-none-any.whl", hash = "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304"}, - {file = "certifi-2020.4.5.1.tar.gz", hash = "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"}, + {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, + {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, ] chardet = [ {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, @@ -361,6 +377,10 @@ configparser = [ {file = "configparser-3.8.1-py2.py3-none-any.whl", hash = "sha256:45d1272aad6cfd7a8a06cf5c73f2ceb6a190f6acc1fa707e7f82a4c053b28b18"}, {file = "configparser-3.8.1.tar.gz", hash = "sha256:bc37850f0cc42a1725a796ef7d92690651bf1af37d744cc63161dac62cabee17"}, ] +contextlib2 = [ + {file = "contextlib2-0.6.0.post1-py2.py3-none-any.whl", hash = "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"}, + {file = "contextlib2-0.6.0.post1.tar.gz", hash = "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e"}, +] docopt = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, ] @@ -368,60 +388,60 @@ future = [ {file = "future-0.17.1.tar.gz", hash = "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"}, ] google-api-core = [ - {file = "google-api-core-1.17.0.tar.gz", hash = "sha256:e4082a0b479dc2dee2f8d7b80ea8b5d0184885b773caab15ab1836277a01d689"}, - {file = "google_api_core-1.17.0-py2.py3-none-any.whl", hash = "sha256:c0e430658ed6be902d7ba7095fb0a9cac810270d71bf7ac4484e76c300407aae"}, + {file = "google-api-core-1.22.2.tar.gz", hash = "sha256:779107f17e0fef8169c5239d56a8fbff03f9f72a3893c0c9e5842ec29dfedd54"}, + {file = "google_api_core-1.22.2-py2.py3-none-any.whl", hash = "sha256:67e33a852dcca7cb7eff49abc35c8cc2c0bb8ab11397dc8306d911505cae2990"}, ] google-api-python-client = [ - {file = "google-api-python-client-1.8.2.tar.gz", hash = "sha256:bf482c13fb41a6d01770f9d62be6b33fdcd41d68c97f2beb9be02297bdd9e725"}, - {file = "google_api_python_client-1.8.2-py3-none-any.whl", hash = "sha256:8dd35a3704650c2db44e6cf52abdaf9de71f409c93c56bbe48a321ab5e14ebad"}, + {file = "google-api-python-client-1.12.1.tar.gz", hash = "sha256:ddadc243ce627512c2a27e11d369f5ddf658ef80dbffb247787499486ef1ea98"}, + {file = "google_api_python_client-1.12.1-py2.py3-none-any.whl", hash = "sha256:750316d670119bf680c24ff73825a05b1b4f48b9157bd48c6e3f2bea15ceb586"}, ] google-auth = [ - {file = "google-auth-1.14.1.tar.gz", hash = "sha256:e63b2210e03c4ed829063b72c4af0c4b867c2788efb3210b6b9439b488bd3afd"}, - {file = "google_auth-1.14.1-py2.py3-none-any.whl", hash = "sha256:0c41a453b9a8e77975bfa436b8daedac00aed1c545d84410daff8272fff40fbb"}, + {file = "google-auth-1.21.1.tar.gz", hash = "sha256:bcbd9f970e7144fe933908aa286d7a12c44b7deb6d78a76871f0377a29d09789"}, + {file = "google_auth-1.21.1-py2.py3-none-any.whl", hash = "sha256:f4d5093f13b1b1c0a434ab1dc851cd26a983f86a4d75c95239974e33ed406a87"}, ] google-auth-httplib2 = [ - {file = "google-auth-httplib2-0.0.3.tar.gz", hash = "sha256:098fade613c25b4527b2c08fa42d11f3c2037dda8995d86de0745228e965d445"}, - {file = "google_auth_httplib2-0.0.3-py2.py3-none-any.whl", hash = "sha256:f1c437842155680cf9918df9bc51c1182fda41feef88c34004bd1978c8157e08"}, + {file = "google-auth-httplib2-0.0.4.tar.gz", hash = "sha256:8d092cc60fb16517b12057ec0bba9185a96e3b7169d86ae12eae98e645b7bc39"}, + {file = "google_auth_httplib2-0.0.4-py2.py3-none-any.whl", hash = "sha256:aeaff501738b289717fac1980db9711d77908a6c227f60e4aa1923410b43e2ee"}, ] google-auth-oauthlib = [ - {file = "google-auth-oauthlib-0.2.0.tar.gz", hash = "sha256:226d1d0960f86ba5d9efd426a70b291eaba96f47d071657e0254ea969025728a"}, - {file = "google_auth_oauthlib-0.2.0-py2.py3-none-any.whl", hash = "sha256:81ba22acada4d13b1d83f9371ab19fd61f1250a542d21cf49e4dcf0637a7344a"}, + {file = "google-auth-oauthlib-0.4.1.tar.gz", hash = "sha256:88d2cd115e3391eb85e1243ac6902e76e77c5fe438b7276b297fbe68015458dd"}, + {file = "google_auth_oauthlib-0.4.1-py2.py3-none-any.whl", hash = "sha256:a92a0f6f41a0fb6138454fbc02674e64f89d82a244ea32f98471733c8ef0e0e1"}, ] googleapis-common-protos = [ - {file = "googleapis-common-protos-1.51.0.tar.gz", hash = "sha256:013c91704279119150e44ef770086fdbba158c1f978a6402167d47d5409e226e"}, + {file = "googleapis-common-protos-1.52.0.tar.gz", hash = "sha256:560716c807117394da12cecb0a54da5a451b5cf9866f1d37e9a5e2329a665351"}, + {file = "googleapis_common_protos-1.52.0-py2.py3-none-any.whl", hash = "sha256:c8961760f5aad9a711d37b675be103e0cc4e9a39327e0d6d857872f698403e24"}, ] httplib2 = [ {file = "httplib2-0.12.3-py3-none-any.whl", hash = "sha256:23914b5487dfe8ef09db6656d6d63afb0cf3054ad9ebc50868ddc8e166b5f8e8"}, {file = "httplib2-0.12.3.tar.gz", hash = "sha256:a18121c7c72a56689efbf1aef990139ad940fee1e64c6f2458831736cd593600"}, ] idna = [ - {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, - {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] oauthlib = [ {file = "oauthlib-2.1.0-py2.py3-none-any.whl", hash = "sha256:d883b36b21a6ad813953803edfa563b1b579d79ca758fe950d1bc9e8b326025b"}, {file = "oauthlib-2.1.0.tar.gz", hash = "sha256:ac35665a61c1685c56336bda97d5eefa246f1202618a1d6f34fccb1bdd404162"}, ] protobuf = [ - {file = "protobuf-3.11.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481"}, - {file = "protobuf-3.11.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7"}, - {file = "protobuf-3.11.3-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a"}, - {file = "protobuf-3.11.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961"}, - {file = "protobuf-3.11.3-cp35-cp35m-win32.whl", hash = "sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80"}, - {file = "protobuf-3.11.3-cp35-cp35m-win_amd64.whl", hash = "sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306"}, - {file = "protobuf-3.11.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a"}, - {file = "protobuf-3.11.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151"}, - {file = "protobuf-3.11.3-cp36-cp36m-win32.whl", hash = "sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab"}, - {file = "protobuf-3.11.3-cp36-cp36m-win_amd64.whl", hash = "sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956"}, - {file = "protobuf-3.11.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2"}, - {file = "protobuf-3.11.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07"}, - {file = "protobuf-3.11.3-cp37-cp37m-win32.whl", hash = "sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a"}, - {file = "protobuf-3.11.3-cp37-cp37m-win_amd64.whl", hash = "sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee"}, - {file = "protobuf-3.11.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4"}, - {file = "protobuf-3.11.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0"}, - {file = "protobuf-3.11.3-py2.7.egg", hash = "sha256:2affcaba328c4662f3bc3c0e9576ea107906b2c2b6422344cdad961734ff6b93"}, - {file = "protobuf-3.11.3-py2.py3-none-any.whl", hash = "sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f"}, - {file = "protobuf-3.11.3.tar.gz", hash = "sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f"}, + {file = "protobuf-3.13.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9c2e63c1743cba12737169c447374fab3dfeb18111a460a8c1a000e35836b18c"}, + {file = "protobuf-3.13.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1e834076dfef9e585815757a2c7e4560c7ccc5962b9d09f831214c693a91b463"}, + {file = "protobuf-3.13.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:df3932e1834a64b46ebc262e951cd82c3cf0fa936a154f0a42231140d8237060"}, + {file = "protobuf-3.13.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8c35bcbed1c0d29b127c886790e9d37e845ffc2725cc1db4bd06d70f4e8359f4"}, + {file = "protobuf-3.13.0-cp35-cp35m-win32.whl", hash = "sha256:339c3a003e3c797bc84499fa32e0aac83c768e67b3de4a5d7a5a9aa3b0da634c"}, + {file = "protobuf-3.13.0-cp35-cp35m-win_amd64.whl", hash = "sha256:361acd76f0ad38c6e38f14d08775514fbd241316cce08deb2ce914c7dfa1184a"}, + {file = "protobuf-3.13.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9edfdc679a3669988ec55a989ff62449f670dfa7018df6ad7f04e8dbacb10630"}, + {file = "protobuf-3.13.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5db9d3e12b6ede5e601b8d8684a7f9d90581882925c96acf8495957b4f1b204b"}, + {file = "protobuf-3.13.0-cp36-cp36m-win32.whl", hash = "sha256:c8abd7605185836f6f11f97b21200f8a864f9cb078a193fe3c9e235711d3ff1e"}, + {file = "protobuf-3.13.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4d1174c9ed303070ad59553f435846a2f877598f59f9afc1b89757bdf846f2a7"}, + {file = "protobuf-3.13.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0bba42f439bf45c0f600c3c5993666fcb88e8441d011fad80a11df6f324eef33"}, + {file = "protobuf-3.13.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c0c5ab9c4b1eac0a9b838f1e46038c3175a95b0f2d944385884af72876bd6bc7"}, + {file = "protobuf-3.13.0-cp37-cp37m-win32.whl", hash = "sha256:f68eb9d03c7d84bd01c790948320b768de8559761897763731294e3bc316decb"}, + {file = "protobuf-3.13.0-cp37-cp37m-win_amd64.whl", hash = "sha256:91c2d897da84c62816e2f473ece60ebfeab024a16c1751aaf31100127ccd93ec"}, + {file = "protobuf-3.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3dee442884a18c16d023e52e32dd34a8930a889e511af493f6dc7d4d9bf12e4f"}, + {file = "protobuf-3.13.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e7662437ca1e0c51b93cadb988f9b353fa6b8013c0385d63a70c8a77d84da5f9"}, + {file = "protobuf-3.13.0-py2.py3-none-any.whl", hash = "sha256:d69697acac76d9f250ab745b46c725edf3e98ac24763990b24d58c16c642947a"}, + {file = "protobuf-3.13.0.tar.gz", hash = "sha256:6a82e0c8bb2bf58f606040cc5814e07715b2094caeba281e2e7d0b0e2e397db5"}, ] pyasn1 = [ {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, @@ -454,8 +474,8 @@ pyasn1-modules = [ {file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"}, ] python-magic = [ - {file = "python-magic-0.4.15.tar.gz", hash = "sha256:f3765c0f582d2dfc72c15f3b5a82aecfae9498bd29ca840d72f37d7bd38bfcd5"}, - {file = "python_magic-0.4.15-py2.py3-none-any.whl", hash = "sha256:f2674dcfad52ae6c49d4803fa027809540b130db1dec928cfbb9240316831375"}, + {file = "python-magic-0.4.18.tar.gz", hash = "sha256:b757db2a5289ea3f1ced9e60f072965243ea43a2221430048fd8cacab17be0ce"}, + {file = "python_magic-0.4.18-py2.py3-none-any.whl", hash = "sha256:356efa93c8899047d1eb7d3eb91e871ba2f5b1376edbaf4cc305e3c872207355"}, ] python-magic-bin = [ {file = "python_magic_bin-0.4.14-py2.py3-none-macosx_10_6_intel.whl", hash = "sha256:7b1743b3dbf16601d6eedf4e7c2c9a637901b0faaf24ad4df4d4527e7d8f66a4"}, @@ -463,12 +483,12 @@ python-magic-bin = [ {file = "python_magic_bin-0.4.14-py2.py3-none-win_amd64.whl", hash = "sha256:90be6206ad31071a36065a2fc169c5afb5e0355cbe6030e87641c6c62edc2b69"}, ] pytz = [ - {file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"}, - {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"}, + {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, + {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, ] requests = [ - {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, - {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, + {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, + {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, ] requests-oauthlib = [ {file = "requests-oauthlib-0.8.0.tar.gz", hash = "sha256:883ac416757eada6d3d07054ec7092ac21c7f35cb1d2cf82faf205637081f468"}, @@ -479,16 +499,16 @@ requests-toolbelt = [ {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, ] rsa = [ - {file = "rsa-4.0-py2.py3-none-any.whl", hash = "sha256:14ba45700ff1ec9eeb206a2ce76b32814958a98e372006c8fb76ba820211be66"}, - {file = "rsa-4.0.tar.gz", hash = "sha256:1a836406405730121ae9823e19c6e806c62bbad73f890574fff50efa4122c487"}, + {file = "rsa-4.4-py2.py3-none-any.whl", hash = "sha256:4afbaaecc3e9550c7351fdf0ab3fea1857ff616b85bab59215f00fb42e0e9582"}, + {file = "rsa-4.4.tar.gz", hash = "sha256:5d95293bbd0fbee1dd9cb4b72d27b723942eb50584abc8c4f5f00e4bcfa55307"}, ] schema = [ - {file = "schema-0.6.8-py2.py3-none-any.whl", hash = "sha256:d994b0dc4966000037b26898df638e3e2a694cc73636cb2050e652614a350687"}, - {file = "schema-0.6.8.tar.gz", hash = "sha256:fa1a53fe5f3b6929725a4e81688c250f46838e25d8c1885a10a590c8c01a7b74"}, + {file = "schema-0.7.3-py2.py3-none-any.whl", hash = "sha256:c331438b60f634cab5664ab720d3083cc444f924d55269530c36b33e3354276f"}, + {file = "schema-0.7.3.tar.gz", hash = "sha256:4cf529318cfd1e844ecbe02f41f7e5aa027463e7403666a52746f31f04f47a5e"}, ] six = [ - {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, - {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, ] tzlocal = [ {file = "tzlocal-1.5.1.tar.gz", hash = "sha256:4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e"}, diff --git a/prismedia/pt_upload.py b/prismedia/pt_upload.py index 8e1aa1f..b95e550 100644 --- a/prismedia/pt_upload.py +++ b/prismedia/pt_upload.py @@ -5,6 +5,7 @@ import os import mimetypes import json import logging +import sys import datetime import pytz from os.path import splitext, basename, abspath @@ -16,6 +17,7 @@ from oauthlib.oauth2 import LegacyApplicationClient from requests_toolbelt.multipart.encoder import MultipartEncoder from . import utils +logger = logging.getLogger('Prismedia') PEERTUBE_SECRETS_FILE = 'peertube_secret' PEERTUBE_PRIVACY = { @@ -43,10 +45,10 @@ def get_authenticated_service(secret): ) except Exception as e: if hasattr(e, 'message'): - logging.error("Peertube: Error: " + str(e.message)) + logger.critical("Peertube: " + str(e.message)) exit(1) else: - logging.error("Peertube: Error: " + str(e)) + logger.critical("Peertube: " + str(e)) exit(1) return oauth @@ -63,7 +65,7 @@ def get_channel_by_name(user_info, options): def create_channel(oauth, url, options): template = ('Peertube: Channel %s does not exist, creating it.') - logging.info(template % (str(options.get('--channel')))) + logger.info(template % (str(options.get('--channel')))) channel_name = utils.cleanString(str(options.get('--channel'))) # Peertube allows 20 chars max for channel name channel_name = channel_name[:19] @@ -81,23 +83,23 @@ def create_channel(oauth, url, options): headers=headers) except Exception as e: if hasattr(e, 'message'): - logging.error("Error: " + str(e.message)) + logger.error("Peertube: " + str(e.message)) else: - logging.error("Error: " + str(e)) + logger.error("Peertube: " + str(e)) if response is not None: if response.status_code == 200: jresponse = response.json() jresponse = jresponse['videoChannel'] return jresponse['id'] if response.status_code == 409: - logging.error('Peertube: Error: It seems there is a conflict with an existing channel named ' + logger.critical('Peertube: It seems there is a conflict with an existing channel named ' + channel_name + '.' ' Please beware Peertube internal name is compiled from 20 firsts characters of channel name.' ' Also note that channel name are not case sensitive (no uppercase nor accent)' ' Please check your channel name and retry.') exit(1) else: - logging.error(('Peertube: Creating channel failed with an unexpected response: ' + logger.critical(('Peertube: Creating channel failed with an unexpected response: ' '%s') % response) exit(1) @@ -114,7 +116,7 @@ def get_playlist_by_name(user_playlists, options): def create_playlist(oauth, url, options, channel): template = ('Peertube: Playlist %s does not exist, creating it.') - logging.info(template % (str(options.get('--playlist')))) + logger.info(template % (str(options.get('--playlist')))) # We use files for form-data Content # see https://requests.readthedocs.io/en/latest/user/quickstart/#post-a-multipart-encoded-file # None is used to mute "filename" field @@ -128,22 +130,22 @@ def create_playlist(oauth, url, options, channel): files=files) except Exception as e: if hasattr(e, 'message'): - logging.error("Error: " + str(e.message)) + logger.error("Peertube: " + str(e.message)) else: - logging.error("Error: " + str(e)) + logger.error("Peertube: " + str(e)) if response is not None: if response.status_code == 200: jresponse = response.json() jresponse = jresponse['videoPlaylist'] return jresponse['id'] else: - logging.error(('Peertube: Creating the playlist failed with an unexpected response: ' + logger.critical(('Peertube: Creating the playlist failed with an unexpected response: ' '%s') % response) exit(1) def set_playlist(oauth, url, video_id, playlist_id): - logging.info('Peertube: add video to playlist.') + logger.info('Peertube: add video to playlist.') data = '{"videoId":"' + str(video_id) + '"}' headers = { @@ -155,14 +157,14 @@ def set_playlist(oauth, url, video_id, playlist_id): headers=headers) except Exception as e: if hasattr(e, 'message'): - logging.error("Error: " + str(e.message)) + logger.error("Peertube: " + str(e.message)) else: - logging.error("Error: " + str(e)) + logger.error("Peertube: " + str(e)) if response is not None: if response.status_code == 200: - logging.info('Peertube: Video is successfully added to the playlist.') + logger.info('Peertube: Video is successfully added to the playlist.') else: - logging.error(('Peertube: Configuring the playlist failed with an unexpected response: ' + logger.critical(('Peertube: Configuring the playlist failed with an unexpected response: ' '%s') % response) exit(1) @@ -205,8 +207,9 @@ def upload_video(oauth, secret, options): continue # Tag more than 30 chars crashes Peertube, so exit and check tags if len(strtag) >= 30: - logging.warning("Peertube: Sorry, Peertube does not support tag with more than 30 characters, please reduce tag: " + strtag) - exit(1) + logger.error("Peertube: Sorry, Peertube does not support tag with more than 30 characters, please reduce tag: " + strtag) + logger.error("Peertube: Meanwhile, this tag will be skipped") + continue fields.append(("tags[]", strtag)) if options.get('--category'): @@ -256,7 +259,7 @@ def upload_video(oauth, secret, options): if not channel_id and options.get('--channelCreate'): channel_id = create_channel(oauth, url, options) elif not channel_id: - logging.warning("Channel `" + options.get('--channel') + "` is unknown, using default channel.") + logger.warning("Peertube: Channel `" + options.get('--channel') + "` is unknown, using default channel.") channel_id = get_default_channel(user_info) else: channel_id = get_default_channel(user_info) @@ -268,10 +271,14 @@ def upload_video(oauth, secret, options): if not playlist_id and options.get('--playlistCreate'): playlist_id = create_playlist(oauth, url, options, channel_id) elif not playlist_id: - logging.warning("Playlist `" + options.get('--playlist') + "` does not exist, please set --playlistCreate" + logger.critical("Peertube: Playlist `" + options.get('--playlist') + "` does not exist, please set --playlistCreate" " if you want to create it") exit(1) + logger_stdout = None + if options.get('--url-only') or options.get('--batch'): + logger_stdout = logging.getLogger('stdoutlogs') + multipart_data = MultipartEncoder(fields) headers = { @@ -286,14 +293,19 @@ def upload_video(oauth, secret, options): jresponse = jresponse['video'] uuid = jresponse['uuid'] video_id = str(jresponse['id']) - logging.info('Peertube : Video was successfully uploaded.') + logger.info('Peertube : Video was successfully uploaded.') template = 'Peertube: Watch it at %s/videos/watch/%s.' - logging.info(template % (url, uuid)) + logger.info(template % (url, uuid)) + template_stdout = '%s/videos/watch/%s' + if options.get('--url-only'): + logger_stdout.info(template_stdout % (url, uuid)) + elif options.get('--batch'): + logger_stdout.info("Peertube: " + template_stdout % (url, uuid)) # Upload is successful we may set playlist if options.get('--playlist'): set_playlist(oauth, url, video_id, playlist_id) else: - logging.error(('Peertube: The upload failed with an unexpected response: ' + logger.critical(('Peertube: The upload failed with an unexpected response: ' '%s') % response) exit(1) @@ -303,16 +315,16 @@ def run(options): try: secret.read(PEERTUBE_SECRETS_FILE) except Exception as e: - logging.error("Peertube: Error loading " + str(PEERTUBE_SECRETS_FILE) + ": " + str(e)) + logger.critical("Peertube: Error loading " + str(PEERTUBE_SECRETS_FILE) + ": " + str(e)) exit(1) insecure_transport = secret.get('peertube', 'OAUTHLIB_INSECURE_TRANSPORT') os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = insecure_transport oauth = get_authenticated_service(secret) try: - logging.info('Peertube: Uploading video...') + logger.info('Peertube: Uploading video...') upload_video(oauth, secret, options) except Exception as e: if hasattr(e, 'message'): - logging.error("Peertube: Error: " + str(e.message)) + logger.error("Peertube: " + str(e.message)) else: - logging.error("Peertube: Error: " + str(e)) + logger.error("Peertube: " + str(e)) diff --git a/prismedia/upload.py b/prismedia/upload.py index ee328e5..9e59637 100755 --- a/prismedia/upload.py +++ b/prismedia/upload.py @@ -11,9 +11,8 @@ Usage: prismedia --version Options: - -f, --file=STRING Path to the video file to upload in mp4 + -f, --file=STRING Path to the video file to upload in mp4. This is the only mandatory option. --name=NAME Name of the video to upload. (default to video filename) - --debug Trigger some debug information like options used (default: no) -d, --description=STRING Description of the video. (default: default description) -t, --tags=STRING Tags for the video. comma separated. WARN: tags with punctuation (!, ', ", ?, ...) @@ -49,6 +48,34 @@ Options: -h --help Show this help. --version Show version. +Logging options + -q --quiet Suppress any log except Critical (alias for --log=critical). + --log=STRING Log level, between debug, info, warning, error, critical. Ignored if --quiet is set (default to info) + -u --url-only Display generated URL after upload directly on stdout, implies --quiet + --batch Display generated URL after upload with platform information for easier parsing. Implies --quiet + Be careful --batch and --url-only are mutually exclusives. + --debug (Deprecated) Alias for --log=debug. Ignored if --log is set + +Strict options: + Strict options allow you to force some option to be present when uploading a video. It's useful to be sure you do not + forget something when uploading a video, for example if you use multiples NFO. You may force the presence of description, + tags, thumbnail, ... + All strict option are optionals and are provided only to avoid errors when uploading :-) + All strict options can be specified in NFO directly, the only strict option mandatory on cli is --withNFO + All strict options are off by default + + --withNFO Prevent the upload without a NFO, either specified via cli or found in the directory + --withThumbnail Prevent the upload without a thumbnail + --withName Prevent the upload if no name are found + --withDescription Prevent the upload without description + --withTags Prevent the upload without tags + --withPlaylist Prevent the upload if no playlist + --withPublishAt Prevent the upload if no schedule + --withPlatform Prevent the upload if at least one platform is not specified + --withCategory Prevent the upload if no category + --withLanguage Prevent upload if no language + --withChannel Prevent upload if no channel + Categories: Category is the type of video you upload. Default is films. Here are available categories from Peertube and Youtube: @@ -69,9 +96,16 @@ import sys if sys.version_info[0] < 3: raise Exception("Python 3 or a more recent version is required.") +import os import datetime import logging -logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO) +logger = logging.getLogger('Prismedia') +logger.setLevel(logging.INFO) +ch = logging.StreamHandler() +ch.setLevel(logging.INFO) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s: %(message)s') +ch.setFormatter(formatter) +logger.addHandler(ch) from docopt import docopt @@ -81,9 +115,9 @@ from . import utils try: # noinspection PyUnresolvedReferences - from schema import Schema, And, Or, Optional, SchemaError + from schema import Schema, And, Or, Optional, SchemaError, Hook, Use except ImportError: - logging.error('This program requires that the `schema` data-validation library' + logger.critical('This program requires that the `schema` data-validation library' ' is installed: \n' 'see https://github.com/halst/schema\n') exit(1) @@ -91,12 +125,12 @@ try: # noinspection PyUnresolvedReferences import magic except ImportError: - logging.error('This program requires that the `python-magic` library' + logger.critical('This program requires that the `python-magic` library' ' is installed, NOT the Python bindings to libmagic API \n' 'see https://github.com/ahupp/python-magic\n') exit(1) -VERSION = "prismedia v0.9.1" +VERSION = "prismedia v0.10.0" VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted') VALID_CATEGORIES = ( @@ -170,17 +204,104 @@ def validatePublish(publish): def validateThumbnail(thumbnail): supported_types = ['image/jpg', 'image/jpeg'] - if magic.from_file(thumbnail, mime=True) in supported_types: + if os.path.exists(thumbnail) and \ + magic.from_file(thumbnail, mime=True) in supported_types: return thumbnail else: return False +def validateLogLevel(loglevel): + numeric_level = getattr(logging, loglevel, None) + if not isinstance(numeric_level, int): + return False + return True + +def _optionnalOrStrict(key, scope, error): + option = key.replace('-', '') + option = option[0].upper() + option[1:] + if scope["--with" + option] is True and scope[key] is None: + logger.critical("Prismedia: you have required the strict presence of " + key + " but none is found") + exit(1) + return True + + +def configureLogs(options): + if options.get('--batch') and options.get('--url-only'): + logger.critical("Prismedia: Please use either --batch OR --url-only, not both.") + exit(1) + # batch and url-only implies quiet + if options.get('--batch') or options.get('--url-only'): + options['--quiet'] = True + + if options.get('--quiet'): + # We need to set both log level in the same time + logger.setLevel(50) + ch.setLevel(50) + elif options.get('--log'): + numeric_level = getattr(logging, options["--log"], None) + # We need to set both log level in the same time + logger.setLevel(numeric_level) + ch.setLevel(numeric_level) + elif options.get('--debug'): + logger.warning("DEPRECATION: --debug is deprecated, please use --log=debug instead") + logger.setLevel(10) + ch.setLevel(10) + + +def configureStdoutLogs(): + logger_stdout = logging.getLogger('stdoutlogs') + logger_stdout.setLevel(logging.INFO) + ch_stdout = logging.StreamHandler(stream=sys.stdout) + ch_stdout.setLevel(logging.INFO) + # Default stdout logs is url only + formatter_stdout = logging.Formatter('%(message)s') + ch_stdout.setFormatter(formatter_stdout) + logger_stdout.addHandler(ch_stdout) + def main(): options = docopt(__doc__, version=VERSION) + earlyoptionSchema = Schema({ + Optional('--log'): Or(None, And( + str, + Use(str.upper), + validateLogLevel, + error="Log level not recognized") + ), + Optional('--quiet', default=False): bool, + Optional('--debug'): bool, + Optional('--url-only', default=False): bool, + Optional('--batch', default=False): bool, + Optional('--withNFO', default=False): bool, + Optional('--withThumbnail', default=False): bool, + Optional('--withName', default=False): bool, + Optional('--withDescription', default=False): bool, + Optional('--withTags', default=False): bool, + Optional('--withPlaylist', default=False): bool, + Optional('--withPublishAt', default=False): bool, + Optional('--withPlatform', default=False): bool, + Optional('--withCategory', default=False): bool, + Optional('--withLanguage', default=False): bool, + Optional('--withChannel', default=False): bool, + # This allow to return all other options for further use: https://github.com/keleshev/schema#extra-keys + object: object + }) + schema = Schema({ - '--file': And(str, validateVideo, error='file is not supported, please use mp4'), + '--file': And(str, os.path.exists, validateVideo, error='file is not supported, please use mp4'), + # Strict option checks - at the moment Schema needs to check Hook and Optional separately # + Hook('--name', handler=_optionnalOrStrict): object, + Hook('--description', handler=_optionnalOrStrict): object, + Hook('--tags', handler=_optionnalOrStrict): object, + Hook('--category', handler=_optionnalOrStrict): object, + Hook('--language', handler=_optionnalOrStrict): object, + Hook('--platform', handler=_optionnalOrStrict): object, + Hook('--publishAt', handler=_optionnalOrStrict): object, + Hook('--thumbnail', handler=_optionnalOrStrict): object, + Hook('--channel', handler=_optionnalOrStrict): object, + Hook('--playlist', handler=_optionnalOrStrict): object, + # Validate checks # Optional('--name'): Or(None, And( str, lambda x: not x.isdigit(), @@ -228,7 +349,6 @@ def main(): validatePublish, error="DATE should be the form YYYY-MM-DDThh:mm:ss and has to be in the future") ), - Optional('--debug'): bool, Optional('--cca'): bool, Optional('--disable-comments'): bool, Optional('--nsfw'): bool, @@ -240,22 +360,41 @@ def main(): Optional('--playlist'): Or(None, str), Optional('--playlistCreate'): bool, '--help': bool, - '--version': bool + '--version': bool, + # This allow to return all other options for further use: https://github.com/keleshev/schema#extra-keys + object: object }) + # We need to validate early options first as withNFO and logs options should be prioritized + try: + options = earlyoptionSchema.validate(options) + configureLogs(options) + except SchemaError as e: + logger.critical(e) + exit(1) + + if options.get('--url-only') or options.get('--batch'): + configureStdoutLogs() options = utils.parseNFO(options) + # Once NFO are loaded, we need to revalidate strict options in case some were in NFO + try: + options = earlyoptionSchema.validate(options) + except SchemaError as e: + logger.critical(e) + exit(1) + if not options.get('--thumbnail'): options = utils.searchThumbnail(options) try: options = schema.validate(options) except SchemaError as e: - exit(e) + logger.critical(e) + exit(1) - if options.get('--debug'): - print(sys.version) - print(options) + logger.debug("Python " + sys.version) + logger.debug(options) if options.get('--platform') is None or "peertube" in options.get('--platform'): pt_upload.run(options) @@ -264,6 +403,5 @@ def main(): if __name__ == '__main__': - import warnings - warnings.warn("use 'python -m prismedia', not 'python -m prismedia.upload'", DeprecationWarning) + logger.warning("DEPRECATION: use 'python -m prismedia', not 'python -m prismedia.upload'") main() diff --git a/prismedia/utils.py b/prismedia/utils.py index b9e4c89..c2239c6 100644 --- a/prismedia/utils.py +++ b/prismedia/utils.py @@ -9,6 +9,8 @@ from subprocess import check_call, CalledProcessError, STDOUT import unidecode import logging +logger = logging.getLogger('Prismedia') + ### CATEGORIES ### YOUTUBE_CATEGORY = { "music": 10, @@ -123,18 +125,25 @@ def searchThumbnail(options): options['--thumbnail'] = video_directory + video_file + ".jpg" elif isfile(video_directory + video_file + ".jpeg"): options['--thumbnail'] = video_directory + video_file + ".jpeg" + + # Display some info after research + if not options.get('--thumbnail'): + logger.debug("No thumbnail has been found, continuing") + else: + logger.info("Using " + options.get('--thumbnail') + "as thumbnail") + return options # return the nfo as a RawConfigParser object def loadNFO(filename): try: - logging.info("Loading " + filename + " as NFO") + logger.info("Loading " + filename + " as NFO") nfo = RawConfigParser() nfo.read(filename, encoding='utf-8') return nfo except Exception as e: - logging.error("Problem loading NFO file " + filename + ": " + str(e)) + logger.critical("Problem loading NFO file " + filename + ": " + str(e)) exit(1) return False @@ -168,7 +177,17 @@ def parseNFO(options): if isfile(options.get('--nfo')): nfo_cli = loadNFO(options.get('--nfo')) else: - logging.error("Given NFO file does not exist, please check your path.") + logger.critical("Given NFO file does not exist, please check your path.") + exit(1) + + # If there is no NFO and strict option is enabled, then stop there + if options.get('--withNFO'): + if not isinstance(nfo_cli, RawConfigParser) and \ + not isinstance(nfo_file, RawConfigParser) and \ + not isinstance(nfo_videoname, RawConfigParser) and \ + not isinstance(nfo_directory, RawConfigParser) and \ + not isinstance(nfo_txt, RawConfigParser): + logger.critical("You have required the strict presence of NFO but none is found, please use a NFO.") exit(1) # We need to load NFO in this exact order to keep the priorities @@ -188,7 +207,7 @@ def parseNFO(options): except NoOptionError: continue except NoSectionError: - logging.error(nfo + " misses section [video], please check syntax of your NFO.") + logger.critical(nfo + " misses section [video], please check syntax of your NFO.") exit(1) return options diff --git a/prismedia/yt_upload.py b/prismedia/yt_upload.py index da21ccc..b96df94 100644 --- a/prismedia/yt_upload.py +++ b/prismedia/yt_upload.py @@ -23,9 +23,7 @@ from google_auth_oauthlib.flow import InstalledAppFlow from . import utils - -logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO) - +logger = logging.getLogger('Prismedia') # Explicitly tell the underlying HTTP transport library not to retry, since # we are handling retry logic ourselves. @@ -87,7 +85,7 @@ def check_authenticated_scopes(): credential_params = json.load(f) # Check if all scopes are present if credential_params["_scopes"] != SCOPES: - logging.warning("Youtube: Credentials are obsolete, need to re-authenticate.") + logger.warning("Youtube: Credentials are obsolete, need to re-authenticate.") os.remove(CREDENTIALS_PATH) @@ -144,8 +142,8 @@ def initialize_upload(youtube, options): if not playlist_id and options.get('--playlistCreate'): playlist_id = create_playlist(youtube, options.get('--playlist')) elif not playlist_id: - logging.warning("Youtube: Playlist `" + options.get('--playlist') + "` is unknown.") - logging.warning("If you want to create it, set the --playlistCreate option.") + logger.warning("Youtube: Playlist `" + options.get('--playlist') + "` is unknown.") + logger.warning("Youtube: If you want to create it, set the --playlistCreate option.") playlist_id = "" else: playlist_id = "" @@ -156,7 +154,7 @@ def initialize_upload(youtube, options): body=body, media_body=MediaFileUpload(path, chunksize=-1, resumable=True) ) - video_id = resumable_upload(insert_request, 'video', 'insert') + video_id = resumable_upload(insert_request, 'video', 'insert', options) # If we get a video_id, upload is successful and we are able to set thumbnail if video_id and options.get('--thumbnail'): @@ -179,8 +177,8 @@ def get_playlist_by_name(youtube, playlist_name): def create_playlist(youtube, playlist_name): - template = ('Youtube: Playlist %s does not exist, creating it.') - logging.info(template % (str(playlist_name))) + template = 'Youtube: Playlist %s does not exist, creating it.' + logger.info(template % (str(playlist_name))) resources = build_resource({'snippet.title': playlist_name, 'snippet.description': '', 'status.privacyStatus': 'public'}) @@ -244,7 +242,7 @@ def set_thumbnail(youtube, media_file, **kwargs): def set_playlist(youtube, playlist_id, video_id): - logging.info('Youtube: Configuring playlist...') + logger.info('Youtube: Configuring playlist...') resource = build_resource({'snippet.playlistId': playlist_id, 'snippet.resourceId.kind': 'youtube#video', 'snippet.resourceId.videoId': video_id, @@ -256,38 +254,48 @@ def set_playlist(youtube, playlist_id, video_id): part='snippet' ).execute() except Exception as e: - if hasattr(e, 'message'): - logging.error("Youtube: Error: " + str(e.message)) - exit(1) - else: - logging.error("Youtube: Error: " + str(e)) - exit(1) - logging.info('Youtube: Video is correctly added to the playlist.') + # Workaround while youtube API is broken, see issue #47 for details + if e.resp.status != 404 and "Video not found" not in str(e): + if hasattr(e, 'message'): + logger.critical("Youtube: " + str(e.message)) + exit(1) + else: + logger.critical("Youtube: " + str(e)) + exit(1) + logger.info('Youtube: Video is correctly added to the playlist.') # This method implements an exponential backoff strategy to resume a # failed upload. -def resumable_upload(request, resource, method): +def resumable_upload(request, resource, method, options): response = None error = None retry = 0 + logger_stdout = None + if options.get('--url-only') or options.get('--batch'): + logger_stdout = logging.getLogger('stdoutlogs') while response is None: try: template = 'Youtube: Uploading %s...' - logging.info(template % resource) + logger.info(template % resource) status, response = request.next_chunk() if response is not None: if method == 'insert' and 'id' in response: - logging.info('Youtube : Video was successfully uploaded.') + logger.info('Youtube : Video was successfully uploaded.') template = 'Youtube: Watch it at https://youtu.be/%s (post-encoding could take some time)' - logging.info(template % response['id']) + logger.info(template % response['id']) + template_stdout = 'https://youtu.be/%s' + if options.get('--url-only'): + logger_stdout.info(template_stdout % response['id']) + elif options.get('--batch'): + logger_stdout.info("Youtube: " + template_stdout % response['id']) return response['id'] elif method != 'insert' or "id" not in response: - logging.info('Youtube: Thumbnail was successfully set.') + logger.info('Youtube: Thumbnail was successfully set.') else: template = ('Youtube : The upload failed with an ' 'unexpected response: %s') - logging.error(template % response) + logger.critical(template % response) exit(1) except HttpError as e: if e.resp.status in RETRIABLE_STATUS_CODES: @@ -299,15 +307,14 @@ def resumable_upload(request, resource, method): error = 'Youtube : A retriable error occurred: %s' % e if error is not None: - logging.warning(error) + logger.warning(error) retry += 1 if retry > MAX_RETRIES: - logging.error('Youtube : No longer attempting to retry.') - exit(1) + logger.error('Youtube : No longer attempting to retry.') max_sleep = 2 ** retry sleep_seconds = random.random() * max_sleep - logging.warning('Youtube : Sleeping %f seconds and then retrying...' + logger.warning('Youtube : Sleeping %f seconds and then retrying...' % sleep_seconds) time.sleep(sleep_seconds) @@ -317,5 +324,5 @@ def run(options): try: initialize_upload(youtube, options) except HttpError as e: - logging.error('Youtube : An HTTP error %d occurred:\n%s' % (e.resp.status, + logger.error('Youtube : An HTTP error %d occurred:\n%s' % (e.resp.status, e.content)) diff --git a/pyproject.toml b/pyproject.toml index 14e0022..529da70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "prismedia" -version = "0.9.1" +version = "0.10.0" description = "scripting your way to upload videos on peertube and youtube" authors = [ "LecygneNoir ", @@ -21,10 +21,10 @@ python = ">=3.5" configparser = "^3.7.1" docopt = "^0.6.2" future = "^0.17.1" -google-api-python-client = "^1.7.6" -google-auth = "^1.6.1" -google-auth-httplib2 = "^0.0.3" -google-auth-oauthlib = "^0.2.0" +google-api-python-client = ">=1.7.6" +google-auth = ">=1.6.1" +google-auth-httplib2 = ">=0.0.3" +google-auth-oauthlib = ">=0.2.0" httplib2 = "^0.12.1" oauthlib = "^2.1.0" python-magic = "^0.4.15" @@ -32,7 +32,7 @@ python-magic-bin = { version = "^0.4.14", markers = "platform_system == 'Windows requests = "^2.18.4" requests-oauthlib = "^0.8.0" requests-toolbelt = "^0.9.1" -schema = "^0.6.8" +schema = ">=0.7.1" tzlocal = "^1.5.1" Unidecode = "^1.0.23" uritemplate = "^3.0.0"