Compare commits

...

18 Commits

Author SHA1 Message Date
  Zykino b895a4543e Add progress for peertube plugin 2 years ago
  Zykino 46bd46455d WIP Add a configuration global object 2 years ago
  Zykino f90e084b67 WIP Rename and move a bunch of files 2 years ago
  Zykino a2c76bcbe1 WIP Fix typos and other recommendations from pycharm 2 years ago
  Zykino 819f7e04e2 WIP Finally add Yapsy as a dependency 2 years ago
  Zykino 67b8732c13 WIP add a quick debug plugin 3 years ago
  Zykino 311771a555 WIP make Peertube platform’s plugin upload videos. Light mode 3 years ago
  Zykino fe37d0be74 WIP Implements the core flow 3 years ago
  Zykino e7d55bc97e WIP Add a help plugin 3 years ago
  Zykino bcfc8e723a WIP Populate the Video object 3 years ago
  Zykino b5004a63ed WIP Add hearthbeat functionnality 3 years ago
  Zykino 85b86db751 WIP Use spaces for indentation 3 years ago
  Zykino 15edd7987b WIP Prepare the docstring 3 years ago
  Zykino 4f4842fd1f WIP Enumerates the plugins types instead of using magic strings 3 years ago
  Zykino c1e6a6e62d WIP Makes the plugins folder an modifiable if needed 3 years ago
  Zykino 979229aa01 WIP Include some command lines options 3 years ago
  Zykino cedce92dbe WIP Add a Video class to hold all of its properties 3 years ago
  Zykino c9a9b380ab WIP basis for integrating a plugin system 3 years ago
23 changed files with 1604 additions and 1012 deletions
Split View
  1. +13
    -0
      PLUGINS.md
  2. +3
    -5
      README.md
  3. +123
    -203
      poetry.lock
  4. +58
    -0
      prismedia/configuration.py
  5. +254
    -0
      prismedia/core.py
  6. +72
    -0
      prismedia/pluginInterfaces.py
  7. +9
    -0
      prismedia/plugins/consumers/debug.prismedia-plugin
  8. +23
    -0
      prismedia/plugins/consumers/debug.py
  9. +10
    -0
      prismedia/plugins/interfaces/cli.prismedia-plugin
  10. +68
    -0
      prismedia/plugins/interfaces/cli.py
  11. +9
    -0
      prismedia/plugins/interfaces/help.prismedia-plugin
  12. +51
    -0
      prismedia/plugins/interfaces/help.py
  13. +11
    -0
      prismedia/plugins/platforms/peertube.prismedia-plugin
  14. +408
    -0
      prismedia/plugins/platforms/peertube.py
  15. +42
    -11
      prismedia/plugins/platforms/youtube.py
  16. +0
    -398
      prismedia/pt_upload.py
  17. +0
    -0
      prismedia/samples/peertube_secret.sample
  18. +0
    -0
      prismedia/samples/youtube_secret.json.sample
  19. +8
    -112
      prismedia/upload.py
  20. +215
    -191
      prismedia/utils.py
  21. +123
    -0
      prismedia/video.py
  22. +1
    -0
      pyproject.toml
  23. +103
    -92
      requirements.txt

+ 13
- 0
PLUGINS.md View File

@ -0,0 +1,13 @@
For our plugins we are using [yaspy](http://yapsy.sourceforge.net).
# Types
For an example of the exact methods required to be recognized as a particular type of plugin, see the concerned interface definition.
## Interface
Plugins that present an interface (cli, gui, configuration folders, …) for the user to tell Prismedia wich video needs to be uploaded, the infos of the videos, …
## Platform
Also called uploaders, they are the one doing the actual work of uploading video to a particular platform.
## Consumer
Thoses do actions once the upload is finished (successful or failed).

+ 3
- 5
README.md View File

@ -41,7 +41,7 @@ git clone https://git.lecygnenoir.info/LecygneNoir/prismedia.git prismedia
```
You may use pip to install requirements: `pip install -r requirements.txt` if you want to use the script directly.
(**note:** requirements are generated via `poetry export -f requirements.txt`)
(**note:** requirements are generated via `poetry export -f requirements.txt --output requirements.txt`)
Otherwise, you can use [poetry](https://python-poetry.org), which create a virtualenv for the project directly
(Or use the existing virtualenv if one is activated)
@ -55,7 +55,7 @@ poetry install
Generate configuration files by running `prismedia-init`.
Then, edit them to fill your credential as explained below.
Then, edit them to fill your credential as explained below.
### Peertube
Configuration is in **peertube_secret** file.
@ -64,8 +64,6 @@ You need your usual credentials and Peertube instance URL, in addition with API
You can get client_id and client_secret by logging in your peertube instance and reaching the URL:
https://domain.example/api/v1/oauth-clients/local
*Alternatively, you can set ``OAUTHLIB_INSECURE_TRANSPORT`` to 1 if you do not use https (not recommended)*
### Youtube
Configuration is in **youtube_secret.json** file.
Youtube uses combination of oauth and API access to identify.
@ -233,4 +231,4 @@ Available strict options:
Inspired by [peeror](https://git.rigelk.eu/rigelk/peeror) and [youtube-upload](https://github.com/tokland/youtube-upload)
## Contributors
Thanks to: @LecygneNoir, @Zykino, @meewan, @rigelk 😘
Thanks to: @LecygneNoir, @Zykino, @meewan, @rigelk 😘

+ 123
- 203
poetry.lock View File

@ -1,119 +1,89 @@
[[package]]
category = "main"
description = "Command Arguments for Humans."
name = "args"
optional = false
python-versions = "*"
version = "0.1.0"
[[package]]
description = "Command Arguments for Humans."
category = "main"
description = "Extensible memoizing collections and decorators"
name = "cachetools"
optional = false
python-versions = "*"
version = "3.1.1"
[[package]]
category = "main"
description = "Extensible memoizing collections and decorators"
name = "cachetools"
version = "4.2.0"
description = "Extensible memoizing collections and decorators"
category = "main"
optional = false
python-versions = "~=3.5"
version = "4.2.0"
[[package]]
category = "main"
description = "Python package for providing Mozilla's CA Bundle."
name = "certifi"
version = "2020.12.5"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = "*"
version = "2020.12.5"
[[package]]
category = "main"
description = "Universal encoding detector for Python 2 and 3"
name = "chardet"
version = "4.0.0"
description = "Universal encoding detector for Python 2 and 3"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "4.0.0"
[[package]]
category = "main"
description = "Python Command Line Interface Tools"
name = "clint"
version = "0.5.1"
description = "Python Command Line Interface Tools"
category = "main"
optional = false
python-versions = "*"
version = "0.5.1"
[package.dependencies]
args = "*"
[[package]]
category = "main"
description = "Updated configparser from Python 3.7 for Python 2.6+."
name = "configparser"
version = "3.8.1"
description = "Updated configparser from Python 3.7 for Python 2.6+."
category = "main"
optional = false
python-versions = ">=2.6"
version = "3.8.1"
[package.extras]
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"]
testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2)", "pytest-flake8"]
[[package]]
category = "main"
description = "Backports and enhancements for the contextlib module"
name = "contextlib2"
version = "0.6.0.post1"
description = "Backports and enhancements for the contextlib module"
category = "main"
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"
name = "docopt"
version = "0.6.2"
description = "Pythonic argument parser, that will make you smile"
category = "main"
optional = false
python-versions = "*"
version = "0.6.2"
[[package]]
category = "main"
description = "Clean single-source support for Python 3 and 2"
name = "future"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "0.17.1"
[[package]]
description = "Clean single-source support for Python 3 and 2"
category = "main"
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.23.0"
[package.dependencies]
google-auth = ">=1.21.1,<2.0dev"
googleapis-common-protos = ">=1.6.0,<2.0dev"
protobuf = ">=3.12.0"
pytz = "*"
requests = ">=2.18.0,<3.0.0dev"
setuptools = ">=34.0.0"
six = ">=1.13.0"
[package.extras]
grpc = ["grpcio (>=1.29.0,<2.0dev)"]
grpcgcp = ["grpcio-gcp (>=0.2.2)"]
grpcio-gcp = ["grpcio-gcp (>=0.2.2)"]
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
category = "main"
description = "Google API client core library"
name = "google-api-core"
version = "1.25.0"
description = "Google API client core library"
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"
version = "1.25.0"
[package.dependencies]
google-auth = ">=1.21.1,<2.0dev"
@ -121,7 +91,6 @@ googleapis-common-protos = ">=1.6.0,<2.0dev"
protobuf = ">=3.12.0"
pytz = "*"
requests = ">=2.18.0,<3.0.0dev"
setuptools = ">=40.3.0"
six = ">=1.13.0"
[package.extras]
@ -130,12 +99,12 @@ grpcgcp = ["grpcio-gcp (>=0.2.2)"]
grpcio-gcp = ["grpcio-gcp (>=0.2.2)"]
[[package]]
category = "main"
description = "Google API Client Library for Python"
name = "google-api-python-client"
version = "1.12.2"
description = "Google API Client Library for Python"
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "1.12.2"
[package.dependencies]
google-api-core = ">=1.21.0,<2dev"
@ -146,54 +115,32 @@ six = ">=1.13.0,<2dev"
uritemplate = ">=3.0.0,<4dev"
[[package]]
category = "main"
description = "Google Authentication Library"
name = "google-auth"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "1.23.0"
[package.dependencies]
cachetools = ">=2.0.0,<5.0"
pyasn1-modules = ">=0.2.1"
setuptools = ">=40.3.0"
six = ">=1.9.0"
[package.dependencies.rsa]
python = ">=3.5"
version = ">=3.1.4,<5"
[package.extras]
aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)"]
[[package]]
category = "main"
version = "1.24.0"
description = "Google Authentication Library"
name = "google-auth"
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"
version = "1.24.0"
[package.dependencies]
cachetools = ">=2.0.0,<5.0"
pyasn1-modules = ">=0.2.1"
setuptools = ">=40.3.0"
rsa = [
{version = "<4.6", markers = "python_version < \"3.6\""},
{version = ">=3.1.4,<5", markers = "python_version >= \"3.6\""},
]
six = ">=1.9.0"
[package.dependencies.rsa]
python = ">=3.6"
version = ">=3.1.4,<5"
[package.extras]
aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)"]
[[package]]
category = "main"
description = "Google Authentication Library: httplib2 transport"
name = "google-auth-httplib2"
version = "0.0.4"
description = "Google Authentication Library: httplib2 transport"
category = "main"
optional = false
python-versions = "*"
version = "0.0.4"
[package.dependencies]
google-auth = "*"
@ -201,27 +148,12 @@ httplib2 = ">=0.9.1"
six = "*"
[[package]]
category = "main"
description = "Google Authentication Library"
name = "google-auth-oauthlib"
optional = false
python-versions = "*"
version = "0.4.1"
[package.dependencies]
google-auth = "*"
requests-oauthlib = ">=0.7.0"
[package.extras]
tool = ["click"]
[[package]]
category = "main"
version = "0.4.2"
description = "Google Authentication Library"
name = "google-auth-oauthlib"
category = "main"
optional = false
python-versions = ">=3.6"
version = "0.4.2"
[package.dependencies]
google-auth = "*"
@ -231,12 +163,12 @@ requests-oauthlib = ">=0.7.0"
tool = ["click"]
[[package]]
category = "main"
description = "Common protobufs used in Google APIs"
name = "googleapis-common-protos"
version = "1.52.0"
description = "Common protobufs used in Google APIs"
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "1.52.0"
[package.dependencies]
protobuf = ">=3.6.0"
@ -245,28 +177,28 @@ protobuf = ">=3.6.0"
grpc = ["grpcio (>=1.0.0)"]
[[package]]
category = "main"
description = "A comprehensive HTTP client library."
name = "httplib2"
version = "0.12.3"
description = "A comprehensive HTTP client library."
category = "main"
optional = false
python-versions = "*"
version = "0.12.3"
[[package]]
category = "main"
description = "Internationalized Domain Names in Applications (IDNA)"
name = "idna"
version = "2.10"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.10"
[[package]]
category = "main"
description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
name = "oauthlib"
version = "2.1.0"
description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
category = "main"
optional = false
python-versions = "*"
version = "2.1.0"
[package.extras]
rsa = ["cryptography"]
@ -275,67 +207,66 @@ signedtoken = ["cryptography", "pyjwt (>=1.0.0)"]
test = ["nose", "unittest2", "cryptography", "mock", "pyjwt (>=1.0.0)", "blinker"]
[[package]]
category = "main"
description = "Protocol Buffers"
name = "protobuf"
version = "3.14.0"
description = "Protocol Buffers"
category = "main"
optional = false
python-versions = "*"
version = "3.14.0"
[package.dependencies]
six = ">=1.9"
[[package]]
category = "main"
description = "ASN.1 types and codecs"
name = "pyasn1"
version = "0.4.8"
description = "ASN.1 types and codecs"
category = "main"
optional = false
python-versions = "*"
version = "0.4.8"
[[package]]
category = "main"
description = "A collection of ASN.1-based protocols modules."
name = "pyasn1-modules"
version = "0.2.8"
description = "A collection of ASN.1-based protocols modules."
category = "main"
optional = false
python-versions = "*"
version = "0.2.8"
[package.dependencies]
pyasn1 = ">=0.4.6,<0.5.0"
[[package]]
category = "main"
description = "File type identification using libmagic"
name = "python-magic"
version = "0.4.20"
description = "File type identification using libmagic"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.4.20"
[[package]]
category = "main"
description = "File type identification using libmagic binary package"
marker = "platform_system == \"Windows\""
name = "python-magic-bin"
version = "0.4.14"
description = "File type identification using libmagic binary package"
category = "main"
optional = false
python-versions = "*"
version = "0.4.14"
[[package]]
category = "main"
description = "World timezone definitions, modern and historical"
name = "pytz"
version = "2020.5"
description = "World timezone definitions, modern and historical"
category = "main"
optional = false
python-versions = "*"
version = "2020.5"
[[package]]
category = "main"
description = "Python HTTP for Humans."
name = "requests"
version = "2.25.1"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.25.1"
[package.dependencies]
certifi = ">=2017.4.17"
@ -345,140 +276,133 @@ urllib3 = ">=1.21.1,<1.27"
[package.extras]
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
[[package]]
category = "main"
description = "OAuthlib authentication support for Requests."
name = "requests-oauthlib"
version = "0.8.0"
description = "OAuthlib authentication support for Requests."
category = "main"
optional = false
python-versions = "*"
version = "0.8.0"
[package.dependencies]
oauthlib = ">=0.6.2"
requests = ">=2.0.0"
[package.extras]
rsa = ["oauthlib (>=0.6.2)", "requests (>=2.0.0)"]
rsa = ["oauthlib[rsa] (>=0.6.2)", "requests (>=2.0.0)"]
[[package]]
category = "main"
description = "A utility belt for advanced users of python-requests"
name = "requests-toolbelt"
version = "0.9.1"
description = "A utility belt for advanced users of python-requests"
category = "main"
optional = false
python-versions = "*"
version = "0.9.1"
[package.dependencies]
requests = ">=2.0.1,<3.0.0"
[[package]]
category = "main"
description = "Pure-Python RSA implementation"
marker = "python_version >= \"3.6\""
name = "rsa"
version = "4.5"
description = "Pure-Python RSA implementation"
category = "main"
optional = false
python-versions = "*"
version = "4.4"
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.dependencies]
pyasn1 = ">=0.1.3"
[[package]]
category = "main"
description = "Pure-Python RSA implementation"
marker = "python_version >= \"3.5\""
name = "rsa"
version = "4.7"
description = "Pure-Python RSA implementation"
category = "main"
optional = false
python-versions = ">=3.5, <4"
version = "4.7"
[package.dependencies]
pyasn1 = ">=0.1.3"
[[package]]
category = "main"
description = "Simple data validation library"
name = "schema"
version = "0.7.3"
description = "Simple data validation library"
category = "main"
optional = false
python-versions = "*"
version = "0.7.3"
[package.dependencies]
contextlib2 = ">=0.5.5"
[[package]]
category = "main"
description = "Python 2 and 3 compatibility utilities"
name = "six"
version = "1.15.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
version = "1.15.0"
[[package]]
category = "main"
description = "tzinfo object for the local timezone"
name = "tzlocal"
version = "1.5.1"
description = "tzinfo object for the local timezone"
category = "main"
optional = false
python-versions = "*"
version = "1.5.1"
[package.dependencies]
pytz = "*"
[[package]]
category = "main"
description = "ASCII transliterations of Unicode text"
name = "unidecode"
version = "1.1.2"
description = "ASCII transliterations of Unicode text"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.1.2"
[[package]]
category = "main"
description = "URI templates"
name = "uritemplate"
version = "3.0.1"
description = "URI templates"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "3.0.1"
[[package]]
category = "main"
description = "HTTP library with thread-safe connection pooling, file post, and more."
name = "urllib3"
version = "1.26.2"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = "*"
version = "1.22"
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.extras]
brotli = ["brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "yapsy"
version = "1.12.2"
description = "Yet another plugin system"
category = "main"
description = "HTTP library with thread-safe connection pooling, file post, and more."
name = "urllib3"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
version = "1.26.2"
[package.extras]
brotli = ["brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
python-versions = "*"
[metadata]
content-hash = "41a9471d93da0f5e3d684cdf9e8f981659030d67f29ef6bf55c07a0d49a3ee93"
lock-version = "1.1"
python-versions = ">=3.5"
content-hash = "111e189319a1806c4ccf078c97a6a58738985f31a70a64a9bfc153db7b132008"
[metadata.files]
args = [
{file = "args-0.1.0.tar.gz", hash = "sha256:a785b8d837625e9b61c39108532d95b85274acd679693b71ebb5156848fcf814"},
]
cachetools = [
{file = "cachetools-3.1.1-py2.py3-none-any.whl", hash = "sha256:428266a1c0d36dc5aca63a2d7c5942e88c2c898d72139fca0e97fdd2380517ae"},
{file = "cachetools-3.1.1.tar.gz", hash = "sha256:8ea2d3ce97850f31e4a08b0e2b5e6c34997d7216a9d2c98e0f3978630d4da69a"},
{file = "cachetools-4.2.0-py3-none-any.whl", hash = "sha256:c6b07a6ded8c78bf36730b3dc452dfff7d95f2a12a2fed856b1a0cb13ca78c61"},
{file = "cachetools-4.2.0.tar.gz", hash = "sha256:3796e1de094f0eaca982441c92ce96c68c89cced4cd97721ab297ea4b16db90e"},
]
@ -508,8 +432,6 @@ future = [
{file = "future-0.17.1.tar.gz", hash = "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"},
]
google-api-core = [
{file = "google-api-core-1.23.0.tar.gz", hash = "sha256:1bb3c485c38eacded8d685b1759968f6cf47dd9432922d34edb90359eaa391e2"},
{file = "google_api_core-1.23.0-py2.py3-none-any.whl", hash = "sha256:94d8c707d358d8d9e8b0045c42be20efb58433d308bd92cf748511c7825569c8"},
{file = "google-api-core-1.25.0.tar.gz", hash = "sha256:d967beae8d8acdb88fb2f6f769e2ee0ee813042576a08891bded3b8e234150ae"},
{file = "google_api_core-1.25.0-py2.py3-none-any.whl", hash = "sha256:4656345cba9627ab1290eab51300a6397cc50370d99366133df1ae64b744e1eb"},
]
@ -518,8 +440,6 @@ google-api-python-client = [
{file = "google_api_python_client-1.12.2-py2.py3-none-any.whl", hash = "sha256:05cb331ed1aa15746f606c7e36ea51dbe7c29b1a5df9bbf58140901fe23d7142"},
]
google-auth = [
{file = "google-auth-1.23.0.tar.gz", hash = "sha256:5176db85f1e7e837a646cd9cede72c3c404ccf2e3373d9ee14b2db88febad440"},
{file = "google_auth-1.23.0-py2.py3-none-any.whl", hash = "sha256:b728625ff5dfce8f9e56a499c8a4eb51443a67f20f6d28b67d5774c310ec4b6b"},
{file = "google-auth-1.24.0.tar.gz", hash = "sha256:0b0e026b412a0ad096e753907559e4bdb180d9ba9f68dd9036164db4fdc4ad2e"},
{file = "google_auth-1.24.0-py2.py3-none-any.whl", hash = "sha256:ce752cc51c31f479dbf9928435ef4b07514b20261b021c7383bee4bda646acb8"},
]
@ -528,8 +448,6 @@ google-auth-httplib2 = [
{file = "google_auth_httplib2-0.0.4-py2.py3-none-any.whl", hash = "sha256:aeaff501738b289717fac1980db9711d77908a6c227f60e4aa1923410b43e2ee"},
]
google-auth-oauthlib = [
{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"},
{file = "google-auth-oauthlib-0.4.2.tar.gz", hash = "sha256:65b65bc39ad8cab15039b35e5898455d3d66296d0584d96fe0e79d67d04c51d9"},
{file = "google_auth_oauthlib-0.4.2-py2.py3-none-any.whl", hash = "sha256:d4d98c831ea21d574699978827490a41b94f05d565c617fe1b420e88f1fc8d8d"},
]
@ -625,8 +543,8 @@ requests-toolbelt = [
{file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"},
]
rsa = [
{file = "rsa-4.4-py2.py3-none-any.whl", hash = "sha256:4afbaaecc3e9550c7351fdf0ab3fea1857ff616b85bab59215f00fb42e0e9582"},
{file = "rsa-4.4.tar.gz", hash = "sha256:5d95293bbd0fbee1dd9cb4b72d27b723942eb50584abc8c4f5f00e4bcfa55307"},
{file = "rsa-4.5-py2.py3-none-any.whl", hash = "sha256:35c5b5f6675ac02120036d97cf96f1fde4d49670543db2822ba5015e21a18032"},
{file = "rsa-4.5.tar.gz", hash = "sha256:4d409f5a7d78530a4a2062574c7bd80311bc3af29b364e293aa9b03eea77714f"},
{file = "rsa-4.7-py3-none-any.whl", hash = "sha256:a8774e55b59fd9fc893b0d05e9bfc6f47081f46ff5b46f39ccf24631b7be356b"},
{file = "rsa-4.7.tar.gz", hash = "sha256:69805d6b69f56eb05b62daea3a7dbd7aa44324ad1306445e05da8060232d00f4"},
]
@ -650,8 +568,10 @@ uritemplate = [
{file = "uritemplate-3.0.1.tar.gz", hash = "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"},
]
urllib3 = [
{file = "urllib3-1.22-py2.py3-none-any.whl", hash = "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b"},
{file = "urllib3-1.22.tar.gz", hash = "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"},
{file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"},
{file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"},
]
yapsy = [
{file = "Yapsy-1.12.2-py3.6.egg", hash = "sha256:83891e22db0a74445726981df0ef7818dae595454de9cf10b7ba603d45ccd157"},
{file = "Yapsy-1.12.2.tar.gz", hash = "sha256:d8113d9f9c74eacf65b4663c9c037d278c9cb273b5eee5f0e1803baeedb23f8b"},
]

+ 58
- 0
prismedia/configuration.py View File

@ -0,0 +1,58 @@
from configparser import RawConfigParser
from os.path import splitext, basename, dirname, abspath
class Configuration:
"""
Configuration manager that read the configuration from multiples nfo files that are commons for all interfaces
The configuration will be read and overridden in the following order:
NFO.txt -> nfo.txt -> directory_name.txt -> video_file.txt -> video_name.txt
(value in the rightmost file override values in preceding files)
Your interface may add to this list from either side. Refer to the plugin's description.
A plugin can also override completely this configuration by using other means. For example the cli plugins takes
command line arguments with more importance than any nfo file.
Attributes:
CONFIG_FILE Filename for the base configuration file that should be used to set global behavior for
prismedia and its plugins
"""
CONFIG_FILE = "prismedia.config" # TODO: replace with "config.txt"? something else?
def __init__(self):
self.root_path = dirname(abspath(__file__))
self.config_parser = RawConfigParser()
self.configuration_file_list = []
self.base_configuration_file = self.root_path + "/config/" + self.CONFIG_FILE
self.config_parser.read(self.base_configuration_file)
def read_commons_nfo(self, video_path, video_name):
video_directory = dirname(video_path)
directory_name = basename(video_directory)
video_file = splitext(basename(video_path))[0]
self.configuration_file_list.append(video_directory + "/" + "NFO.txt")
self.configuration_file_list.append(video_directory + "/" + "nfo.txt")
self.configuration_file_list.append(video_directory + "/" + directory_name + ".txt")
self.configuration_file_list.append(video_directory + "/" + video_file + ".txt")
if video_name and video_name != video_file:
self.configuration_file_list.append(video_directory + "/" + video_name + ".txt")
self.config_parser.read(self.configuration_file_list)
# Do not use this in actual production ready code. Prismedia should not write any file
# This can be used in local to see how a plugin's variable needs to be written in the global NFO
# I am afraid that trying to save a plugin info will also save the current config for the video
def _write_config(self):
"""
Write the content of the ConfigParser in a file.
"""
# Do not use `assert` since we want to do the opposite and optimize the other way around
if not __debug__:
raise AssertionError("The method `Configuration._write_config` should not be called in production")
with open(self.base_configuration_file + "_generated", "w") as configFile:
self.config_parser.write(configFile)
configuration_instance = Configuration()

+ 254
- 0
prismedia/core.py View File

@ -0,0 +1,254 @@
#!/usr/bin/python
# coding: utf-8
# NOTE: Since we use config file to set some defaults values, it is not possible to use the standard syntax with brackets, we use parenthesis instead.
# If we were to use them we would override configuration file values with default values of cli.
# TODO: change `youtube-at` and `peertube-at` that are not easily expendable as options in my opinion
# TODO: remove `--url-only` and `--batch`
"""
prismedia - tool to upload videos to different platforms (historically Peertube and Youtube)
Usage:
prismedia [cli] [options] --file=<file>
prismedia <interface> [<parameters>...]
prismedia --heartbeat
prismedia --help | -h | --version | -V
Options:
-f, --file=STRING Path to the video file to upload in mp4. This is the only mandatory option except if you provide the name of a plugin interface (see <interface>).
--thumbnail=STRING Path to a file to use as a thumbnail for the video.
--name=NAME Name of the video to upload. (default to video filename)
-d, --description=STRING Description of the video. (default: default description)
-t, --tag=STRING Tags for the video. comma separated.
WARN: tags with punctuation (!, ', ", ?, ...)
are not supported by Mastodon to be published from Peertube
-c, --category=STRING Category for the videos, see below. (default: Films)
--licence=STRING Creative Common licence tag (for example: CC-BY-SA) (default: proprietary)
-p, --privacy=STRING Choose between public, unlisted or private. (default: private)
--disable-comments Disable comments (Peertube only as YT API does not support) (default: comments are enabled)
--nsfw Set the video as No Safe For Work (Peertube only as YT API does not support) (default: video is safe)
--nfo=STRING Configure a specific nfo file to set options for the video.
By default Prismedia search a .txt based on the video name and will
decode the file as UTF-8 (so make sure your nfo file is UTF-8 encoded)
See nfo_example.txt for more details
--language=STRING Specify the default language for video. See below for supported language. (default is English)
--publish-at=DATE Publish the video at the given DATE using local server timezone.
DATE should be on the form YYYY-MM-DDThh:mm:ss eg: 2018-03-12T19:00:00
DATE should be in the future
--peertube-at=DATE Override publish-at for the corresponding platform. Allow to create preview on specific platform
--youtube-at=DATE Override publish-at for the corresponding platform. Allow to create preview on specific platform
--original-date=DATE Configure the video as initially recorded at DATE
DATE should be on the form YYYY-MM-DDThh:mm:ss eg: 2018-03-12T19:00:00
DATE should be in the past
--auto-original-date Automatically use the file modification time as original date
Supported types are jpg and jpeg.
By default, prismedia search for an image based on video name followed by .jpg or .jpeg
--channel=STRING Set the channel to use for the video (Peertube only)
If the channel is not found, spawn an error except if --channelCreate is set.
--channel-create Create the channel if not exists. (Peertube only, default do not create)
Only relevant if --channel is set.
--playlist=STRING Set the playlist to use for the video.
If the playlist is not found, spawn an error except if --playlistCreate is set.
--playlist-create Create the playlist if not exists. (default do not create)
Only relevant if --playlist is set.
--progress=STRING Set the progress bar view, one of percentage, bigFile, accurate. [default: percentage]
--heartbeat Use some credits to show some activity for you apikey so the platform know it is used and would not inactivate your keys.
-h, --help Show this help. Note that calling `help` without the `--` calls a plugin showing a different help for the plugins.
-V, --version Show the version.
Plugins options:
<interface> Interface plugin to use to provide the video to upload. Select the interface you want to use. If `--file` is provided instead the interface will be the command line.
--platform=STRING Platforms plugins to use. Usually one platform plugin upload to one platform website (comma separated list) (default: all)
--consumer=STRING Consumers plugins to use. They are executed after an upload has been done (comma separated list) (default: all)
Logging options:
--log=STRING Log level, between debug, info, warning, error, critical. Ignored if --quiet is set (default to info)
-q, --quiet Suppress any log except Critical (alias for --log=critical).
-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.
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
--with-NFO Prevent the upload without a NFO, either specified via cli or found in the directory
--with-thumbnail Prevent the upload without a thumbnail
--with-name Prevent the upload if no name are found
--with-description Prevent the upload without description
--with-tag Prevent the upload without tags
--with-playlist Prevent the upload if no playlist
--with-publish-at Prevent the upload if no schedule
--with-original-date Prevent the upload if no original date configured
--with-platform Prevent the upload if at least one platform is not specified
--with-category Prevent the upload if no category
--with-language Prevent upload if no language
--with-channel 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:
music, films, vehicles, sports, travels, gaming, people,
comedy, entertainment, news, how to, education, activism,
science & technology, science, technology, animals
Languages:
Language of the video (audio track), choose one. Default is English
Here are available languages from Peertube and Youtube:
Arabic, English, French, German, Hindi, Italian, Japanese,
Korean, Mandarin, Portuguese, Punjabi, Russian, Spanish
"""
import os
import logging
import pluginInterfaces as pi
import configuration
import utils
import video as vid
from docopt import docopt
from yapsy.PluginManager import PluginManagerSingleton
# logging.basicConfig(level=logging.DEBUG)
VERSION = "prismedia v1.0.0-plugins-alpha"
def loadPlugins():
from yapsy.ConfigurablePluginManager import ConfigurablePluginManager
config = configuration.configuration_instance
basePluginsPath = [config.root_path + "/plugins"]
# TODO: check if AutoInstallPluginManager can help install new plugins or if it is already easy enough to download
# and unzip a file.
PluginManagerSingleton.setBehaviour([ConfigurablePluginManager])
pluginManager = PluginManagerSingleton.get()
pluginManager.setPluginPlaces(directories_list=basePluginsPath)
pluginManager.setPluginInfoExtension("prismedia-plugin")
pluginManager.setConfigParser(config.config_parser, pluginManager.config_has_changed)
# Define the various categories corresponding to the different
# kinds of plugins you have defined
pluginManager.setCategoriesFilter({
pi.PluginTypes.ALL: pi.IPrismediaBasePlugin,
pi.PluginTypes.INTERFACE: pi.IInterfacePlugin,
pi.PluginTypes.PLATFORM: pi.IPlatformPlugin,
pi.PluginTypes.CONSUMER: pi.IConsumerPlugin,
})
pluginManager.collectPlugins()
return pluginManager
# TODO: cut this function into smaller ones
def main():
logger = logging.getLogger('Prismedia')
# TODO: Check: Maybe this does not work good when installed via pip.
pluginManager = loadPlugins()
# TODO: add the arguments’s verification (copy/adapt the Schema table)
options = docopt(__doc__, version=VERSION)
# Helper functionalities help the user but do not upload anything
if not utils.helperFunctionalities(options):
exit(os.EX_OK)
# Get all arguments needed by core only before calling any plugin
listPlatforms = utils.getOption(options, "--platform")
listConsumers = utils.getOption(options, "--consumer")
if options["<interface>"]:
interface_name = utils.getOption("<interface>")
else:
interface_name = "cli"
interface = pluginManager.getPluginByName(interface_name, pi.PluginTypes.INTERFACE)
video = vid.Video()
try:
if not interface.plugin_object.prepare_options(video, options):
# The plugin asked to stop execution.
exit(os.EX_OK)
except Exception as e:
logger.critical(utils.get_exception_string(e))
exit(os.EX_CONFIG)
if listPlatforms:
platforms = pluginManager.getPluginsOf(categories=pi.PluginTypes.PLATFORM, name=[listPlatforms.split(",")])
else:
platforms = pluginManager.getPluginsOfCategory(pi.PluginTypes.PLATFORM)
if listConsumers:
consumers = pluginManager.getPluginsOf(categories=pi.PluginTypes.CONSUMER, name=[listConsumers.split(",")])
else:
consumers = pluginManager.getPluginsOfCategory(pi.PluginTypes.CONSUMER)
# Let each plugin check its options before starting any upload
# We cannot merge this loop with the one from interface since the interface can change which plugin to use
# We need to create each platform object in video, so we cannot merge this loop with the following one
for plugin in platforms:
# TODO: Check this is needed or not: in case of no plugin or wrong name maybe the list is empty instead of there being a None value
if plugin is None:
# TODO: log instead to error? critical?
print("No plugin installed name `" + plugin.name + "`.")
exit(os.EX_USAGE)
try:
video.platform[plugin.name] = vid.Platform()
if not plugin.plugin_object.prepare_options(video, options):
# A plugin found ill formed options, it should have logged the precises info
print(plugin.name + " found a malformed option.")
exit(os.EX_CONFIG)
except Exception as e:
logger.critical("Error while preparing plugin `" + plugin.name + "`: " + utils.get_exception_string(e))
exit(os.EX_CONFIG)
for plugin in consumers:
# TODO: Check this is needed or not: in case of no plugin or wrong name maybe the list is empty instead of there being a None value
if plugin is None:
# TODO: log instead to error? critical?
print("No plugin installed name `" + plugin.name + "`.")
exit(os.EX_USAGE)
try:
if not plugin.plugin_object.prepare_options(video, options):
# A plugin found ill formed options, it should have logged the precises info
print(plugin.name + " found a malformed option.")
exit(os.EX_CONFIG)
except Exception as e:
logger.critical(utils.get_exception_string(e))
exit(os.EX_CONFIG)
if video.path == "":
# TODO: log instead to error? critical?
print("No valid path to a video file has been provided.")
exit(os.EX_USAGE)
print("All options validated, starting uploads onto platforms")
for platform in platforms:
print("Uploading to: " + platform.name)
try:
platform.plugin_object.upload(video, options)
except Exception as e: # TODO: Maybe not catch every Exception?
logger.critical(utils.get_exception_string(e))
video.platform[platform.name].error = e
video.platform[platform.name].publishAt = None
video.platform[platform.name].url = None
print("All uploads have been done, calling consumers plugins")
for consumer in consumers:
print("Calling consumer: " + consumer.name)
consumer.plugin_object.finished(video, options)
main()

+ 72
- 0
prismedia/pluginInterfaces.py View File

@ -0,0 +1,72 @@
from enum import Enum
from yapsy.IPlugin import IPlugin
class PluginTypes(Enum):
"""Plugin Types possibles to instantiate in this program."""
ALL = "All"
INTERFACE = "Interface"
PLATFORM = "Platform"
CONSUMER = "Consumer"
class IPrismediaBasePlugin(IPlugin):
"""
Base for prismedias plugin.
"""
def prepare_options(self, video, options):
"""
Return a falsy value to exit the program.
- `video`: video object to be uploaded
- `options`: a dictionary of options to be used by Prismedia and other plugins
"""
raise NotImplementedError("`prepare_options` must be reimplemented by %s" % self)
###
# Interface
###
class IInterfacePlugin(IPrismediaBasePlugin):
"""
Interface for the Interface plugin category.
"""
# TODO: Add callback for communicating upload’s progress to the user
###
# Platform
###
class IPlatformPlugin(IPrismediaBasePlugin):
"""
Interface for the Platform plugin category.
"""
def heartbeat(self):
"""
If needed for your platform, use a bit of the api so the platform is aware the keys are still in use.
"""
raise NotImplementedError("`heartbeat` must be reimplemented by %s" % self)
def upload(self, video, options):
"""
The upload function
"""
raise NotImplementedError("`upload` must be reimplemented by %s" % self)
###
# Consumer
###
class IConsumerPlugin(IPrismediaBasePlugin):
"""
Interface for the Consumer plugin category.
"""
def finished(self, video, options):
"""
What to do once the uploads are done.
- `video` is an object containing the video details. The `platforms` key contain a list of the platforms the video has been uploaded to and the status
"""
raise NotImplementedError("`finished` must be reimplemented by %s" % self)

+ 9
- 0
prismedia/plugins/consumers/debug.prismedia-plugin View File

@ -0,0 +1,9 @@
[Core]
Name = debug
Module = debug
[Documentation]
Author = Zykino
Version = 0.1
Website = https://git.lecygnenoir.info/LecygneNoir/prismedia
Description = Show status of elements

+ 23
- 0
prismedia/plugins/consumers/debug.py View File

@ -0,0 +1,23 @@
#!/usr/bin/env python
# coding: utf-8
import pluginInterfaces as pi
import utils
import video as vid
class Debug(pi.IConsumerPlugin):
"""
Plugin to help knowing the state of prismedia and its variables.
"""
def prepare_options(self, video, options):
print("Debug plugin prepare_options:")
print("Video: ", video)
print("Options: ", options)
return True
def finished(self, video, options):
print("Debug plugin finished:")
print("Video: ", video)
print("Options: ", options)

+ 10
- 0
prismedia/plugins/interfaces/cli.prismedia-plugin View File

@ -0,0 +1,10 @@
[Core]
Name = cli
Module = cli
[Documentation]
Author = Zykino
Version = 0.1
Website = https://git.lecygnenoir.info/LecygneNoir/prismedia
Description = This interface plugin is used to get the videos details from the Command Line Interface (cli).
To work properly it needs to have the `--file="<path/to/you/vide/file>"` argument

+ 68
- 0
prismedia/plugins/interfaces/cli.py View File

@ -0,0 +1,68 @@
import pluginInterfaces as pi
import utils
import configuration
class Cli(pi.IInterfacePlugin):
"""
This is the default interface plugin. It is used when no interface plugin is specified.
Its core functionality is available as a function call to `prepare_options(video, options)` if you do not need the
Cli object.
This can be useful to let other plugins to rely on the defaults behaviors proposed by this one and extend it.
"""
def prepare_options(self, video, options):
prepare_options(video, options)
def prepare_options(video, options):
# TODO: Add the configuration file from the `--nfo` cli argument
_store_docopt_to_configuration(video, options)
_populate_configuration_into_video(video)
def _store_docopt_to_configuration(options):
items = {}
for key, value in options:
if key.startswith("--"):
options.pop(key)
items[key.strip("- ")] = value
configuration.configuration_instance.config_parser.read_dict(items)
def _populate_configuration_into_video(self, video):
config = configuration.configuration_instance
video.path = utils.getOption(config, "file", video.path)
video.thumbnail = utils.getOption(config, "thumbnail", video.thumbnail)
video.name = utils.getOption(config, "name", video.name)
video.description = utils.getOption(config, "description", video.description)
video.playlistName = utils.getOption(config, "playlist", video.playlistName)
video.privacy = utils.getOption(config, "privacy", video.privacy).lower()
video.category = utils.getOption(config, "category", video.category).lower()
tags = utils.getOption(config, "tag", video.tags)
if isinstance(tags, str):
tags = tags.split(",")
video.tags = tags
video.language = utils.getOption(config, "language", video.language).lower()
video.originalDate = utils.getOption(config, "original-date", video.originalDate)
# TODO: set as an object: { "all": date1, "platformX": date2, … }?
# Maybe the publishAt by platform is better placed in `self.platform`
# And so publishAt would only contains the global date.
video.publishAt = utils.getOption(config, "publish-at", video.publishAt)
# TODO: Add a list of licences
video.licence = utils.getOption(config, "licence", video.licence)
video.disableComments = utils.getOption(config, "disable-comments", video.disableComments)
video.nsfw = utils.getOption(config, "nsfw", video.nsfw)
autoOriginalDate = utils.getOption(config, "auto-original-date", False)
if autoOriginalDate:
# TODO: Implement
raise NotImplementedError("auto-original-date functionality is not yet implemented.")
return video

+ 9
- 0
prismedia/plugins/interfaces/help.prismedia-plugin View File

@ -0,0 +1,9 @@
[Core]
Name = help
Module = help
[Documentation]
Author = Zykino
Version = 0.1
Website = https://git.lecygnenoir.info/LecygneNoir/prismedia
Description = Give information about plugins usage by using the syntax `prismedia help [<plugin_name>...]`.

+ 51
- 0
prismedia/plugins/interfaces/help.py View File

@ -0,0 +1,51 @@
import pluginInterfaces as pi
from yapsy.PluginManager import PluginManagerSingleton
class Help(pi.IInterfacePlugin):
"""
The help plugin print the usage information of prismedias plugins.
Use it by simply calling `prismedia help <plugin_name>...`.
For example `prismedia help help` bring this help.
"""
def prepare_options(self, video, options):
print(__name__)
pluginManager = PluginManagerSingleton.get()
if options["<parameters>"]:
parameters = options["<parameters>"]
else:
parameters = ["help"]
for p in parameters:
plugin = pluginManager.getPluginByName(p, pi.PluginTypes.ALL)
if plugin is None:
# TODO: log instead to warning? error?
print("No plugin was found with name:", p)
continue
print(plugin.name + "\t" + plugin.description)
print("Usage:", plugin.plugin_object.__doc__)
# Generic help this plugin is able to give for the
if p == "help":
print("The plugins are stored in the following folders:", pluginManager.getPluginLocator().plugins_places)
print("Category:", pi.PluginTypes.INTERFACE.value)
for plugin in pluginManager.getPluginsOfCategory(pi.PluginTypes.INTERFACE):
print("\t" + plugin.name)
print("Category:", pi.PluginTypes.PLATFORM.value)
for plugin in pluginManager.getPluginsOfCategory(pi.PluginTypes.PLATFORM):
print("\t" + plugin.name)
print("Category:", pi.PluginTypes.CONSUMER.value)
for plugin in pluginManager.getPluginsOfCategory(pi.PluginTypes.CONSUMER):
print("\t" + plugin.name)
# Print a line break between each plugin help.
print()
return False

+ 11
- 0
prismedia/plugins/platforms/peertube.prismedia-plugin View File

@ -0,0 +1,11 @@
[Core]
Name = peertube
Module = peertube
[Documentation]
Author = Le Cygne Noir
Version = 0.1
Website = https://git.lecygnenoir.info/LecygneNoir/prismedia
Description = Upload to the peertube platform
**NOT SECURE:** If the peertube instance you want to upload to has no SSL certificate (http but not https), you can set the environment variable `OAUTHLIB_INSECURE_TRANSPORT=1`. Keep in mind that using this option makes your credentials vulnerable to interception by a malicious 3rd party. Use this only with dummy credential on a test instance.

+ 408
- 0
prismedia/plugins/platforms/peertube.py View File

@ -0,0 +1,408 @@
#!/usr/bin/env python
# coding: utf-8
import pluginInterfaces as pi
import utils
import mimetypes
import json
import logging
import datetime
import pytz
from os.path import splitext, basename, abspath
from tzlocal import get_localzone
from configparser import RawConfigParser
from requests_oauthlib import OAuth2Session
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
from oauthlib.oauth2 import LegacyApplicationClient
from clint.textui.progress import Bar as ProgressBar
from yapsy.PluginManager import PluginManagerSingleton
logger = logging.getLogger('Prismedia')
upload_finished = False
class Peertube(pi.IPlatformPlugin):
"""
Plugin to upload to the Peertube platform.
The connections files should be set as # TODO: EXPLAIN HOW TO SETUP THE SECRET FILES
- `publish-at-peertube=DATE`: overrides the default `publish-at=DATE` for this platform. # TODO: Maybe we will use a [<plugin_name>] section on the config fire, explain that.
"""
NAME = "peertube" # TODO: find if it is possible to get the plugin’s name from inside the plugin
SECRETS_FILE = "peertube_secret"
PRIVACY = {
"public": 1,
"unlisted": 2,
"private": 3
}
CATEGORY = {
"music": 1,
"films": 2,
"vehicles": 3,
"sport": 5,
"travels": 6,
"gaming": 7,
"people": 8,
"comedy": 9,
"entertainment": 10,
"news": 11,
"how to": 12,
"education": 13,
"activism": 14,
"science & technology": 15,
"science": 15,
"technology": 15,
"animals": 16
}
LANGUAGE = {
"arabic": "ar",
"english": "en",
"french": "fr",
"german": "de",
"hindi": "hi",
"italian": "it",
"japanese": "ja",
"korean": "ko",
"mandarin": "zh",
"portuguese": "pt",
"punjabi": "pa",
"russian": "ru",
"spanish": "es"
}
def __init__(self):
self.channelCreate = False
self.oauth = {}
self.secret = {}
def prepare_options(self, video, options):
pluginManager = PluginManagerSingleton.get()
# TODO: get the `publish-at-peertube=DATE` option
# TODO: get the `channel` and `channel-create` options
pluginManager.registerOptionFromPlugin("Platform", self.NAME, "publish-at", "2034-05-07T19:00:00")
pluginManager.registerOptionFromPlugin("Platform", self.NAME, "channel", "toto")
pluginManager.registerOptionFromPlugin("Platform", self.NAME, "channel-create", False)
video.platform[self.NAME].channel = ""
self.secret = RawConfigParser()
self.secret.read(self.SECRETS_FILE)
self.get_authenticated_service()
return True
def get_authenticated_service(self):
instance_url = str(self.secret.get('peertube', 'peertube_url')).rstrip("/")
oauth_client = LegacyApplicationClient(
client_id=str(self.secret.get('peertube', 'client_id'))
)
self.oauth = OAuth2Session(client=oauth_client)
self.oauth.fetch_token(
token_url=str(instance_url + '/api/v1/users/token'),
# lower as peertube does not store uppercase for pseudo
username=str(self.secret.get('peertube', 'username').lower()),
password=str(self.secret.get('peertube', 'password')),
client_id=str(self.secret.get('peertube', 'client_id')),
client_secret=str(self.secret.get('peertube', 'client_secret'))
)
def convert_peertube_date(self, date):
date = datetime.datetime.strptime(date, '%Y-%m-%dT%H:%M:%S')
tz = get_localzone()
tz = pytz.timezone(str(tz))
return tz.localize(date).isoformat()
def get_default_channel(self, user_info):
return user_info['videoChannels'][0]['id']
def get_channel_by_name(self, user_info, video):
for channel in user_info["videoChannels"]:
if channel['displayName'] == video.platform[self.NAME].channel:
return channel['id']
def create_channel(self, instance_url, video):
template = ('Peertube: Channel %s does not exist, creating it.')
logger.info(template % (video.platform[self.NAME].channel))
channel_name = utils.cleanString(video.platform[self.NAME].channel)
# Peertube allows 20 chars max for channel name
channel_name = channel_name[:19]
data = '{"name":"' + channel_name + '", \
"displayName":"' + video.platform[self.NAME].channel + '", \
"description":null, \
"support":null}'
headers = {
'Content-Type': "application/json; charset=UTF-8"
}
try:
response = self.oauth.post(instance_url + "/api/v1/video-channels/",
data=data.encode('utf-8'),
headers=headers)
except Exception as e:
logger.error("Peertube: " + utils.get_exception_string(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:
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:
logger.critical(('Peertube: Creating channel failed with an unexpected response: '
'%s') % response)
exit(1)
def get_default_playlist(self, user_info):
return user_info['videoChannels'][0]['id']
def get_playlist_by_name(self, instance_url, username, video):
start = 0
user_playlists = json.loads(self.oauth.get(
instance_url + "/api/v1/accounts/" + username + "/video-playlists?start=" + str(
start) + "&count=100").content)
total = user_playlists["total"]
data = user_playlists["data"]
# We need to iterate on pagination as peertube returns max 100 playlists (see #41)
while start < total:
for playlist in data:
if playlist['displayName'] == video.playlistName:
return playlist['id']
start = start + 100
user_playlists = json.loads(self.oauth.get(
instance_url + "/api/v1/accounts/" + username + "/video-playlists?start=" + str(
start) + "&count=100").content)
data = user_playlists["data"]
def create_playlist(self, instance_url, video, channel):
template = ('Peertube: Playlist %s does not exist, creating it.')
logger.info(template % (str(video.playlistName)))
# 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
files = {'displayName': (None, str(video.playlistName)),
'privacy': (None, "1"),
'description': (None, "null"),
'videoChannelId': (None, str(channel)),
'thumbnailfile': (None, "null")}
try:
response = self.oauth.post(instance_url + "/api/v1/video-playlists/",
files=files)
except Exception as e:
logger.error("Peertube: " + utils.get_exception_string(e))
if response is not None:
if response.status_code == 200:
jresponse = response.json()
jresponse = jresponse['videoPlaylist']
return jresponse['id']
else:
logger.critical(('Peertube: Creating the playlist failed with an unexpected response: '
'%s') % response)
exit(1)
def set_playlist(self, instance_url, video_id, playlist_id):
logger.info('Peertube: add video to playlist.')
data = '{"videoId":"' + str(video_id) + '"}'
headers = {
'Content-Type': "application/json"
}
try:
response = self.oauth.post(instance_url + "/api/v1/video-playlists/" + str(playlist_id) + "/videos",
data=data,
headers=headers)
except Exception as e:
logger.error("Peertube: " + utils.get_exception_string(e))
if response is not None:
if response.status_code == 200:
logger.info('Peertube: Video is successfully added to the playlist.')
else:
logger.critical(('Peertube: Configuring the playlist failed with an unexpected response: '
'%s') % response)
exit(1)
def upload_video(self, video, options):
def get_userinfo(base_url):
return json.loads(self.oauth.get(base_url + "/api/v1/users/me").content)
def get_file(video_path):
mimetypes.init()
return (basename(video_path), open(abspath(video_path), 'rb'),
mimetypes.types_map[splitext(video_path)[1]])
path = video.path
instance_url = str(self.secret.get('peertube', 'peertube_url')).rstrip('/')
user_info = get_userinfo(instance_url)
username = str(self.secret.get('peertube', 'username').lower())
# We need to transform fields into tuple to deal with tags as
# MultipartEncoder does not support list refer
# https://github.com/requests/toolbelt/issues/190 and
# https://github.com/requests/toolbelt/issues/205
fields = [
("name", video.name),
("licence", "1"), # TODO: get licence from video object
("description", video.description),
("category", str(self.CATEGORY[video.category])),
("language", str(self.LANGUAGE[video.language])),
("commentsEnabled", "0" if video.disableComments else "1"),
("nsfw", "1" if video.nsfw else "0"),
("videofile", get_file(path))
]
tag_number = 0
for strtag in video.tags:
tag_number = tag_number + 1
# Empty tag crashes Peertube, so skip them
if strtag == "":
continue
# Tag more than 30 chars crashes Peertube, so skip tags
if len(strtag) >= 30:
logger.warning(
"Peertube: Sorry, Peertube does not support tag with more than 30 characters, please reduce tag: " + strtag)
logger.warning("Peertube: Meanwhile, this tag will be skipped")
continue
# Peertube supports only 5 tags at the moment
if tag_number > 5:
logger.warning("Peertube: Sorry, Peertube support 5 tags max, additional tag will be skipped")
logger.warning("Peertube: Skipping tag " + strtag)
continue
fields.append(("tags[]", strtag))
# If peertubeAt exists, use instead of publishAt
if video.platform[self.NAME].publishAt:
publishAt = video.platform[self.NAME].publishAt
elif video.publishAt:
publishAt = video.publishAt
if 'publishAt' in locals():
publishAt = convert_peertube_date(publishAt)
fields.append(("scheduleUpdate[updateAt]", publishAt))
fields.append(("scheduleUpdate[privacy]", str(self.PRIVACY["public"])))
fields.append(("privacy", str(self.PRIVACY["private"])))
else:
fields.append(("privacy", str(self.PRIVACY[video.privacy])))
if video.originalDate:
originalDate = convert_peertube_date(video.originalDate)
fields.append(("originallyPublishedAt", originalDate))
if video.thumbnail:
fields.append(("thumbnailfile", get_file(video.thumbnail)))
fields.append(("previewfile", get_file(video.thumbnail)))
if hasattr(video.platform[self.NAME], "channel"): # TODO: Should always be present
channel_id = self.get_channel_by_name(user_info, video)
if not channel_id and self.channelCreate:
channel_id = self.create_channel(instance_url, video)
elif not channel_id:
logger.warning("Peertube: Channel `" + video.platform[
self.NAME].channel + "` is unknown, using default channel.") # TODO: debate if we should have the same message and behavior than playlist: "does not exist, please set --channelCreate"
channel_id = self.get_default_channel(user_info)
else:
channel_id = self.get_default_channel(user_info)
fields.append(("channelId", str(channel_id)))
if video.playlistName:
playlist_id = get_playlist_by_name(instance_url, username, video)
if not playlist_id and video.playlistCreate:
playlist_id = create_playlist(instance_url, video, channel_id)
elif not playlist_id:
logger.critical(
"Peertube: Playlist `" + video.playlistName + "` does not exist, please set --playlistCreate"
" if you want to create it")
exit(1)
encoder = MultipartEncoder(fields)
if options.get('--quiet'):
multipart_data = encoder
else:
progress_callback = self.create_callback(encoder, options.get('--progress'))
multipart_data = MultipartEncoderMonitor(encoder, progress_callback)
headers = {
'Content-Type': multipart_data.content_type
}
response = self.oauth.post(instance_url + "/api/v1/videos/upload",
data=multipart_data,
headers=headers)
if response is not None:
if response.status_code == 200:
jresponse = response.json()
jresponse = jresponse['video']
uuid = jresponse['uuid']
video_id = str(jresponse['id'])
logger.info("Peertube: Video was successfully uploaded.")
template_url = "%s/videos/watch/%s"
video.platform[self.NAME].url = template_url % (instance_url, uuid)
logger.info("Peertube: Watch it at " + video.platform[self.NAME].url + ".")
# Upload is successful we may set playlist
if 'playlist_id' in locals():
set_playlist(instance_url, video_id, playlist_id)
else:
logger.critical(('Peertube: The upload failed with an unexpected response: '
'%s') % response)
exit(1)
def create_callback(self, encoder, progress_type):
upload_size_MB = encoder.len * (1 / (1024 * 1024))
if progress_type is None or "percentage" in progress_type.lower():
progress_lambda = lambda x: int((x / encoder.len) * 100) # Default to percentage
elif "bigfile" in progress_type.lower():
progress_lambda = lambda x: x * (1 / (1024 * 1024)) # MB
elif "accurate" in progress_type.lower():
progress_lambda = lambda x: x * (1 / (1024)) # kB
else:
# Should not happen outside of development when adding partly a progress type
logger.critical("Peertube: Unknown progress type `" + progress_type + "`")
exit(1)
bar = ProgressBar(expected_size=progress_lambda(encoder.len), label=f"Peertube upload progress ({upload_size_MB:.2f}MB) ", filled_char='=')
def callback(monitor):
# We want the condition to capture the varible from the parent scope, not a local variable that is created after
global upload_finished
progress = progress_lambda(monitor.bytes_read)
bar.show(progress)
if monitor.bytes_read == encoder.len:
if not upload_finished:
# We get two time in the callback with both bytes equals, skip the first
upload_finished = True
else:
# Print a blank line to not (partly) override the progress bar
print()
logger.info("Peertube: Upload finish, Processing…")
return callback
def heartbeat(self):
"""
If needed for your platform, use a bit of the api so the platform is aware the keys are still in use.
"""
print("heartbeat for peertube (nothing to do)")
pass
# def run(options):
def upload(self, video, options):
logger.info('Peertube: Uploading video...')
self.upload_video(video, options)

prismedia/yt_upload.py → prismedia/plugins/platforms/youtube.py View File

@ -54,6 +54,41 @@ SCOPES = ['https://www.googleapis.com/auth/youtube.upload', 'https://www.googlea
API_SERVICE_NAME = 'youtube'
API_VERSION = 'v3'
CATEGORY = {
"music": 10,
"films": 1,
"vehicles": 2,
"sport": 17,
"travels": 19,
"gaming": 20,
"people": 22,
"comedy": 23,
"entertainment": 24,
"news": 25,
"how to": 26,
"education": 27,
"activism": 29,
"science & technology": 28,
"science": 28,
"technology": 28,
"animals": 15
}
LANGUAGE = {
"arabic": 'ar',
"english": 'en',
"french": 'fr',
"german": 'de',
"hindi": 'hi',
"italian": 'it',
"japanese": 'ja',
"korean": 'ko',
"mandarin": 'zh-CN',
"portuguese": 'pt-PT',
"punjabi": 'pa',
"russian": 'ru',
"spanish": 'es'
}
# Authorize the request and store authorization credentials.
def get_authenticated_service():
@ -107,11 +142,11 @@ def initialize_upload(youtube, options):
category = None
if options.get('--category'):
category = utils.getCategory(options.get('--category'), 'youtube')
category = CATEGORY[options.get('--category').lower()]
language = None
if options.get('--language'):
language = utils.getLanguage(options.get('--language'), "youtube")
language = LANGUAGE[options.get('--language').lower()]
license = None
if options.get('--cca'):
@ -121,7 +156,7 @@ def initialize_upload(youtube, options):
# and if empty, it does not cause problem during upload
body = {
"snippet": {
"title": options.get('--name') or splitext(basename(path))[0],
"title": options.get('--name'),
"description": options.get('--description') or "default description",
"tags": tags,
# if no category, set default to 1 (Films)
@ -279,12 +314,8 @@ def set_playlist(youtube, playlist_id, video_id):
part='snippet'
).execute()
except Exception as e:
if hasattr(e, 'message'):
logger.critical("Youtube: " + str(e.message))
exit(1)
else:
logger.critical("Youtube: " + str(e))
exit(1)
logger.critical("Youtube: " + utils.get_exception_string(e))
exit(1)
logger.info('Youtube: Video is correctly added to the playlist.')
@ -342,7 +373,7 @@ def resumable_upload(request, resource, method, options):
time.sleep(sleep_seconds)
def hearthbeat():
def heartbeat():
"""Use the minimums credits possibles of the API so google does not readuce to 0 the allowed credits.
This apparently happens after 90 days without any usage of credits.
For more info see the official documentations:
@ -353,7 +384,7 @@ def hearthbeat():
try:
get_playlist_by_name(youtube, "Foo")
except HttpError as e:
logger.error('Youtube: An HTTP error %d occurred on hearthbeat:\n%s' %
logger.error('Youtube: An HTTP error %d occurred on heartbeat:\n%s' %
(e.resp.status, e.content))

+ 0
- 398
prismedia/pt_upload.py View File

@ -1,398 +0,0 @@
#!/usr/bin/env python
# coding: utf-8
import os
import mimetypes
import json
import logging
import sys
import datetime
import pytz
from os.path import splitext, basename, abspath
from tzlocal import get_localzone
from configparser import RawConfigParser
from requests_oauthlib import OAuth2Session
from oauthlib.oauth2 import LegacyApplicationClient
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
from clint.textui.progress import Bar as ProgressBar
from . import utils
logger = logging.getLogger('Prismedia')
PEERTUBE_SECRETS_FILE = 'peertube_secret'
PEERTUBE_PRIVACY = {
"public": 1,
"unlisted": 2,
"private": 3
}
def get_authenticated_service(secret):
peertube_url = str(secret.get('peertube', 'peertube_url')).rstrip("/")
oauth_client = LegacyApplicationClient(
client_id=str(secret.get('peertube', 'client_id'))
)
try:
oauth = OAuth2Session(client=oauth_client)
oauth.fetch_token(
token_url=str(peertube_url + '/api/v1/users/token'),
# lower as peertube does not store uppercase for pseudo
username=str(secret.get('peertube', 'username').lower()),
password=str(secret.get('peertube', 'password')),
client_id=str(secret.get('peertube', 'client_id')),
client_secret=str(secret.get('peertube', 'client_secret'))
)
except Exception as e:
if hasattr(e, 'message'):
logger.critical("Peertube: " + str(e.message))
exit(1)
else:
logger.critical("Peertube: " + str(e))
exit(1)
return oauth
def get_default_channel(user_info):
return user_info['videoChannels'][0]['id']
def get_channel_by_name(user_info, options):
for channel in user_info["videoChannels"]:
if channel['displayName'] == options.get('--channel'):
return channel['id']
def convert_peertube_date(date):
date = datetime.datetime.strptime(date, '%Y-%m-%dT%H:%M:%S')
tz = get_localzone()
tz = pytz.timezone(str(tz))
return tz.localize(date).isoformat()
def create_channel(oauth, url, options):
template = ('Peertube: Channel %s does not exist, creating it.')
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]
data = '{"name":"' + channel_name + '", \
"displayName":"' + options.get('--channel') + '", \
"description":null, \
"support":null}'
headers = {
'Content-Type': "application/json; charset=UTF-8"
}
try:
response = oauth.post(url + "/api/v1/video-channels/",
data=data.encode('utf-8'),
headers=headers)
except Exception as e:
if hasattr(e, 'message'):
logger.error("Peertube: " + str(e.message))
else:
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:
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:
logger.critical(('Peertube: Creating channel failed with an unexpected response: '
'%s') % response)
exit(1)
def get_default_playlist(user_info):
return user_info['videoChannels'][0]['id']
def get_playlist_by_name(oauth, url, username, options):
start = 0
user_playlists = json.loads(oauth.get(
url+"/api/v1/accounts/"+username+"/video-playlists?start="+str(start)+"&count=100").content)
total = user_playlists["total"]
data = user_playlists["data"]
# We need to iterate on pagination as peertube returns max 100 playlists (see #41)
while start < total:
for playlist in data:
if playlist['displayName'] == options.get('--playlist'):
return playlist['id']
start = start + 100
user_playlists = json.loads(oauth.get(
url+"/api/v1/accounts/"+username+"/video-playlists?start="+str(start)+"&count=100").content)
data = user_playlists["data"]
def create_playlist(oauth, url, options, channel):
template = ('Peertube: Playlist %s does not exist, creating it.')
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
files = {'displayName': (None, str(options.get('--playlist'))),
'privacy': (None, "1"),
'description': (None, "null"),
'videoChannelId': (None, str(channel)),
'thumbnailfile': (None, "null")}
try:
response = oauth.post(url + "/api/v1/video-playlists/",
files=files)
except Exception as e:
if hasattr(e, 'message'):
logger.error("Peertube: " + str(e.message))
else:
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:
logger.critical(('Peertube: Creating the playlist failed with an unexpected response: '
'%s') % response)
exit(1)
def set_playlist(oauth, url, video_id, playlist_id):
logger.info('Peertube: add video to playlist.')
data = '{"videoId":"' + str(video_id) + '"}'
headers = {
'Content-Type': "application/json"
}
try:
response = oauth.post(url + "/api/v1/video-playlists/"+str(playlist_id)+"/videos",
data=data,
headers=headers)
except Exception as e:
if hasattr(e, 'message'):
logger.error("Peertube: " + str(e.message))
else:
logger.error("Peertube: " + str(e))
if response is not None:
if response.status_code == 200:
logger.info('Peertube: Video is successfully added to the playlist.')
else:
logger.critical(('Peertube: Configuring the playlist failed with an unexpected response: '
'%s') % response)
exit(1)
def upload_video(oauth, secret, options):
def get_userinfo():
return json.loads(oauth.get(url+"/api/v1/users/me").content)
def get_file(path):
mimetypes.init()
return (basename(path), open(abspath(path), 'rb'),
mimetypes.types_map[splitext(path)[1]])
path = options.get('--file')
url = str(secret.get('peertube', 'peertube_url')).rstrip('/')
user_info = get_userinfo()
username = str(secret.get('peertube', 'username').lower())
# We need to transform fields into tuple to deal with tags as
# MultipartEncoder does not support list refer
# https://github.com/requests/toolbelt/issues/190 and
# https://github.com/requests/toolbelt/issues/205
fields = [
("name", options.get('--name') or splitext(basename(options.get('--file')))[0]),
("licence", "1"),
("description", options.get('--description') or "default description"),
("nsfw", str(int(options.get('--nsfw')) or "0")),
("videofile", get_file(path))
]
if options.get('--tags'):
tags = options.get('--tags').split(',')
tag_number = 0
for strtag in tags:
tag_number = tag_number + 1
# Empty tag crashes Peertube, so skip them
if strtag == "":
continue
# Tag more than 30 chars crashes Peertube, so skip tags
if len(strtag) >= 30:
logger.warning("Peertube: Sorry, Peertube does not support tag with more than 30 characters, please reduce tag: " + strtag)
logger.warning("Peertube: Meanwhile, this tag will be skipped")
continue
# Peertube supports only 5 tags at the moment
if tag_number > 5:
logger.warning("Peertube: Sorry, Peertube support 5 tags max, additional tag will be skipped")
logger.warning("Peertube: Skipping tag " + strtag)
continue
fields.append(("tags[]", strtag))
if options.get('--category'):
fields.append(("category", str(utils.getCategory(options.get('--category'), 'peertube'))))
else:
# if no category, set default to 2 (Films)
fields.append(("category", "2"))
if options.get('--language'):
fields.append(("language", str(utils.getLanguage(options.get('--language'), "peertube"))))
else:
# if no language, set default to 1 (English)
fields.append(("language", "en"))
if options.get('--disable-comments'):
fields.append(("commentsEnabled", "0"))
else:
fields.append(("commentsEnabled", "1"))
privacy = None
if options.get('--privacy'):
privacy = options.get('--privacy').lower()
# If peertubeAt exists, use instead of publishAt
if options.get('--peertubeAt'):
publishAt = options.get('--peertubeAt')
elif options.get('--publishAt'):
publishAt = options.get('--publishAt')
if 'publishAt' in locals():
publishAt = convert_peertube_date(publishAt)
fields.append(("scheduleUpdate[updateAt]", publishAt))
fields.append(("scheduleUpdate[privacy]", str(PEERTUBE_PRIVACY["public"])))
fields.append(("privacy", str(PEERTUBE_PRIVACY["private"])))
else:
fields.append(("privacy", str(PEERTUBE_PRIVACY[privacy or "private"])))
# Set originalDate except if the user force no originalDate
if options.get('--originalDate'):
originalDate = convert_peertube_date(options.get('--originalDate'))
fields.append(("originallyPublishedAt", originalDate))
if options.get('--thumbnail'):
fields.append(("thumbnailfile", get_file(options.get('--thumbnail'))))
fields.append(("previewfile", get_file(options.get('--thumbnail'))))
if options.get('--channel'):
channel_id = get_channel_by_name(user_info, options)
if not channel_id and options.get('--channelCreate'):
channel_id = create_channel(oauth, url, options)
elif not channel_id:
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)
fields.append(("channelId", str(channel_id)))
if options.get('--playlist'):
playlist_id = get_playlist_by_name(oauth, url, username, options)
if not playlist_id and options.get('--playlistCreate'):
playlist_id = create_playlist(oauth, url, options, channel_id)
elif not playlist_id:
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')
encoder = MultipartEncoder(fields)
if options.get('--quiet'):
multipart_data = encoder
else:
progress_callback = create_callback(encoder, options.get('--progress'))
multipart_data = MultipartEncoderMonitor(encoder, progress_callback)
headers = {
'Content-Type': multipart_data.content_type
}
response = oauth.post(url + "/api/v1/videos/upload",
data=multipart_data,
headers=headers)
if response is not None:
if response.status_code == 200:
jresponse = response.json()
jresponse = jresponse['video']
uuid = jresponse['uuid']
video_id = str(jresponse['id'])
logger.info('Peertube: Video was successfully uploaded.')
template = 'Peertube: Watch it at %s/videos/watch/%s.'
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:
logger.critical(('Peertube: The upload failed with an unexpected response: '
'%s') % response)
exit(1)
upload_finished = False
def create_callback(encoder, progress_type):
upload_size_MB = encoder.len * (1 / (1024 * 1024))
if progress_type is None or "percentage" in progress_type.lower():
progress_lambda = lambda x: int((x / encoder.len) * 100) # Default to percentage
elif "bigfile" in progress_type.lower():
progress_lambda = lambda x: x * (1 / (1024 * 1024)) # MB
elif "accurate" in progress_type.lower():
progress_lambda = lambda x: x * (1 / (1024)) # kB
else:
# Should not happen outside of development when adding partly a progress type
logger.critical("Peertube: Unknown progress type `" + progress_type + "`")
exit(1)
bar = ProgressBar(expected_size=progress_lambda(encoder.len), label=f"Peertube upload progress ({upload_size_MB:.2f}MB) ", filled_char='=')
def callback(monitor):
# We want the condition to capture the varible from the parent scope, not a local variable that is created after
global upload_finished
progress = progress_lambda(monitor.bytes_read)
bar.show(progress)
if monitor.bytes_read == encoder.len:
if not upload_finished:
# We get two time in the callback with both bytes equals, skip the first
upload_finished = True
else:
# Print a blank line to not (partly) override the progress bar
print()
logger.info("Peertube: Upload finish, Processing…")
return callback
def run(options):
secret = RawConfigParser()
try:
secret.read(PEERTUBE_SECRETS_FILE)
except Exception as 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:
logger.info('Peertube: Uploading video...')
upload_video(oauth, secret, options)
except Exception as e:
if hasattr(e, 'message'):
logger.error("Peertube: " + str(e.message))
else:
logger.error("Peertube: " + str(e))

prismedia/config/peertube_secret.sample → prismedia/samples/peertube_secret.sample View File


prismedia/config/youtube_secret.json.sample → prismedia/samples/youtube_secret.json.sample View File


+ 8
- 112
prismedia/upload.py View File

@ -7,7 +7,7 @@ prismedia - tool to upload videos to Peertube and Youtube
Usage:
prismedia --file=<FILE> [options]
prismedia -f <FILE> --tags=STRING [options]
prismedia --hearthbeat
prismedia --heartbeat
prismedia -h | --help
prismedia --version
@ -52,7 +52,7 @@ Options:
Only relevant if --playlist is set.
--progress=STRING Set the progress bar view, one of percentage, bigFile (MB), accurate (KB).
--hearthbeat Use some credits to show some activity for you apikey so the platform know it is used and would not put your quota to 0 (only Youtube currently)
--heartbeat Use some credits to show some activity for you apikey so the platform know it is used and would not put your quota to 0 (only Youtube currently)
-h --help Show this help.
--version Show version.
@ -109,7 +109,6 @@ if sys.version_info[0] < 3:
import os
import datetime
import logging
logger = logging.getLogger('Prismedia')
from docopt import docopt
@ -117,6 +116,8 @@ from . import yt_upload
from . import pt_upload
from . import utils
logger = logging.getLogger('Prismedia')
try:
# noinspection PyUnresolvedReferences
from schema import Schema, And, Or, Optional, SchemaError, Hook, Use
@ -136,112 +137,6 @@ except ImportError:
VERSION = "prismedia v0.11.0"
VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted')
VALID_CATEGORIES = (
"music", "films", "vehicles",
"sports", "travels", "gaming", "people",
"comedy", "entertainment", "news",
"how to", "education", "activism", "science & technology",
"science", "technology", "animals"
)
VALID_PLATFORM = ('youtube', 'peertube', 'none')
VALID_LANGUAGES = ('arabic', 'english', 'french',
'german', 'hindi', 'italian',
'japanese', 'korean', 'mandarin',
'portuguese', 'punjabi', 'russian', 'spanish')
VALID_PROGRESS = ('percentage', 'bigfile', 'accurate')
def validateVideo(path):
supported_types = ['video/mp4']
detected_type = magic.from_file(path, mime=True)
if detected_type not in supported_types:
print("File", path, "detected type is", detected_type, "which is not one of", supported_types)
force_file = ['y', 'yes']
is_forcing = input("Are you sure you selected the correct file? (y/N)")
if is_forcing.lower() not in force_file:
return False
return path
def validateCategory(category):
if category.lower() in VALID_CATEGORIES:
return True
else:
return False
def validatePrivacy(privacy):
if privacy.lower() in VALID_PRIVACY_STATUSES:
return True
else:
return False
def validatePlatform(platform):
for plfrm in platform.split(','):
if plfrm.lower().replace(" ", "") not in VALID_PLATFORM:
return False
return True
def validateLanguage(language):
if language.lower() in VALID_LANGUAGES:
return True
else:
return False
def validatePublishDate(publishDate):
# Check date format and if date is future
try:
now = datetime.datetime.now()
publishAt = datetime.datetime.strptime(publishDate, '%Y-%m-%dT%H:%M:%S')
if now >= publishAt:
return False
except ValueError:
return False
return True
def validateOriginalDate(originalDate):
# Check date format and if date is past
try:
now = datetime.datetime.now()
originalDate = datetime.datetime.strptime(originalDate, '%Y-%m-%dT%H:%M:%S')
if now <= originalDate:
return False
except ValueError:
return False
return True
def validateThumbnail(thumbnail):
supported_types = ['image/jpg', 'image/jpeg']
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 validateProgress(progress):
for prgs in progress.split(','):
if prgs.lower().replace(" ", "") not in VALID_PROGRESS:
return False
return True
def _optionnalOrStrict(key, scope, error):
option = key.replace('-', '')
@ -285,6 +180,7 @@ def configureStdoutLogs():
ch_stdout.setFormatter(formatter_stdout)
logger_stdout.addHandler(ch_stdout)
def main():
options = docopt(__doc__, version=VERSION)
@ -394,15 +290,15 @@ def main():
Optional('--playlist'): Or(None, str),
Optional('--playlistCreate'): bool,
Optional('--progress'): Or(None, And(str, validateProgress, error="Sorry, progress visualisation not supported")),
'--hearthbeat': bool,
'--heartbeat': bool,
'--help': bool,
'--version': bool,
# This allow to return all other options for further use: https://github.com/keleshev/schema#extra-keys
object: object
})
if options.get('--hearthbeat'):
yt_upload.hearthbeat()
if options.get('--heartbeat'):
yt_upload.heartbeat()
exit(0)
# We need to validate early options first as withNFO and logs options should be prioritized

+ 215
- 191
prismedia/utils.py View File

@ -2,102 +2,146 @@
# coding: utf-8
from configparser import RawConfigParser, NoOptionError, NoSectionError
from os.path import dirname, splitext, basename, isfile, getmtime
from os.path import dirname, splitext, basename, isfile, getmtime, exists
from yapsy.PluginManager import PluginManagerSingleton
import pluginInterfaces as pi
import re
import unidecode
import logging
import datetime
logger = logging.getLogger('Prismedia')
### CATEGORIES ###
YOUTUBE_CATEGORY = {
"music": 10,
"films": 1,
"vehicles": 2,
"sport": 17,
"travels": 19,
"gaming": 20,
"people": 22,
"comedy": 23,
"entertainment": 24,
"news": 25,
"how to": 26,
"education": 27,
"activism": 29,
"science & technology": 28,
"science": 28,
"technology": 28,
"animals": 15
}
PEERTUBE_CATEGORY = {
"music": 1,
"films": 2,
"vehicles": 3,
"sport": 5,
"travels": 6,
"gaming": 7,
"people": 8,
"comedy": 9,
"entertainment": 10,
"news": 11,
"how to": 12,
"education": 13,
"activism": 14,
"science & technology": 15,
"science": 15,
"technology": 15,
"animals": 16
}
### LANGUAGES ###
YOUTUBE_LANGUAGE = {
"arabic": 'ar',
"english": 'en',
"french": 'fr',
"german": 'de',
"hindi": 'hi',
"italian": 'it',
"japanese": 'ja',
"korean": 'ko',
"mandarin": 'zh-CN',
"portuguese": 'pt-PT',
"punjabi": 'pa',
"russian": 'ru',
"spanish": 'es'
}
PEERTUBE_LANGUAGE = {
"arabic": "ar",
"english": "en",
"french": "fr",
"german": "de",
"hindi": "hi",
"italian": "it",
"japanese": "ja",
"korean": "ko",
"mandarin": "zh",
"portuguese": "pt",
"punjabi": "pa",
"russian": "ru",
"spanish": "es"
}
######################
def getCategory(category, platform):
if platform == "youtube":
return YOUTUBE_CATEGORY[category.lower()]
logger = logging.getLogger("Prismedia")
VALID_PRIVACY_STATUSES = ("public", "private", "unlisted")
VALID_CATEGORIES = (
"music", "films", "vehicles",
"sports", "travels", "gaming", "people",
"comedy", "entertainment", "news",
"how to", "education", "activism", "science & technology",
"science", "technology", "animals"
)
VALID_LANGUAGES = ("arabic", "english", "french",
"german", "hindi", "italian",
"japanese", "korean", "mandarin",
"portuguese", "punjabi", "russian", "spanish")
VALID_PROGRESS = ("percentage", "bigfile", "accurate")
def helperFunctionalities(options):
pluginManager = PluginManagerSingleton.get()
optionName = "--heartbeat"
if options.get(optionName):
for plugin in pluginManager.getPluginsOfCategory(pi.PluginTypes.PLATFORM):
plugin.plugin_object.heartbeat()
return False
else:
return PEERTUBE_CATEGORY[category.lower()]
options.pop(optionName)
return True
def getLanguage(language, platform):
if platform == "youtube":
return YOUTUBE_LANGUAGE[language.lower()]
def get_exception_string(e):
if hasattr(e, "message"):
return str(e.message)
else:
return str(e)
def validateVideo(path):
supported_types = ["video/mp4"]
detected_type = magic.from_file(path, mime=True)
if detected_type not in supported_types:
print("File", path, "detected type is `" + detected_type + "` which is not one of", supported_types)
force_file = ["y", "yes"]
is_forcing = input("Are you sure you selected the correct file? (y/N)")
if is_forcing.lower() not in force_file:
return False
return path
def validateCategory(category):
if category.lower() in VALID_CATEGORIES:
return True
else:
return False
def validatePrivacy(privacy):
if privacy.lower() in VALID_PRIVACY_STATUSES:
return True
else:
return False
# TODO: remove me?
# def validatePlatform(platform):
# for plfrm in platform.split(','):
# if plfrm.lower().replace(" ", "") not in VALID_PLATFORM:
# return False
#
# return True
def validateLanguage(language):
if language.lower() in VALID_LANGUAGES:
return True
else:
return False
def validateDate(date):
return datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%S")
def validatePublishDate(publishDate):
# Check date format and if date is future
try:
now = datetime.datetime.now()
publishAt = validateDate(publishDate)
if now >= publishAt:
return False
except ValueError:
return False
return True
def validateOriginalDate(originalDate):
# Check date format and if date is past
try:
now = datetime.datetime.now()
originalDate = validateDate(originalDate)
if now <= originalDate:
return False
except ValueError:
return False
return True
def validateThumbnail(thumbnail):
supported_types = ['image/jpg', 'image/jpeg']
if exists(thumbnail) and \
magic.from_file(thumbnail, mime=True) in supported_types:
return thumbnail
else:
return PEERTUBE_LANGUAGE[language.lower()]
return False
def validateLogLevel(loglevel):
numeric_level = getattr(logging, loglevel, None)
if not isinstance(numeric_level, int):
return False
return True
def validateProgress(progress):
for prgs in progress.split(','):
if prgs.lower().replace(" ", "") not in VALID_PROGRESS:
return False
return True
def ask_overwrite(question):
@ -118,115 +162,86 @@ def remove_empty_kwargs(**kwargs):
return good_kwargs
def searchThumbnail(options):
video_directory = dirname(options.get('--file')) + "/"
# First, check for thumbnail based on videoname
if options.get('--name'):
if isfile(video_directory + options.get('--name') + ".jpg"):
options['--thumbnail'] = video_directory + options.get('--name') + ".jpg"
elif isfile(video_directory + options.get('--name') + ".jpeg"):
options['--thumbnail'] = video_directory + options.get('--name') + ".jpeg"
# Then, if we still not have thumbnail, check for thumbnail based on videofile name
if not options.get('--thumbnail'):
video_file = splitext(basename(options.get('--file')))[0]
if isfile(video_directory + video_file + ".jpg"):
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
def searchOriginalDate(options):
fileModificationDate = str(getmtime(options.get('--file'))).split('.')
return datetime.datetime.fromtimestamp(int(fileModificationDate[0])).isoformat()
# return the nfo as a RawConfigParser object
def loadNFO(filename):
try:
logger.info("Loading " + filename + " as NFO")
nfo = RawConfigParser()
nfo.read(filename, encoding='utf-8')
return nfo
except Exception as e:
logger.critical("Problem loading NFO file " + filename + ": " + str(e))
exit(1)
return False
def parseNFO(options):
video_directory = dirname(options.get('--file'))
directory_name = basename(video_directory)
nfo_txt = False
nfo_directory = False
nfo_videoname = False
nfo_file = False
nfo_cli = False
if isfile(video_directory + "/" + "nfo.txt"):
nfo_txt = loadNFO(video_directory + "/" + "nfo.txt")
elif isfile(video_directory + "/" + "NFO.txt"):
nfo_txt = loadNFO(video_directory + "/" + "NFO.txt")
if isfile(video_directory + "/" + directory_name + ".txt"):
nfo_directory = loadNFO(video_directory + "/" + directory_name + ".txt")
if options.get('--name'):
if isfile(video_directory + "/" + options.get('--name')):
nfo_videoname = loadNFO(video_directory + "/" + options.get('--name') + ".txt")
video_file = splitext(basename(options.get('--file')))[0]
if isfile(video_directory + "/" + video_file + ".txt"):
nfo_file = loadNFO(video_directory + "/" + video_file + ".txt")
if options.get('--nfo'):
if isfile(options.get('--nfo')):
nfo_cli = loadNFO(options.get('--nfo'))
else:
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
# options in cli > nfo_cli > nfo_file > nfo_videoname > nfo_directory > nfo_txt
for nfo in [nfo_cli, nfo_file, nfo_videoname, nfo_directory, nfo_txt]:
if nfo:
# We need to check all options and replace it with the nfo value if not defined (None or False)
for key, value in options.items():
key = key.replace("--", "")
try:
# get string options
if value is None and nfo.get('video', key):
options['--' + key] = nfo.get('video', key)
# get boolean options
elif value is False and nfo.getboolean('video', key):
options['--' + key] = nfo.getboolean('video', key)
except NoOptionError:
continue
except NoSectionError:
logger.critical(nfo + " misses section [video], please check syntax of your NFO.")
exit(1)
return options
def upcaseFirstLetter(s):
return s[0].upper() + s[1:]
# # return the nfo as a RawConfigParser object
# def loadNFO(filename):
# try:
# logger.info("Loading " + filename + " as NFO")
# nfo = RawConfigParser()
# nfo.read(filename, encoding='utf-8')
# return nfo
# except Exception as e:
# logger.critical("Problem loading NFO file " + filename + ": " + str(e))
# exit(1)
# return False
#
#
# def parseNFO(options):
# video_directory = dirname(options.get('--file'))
# directory_name = basename(video_directory)
# nfo_txt = False
# nfo_directory = False
# nfo_videoname = False
# nfo_file = False
# nfo_cli = False
#
# if isfile(video_directory + "/" + "nfo.txt"):
# nfo_txt = loadNFO(video_directory + "/" + "nfo.txt")
# elif isfile(video_directory + "/" + "NFO.txt"):
# nfo_txt = loadNFO(video_directory + "/" + "NFO.txt")
#
# if isfile(video_directory + "/" + directory_name + ".txt"):
# nfo_directory = loadNFO(video_directory + "/" + directory_name + ".txt")
#
# if options.get('--name'):
# if isfile(video_directory + "/" + options.get('--name')):
# nfo_videoname = loadNFO(video_directory + "/" + options.get('--name') + ".txt")
#
# video_file = splitext(basename(options.get('--file')))[0]
# if isfile(video_directory + "/" + video_file + ".txt"):
# nfo_file = loadNFO(video_directory + "/" + video_file + ".txt")
#
# if options.get('--nfo'):
# if isfile(options.get('--nfo')):
# nfo_cli = loadNFO(options.get('--nfo'))
# else:
# 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
# # options in cli > nfo_cli > nfo_file > nfo_videoname > nfo_directory > nfo_txt
# for nfo in [nfo_cli, nfo_file, nfo_videoname, nfo_directory, nfo_txt]:
# if nfo:
# # We need to check all options and replace it with the nfo value if not defined (None or False)
# for key, value in options.items():
# key = key.replace("--", "")
# try:
# # get string options
# if value is None and nfo.get('video', key):
# options['--' + key] = nfo.get('video', key)
# # get boolean options
# elif value is False and nfo.getboolean('video', key):
# options['--' + key] = nfo.getboolean('video', key)
# except NoOptionError:
# continue
# except NoSectionError:
# logger.critical(nfo + " misses section [video], please check syntax of your NFO.")
# exit(1)
# return options
def cleanString(toclean):
@ -234,3 +249,12 @@ def cleanString(toclean):
cleaned = re.sub('[^A-Za-z0-9]+', '', toclean)
return cleaned
def getOption(options, optionName, defaultValue=None):
value = options.get(optionName)
options.pop(optionName)
if value is None:
return defaultValue
return value

+ 123
- 0
prismedia/video.py View File

@ -0,0 +1,123 @@
from os.path import dirname, splitext, basename, isfile, normpath, expanduser
class Platform(object):
"""
Store data representing a Platform.
"""
def __init__(self):
self.error = None
self.publishAt = None
self.url = None
def __repr__(self):
return str(self.__dict__)
# TODO: Add container for `with-*` and a `isValid` method to check that all `with-*` options are present
# TODO: We need some list (using enum?) for the commons licences, language, privacy, categories options
class Video(object):
"""
Store data representing a Video.
"""
def __init__(self):
self.path = ""
self.thumbnail = None
self.name = None
self.description = "Video uploaded with Prismedia"
self.playlistName = None
self.playlistCreate = False
self.privacy = "private"
self.category = "films"
self.tags = []
self.language = "english"
self.originalDate = None # TODO: add a method "extract original date"? -> I feal that needs to be done outside of this class
# TODO: set as an object: { "all": date1, "platformX": date2, …}?
# Maybe the publishAt by platform is better placed in `self.platform`
# And so publishAt would only contains the global date.
self.publishAt = None
# TODO: Add a list of licences
self.licence = "proprietary"
self.disableComments = False
self.nsfw = False
# Each platform should insert here the upload state
self.platform = {}
@property
def path(self):
return self._path
@path.setter
def path(self, value):
path = normpath(expanduser(value))
if value == "":
self._path = ""
elif isfile(path):
self._path = path
else:
# TODO: log instead to debug? info?
print("The path `" + value + "` does not point to a video")
self._path = ""
@property
def thumbnail(self):
if self._thumbnail is not None:
return self._thumbnail
else:
result = None
video_directory = dirname(self.path) + "/"
# First, check for thumbnail based on videoname
if isfile(video_directory + self.name + ".jpg"):
result = video_directory + self.name + ".jpg"
elif isfile(video_directory + self.name + ".jpeg"):
result = video_directory + self.name + ".jpeg"
# Then, if we still not have thumbnail, check for thumbnail based on videofile name
# NOTE: This may be a the exact same check from the previous conditions if self._name = None.
# Bus as far as I know it is not recommended to use privates properties even in the same class
# Maybe check if self.name == splitext(basename(self.path))[0]
# Not done since it is early dev for the plugins rewrite
if not result:
video_file = splitext(basename(self.path))[0]
if isfile(video_directory + video_file + ".jpg"):
result = video_directory + video_file + ".jpg"
elif isfile(video_directory + video_file + ".jpeg"):
result = video_directory + video_file + ".jpeg"
# TODO: move to caller. Logging the output is its responsibility
# Display some info after research
# if not result:
# logger.debug("No thumbnail has been found, continuing")
# else:
# logger.info("Using " + result + " as thumbnail")
return result
@thumbnail.setter
def thumbnail(self, value):
self._thumbnail = value
@property
def name(self):
if self._name is not None:
return self._name
else:
return splitext(basename(self.path))[0]
@name.setter
def name(self, value):
self._name = value
def __repr__(self):
result = "{\n"
for key in self.__dict__:
result += "\t'" + key + "': " + str(self.__dict__[key]) + ",\n"
result += "}\n"
return result

+ 1
- 0
pyproject.toml View File

@ -38,6 +38,7 @@ tzlocal = "^1.5.1"
Unidecode = "^1.0.23"
uritemplate = "^3.0.0"
urllib3 = "^1.22"
Yapsy = "^1.12.2"
[tool.poetry.dev-dependencies]

+ 103
- 92
requirements.txt View File

@ -1,81 +1,73 @@
cachetools==3.1.1 \
--hash=sha256:428266a1c0d36dc5aca63a2d7c5942e88c2c898d72139fca0e97fdd2380517ae \
--hash=sha256:8ea2d3ce97850f31e4a08b0e2b5e6c34997d7216a9d2c98e0f3978630d4da69a
certifi==2020.4.5.1 \
--hash=sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304 \
--hash=sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519
chardet==3.0.4 \
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 \
--hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae
clint==0.5.1
configparser==3.8.1 \
args==0.1.0 \
--hash=sha256:a785b8d837625e9b61c39108532d95b85274acd679693b71ebb5156848fcf814
cachetools==4.2.0; python_version >= "3.5" and python_version < "4.0" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") \
--hash=sha256:c6b07a6ded8c78bf36730b3dc452dfff7d95f2a12a2fed856b1a0cb13ca78c61 \
--hash=sha256:3796e1de094f0eaca982441c92ce96c68c89cced4cd97721ab297ea4b16db90e
certifi==2020.12.5; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \
--hash=sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830 \
--hash=sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c
chardet==4.0.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \
--hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 \
--hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa
clint==0.5.1 \
--hash=sha256:05224c32b1075563d0b16d0015faaf9da43aa214e4a2140e51f08789e7a4c5aa
configparser==3.8.1; python_version >= "2.6" \
--hash=sha256:45d1272aad6cfd7a8a06cf5c73f2ceb6a190f6acc1fa707e7f82a4c053b28b18 \
--hash=sha256:bc37850f0cc42a1725a796ef7d92690651bf1af37d744cc63161dac62cabee17
contextlib2==0.6.0.post1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" \
--hash=sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b \
--hash=sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e
docopt==0.6.2 \
--hash=sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491
future==0.17.1 \
future==0.17.1; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") \
--hash=sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8
google-api-core==1.16.0 \
--hash=sha256:92e962a087f1c4b8d1c5c88ade1c1dfd550047dcffb320c57ef6a534a20403e2 \
--hash=sha256:859f7392676761f2b160c6ee030c3422135ada4458f0948c5690a6a7c8d86294
google-api-python-client==1.8.0 \
--hash=sha256:0f5b42a14e2d2f7dee40f2e4514531dbe95ebde9c2173b1c4040a65c427e7900 \
--hash=sha256:5032ad1af5046889649b3848f2e871889fbb6ae440198a549fe1699581300386
google-auth==1.13.1 \
--hash=sha256:a5ee4c40fef77ea756cf2f1c0adcf475ecb53af6700cf9c133354cdc9b267148 \
--hash=sha256:cab6c707e6ee20e567e348168a5c69dc6480384f777a9e5159f4299ad177dcc0
google-auth-httplib2==0.0.3 \
--hash=sha256:098fade613c25b4527b2c08fa42d11f3c2037dda8995d86de0745228e965d445 \
--hash=sha256:f1c437842155680cf9918df9bc51c1182fda41feef88c34004bd1978c8157e08
google-auth-oauthlib==0.2.0 \
--hash=sha256:226d1d0960f86ba5d9efd426a70b291eaba96f47d071657e0254ea969025728a \
--hash=sha256:81ba22acada4d13b1d83f9371ab19fd61f1250a542d21cf49e4dcf0637a7344a
googleapis-common-protos==1.51.0 \
--hash=sha256:013c91704279119150e44ef770086fdbba158c1f978a6402167d47d5409e226e
google-api-core==1.25.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \
--hash=sha256:d967beae8d8acdb88fb2f6f769e2ee0ee813042576a08891bded3b8e234150ae \
--hash=sha256:4656345cba9627ab1290eab51300a6397cc50370d99366133df1ae64b744e1eb
google-api-python-client==1.12.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \
--hash=sha256:54a7d330833a2e7b0587446d7e4ae6d0244925a9a8e1dfe878f3f7e06cdedb62 \
--hash=sha256:05cb331ed1aa15746f606c7e36ea51dbe7c29b1a5df9bbf58140901fe23d7142
google-auth-httplib2==0.0.4 \
--hash=sha256:8d092cc60fb16517b12057ec0bba9185a96e3b7169d86ae12eae98e645b7bc39 \
--hash=sha256:aeaff501738b289717fac1980db9711d77908a6c227f60e4aa1923410b43e2ee
google-auth-oauthlib==0.4.2; python_version >= "3.6" \
--hash=sha256:65b65bc39ad8cab15039b35e5898455d3d66296d0584d96fe0e79d67d04c51d9 \
--hash=sha256:d4d98c831ea21d574699978827490a41b94f05d565c617fe1b420e88f1fc8d8d
google-auth==1.24.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") \
--hash=sha256:0b0e026b412a0ad096e753907559e4bdb180d9ba9f68dd9036164db4fdc4ad2e \
--hash=sha256:ce752cc51c31f479dbf9928435ef4b07514b20261b021c7383bee4bda646acb8
googleapis-common-protos==1.52.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \
--hash=sha256:560716c807117394da12cecb0a54da5a451b5cf9866f1d37e9a5e2329a665351 \
--hash=sha256:c8961760f5aad9a711d37b675be103e0cc4e9a39327e0d6d857872f698403e24
httplib2==0.12.3 \
--hash=sha256:23914b5487dfe8ef09db6656d6d63afb0cf3054ad9ebc50868ddc8e166b5f8e8 \
--hash=sha256:a18121c7c72a56689efbf1aef990139ad940fee1e64c6f2458831736cd593600
idna==2.9 \
--hash=sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa \
--hash=sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb
idna==2.10; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" \
--hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 \
--hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6
oauthlib==2.1.0 \
--hash=sha256:d883b36b21a6ad813953803edfa563b1b579d79ca758fe950d1bc9e8b326025b \
--hash=sha256:ac35665a61c1685c56336bda97d5eefa246f1202618a1d6f34fccb1bdd404162
protobuf==3.11.3 \
--hash=sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481 \
--hash=sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7 \
--hash=sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a \
--hash=sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961 \
--hash=sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80 \
--hash=sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306 \
--hash=sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a \
--hash=sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151 \
--hash=sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab \
--hash=sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956 \
--hash=sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2 \
--hash=sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07 \
--hash=sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a \
--hash=sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee \
--hash=sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4 \
--hash=sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0 \
--hash=sha256:2affcaba328c4662f3bc3c0e9576ea107906b2c2b6422344cdad961734ff6b93 \
--hash=sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f \
--hash=sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f
pyasn1==0.4.8 \
--hash=sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3 \
--hash=sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf \
--hash=sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00 \
--hash=sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8 \
--hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \
--hash=sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86 \
--hash=sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7 \
--hash=sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576 \
--hash=sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12 \
--hash=sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2 \
--hash=sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359 \
--hash=sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776 \
--hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba
pyasn1-modules==0.2.8 \
protobuf==3.14.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \
--hash=sha256:629b03fd3caae7f815b0c66b41273f6b1900a579e2ccb41ef4493a4f5fb84f3a \
--hash=sha256:5b7a637212cc9b2bcf85dd828b1178d19efdf74dbfe1ddf8cd1b8e01fdaaa7f5 \
--hash=sha256:43b554b9e73a07ba84ed6cf25db0ff88b1e06be610b37656e292e3cbb5437472 \
--hash=sha256:5e9806a43232a1fa0c9cf5da8dc06f6910d53e4390be1fa06f06454d888a9142 \
--hash=sha256:1c51fda1bbc9634246e7be6016d860be01747354ed7015ebe38acf4452f470d2 \
--hash=sha256:4b74301b30513b1a7494d3055d95c714b560fbb630d8fb9956b6f27992c9f980 \
--hash=sha256:86a75477addde4918e9a1904e5c6af8d7b691f2a3f65587d73b16100fbe4c3b2 \
--hash=sha256:ecc33531a213eee22ad60e0e2aaea6c8ba0021f0cce35dbf0ab03dee6e2a23a1 \
--hash=sha256:72230ed56f026dd664c21d73c5db73ebba50d924d7ba6b7c0d81a121e390406e \
--hash=sha256:0fc96785262042e4863b3f3b5c429d4636f10d90061e1840fce1baaf59b1a836 \
--hash=sha256:4e75105c9dfe13719b7293f75bd53033108f4ba03d44e71db0ec2a0e8401eafd \
--hash=sha256:2a7e2fe101a7ace75e9327b9c946d247749e564a267b0515cf41dfe450b69bac \
--hash=sha256:b0d5d35faeb07e22a1ddf8dce620860c8fe145426c02d1a0ae2688c6e8ede36d \
--hash=sha256:8971c421dbd7aad930c9bd2694122f332350b6ccb5202a8b7b06f3f1a5c41ed5 \
--hash=sha256:9616f0b65a30851e62f1713336c931fcd32c057202b7ff2cfbfca0fc7d5e3043 \
--hash=sha256:22bcd2e284b3b1d969c12e84dc9b9a71701ec82d8ce975fdda19712e1cfd4e00 \
--hash=sha256:0e247612fadda953047f53301a7b0407cb0c3cb4ae25a6fde661597a04039b3c \
--hash=sha256:1d63eb389347293d8915fb47bee0951c7b5dab522a4a60118b9a18f33e21f8ce
pyasn1-modules==0.2.8; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \
--hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \
--hash=sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199 \
--hash=sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405 \
@ -89,42 +81,61 @@ pyasn1-modules==0.2.8 \
--hash=sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed \
--hash=sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0 \
--hash=sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd
python-magic==0.4.15 \
--hash=sha256:f3765c0f582d2dfc72c15f3b5a82aecfae9498bd29ca840d72f37d7bd38bfcd5 \
--hash=sha256:f2674dcfad52ae6c49d4803fa027809540b130db1dec928cfbb9240316831375
pyasn1==0.4.8; python_version >= "2.7" and python_full_version < "3.0.0" and python_version < "3.6" or python_full_version >= "3.6.0" and python_version < "3.6" \
--hash=sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3 \
--hash=sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf \
--hash=sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00 \
--hash=sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8 \
--hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \
--hash=sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86 \
--hash=sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7 \
--hash=sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576 \
--hash=sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12 \
--hash=sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2 \
--hash=sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359 \
--hash=sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776 \
--hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba
python-magic-bin==0.4.14; platform_system == "Windows" \
--hash=sha256:7b1743b3dbf16601d6eedf4e7c2c9a637901b0faaf24ad4df4d4527e7d8f66a4 \
--hash=sha256:34a788c03adde7608028203e2dbb208f1f62225ad91518787ae26d603ae68892 \
--hash=sha256:90be6206ad31071a36065a2fc169c5afb5e0355cbe6030e87641c6c62edc2b69
pytz==2019.3 \
--hash=sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d \
--hash=sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be
requests==2.23.0 \
--hash=sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee \
--hash=sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6
python-magic==0.4.20; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \
--hash=sha256:0cc52ccad086c377b9194014e3dbf98d94b194344630172510a6a3e716b47801 \
--hash=sha256:33ce94d9395aa269a9c5fac10ae124a5fb328ebe248f36efc5a43922edee662e
pytz==2020.5; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \
--hash=sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4 \
--hash=sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5
requests-oauthlib==0.8.0 \
--hash=sha256:883ac416757eada6d3d07054ec7092ac21c7f35cb1d2cf82faf205637081f468 \
--hash=sha256:50a8ae2ce8273e384895972b56193c7409601a66d4975774c60c2aed869639ca
requests-toolbelt==0.9.1 \
--hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0 \
--hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f
rsa==4.0 \
--hash=sha256:14ba45700ff1ec9eeb206a2ce76b32814958a98e372006c8fb76ba820211be66 \
--hash=sha256:1a836406405730121ae9823e19c6e806c62bbad73f890574fff50efa4122c487
schema==0.6.8 \
--hash=sha256:d994b0dc4966000037b26898df638e3e2a694cc73636cb2050e652614a350687 \
--hash=sha256:fa1a53fe5f3b6929725a4e81688c250f46838e25d8c1885a10a590c8c01a7b74
six==1.14.0 \
--hash=sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c \
--hash=sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a
requests==2.25.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \
--hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e \
--hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804
rsa==4.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4") \
--hash=sha256:35c5b5f6675ac02120036d97cf96f1fde4d49670543db2822ba5015e21a18032 \
--hash=sha256:4d409f5a7d78530a4a2062574c7bd80311bc3af29b364e293aa9b03eea77714f \
--hash=sha256:a8774e55b59fd9fc893b0d05e9bfc6f47081f46ff5b46f39ccf24631b7be356b \
--hash=sha256:69805d6b69f56eb05b62daea3a7dbd7aa44324ad1306445e05da8060232d00f4
schema==0.7.3 \
--hash=sha256:c331438b60f634cab5664ab720d3083cc444f924d55269530c36b33e3354276f \
--hash=sha256:4cf529318cfd1e844ecbe02f41f7e5aa027463e7403666a52746f31f04f47a5e
six==1.15.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" \
--hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced \
--hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259
tzlocal==1.5.1 \
--hash=sha256:4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e
unidecode==1.1.1 \
--hash=sha256:1d7a042116536098d05d599ef2b8616759f02985c85b4fef50c78a5aaf10822a \
--hash=sha256:2b6aab710c2a1647e928e36d69c21e76b453cd455f4e2621000e54b2a9b8cce8
uritemplate==3.0.1 \
unidecode==1.1.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \
--hash=sha256:4c9d15d2f73eb0d2649a151c566901f80a030da1ccb0a2043352e1dbf647586b \
--hash=sha256:a039f89014245e0cad8858976293e23501accc9ff5a7bdbc739a14a2b7b85cdc
uritemplate==3.0.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \
--hash=sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f \
--hash=sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae
urllib3==1.22 \
--hash=sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b \
--hash=sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f
urllib3==1.26.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4") \
--hash=sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473 \
--hash=sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08
yapsy==1.12.2 \
--hash=sha256:83891e22db0a74445726981df0ef7818dae595454de9cf10b7ba603d45ccd157 \
--hash=sha256:d8113d9f9c74eacf65b4663c9c037d278c9cb273b5eee5f0e1803baeedb23f8b

Loading…
Cancel
Save