mirror of
https://git.lecygnenoir.info/LecygneNoir/prismedia.git
synced 2025-10-03 17:39:16 +02:00
Compare commits
58 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4e5e0d256f | ||
![]() |
524be1d93c | ||
![]() |
325cee4a69 | ||
![]() |
40b0c43159 | ||
![]() |
8199286023 | ||
![]() |
822cf0fa9b | ||
![]() |
e7531cc340 | ||
![]() |
d8aa349ad3 | ||
![]() |
4d1828a3ad | ||
![]() |
03a8f4e71f | ||
![]() |
5ca5ff5238 | ||
![]() |
6c16b2f037 | ||
![]() |
36110432da | ||
![]() |
388f76b855 | ||
![]() |
ef92fed69d | ||
![]() |
bcb0e267f3 | ||
![]() |
8bc79853c8 | ||
![]() |
45a1cbccff | ||
![]() |
0a1360d8e2 | ||
![]() |
f8ae2b1c5e | ||
![]() |
0a53e77bd6 | ||
![]() |
2f7629ef1e | ||
![]() |
e0a63ed4b2 | ||
![]() |
ba2a1ebb79 | ||
![]() |
cf3d4c32c3 | ||
![]() |
85f0fe9b6f | ||
![]() |
1a006f3b6c | ||
![]() |
cdef038323 | ||
![]() |
cbf3386bac | ||
![]() |
ca733e0dc3 | ||
![]() |
a4f162320d | ||
![]() |
29b1747c3e | ||
![]() |
ea39fe9854 | ||
![]() |
a725e848ab | ||
![]() |
9b6da1e3dc | ||
![]() |
194e2e4606 | ||
![]() |
339caeb7f7 | ||
![]() |
e6375b5aa0 | ||
![]() |
6add140732 | ||
![]() |
c4e3243131 | ||
![]() |
93f1205ab8 | ||
![]() |
09c2d84357 | ||
![]() |
230ac545c4 | ||
![]() |
42ee7d761b | ||
![]() |
1a937098d8 | ||
![]() |
736582b495 | ||
![]() |
4a9fda5e77 | ||
![]() |
8dc3a86aab | ||
![]() |
4b7c01a707 | ||
![]() |
dc98f2e155 | ||
![]() |
60bf26418d | ||
![]() |
447310a17e | ||
![]() |
9b597f461e | ||
![]() |
8b1470ab31 | ||
![]() |
6d15ad18ca | ||
![]() |
5c991581e8 | ||
![]() |
4956a19d0e | ||
![]() |
2f8543b43c |
12 changed files with 1074 additions and 776 deletions
60
CHANGELOG.md
60
CHANGELOG.md
|
@ -1,10 +1,46 @@
|
|||
# Changelog
|
||||
|
||||
## v0.12.2
|
||||
|
||||
### Fix
|
||||
- Adjust dependencies version, as `oauthlib` and `request-oauthlib` are incompatible with peertube process in their new versions
|
||||
- Add `pytz` as explicit dependency since the previous unexplicit dependancy now install `pytz-deprecation-shim` - which does not work
|
||||
- Remove peertube-mirror link as it's now (unfortunately) a dead project (fix #63)
|
||||
|
||||
## v0.12.1
|
||||
|
||||
### Fix
|
||||
- Fix an error when setting log level in configuration
|
||||
|
||||
## v0.12.0
|
||||
|
||||
### Features
|
||||
- Add `--heartbeat` option to send request to youtube API, avoiding youtube to disabling you API account if you do not upload video often (Thanks @Zykino see #54)
|
||||
- Rework and improve genconfig process to avoid erasing existing configuration and make it more easy to use
|
||||
- Add a `prismedia-init` script when installing prismedia to easily generate basic configuration (see #55)
|
||||
- Update multiple dependencies used for prismedia as they were very old.
|
||||
- Add auto search for thumbnail in `.png` in addition to `.jpg` and `.jepg`.
|
||||
|
||||
### Fixes
|
||||
- Add pagination for youtube playlist to search for all user playlists (Thanks @Zykino)
|
||||
- Remove file format check for both videos and thumbnail as Youtube and Peertube now accepts more than .mp4 and .jpg (see #60)
|
||||
|
||||
## v0.11.0
|
||||
|
||||
### Features
|
||||
- Add the configuration of Original date of Record for Youtube and Peertube (see #50)
|
||||
- Add a progress bar when uploading on Peertube (Thanks @Zykino, see #52)
|
||||
|
||||
## v0.10.3
|
||||
|
||||
### Fix
|
||||
- Fix the pagination for Peertube playlist, as index begins at 0, not 1
|
||||
|
||||
## v0.10.2
|
||||
|
||||
### Fixes
|
||||
- Fix a typo in log (missing space when displaying thumbnail) (see #49)
|
||||
- Add pagination when searching playlist in Peertube as default pagination show only 14 playlists (see #41)
|
||||
- Add pagination when searching playlist in Peertube as default pagination show only 14 playlists (see #41)
|
||||
- Add a check to avoid uploading video on Peertube with more than 5 tags (see #48)
|
||||
- Revert the workaround for Youtube playlist bug now the bug is fixed by Youtube (see #47)
|
||||
|
||||
|
@ -18,8 +54,8 @@
|
|||
### Features
|
||||
- Add the possibility to specify strict checks option to never forgot parameters when uploading (see #36)
|
||||
- Improve logging system, add options for batch upload and print url-only in the stdout (see #29)
|
||||
- --debug option is now deprecated in favor of --log=debug
|
||||
|
||||
- --debug option is now deprecated in favor of --log=debug
|
||||
|
||||
### Fixes
|
||||
- Workaround against the Youtube API breakdown while adding video in playlist. See #47 for details. Should be removed once Google fix their bugs.
|
||||
|
||||
|
@ -34,7 +70,7 @@
|
|||
## v0.9.0
|
||||
|
||||
### Upgrade from v0.8.0
|
||||
Now using [poetry](https://python-poetry.org/) for packaging and installing! It's easier to maintain and publish package, but means changes when using prismedia from command line.
|
||||
Now using [poetry](https://python-poetry.org/) for packaging and installing! It's easier to maintain and publish package, but means changes when using prismedia from command line.
|
||||
|
||||
**Using poetry** (recommanded)
|
||||
|
||||
|
@ -49,8 +85,8 @@ poetry install
|
|||
prismedia -h
|
||||
```
|
||||
|
||||
**From source**
|
||||
Prismedia is now seen as a python module, so you need to use `python -m prismedia` instead of `./prismedia_upload.py`.
|
||||
**From source**
|
||||
Prismedia is now seen as a python module, so you need to use `python -m prismedia` instead of `./prismedia_upload.py`.
|
||||
Once you have pulled the new v0.9.0, you may update by using:
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
|
@ -66,12 +102,12 @@ python -m prismedia -h
|
|||
## v0.8.0
|
||||
|
||||
### Breaking changes
|
||||
Now work with python 3! Support of python 2 is no longer available.
|
||||
Now work with python 3! Support of python 2 is no longer available.
|
||||
You should now use python 3 in order to use prismedia
|
||||
|
||||
### Features
|
||||
- Add a requirements.txt file to make installing requirement easier.
|
||||
- Add a debug option to show some infos before uploading (thanks to @zykino)
|
||||
- Add a requirements.txt file to make installing requirement easier.
|
||||
- Add a debug option to show some infos before uploading (thanks to @zykino)
|
||||
- Now uploading to Peertube before Youtube (thanks to @zykino)
|
||||
|
||||
## v0.7.1
|
||||
|
@ -82,7 +118,7 @@ Fix bug #42 , crash on Peertube when video has only one tag
|
|||
## v0.7.0
|
||||
|
||||
### Features
|
||||
Support Peertube channel additionally with playlist for Peertube!
|
||||
Support Peertube channel additionally with playlist for Peertube!
|
||||
Peertube only as channel are Peertube's feature. See #40 for details.
|
||||
|
||||
### Fixes
|
||||
|
@ -110,7 +146,7 @@ New feature, the Peertube playlists are now supported!
|
|||
We do not use channel in place of playlist anymore.
|
||||
|
||||
## v0.6.1-1 Hotfix
|
||||
This fix prepares the python3 compatibility.
|
||||
This fix prepares the python3 compatibility.
|
||||
**Warning** you need a new prerequisites: python-unidecode
|
||||
|
||||
- Remove mastodon tags (mt) options as it's deprecated. Compatibility between Peertube and Mastodon is complete.
|
||||
|
@ -146,4 +182,4 @@ This release is fully compatible with Peertube v1.0.0!
|
|||
|
||||
### Fixes
|
||||
- Display datetime for output
|
||||
- plan video only if upload is successful
|
||||
- plan video only if upload is successful
|
||||
|
|
193
README.md
193
README.md
|
@ -23,198 +23,129 @@ Scripting your way to upload videos to peertube and youtube. Works with Python 3
|
|||
|
||||
### From pip
|
||||
|
||||
Simply install with
|
||||
|
||||
```bash
|
||||
Simply install with
|
||||
```sh
|
||||
pip install prismedia
|
||||
```
|
||||
|
||||
Upgrade with
|
||||
|
||||
```bash
|
||||
Upgrade with
|
||||
```sh
|
||||
pip install --upgrade prismedia
|
||||
```
|
||||
|
||||
### From source
|
||||
|
||||
Get the source:
|
||||
|
||||
```bash
|
||||
Get the source:
|
||||
```sh
|
||||
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`)
|
||||
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 > requirements.txt`)
|
||||
|
||||
Otherwise, you can use [poetry](https://python-poetry.org), which create a virtualenv for the project directly
|
||||
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)
|
||||
|
||||
```
|
||||
```sh
|
||||
poetry install
|
||||
```
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
Generate sample files with `python -m prismedia.genconfig`.
|
||||
Then rename and edit `peertube_secret` and `youtube_secret.json` with your credentials. (see below)
|
||||
Generate configuration files by running `prismedia-init`.
|
||||
|
||||
Then, edit them to fill your credential as explained below.
|
||||
|
||||
### Peertube
|
||||
Set your credentials, peertube server URL.
|
||||
You can get client_id and client_secret by logging in your peertube website and reaching the URL:
|
||||
https://domain.example/api/v1/oauth-clients/local
|
||||
You can set ``OAUTHLIB_INSECURE_TRANSPORT`` to 1 if you do not use https (not recommended)
|
||||
Configuration is in **peertube_secret** file.
|
||||
You need your usual credentials and Peertube instance URL, in addition with API client_id and client_secret.
|
||||
|
||||
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.
|
||||
|
||||
**Credentials**
|
||||
The first time you connect, prismedia will open your browser to ask you to authenticate to
|
||||
Youtube and allow the app to use your Youtube channel.
|
||||
**It is here you choose which channel you will upload to**.
|
||||
Once authenticated, the token is stored inside the file ``.youtube_credentials.json``.
|
||||
Youtube and allow the app to use your Youtube channel.
|
||||
**It is here you choose which channel you will upload to**.
|
||||
Once authenticated, the token is stored inside the file `.youtube_credentials.json`.
|
||||
Prismedia will try to use this file at each launch, and re-ask for authentication if it does not exist.
|
||||
|
||||
**Oauth**:
|
||||
The default youtube_secret.json should allow you to upload some videos.
|
||||
**Oauth**:
|
||||
The default youtube_secret.json should allow you to upload some videos.
|
||||
If you plan a larger usage, please consider creating your own youtube_secret file:
|
||||
|
||||
- Go to the [Google console](https://console.developers.google.com/).
|
||||
- Create project.
|
||||
- Side menu: APIs & auth -> APIs
|
||||
- Top menu: Enabled API(s): Enable all Youtube APIs.
|
||||
- Side menu: APIs & auth -> Credentials.
|
||||
- Create a Client ID: Add credentials -> OAuth 2.0 Client ID -> Other -> Name: prismedia1 -> Create -> OK
|
||||
- Side menu: APIs & Services -> APIs
|
||||
- Top menu: Enabled API(s): Enable Youtube Data v3 APIs.
|
||||
- Side menu: OAuth consent screen
|
||||
- Create an app -> User type External -> Add scope from Youtube Data API v3: `.../auth/youtube.force-ssl` and `.../auth/youtube.upload` -> No test user -> save & create
|
||||
- Side menu: APIs & Services -> Credentials.
|
||||
- Create a Client ID: Create credentials -> OAuth Client ID -> Other -> Name: prismedia1 -> Create -> OK
|
||||
- Download JSON: Under the section "OAuth 2.0 client IDs". Save the file to your local system.
|
||||
- Save this JSON as your youtube_secret.json file.
|
||||
|
||||
## Usage
|
||||
Support only mp4 for cross compatibility between Youtube and Peertube.
|
||||
Support only mp4 for cross compatibility between Youtube and Peertube.
|
||||
**Note that all options may be specified in a NFO file!** (see [Enhanced NFO](#enhanced-use-of-nfo))
|
||||
|
||||
Upload a video:
|
||||
Here are some demonstration of main usage:
|
||||
|
||||
```
|
||||
Upload a video:
|
||||
```sh
|
||||
prismedia --file="yourvideo.mp4"
|
||||
```
|
||||
|
||||
Specify description and tags:
|
||||
|
||||
```
|
||||
```sh
|
||||
prismedia --file="yourvideo.mp4" -d "My supa description" -t "tag1,tag2,foo"
|
||||
```
|
||||
|
||||
Provide a thumbnail:
|
||||
|
||||
```
|
||||
```sh
|
||||
prismedia --file="yourvideo.mp4" -d "Video with thumbnail" --thumbnail="/path/to/your/thumbnail.jpg"
|
||||
```
|
||||
|
||||
|
||||
Use a NFO file to specify your video options:
|
||||
(See [Enhanced NFO](#enhanced-use-of-nfo) for more precise example)
|
||||
Publish on Peertube only, while using a channel and a playlist, creating them if they do not exist:
|
||||
```sh
|
||||
prismedia --file="yourvideo.mp4" --platform=peertube --channel="Cooking recipes" --playlist="Cake recipes" --channelCreate --playlistCreate
|
||||
```
|
||||
|
||||
Use a NFO file to specify your video options:
|
||||
(See [Enhanced NFO](#enhanced-use-of-nfo) for more precise example)
|
||||
```sh
|
||||
prismedia --file="yourvideo.mp4" --nfo /path/to/your/nfo.txt
|
||||
```
|
||||
|
||||
|
||||
Use --help to get all available options:
|
||||
|
||||
To prevent Youtube from disabling your apikey after 90days of inactivity it is recommended to launch this command automatically from a script around once a month. It will make a call to use a few credits from your daily quota.
|
||||
On Linux and MacOS, you can use cron, on Windows the "Task Scheduler".
|
||||
```sh
|
||||
prismedia --hearthbeat
|
||||
```
|
||||
Options:
|
||||
-f, --file=STRING Path to the video file to upload in mp4. This is the only mandatory option.
|
||||
--name=NAME Name of the video to upload. (default to video filename)
|
||||
-d, --description=STRING Description of the video. (default: default description)
|
||||
-t, --tags=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)
|
||||
--cca License should be CreativeCommon Attribution (affects Youtube upload only)
|
||||
-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
|
||||
--platform=STRING List of platform(s) to upload to, comma separated.
|
||||
Supported platforms are youtube and peertube (default is both)
|
||||
--language=STRING Specify the default language for video. See below for supported language. (default is English)
|
||||
--publishAt=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
|
||||
--peertubeAt=DATE
|
||||
--youtubeAt=DATE Override publishAt for the corresponding platform. Allow to create preview on specific platform
|
||||
--thumbnail=STRING Path to a file to use as a thumbnail for the video.
|
||||
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.
|
||||
--channelCreate 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.
|
||||
--playlistCreate Create the playlist if not exists. (default do not create)
|
||||
Only relevant if --playlist is set.
|
||||
-h --help Show this help.
|
||||
--version Show version.
|
||||
|
||||
Logging options
|
||||
-q --quiet Suppress any log except Critical (alias for --log=critical).
|
||||
--log=STRING Log level, between debug, info, warning, error, critical. Ignored if --quiet is set (default to info)
|
||||
-u --url-only Display generated URL after upload directly on stdout, implies --quiet
|
||||
--batch Display generated URL after upload with platform information for easier parsing. Implies --quiet
|
||||
Be careful --batch and --url-only are mutually exclusives.
|
||||
--debug (Deprecated) Alias for --log=debug. Ignored if --log is set
|
||||
|
||||
Strict options:
|
||||
Strict options allow you to force some option to be present when uploading a video. It's useful to be sure you do not
|
||||
forget something when uploading a video, for example if you use multiples NFO. You may force the presence of description,
|
||||
tags, thumbnail, ...
|
||||
All strict option are optionals and are provided only to avoid errors when uploading :-)
|
||||
All strict options can be specified in NFO directly, the only strict option mandatory on cli is --withNFO
|
||||
All strict options are off by default
|
||||
|
||||
--withNFO Prevent the upload without a NFO, either specified via cli or found in the directory
|
||||
--withThumbnail Prevent the upload without a thumbnail
|
||||
--withName Prevent the upload if no name are found
|
||||
--withDescription Prevent the upload without description
|
||||
--withTags Prevent the upload without tags
|
||||
--withPlaylist Prevent the upload if no playlist
|
||||
--withPublishAt Prevent the upload if no schedule
|
||||
--withPlatform Prevent the upload if at least one platform is not specified
|
||||
--withCategory Prevent the upload if no category
|
||||
--withLanguage Prevent upload if no language
|
||||
--withChannel Prevent upload if no channel
|
||||
|
||||
Categories:
|
||||
Category is the type of video you upload. Default is films.
|
||||
Here are available categories from Peertube and Youtube:
|
||||
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
|
||||
|
||||
Take a look at all available options with `--help`!
|
||||
```sh
|
||||
prismedia --help
|
||||
```
|
||||
|
||||
## Enhanced use of NFO
|
||||
Since Prismedia v0.9.0, the NFO system has been improved to allow hierarchical loading.
|
||||
First of all, **if you already used nfo**, either with `--nfo` or by using `videoname.txt`, nothing changes :-)
|
||||
First, **if you already used nfo**, either with `--nfo` or by using `videoname.txt`, nothing changes :-)
|
||||
|
||||
But you are now able to use a more flexible NFO system, by using priorities. This allow you to set some defaults to avoid recreating a full nfo for each video
|
||||
But you are now able to use a more flexible NFO system, by using priorities. This allows you to set some defaults to avoid recreating a full nfo for each video
|
||||
|
||||
Basically, Prismedia will now load options in this order, using the last value found in case of conflict:
|
||||
Basically, Prismedia will now load options in this order, using the last value found in case of conflict:
|
||||
`nfo.txt < directory_name.txt < video_name.txt < command line NFO < command line argument`
|
||||
|
||||
You'll find a complete set of samples in the [prismedia/samples](prismedia/samples) directory so let's take it as an example:
|
||||
```
|
||||
```sh
|
||||
$ tree Recipes/
|
||||
Recipes/
|
||||
├── cli_nfo.txt
|
||||
|
@ -227,8 +158,8 @@ Recipes/
|
|||
└── yourvideo2.txt
|
||||
```
|
||||
|
||||
By using
|
||||
```
|
||||
By using
|
||||
```sh
|
||||
prismedia --file=/path/to/Recipes/yourvideo1.mp4 --nfo=/path/to/Recipes/cli_nfo.txt --cca
|
||||
```
|
||||
|
||||
|
@ -264,7 +195,7 @@ Available strict options:
|
|||
- --withPlatform Prevent the upload if at least one platform is not specified
|
||||
- --withCategory Prevent the upload if no category
|
||||
- --withLanguage Prevent upload if no language
|
||||
- --withChannel Prevent upload if no channel
|
||||
- --withChannel Prevent upload if no channel
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -290,7 +221,7 @@ Available strict options:
|
|||
- [x] Usable on Desktop (Linux and/or Windows and/or MacOS)
|
||||
- [x] Different schedules on platforms to prepare preview
|
||||
- [x] Possibility to force the presence of upload options
|
||||
- [ ] Copy and forget, eg possibility to copy video in a directory, and prismedia uploads itself: [Work in progress](https://git.lecygnenoir.info/Zykino/prismedia-autoupload) thanks to @Zykino 🎉 (Discussions in [issue 27](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/27))
|
||||
- [ ] Copy and forget, eg possibility to copy video in a directory, and prismedia uploads itself: [Work in progress](https://git.lecygnenoir.info/Zykino/prismedia-autoupload) thanks to @Zykino 🎉 (Discussions in [issue 27](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/27))
|
||||
- [ ] A usable graphical interface
|
||||
|
||||
## Compatibility
|
||||
|
@ -299,7 +230,7 @@ Available strict options:
|
|||
- If you use peertube before 1.0.0-beta4, use the version inside tag 1.0.0-beta3
|
||||
|
||||
## Inspirations
|
||||
Inspired by [peeror](https://git.rigelk.eu/rigelk/peeror) and [youtube-upload](https://github.com/tokland/youtube-upload)
|
||||
Inspired by peeror (First peertube mirror by Rigelk) and [youtube-upload](https://github.com/tokland/youtube-upload)
|
||||
|
||||
## Contributors
|
||||
Thanks to: @Zykino, @meewan, @rigelk 😘
|
||||
Thanks to: @LecygneNoir, @Zykino, @meewan, @ysalmon, @rigelk 😘
|
||||
|
|
869
poetry.lock
generated
869
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,12 @@
|
|||
from future import standard_library
|
||||
standard_library.install_aliases()
|
||||
import logging
|
||||
logger = logging.getLogger('Prismedia')
|
||||
logger.setLevel(logging.INFO)
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s: %(message)s')
|
||||
ch.setFormatter(formatter)
|
||||
logger.addHandler(ch)
|
||||
|
||||
from . import upload
|
||||
from . import genconfig
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
from os.path import join, abspath, isfile, dirname
|
||||
from os.path import join, abspath, isfile, dirname, exists
|
||||
from os import listdir
|
||||
from shutil import copyfile
|
||||
import logging
|
||||
logger = logging.getLogger('Prismedia')
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
def genconfig():
|
||||
|
@ -8,7 +12,12 @@ def genconfig():
|
|||
files = [f for f in listdir(path) if isfile(join(path, f))]
|
||||
|
||||
for f in files:
|
||||
copyfile(join(path, f), f)
|
||||
final_f = f.replace(".sample", "")
|
||||
if exists(final_f) and not utils.ask_overwrite(final_f + " already exists. Do you want to overwrite it?"):
|
||||
continue
|
||||
|
||||
copyfile(join(path, f), final_f)
|
||||
logger.info(str(final_f) + " correctly generated, you may now edit it to fill your credentials.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -14,7 +14,8 @@ from tzlocal import get_localzone
|
|||
from configparser import RawConfigParser
|
||||
from requests_oauthlib import OAuth2Session
|
||||
from oauthlib.oauth2 import LegacyApplicationClient
|
||||
from requests_toolbelt.multipart.encoder import MultipartEncoder
|
||||
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
|
||||
from clint.textui.progress import Bar as ProgressBar
|
||||
|
||||
from . import utils
|
||||
logger = logging.getLogger('Prismedia')
|
||||
|
@ -63,6 +64,13 @@ def get_channel_by_name(user_info, options):
|
|||
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'))))
|
||||
|
@ -109,7 +117,7 @@ def get_default_playlist(user_info):
|
|||
|
||||
|
||||
def get_playlist_by_name(oauth, url, username, options):
|
||||
start = 1
|
||||
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"]
|
||||
|
@ -255,16 +263,18 @@ def upload_video(oauth, secret, options):
|
|||
publishAt = options.get('--publishAt')
|
||||
|
||||
if 'publishAt' in locals():
|
||||
publishAt = datetime.datetime.strptime(publishAt, '%Y-%m-%dT%H:%M:%S')
|
||||
tz = get_localzone()
|
||||
tz = pytz.timezone(str(tz))
|
||||
publishAt = tz.localize(publishAt).isoformat()
|
||||
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'))))
|
||||
|
@ -294,7 +304,12 @@ def upload_video(oauth, secret, options):
|
|||
if options.get('--url-only') or options.get('--batch'):
|
||||
logger_stdout = logging.getLogger('stdoutlogs')
|
||||
|
||||
multipart_data = MultipartEncoder(fields)
|
||||
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
|
||||
|
@ -302,12 +317,14 @@ def upload_video(oauth, secret, options):
|
|||
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))
|
||||
|
@ -325,10 +342,49 @@ def upload_video(oauth, secret, options):
|
|||
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)
|
||||
if options.get('--credentialsdir') :
|
||||
secret.read(os.path.join(options.get('--credentialsdir'), PEERTUBE_SECRETS_FILE))
|
||||
else :
|
||||
secret.read(PEERTUBE_SECRETS_FILE)
|
||||
except Exception as e:
|
||||
logger.critical("Peertube: Error loading " + str(PEERTUBE_SECRETS_FILE) + ": " + str(e))
|
||||
exit(1)
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
# Some generic options for your videos
|
||||
cca = True
|
||||
privacy = private
|
||||
disable-comments = True
|
||||
disable-comments = False
|
||||
channel = DefaultChannel
|
||||
channelCreate = True
|
||||
channelCreate = True
|
||||
auto-originalDate = True
|
|
@ -7,11 +7,12 @@ prismedia - tool to upload videos to Peertube and Youtube
|
|||
Usage:
|
||||
prismedia --file=<FILE> [options]
|
||||
prismedia -f <FILE> --tags=STRING [options]
|
||||
prismedia --hearthbeat
|
||||
prismedia -h | --help
|
||||
prismedia --version
|
||||
|
||||
Options:
|
||||
-f, --file=STRING Path to the video file to upload in mp4. This is the only mandatory option.
|
||||
-f, --file=STRING Path to the video file to upload. This is the only mandatory option.
|
||||
--name=NAME Name of the video to upload. (default to video filename)
|
||||
-d, --description=STRING Description of the video. (default: default description)
|
||||
-t, --tags=STRING Tags for the video. comma separated.
|
||||
|
@ -26,6 +27,7 @@ Options:
|
|||
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
|
||||
--credentialsdir=STRING Set directory where to search for secret file.
|
||||
--platform=STRING List of platform(s) to upload to, comma separated.
|
||||
Supported platforms are youtube and peertube (default is both)
|
||||
--language=STRING Specify the default language for video. See below for supported language. (default is English)
|
||||
|
@ -34,9 +36,12 @@ Options:
|
|||
DATE should be in the future
|
||||
--peertubeAt=DATE
|
||||
--youtubeAt=DATE Override publishAt for the corresponding platform. Allow to create preview on specific platform
|
||||
--originalDate=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-originalDate Automatically use the file modification time as original date
|
||||
--thumbnail=STRING Path to a file to use as a thumbnail for the video.
|
||||
Supported types are jpg and jpeg.
|
||||
By default, prismedia search for an image based on video name followed by .jpg or .jpeg
|
||||
By default, prismedia search for an image based on video name followed by .jpg, .jpeg or .png
|
||||
--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.
|
||||
--channelCreate Create the channel if not exists. (Peertube only, default do not create)
|
||||
|
@ -45,6 +50,10 @@ Options:
|
|||
If the playlist is not found, spawn an error except if --playlistCreate is set.
|
||||
--playlistCreate 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 (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)
|
||||
|
||||
-h --help Show this help.
|
||||
--version Show version.
|
||||
|
||||
|
@ -54,7 +63,6 @@ Logging options
|
|||
-u --url-only Display generated URL after upload directly on stdout, implies --quiet
|
||||
--batch Display generated URL after upload with platform information for easier parsing. Implies --quiet
|
||||
Be careful --batch and --url-only are mutually exclusives.
|
||||
--debug (Deprecated) Alias for --log=debug. Ignored if --log is set
|
||||
|
||||
Strict options:
|
||||
Strict options allow you to force some option to be present when uploading a video. It's useful to be sure you do not
|
||||
|
@ -71,6 +79,7 @@ Strict options:
|
|||
--withTags Prevent the upload without tags
|
||||
--withPlaylist Prevent the upload if no playlist
|
||||
--withPublishAt Prevent the upload if no schedule
|
||||
--withOriginalDate Prevent the upload if no original date configured
|
||||
--withPlatform Prevent the upload if at least one platform is not specified
|
||||
--withCategory Prevent the upload if no category
|
||||
--withLanguage Prevent upload if no language
|
||||
|
@ -100,12 +109,6 @@ import os
|
|||
import datetime
|
||||
import logging
|
||||
logger = logging.getLogger('Prismedia')
|
||||
logger.setLevel(logging.INFO)
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s: %(message)s')
|
||||
ch.setFormatter(formatter)
|
||||
logger.addHandler(ch)
|
||||
|
||||
from docopt import docopt
|
||||
|
||||
|
@ -121,16 +124,8 @@ except ImportError:
|
|||
' is installed: \n'
|
||||
'see https://github.com/halst/schema\n')
|
||||
exit(1)
|
||||
try:
|
||||
# noinspection PyUnresolvedReferences
|
||||
import magic
|
||||
except ImportError:
|
||||
logger.critical('This program requires that the `python-magic` library'
|
||||
' is installed, NOT the Python bindings to libmagic API \n'
|
||||
'see https://github.com/ahupp/python-magic\n')
|
||||
exit(1)
|
||||
|
||||
VERSION = "prismedia v0.10.2"
|
||||
VERSION = "prismedia v0.12.2"
|
||||
|
||||
VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted')
|
||||
VALID_CATEGORIES = (
|
||||
|
@ -145,20 +140,7 @@ VALID_LANGUAGES = ('arabic', 'english', 'french',
|
|||
'german', 'hindi', 'italian',
|
||||
'japanese', 'korean', 'mandarin',
|
||||
'portuguese', 'punjabi', 'russian', 'spanish')
|
||||
|
||||
|
||||
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
|
||||
VALID_PROGRESS = ('percentage', 'bigfile', 'accurate')
|
||||
|
||||
|
||||
def validateCategory(category):
|
||||
|
@ -190,11 +172,11 @@ def validateLanguage(language):
|
|||
return False
|
||||
|
||||
|
||||
def validatePublish(publish):
|
||||
def validatePublishDate(publishDate):
|
||||
# Check date format and if date is future
|
||||
try:
|
||||
now = datetime.datetime.now()
|
||||
publishAt = datetime.datetime.strptime(publish, '%Y-%m-%dT%H:%M:%S')
|
||||
publishAt = datetime.datetime.strptime(publishDate, '%Y-%m-%dT%H:%M:%S')
|
||||
if now >= publishAt:
|
||||
return False
|
||||
except ValueError:
|
||||
|
@ -202,13 +184,16 @@ def validatePublish(publish):
|
|||
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:
|
||||
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 validateLogLevel(loglevel):
|
||||
|
@ -217,6 +202,15 @@ def validateLogLevel(loglevel):
|
|||
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('-', '')
|
||||
option = option[0].upper() + option[1:]
|
||||
|
@ -234,19 +228,20 @@ def configureLogs(options):
|
|||
if options.get('--batch') or options.get('--url-only'):
|
||||
options['--quiet'] = True
|
||||
|
||||
if options.get('--quiet'):
|
||||
# We need to set both log level in the same time
|
||||
logger.setLevel(50)
|
||||
ch.setLevel(50)
|
||||
elif options.get('--log'):
|
||||
numeric_level = getattr(logging, options["--log"], None)
|
||||
# We need to set both log level in the same time
|
||||
logger.setLevel(numeric_level)
|
||||
ch.setLevel(numeric_level)
|
||||
elif options.get('--debug'):
|
||||
logger.warning("DEPRECATION: --debug is deprecated, please use --log=debug instead")
|
||||
logger.setLevel(10)
|
||||
ch.setLevel(10)
|
||||
for handler in logger.handlers or logger.parent.handlers:
|
||||
if options.get('--quiet'):
|
||||
# We need to set both log level in the same time
|
||||
logger.setLevel(50)
|
||||
handler.setLevel(50)
|
||||
elif options.get('--log'):
|
||||
numeric_level = getattr(logging, options["--log"], None)
|
||||
# We need to set both log level in the same time
|
||||
logger.setLevel(numeric_level)
|
||||
handler.setLevel(numeric_level)
|
||||
elif options.get('--debug'):
|
||||
# Deprecated,
|
||||
logger.setLevel(10)
|
||||
handler.setLevel(10)
|
||||
|
||||
|
||||
def configureStdoutLogs():
|
||||
|
@ -280,16 +275,18 @@ def main():
|
|||
Optional('--withTags', default=False): bool,
|
||||
Optional('--withPlaylist', default=False): bool,
|
||||
Optional('--withPublishAt', default=False): bool,
|
||||
Optional('--withOriginalDate', default=False): bool,
|
||||
Optional('--withPlatform', default=False): bool,
|
||||
Optional('--withCategory', default=False): bool,
|
||||
Optional('--withLanguage', default=False): bool,
|
||||
Optional('--withChannel', default=False): bool,
|
||||
Optional('--credentialsdir'): Or(None, And(str, os.path.exists, error='credentialsdir does not exist')),
|
||||
# This allow to return all other options for further use: https://github.com/keleshev/schema#extra-keys
|
||||
object: object
|
||||
})
|
||||
|
||||
schema = Schema({
|
||||
'--file': And(str, os.path.exists, validateVideo, error='file is not supported, please use mp4'),
|
||||
'--file': And(str, os.path.exists, error='file does not exists, please check path'),
|
||||
# Strict option checks - at the moment Schema needs to check Hook and Optional separately #
|
||||
Hook('--name', handler=_optionnalOrStrict): object,
|
||||
Hook('--description', handler=_optionnalOrStrict): object,
|
||||
|
@ -298,6 +295,7 @@ def main():
|
|||
Hook('--language', handler=_optionnalOrStrict): object,
|
||||
Hook('--platform', handler=_optionnalOrStrict): object,
|
||||
Hook('--publishAt', handler=_optionnalOrStrict): object,
|
||||
Hook('--originalDate', handler=_optionnalOrStrict): object,
|
||||
Hook('--thumbnail', handler=_optionnalOrStrict): object,
|
||||
Hook('--channel', handler=_optionnalOrStrict): object,
|
||||
Hook('--playlist', handler=_optionnalOrStrict): object,
|
||||
|
@ -336,34 +334,47 @@ def main():
|
|||
Optional('--platform'): Or(None, And(str, validatePlatform, error="Sorry, upload platform not supported")),
|
||||
Optional('--publishAt'): Or(None, And(
|
||||
str,
|
||||
validatePublish,
|
||||
error="DATE should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
|
||||
validatePublishDate,
|
||||
error="Publish Date should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
|
||||
),
|
||||
Optional('--peertubeAt'): Or(None, And(
|
||||
str,
|
||||
validatePublish,
|
||||
error="DATE should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
|
||||
validatePublishDate,
|
||||
error="Publish Date should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
|
||||
),
|
||||
Optional('--youtubeAt'): Or(None, And(
|
||||
str,
|
||||
validatePublish,
|
||||
error="DATE should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
|
||||
validatePublishDate,
|
||||
error="Publish Date should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
|
||||
),
|
||||
Optional('--originalDate'): Or(None, And(
|
||||
str,
|
||||
validateOriginalDate,
|
||||
error="Original date should be the form YYYY-MM-DDThh:mm:ss and has to be in the past")
|
||||
),
|
||||
Optional('--auto-originalDate'): bool,
|
||||
Optional('--cca'): bool,
|
||||
Optional('--disable-comments'): bool,
|
||||
Optional('--nsfw'): bool,
|
||||
Optional('--thumbnail'): Or(None, And(
|
||||
str, validateThumbnail, error='thumbnail is not supported, please use jpg/jpeg'),
|
||||
str, os.path.exists, error='Thumbnail does not exists, please check the path.'),
|
||||
),
|
||||
Optional('--channel'): Or(None, str),
|
||||
Optional('--channelCreate'): bool,
|
||||
Optional('--playlist'): Or(None, str),
|
||||
Optional('--playlistCreate'): bool,
|
||||
Optional('--progress'): Or(None, And(str, validateProgress, error="Sorry, progress visualisation not supported")),
|
||||
'--hearthbeat': 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()
|
||||
exit(0)
|
||||
|
||||
# We need to validate early options first as withNFO and logs options should be prioritized
|
||||
try:
|
||||
options = earlyoptionSchema.validate(options)
|
||||
|
@ -377,6 +388,12 @@ def main():
|
|||
|
||||
options = utils.parseNFO(options)
|
||||
|
||||
# If after loading NFO we still has no original date and --auto-originalDate is enabled,
|
||||
# then we need to search from the file
|
||||
# We need to do that before the strict validation in case --withOriginalDate is enabled
|
||||
if not options.get('--originalDate') and options.get('--auto-originalDate'):
|
||||
options['--originalDate'] = utils.searchOriginalDate(options)
|
||||
|
||||
# Once NFO are loaded, we need to revalidate strict options in case some were in NFO
|
||||
try:
|
||||
options = earlyoptionSchema.validate(options)
|
||||
|
|
|
@ -2,12 +2,11 @@
|
|||
# coding: utf-8
|
||||
|
||||
from configparser import RawConfigParser, NoOptionError, NoSectionError
|
||||
from os.path import dirname, splitext, basename, isfile
|
||||
from os.path import dirname, splitext, basename, isfile, getmtime
|
||||
import re
|
||||
from os import devnull
|
||||
from subprocess import check_call, CalledProcessError, STDOUT
|
||||
import unidecode
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
logger = logging.getLogger('Prismedia')
|
||||
|
||||
|
@ -101,6 +100,15 @@ def getLanguage(language, platform):
|
|||
return PEERTUBE_LANGUAGE[language.lower()]
|
||||
|
||||
|
||||
def ask_overwrite(question):
|
||||
while True:
|
||||
reply = str(input(question + ' (Yes/[No]): ') or "No").lower().strip()
|
||||
if reply[:1] == 'y':
|
||||
return True
|
||||
if reply[:1] == 'n':
|
||||
return False
|
||||
|
||||
|
||||
def remove_empty_kwargs(**kwargs):
|
||||
good_kwargs = {}
|
||||
if kwargs is not None:
|
||||
|
@ -118,6 +126,8 @@ def searchThumbnail(options):
|
|||
options['--thumbnail'] = video_directory + options.get('--name') + ".jpg"
|
||||
elif isfile(video_directory + options.get('--name') + ".jpeg"):
|
||||
options['--thumbnail'] = video_directory + options.get('--name') + ".jpeg"
|
||||
elif isfile(video_directory + options.get('--name') + ".png"):
|
||||
options['--thumbnail'] = video_directory + options.get('--name') + ".png"
|
||||
# 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]
|
||||
|
@ -125,6 +135,8 @@ def searchThumbnail(options):
|
|||
options['--thumbnail'] = video_directory + video_file + ".jpg"
|
||||
elif isfile(video_directory + video_file + ".jpeg"):
|
||||
options['--thumbnail'] = video_directory + video_file + ".jpeg"
|
||||
elif isfile(video_directory + video_file + ".png"):
|
||||
options['--thumbnail'] = video_directory + video_file + ".png"
|
||||
|
||||
# Display some info after research
|
||||
if not options.get('--thumbnail'):
|
||||
|
@ -135,6 +147,11 @@ def searchThumbnail(options):
|
|||
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:
|
||||
|
@ -162,8 +179,8 @@ def parseNFO(options):
|
|||
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 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')):
|
||||
|
@ -196,7 +213,7 @@ def parseNFO(options):
|
|||
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("-", "")
|
||||
key = key.replace("--", "")
|
||||
try:
|
||||
# get string options
|
||||
if value is None and nfo.get('video', key):
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
# From Youtube samples : https://raw.githubusercontent.com/youtube/api-samples/master/python/upload_video.py # noqa
|
||||
# From Youtube samples: https://raw.githubusercontent.com/youtube/api-samples/master/python/upload_video.py # noqa
|
||||
|
||||
import http.client
|
||||
import httplib2
|
||||
|
@ -48,8 +48,8 @@ RETRIABLE_EXCEPTIONS = (
|
|||
RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
|
||||
|
||||
|
||||
CLIENT_SECRETS_FILE = 'youtube_secret.json'
|
||||
CREDENTIALS_PATH = ".youtube_credentials.json"
|
||||
CLIENT_SECRETS_FILE_BASE = 'youtube_secret.json'
|
||||
CREDENTIALS_PATH_BASE = ".youtube_credentials.json"
|
||||
SCOPES = ['https://www.googleapis.com/auth/youtube.upload', 'https://www.googleapis.com/auth/youtube.force-ssl']
|
||||
API_SERVICE_NAME = 'youtube'
|
||||
API_VERSION = 'v3'
|
||||
|
@ -60,6 +60,7 @@ def get_authenticated_service():
|
|||
check_authenticated_scopes()
|
||||
flow = InstalledAppFlow.from_client_secrets_file(
|
||||
CLIENT_SECRETS_FILE, SCOPES)
|
||||
|
||||
if exists(CREDENTIALS_PATH):
|
||||
with open(CREDENTIALS_PATH, 'r') as f:
|
||||
credential_params = json.load(f)
|
||||
|
@ -76,7 +77,7 @@ def get_authenticated_service():
|
|||
p = copy.deepcopy(vars(credentials))
|
||||
del p["expiry"]
|
||||
json.dump(p, f)
|
||||
return build(API_SERVICE_NAME, API_VERSION, credentials=credentials, cache_discovery=False)
|
||||
return build(API_SERVICE_NAME, API_VERSION, credentials=credentials, cache_discovery=False)
|
||||
|
||||
|
||||
def check_authenticated_scopes():
|
||||
|
@ -89,6 +90,15 @@ def check_authenticated_scopes():
|
|||
os.remove(CREDENTIALS_PATH)
|
||||
|
||||
|
||||
def convert_youtube_date(date):
|
||||
# Youtube needs microsecond and the local timezone from ISO 8601
|
||||
date = date + ".000001"
|
||||
date = datetime.datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.%f')
|
||||
tz = get_localzone()
|
||||
tz = pytz.timezone(str(tz))
|
||||
return tz.localize(date).isoformat()
|
||||
|
||||
|
||||
def initialize_upload(youtube, options):
|
||||
path = options.get('--file')
|
||||
tags = None
|
||||
|
@ -107,6 +117,8 @@ def initialize_upload(youtube, options):
|
|||
if options.get('--cca'):
|
||||
license = "creativeCommon"
|
||||
|
||||
# We set recordingDetails empty because it's easier to add options if it already exists
|
||||
# and if empty, it does not cause problem during upload
|
||||
body = {
|
||||
"snippet": {
|
||||
"title": options.get('--name') or splitext(basename(path))[0],
|
||||
|
@ -119,6 +131,9 @@ def initialize_upload(youtube, options):
|
|||
"status": {
|
||||
"privacyStatus": str(options.get('--privacy') or "private"),
|
||||
"license": str(license or "youtube"),
|
||||
},
|
||||
"recordingDetails": {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -128,15 +143,16 @@ def initialize_upload(youtube, options):
|
|||
elif options.get('--publishAt'):
|
||||
publishAt = options.get('--publishAt')
|
||||
|
||||
# Check if publishAt variable exists in local variables
|
||||
if 'publishAt' in locals():
|
||||
# Youtube needs microsecond and the local timezone from ISO 8601
|
||||
publishAt = publishAt + ".000001"
|
||||
publishAt = datetime.datetime.strptime(publishAt, '%Y-%m-%dT%H:%M:%S.%f')
|
||||
tz = get_localzone()
|
||||
tz = pytz.timezone(str(tz))
|
||||
publishAt = tz.localize(publishAt).isoformat()
|
||||
publishAt = convert_youtube_date(publishAt)
|
||||
body['status']['publishAt'] = str(publishAt)
|
||||
|
||||
# Set originalDate except if the user force no originalDate
|
||||
if options.get('--originalDate'):
|
||||
originalDate = convert_youtube_date(options.get('--originalDate'))
|
||||
body['recordingDetails']['recordingDate'] = str(originalDate)
|
||||
|
||||
if options.get('--playlist'):
|
||||
playlist_id = get_playlist_by_name(youtube, options.get('--playlist'))
|
||||
if not playlist_id and options.get('--playlistCreate'):
|
||||
|
@ -166,14 +182,24 @@ def initialize_upload(youtube, options):
|
|||
|
||||
|
||||
def get_playlist_by_name(youtube, playlist_name):
|
||||
response = youtube.playlists().list(
|
||||
part='snippet,id',
|
||||
mine=True,
|
||||
maxResults=50
|
||||
).execute()
|
||||
for playlist in response["items"]:
|
||||
if playlist["snippet"]['title'] == playlist_name:
|
||||
return playlist['id']
|
||||
pageToken = ""
|
||||
while pageToken != None:
|
||||
response = youtube.playlists().list(
|
||||
part='snippet,id',
|
||||
mine=True,
|
||||
maxResults=50,
|
||||
pageToken=pageToken
|
||||
).execute()
|
||||
|
||||
for playlist in response["items"]:
|
||||
if playlist["snippet"]["title"] == playlist_name:
|
||||
return playlist["id"]
|
||||
|
||||
# Ask next page if there are any
|
||||
if "nextPageToken" in response:
|
||||
pageToken = response["nextPageToken"]
|
||||
else:
|
||||
pageToken = None
|
||||
|
||||
|
||||
def create_playlist(youtube, playlist_name):
|
||||
|
@ -278,7 +304,7 @@ def resumable_upload(request, resource, method, options):
|
|||
status, response = request.next_chunk()
|
||||
if response is not None:
|
||||
if method == 'insert' and 'id' in response:
|
||||
logger.info('Youtube : Video was successfully uploaded.')
|
||||
logger.info('Youtube: Video was successfully uploaded.')
|
||||
template = 'Youtube: Watch it at https://youtu.be/%s (post-encoding could take some time)'
|
||||
logger.info(template % response['id'])
|
||||
template_stdout = 'https://youtu.be/%s'
|
||||
|
@ -290,36 +316,58 @@ def resumable_upload(request, resource, method, options):
|
|||
elif method != 'insert' or "id" not in response:
|
||||
logger.info('Youtube: Thumbnail was successfully set.')
|
||||
else:
|
||||
template = ('Youtube : The upload failed with an '
|
||||
template = ('Youtube: The upload failed with an '
|
||||
'unexpected response: %s')
|
||||
logger.critical(template % response)
|
||||
exit(1)
|
||||
except HttpError as e:
|
||||
if e.resp.status in RETRIABLE_STATUS_CODES:
|
||||
template = 'Youtube : A retriable HTTP error %d occurred:\n%s'
|
||||
template = 'Youtube: A retriable HTTP error %d occurred:\n%s'
|
||||
error = template % (e.resp.status, e.content)
|
||||
else:
|
||||
raise
|
||||
except RETRIABLE_EXCEPTIONS as e:
|
||||
error = 'Youtube : A retriable error occurred: %s' % e
|
||||
error = 'Youtube: A retriable error occurred: %s' % e
|
||||
|
||||
if error is not None:
|
||||
logger.warning(error)
|
||||
retry += 1
|
||||
if retry > MAX_RETRIES:
|
||||
logger.error('Youtube : No longer attempting to retry.')
|
||||
logger.error('Youtube: No longer attempting to retry.')
|
||||
|
||||
max_sleep = 2 ** retry
|
||||
sleep_seconds = random.random() * max_sleep
|
||||
logger.warning('Youtube : Sleeping %f seconds and then retrying...'
|
||||
logger.warning('Youtube: Sleeping %f seconds and then retrying...'
|
||||
% sleep_seconds)
|
||||
time.sleep(sleep_seconds)
|
||||
|
||||
|
||||
def hearthbeat():
|
||||
"""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:
|
||||
- General informations about quotas: https://developers.google.com/youtube/v3/getting-started#quota
|
||||
- Quota costs for API requests: https://developers.google.com/youtube/v3/determine_quota_cost
|
||||
- ToS (Americas) #Usage and Quotas: https://developers.google.com/youtube/terms/api-services-terms-of-service#usage-and-quotas"""
|
||||
youtube = get_authenticated_service()
|
||||
try:
|
||||
get_playlist_by_name(youtube, "Foo")
|
||||
except HttpError as e:
|
||||
logger.error('Youtube: An HTTP error %d occurred on hearthbeat:\n%s' %
|
||||
(e.resp.status, e.content))
|
||||
|
||||
|
||||
def run(options):
|
||||
global CLIENT_SECRETS_FILE, CREDENTIALS_PATH
|
||||
if options.get('--credentialsdir') :
|
||||
CLIENT_SECRETS_FILE = os.path.join(options.get('--credentialsdir'), CLIENT_SECRETS_FILE_BASE)
|
||||
CREDENTIALS_PATH = os.path.join(options.get('--credentialsdir'), CREDENTIALS_PATH_BASE)
|
||||
else :
|
||||
CLIENT_SECRETS_FILE = CLIENT_SECRETS_FILE_BASE
|
||||
CREDENTIALS_PATH = CREDENTIALS_PATH_BASE
|
||||
youtube = get_authenticated_service()
|
||||
try:
|
||||
initialize_upload(youtube, options)
|
||||
except HttpError as e:
|
||||
logger.error('Youtube : An HTTP error %d occurred:\n%s' % (e.resp.status,
|
||||
logger.error('Youtube: An HTTP error %d occurred:\n%s' % (e.resp.status,
|
||||
e.content))
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
[tool.poetry]
|
||||
name = "prismedia"
|
||||
version = "0.10.1"
|
||||
version = "0.13.0"
|
||||
description = "scripting your way to upload videos on peertube and youtube"
|
||||
authors = [
|
||||
"LecygneNoir <git@lecygnenoir.info>",
|
||||
"Rigel Kent <sendmemail@rigelk.eu>",
|
||||
"Zykino"
|
||||
"Zykino",
|
||||
"YSalmon"
|
||||
]
|
||||
|
||||
license = "AGPL-3.0-only"
|
||||
|
@ -17,33 +18,31 @@ homepage = "https://git.lecygnenoir.info/LecygneNoir/prismedia"
|
|||
keywords = ['peertube', 'youtube', 'prismedia']
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.5"
|
||||
configparser = "^3.7.1"
|
||||
docopt = "^0.6.2"
|
||||
future = "^0.17.1"
|
||||
python = ">=3.9"
|
||||
clint = ">=0.5.1"
|
||||
configparser = ">=3.7.1"
|
||||
docopt = ">=0.6.2"
|
||||
future = ">=0.17.1"
|
||||
google-api-python-client = ">=1.7.6"
|
||||
google-auth = ">=1.6.1"
|
||||
google-auth-httplib2 = ">=0.0.3"
|
||||
google-auth-oauthlib = ">=0.2.0"
|
||||
httplib2 = "^0.12.1"
|
||||
oauthlib = "^2.1.0"
|
||||
python-magic = "^0.4.15"
|
||||
python-magic-bin = { version = "^0.4.14", markers = "platform_system == 'Windows'" }
|
||||
requests = "^2.18.4"
|
||||
requests-oauthlib = "^0.8.0"
|
||||
requests-toolbelt = "^0.9.1"
|
||||
httplib2 = ">=0.12.1"
|
||||
oauthlib = "=2.1.0"
|
||||
requests = ">=2.18.4"
|
||||
requests-oauthlib = "=1.1.0"
|
||||
requests-toolbelt = ">=0.9.1"
|
||||
pytz = "=2022.1"
|
||||
schema = ">=0.7.1"
|
||||
tzlocal = "^1.5.1"
|
||||
Unidecode = "^1.0.23"
|
||||
uritemplate = "^3.0.0"
|
||||
urllib3 = "^1.22"
|
||||
tzlocal = ">=1.5.1"
|
||||
Unidecode = ">=1.0.23"
|
||||
uritemplate = ">=3.0.0"
|
||||
urllib3 = ">=1.22"
|
||||
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
||||
[tool.poetry.scripts]
|
||||
prismedia = 'prismedia.upload:main'
|
||||
prismedia-init = 'prismedia.genconfig:genconfig'
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
build-backend = "poetry.masonry.api"
|
||||
|
||||
|
|
322
requirements.txt
322
requirements.txt
|
@ -1,129 +1,195 @@
|
|||
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
|
||||
configparser==3.8.1 \
|
||||
--hash=sha256:45d1272aad6cfd7a8a06cf5c73f2ceb6a190f6acc1fa707e7f82a4c053b28b18 \
|
||||
--hash=sha256:bc37850f0cc42a1725a796ef7d92690651bf1af37d744cc63161dac62cabee17
|
||||
docopt==0.6.2 \
|
||||
args==0.1.0 ; python_version >= "3.9" \
|
||||
--hash=sha256:a785b8d837625e9b61c39108532d95b85274acd679693b71ebb5156848fcf814
|
||||
cachetools==5.5.2 ; python_version >= "3.9" \
|
||||
--hash=sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4 \
|
||||
--hash=sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a
|
||||
certifi==2025.6.15 ; python_version >= "3.9" \
|
||||
--hash=sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057 \
|
||||
--hash=sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b
|
||||
charset-normalizer==3.4.2 ; python_version >= "3.9" \
|
||||
--hash=sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4 \
|
||||
--hash=sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45 \
|
||||
--hash=sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7 \
|
||||
--hash=sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0 \
|
||||
--hash=sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7 \
|
||||
--hash=sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d \
|
||||
--hash=sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d \
|
||||
--hash=sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0 \
|
||||
--hash=sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184 \
|
||||
--hash=sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db \
|
||||
--hash=sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b \
|
||||
--hash=sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64 \
|
||||
--hash=sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b \
|
||||
--hash=sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8 \
|
||||
--hash=sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff \
|
||||
--hash=sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344 \
|
||||
--hash=sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58 \
|
||||
--hash=sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e \
|
||||
--hash=sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471 \
|
||||
--hash=sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148 \
|
||||
--hash=sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a \
|
||||
--hash=sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836 \
|
||||
--hash=sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e \
|
||||
--hash=sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63 \
|
||||
--hash=sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c \
|
||||
--hash=sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1 \
|
||||
--hash=sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01 \
|
||||
--hash=sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366 \
|
||||
--hash=sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58 \
|
||||
--hash=sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5 \
|
||||
--hash=sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c \
|
||||
--hash=sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2 \
|
||||
--hash=sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a \
|
||||
--hash=sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597 \
|
||||
--hash=sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b \
|
||||
--hash=sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5 \
|
||||
--hash=sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb \
|
||||
--hash=sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f \
|
||||
--hash=sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0 \
|
||||
--hash=sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941 \
|
||||
--hash=sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0 \
|
||||
--hash=sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86 \
|
||||
--hash=sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7 \
|
||||
--hash=sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7 \
|
||||
--hash=sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455 \
|
||||
--hash=sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6 \
|
||||
--hash=sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4 \
|
||||
--hash=sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0 \
|
||||
--hash=sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3 \
|
||||
--hash=sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1 \
|
||||
--hash=sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6 \
|
||||
--hash=sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981 \
|
||||
--hash=sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c \
|
||||
--hash=sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980 \
|
||||
--hash=sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645 \
|
||||
--hash=sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7 \
|
||||
--hash=sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12 \
|
||||
--hash=sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa \
|
||||
--hash=sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd \
|
||||
--hash=sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef \
|
||||
--hash=sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f \
|
||||
--hash=sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2 \
|
||||
--hash=sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d \
|
||||
--hash=sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5 \
|
||||
--hash=sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02 \
|
||||
--hash=sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3 \
|
||||
--hash=sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd \
|
||||
--hash=sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e \
|
||||
--hash=sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214 \
|
||||
--hash=sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd \
|
||||
--hash=sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a \
|
||||
--hash=sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c \
|
||||
--hash=sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681 \
|
||||
--hash=sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba \
|
||||
--hash=sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f \
|
||||
--hash=sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a \
|
||||
--hash=sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28 \
|
||||
--hash=sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691 \
|
||||
--hash=sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82 \
|
||||
--hash=sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a \
|
||||
--hash=sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027 \
|
||||
--hash=sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7 \
|
||||
--hash=sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518 \
|
||||
--hash=sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf \
|
||||
--hash=sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b \
|
||||
--hash=sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9 \
|
||||
--hash=sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544 \
|
||||
--hash=sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da \
|
||||
--hash=sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509 \
|
||||
--hash=sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f \
|
||||
--hash=sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a \
|
||||
--hash=sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f
|
||||
clint==0.5.1 ; python_version >= "3.9" \
|
||||
--hash=sha256:05224c32b1075563d0b16d0015faaf9da43aa214e4a2140e51f08789e7a4c5aa
|
||||
configparser==7.2.0 ; python_version >= "3.9" \
|
||||
--hash=sha256:b629cc8ae916e3afbd36d1b3d093f34193d851e11998920fdcfc4552218b7b70 \
|
||||
--hash=sha256:fee5e1f3db4156dcd0ed95bc4edfa3580475537711f67a819c966b389d09ce62
|
||||
docopt==0.6.2 ; python_version >= "3.9" \
|
||||
--hash=sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491
|
||||
future==0.17.1 \
|
||||
--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
|
||||
httplib2==0.12.3 \
|
||||
--hash=sha256:23914b5487dfe8ef09db6656d6d63afb0cf3054ad9ebc50868ddc8e166b5f8e8 \
|
||||
--hash=sha256:a18121c7c72a56689efbf1aef990139ad940fee1e64c6f2458831736cd593600
|
||||
idna==2.9 \
|
||||
--hash=sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa \
|
||||
--hash=sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb
|
||||
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 \
|
||||
--hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \
|
||||
--hash=sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199 \
|
||||
--hash=sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405 \
|
||||
--hash=sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb \
|
||||
--hash=sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8 \
|
||||
--hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74 \
|
||||
--hash=sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d \
|
||||
--hash=sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45 \
|
||||
--hash=sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4 \
|
||||
--hash=sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811 \
|
||||
--hash=sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed \
|
||||
--hash=sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0 \
|
||||
--hash=sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd
|
||||
python-magic==0.4.15 \
|
||||
--hash=sha256:f3765c0f582d2dfc72c15f3b5a82aecfae9498bd29ca840d72f37d7bd38bfcd5 \
|
||||
--hash=sha256:f2674dcfad52ae6c49d4803fa027809540b130db1dec928cfbb9240316831375
|
||||
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
|
||||
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
|
||||
tzlocal==1.5.1 \
|
||||
--hash=sha256:4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e
|
||||
unidecode==1.1.1 \
|
||||
--hash=sha256:1d7a042116536098d05d599ef2b8616759f02985c85b4fef50c78a5aaf10822a \
|
||||
--hash=sha256:2b6aab710c2a1647e928e36d69c21e76b453cd455f4e2621000e54b2a9b8cce8
|
||||
uritemplate==3.0.1 \
|
||||
--hash=sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f \
|
||||
--hash=sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae
|
||||
urllib3==1.22 \
|
||||
--hash=sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b \
|
||||
--hash=sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f
|
||||
future==1.0.0 ; python_version >= "3.9" \
|
||||
--hash=sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216 \
|
||||
--hash=sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05
|
||||
google-api-core==2.25.1 ; python_version >= "3.9" \
|
||||
--hash=sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7 \
|
||||
--hash=sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8
|
||||
google-api-python-client==2.174.0 ; python_version >= "3.9" \
|
||||
--hash=sha256:9eb7616a820b38a9c12c5486f9b9055385c7feb18b20cbafc5c5a688b14f3515 \
|
||||
--hash=sha256:f695205ceec97bfaa1590a14282559c4109326c473b07352233a3584cdbf4b89
|
||||
google-auth-httplib2==0.2.0 ; python_version >= "3.9" \
|
||||
--hash=sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05 \
|
||||
--hash=sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d
|
||||
google-auth-oauthlib==1.2.2 ; python_version >= "3.9" \
|
||||
--hash=sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684 \
|
||||
--hash=sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2
|
||||
google-auth==2.40.3 ; python_version >= "3.9" \
|
||||
--hash=sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca \
|
||||
--hash=sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77
|
||||
googleapis-common-protos==1.70.0 ; python_version >= "3.9" \
|
||||
--hash=sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257 \
|
||||
--hash=sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8
|
||||
httplib2==0.22.0 ; python_version >= "3.9" \
|
||||
--hash=sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc \
|
||||
--hash=sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81
|
||||
idna==3.10 ; python_version >= "3.9" \
|
||||
--hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
|
||||
--hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
|
||||
oauthlib==2.1.0 ; python_version >= "3.9" \
|
||||
--hash=sha256:ac35665a61c1685c56336bda97d5eefa246f1202618a1d6f34fccb1bdd404162 \
|
||||
--hash=sha256:d883b36b21a6ad813953803edfa563b1b579d79ca758fe950d1bc9e8b326025b
|
||||
proto-plus==1.26.1 ; python_version >= "3.9" \
|
||||
--hash=sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66 \
|
||||
--hash=sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012
|
||||
protobuf==6.31.1 ; python_version >= "3.9" \
|
||||
--hash=sha256:0414e3aa5a5f3ff423828e1e6a6e907d6c65c1d5b7e6e975793d5590bdeecc16 \
|
||||
--hash=sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447 \
|
||||
--hash=sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6 \
|
||||
--hash=sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402 \
|
||||
--hash=sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e \
|
||||
--hash=sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9 \
|
||||
--hash=sha256:8764cf4587791e7564051b35524b72844f845ad0bb011704c3736cce762d8fe9 \
|
||||
--hash=sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39 \
|
||||
--hash=sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a
|
||||
pyasn1-modules==0.4.2 ; python_version >= "3.9" \
|
||||
--hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \
|
||||
--hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6
|
||||
pyasn1==0.6.1 ; python_version >= "3.9" \
|
||||
--hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \
|
||||
--hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034
|
||||
pyparsing==3.2.3 ; python_version >= "3.9" \
|
||||
--hash=sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf \
|
||||
--hash=sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be
|
||||
pytz==2022.1 ; python_version >= "3.9" \
|
||||
--hash=sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7 \
|
||||
--hash=sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c
|
||||
requests-oauthlib==1.1.0 ; python_version >= "3.9" \
|
||||
--hash=sha256:be76f2bb72ca5525998e81d47913e09b1ca8b7957ae89b46f787a79e68ad5e61 \
|
||||
--hash=sha256:eabd8eb700ebed81ba080c6ead96d39d6bdc39996094bd23000204f6965786b0
|
||||
requests-toolbelt==1.0.0 ; python_version >= "3.9" \
|
||||
--hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \
|
||||
--hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06
|
||||
requests==2.32.4 ; python_version >= "3.9" \
|
||||
--hash=sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c \
|
||||
--hash=sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422
|
||||
rsa==4.2 ; python_version >= "3.13" \
|
||||
--hash=sha256:aaefa4b84752e3e99bd8333a2e1e3e7a7da64614042bd66f775573424370108a
|
||||
rsa==4.9.1 ; python_version >= "3.9" and python_version < "3.13" \
|
||||
--hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \
|
||||
--hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75
|
||||
schema==0.7.7 ; python_version >= "3.9" \
|
||||
--hash=sha256:5d976a5b50f36e74e2157b47097b60002bd4d42e65425fcc9c9befadb4255dde \
|
||||
--hash=sha256:7da553abd2958a19dc2547c388cde53398b39196175a9be59ea1caf5ab0a1807
|
||||
tzdata==2025.2 ; python_version >= "3.9" and platform_system == "Windows" \
|
||||
--hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \
|
||||
--hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9
|
||||
tzlocal==5.3.1 ; python_version >= "3.9" \
|
||||
--hash=sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd \
|
||||
--hash=sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d
|
||||
unidecode==1.4.0 ; python_version >= "3.9" \
|
||||
--hash=sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021 \
|
||||
--hash=sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23
|
||||
uritemplate==4.2.0 ; python_version >= "3.9" \
|
||||
--hash=sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e \
|
||||
--hash=sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686
|
||||
urllib3==2.5.0 ; python_version >= "3.9" \
|
||||
--hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \
|
||||
--hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue