mirror of
https://git.lecygnenoir.info/LecygneNoir/prismedia.git
synced 2025-10-04 01:49:15 +02:00
Compare commits
55 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 | ||
![]() |
5c991581e8 | ||
![]() |
2f8543b43c |
12 changed files with 1068 additions and 775 deletions
31
CHANGELOG.md
31
CHANGELOG.md
|
@ -1,5 +1,36 @@
|
||||||
# Changelog
|
# 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
|
## v0.10.3
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|
159
README.md
159
README.md
|
@ -24,55 +24,57 @@ Scripting your way to upload videos to peertube and youtube. Works with Python 3
|
||||||
### From pip
|
### From pip
|
||||||
|
|
||||||
Simply install with
|
Simply install with
|
||||||
|
```sh
|
||||||
```bash
|
|
||||||
pip install prismedia
|
pip install prismedia
|
||||||
```
|
```
|
||||||
|
|
||||||
Upgrade with
|
Upgrade with
|
||||||
|
```sh
|
||||||
```bash
|
|
||||||
pip install --upgrade prismedia
|
pip install --upgrade prismedia
|
||||||
```
|
```
|
||||||
|
|
||||||
### From source
|
### From source
|
||||||
|
|
||||||
Get the source:
|
Get the source:
|
||||||
|
```sh
|
||||||
```bash
|
|
||||||
git clone https://git.lecygnenoir.info/LecygneNoir/prismedia.git prismedia
|
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.
|
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 > 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)
|
(Or use the existing virtualenv if one is activated)
|
||||||
|
|
||||||
```
|
```sh
|
||||||
poetry install
|
poetry install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Generate sample files with `python -m prismedia.genconfig`.
|
Generate configuration files by running `prismedia-init`.
|
||||||
Then rename and edit `peertube_secret` and `youtube_secret.json` with your credentials. (see below)
|
|
||||||
|
Then, edit them to fill your credential as explained below.
|
||||||
|
|
||||||
### Peertube
|
### Peertube
|
||||||
Set your credentials, peertube server URL.
|
Configuration is in **peertube_secret** file.
|
||||||
You can get client_id and client_secret by logging in your peertube website and reaching the URL:
|
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
|
https://domain.example/api/v1/oauth-clients/local
|
||||||
You can set ``OAUTHLIB_INSECURE_TRANSPORT`` to 1 if you do not use https (not recommended)
|
|
||||||
|
*Alternatively, you can set ``OAUTHLIB_INSECURE_TRANSPORT`` to 1 if you do not use https (not recommended)*
|
||||||
|
|
||||||
### Youtube
|
### Youtube
|
||||||
|
Configuration is in **youtube_secret.json** file.
|
||||||
Youtube uses combination of oauth and API access to identify.
|
Youtube uses combination of oauth and API access to identify.
|
||||||
|
|
||||||
**Credentials**
|
**Credentials**
|
||||||
The first time you connect, prismedia will open your browser to ask you to authenticate to
|
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.
|
Youtube and allow the app to use your Youtube channel.
|
||||||
**It is here you choose which channel you will upload to**.
|
**It is here you choose which channel you will upload to**.
|
||||||
Once authenticated, the token is stored inside the file ``.youtube_credentials.json``.
|
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.
|
Prismedia will try to use this file at each launch, and re-ask for authentication if it does not exist.
|
||||||
|
|
||||||
**Oauth**:
|
**Oauth**:
|
||||||
|
@ -81,10 +83,12 @@ If you plan a larger usage, please consider creating your own youtube_secret fil
|
||||||
|
|
||||||
- Go to the [Google console](https://console.developers.google.com/).
|
- Go to the [Google console](https://console.developers.google.com/).
|
||||||
- Create project.
|
- Create project.
|
||||||
- Side menu: APIs & auth -> APIs
|
- Side menu: APIs & Services -> APIs
|
||||||
- Top menu: Enabled API(s): Enable all Youtube APIs.
|
- Top menu: Enabled API(s): Enable Youtube Data v3 APIs.
|
||||||
- Side menu: APIs & auth -> Credentials.
|
- Side menu: OAuth consent screen
|
||||||
- Create a Client ID: Add credentials -> OAuth 2.0 Client ID -> Other -> Name: prismedia1 -> Create -> OK
|
- 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.
|
- 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.
|
- Save this JSON as your youtube_secret.json file.
|
||||||
|
|
||||||
|
@ -92,129 +96,56 @@ If you plan a larger usage, please consider creating your own youtube_secret fil
|
||||||
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))
|
**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"
|
prismedia --file="yourvideo.mp4"
|
||||||
```
|
```
|
||||||
|
|
||||||
Specify description and tags:
|
Specify description and tags:
|
||||||
|
```sh
|
||||||
```
|
|
||||||
prismedia --file="yourvideo.mp4" -d "My supa description" -t "tag1,tag2,foo"
|
prismedia --file="yourvideo.mp4" -d "My supa description" -t "tag1,tag2,foo"
|
||||||
```
|
```
|
||||||
|
|
||||||
Provide a thumbnail:
|
Provide a thumbnail:
|
||||||
|
```sh
|
||||||
```
|
|
||||||
prismedia --file="yourvideo.mp4" -d "Video with thumbnail" --thumbnail="/path/to/your/thumbnail.jpg"
|
prismedia --file="yourvideo.mp4" -d "Video with thumbnail" --thumbnail="/path/to/your/thumbnail.jpg"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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:
|
Use a NFO file to specify your video options:
|
||||||
(See [Enhanced NFO](#enhanced-use-of-nfo) for more precise example)
|
(See [Enhanced NFO](#enhanced-use-of-nfo) for more precise example)
|
||||||
```
|
```sh
|
||||||
prismedia --file="yourvideo.mp4" --nfo /path/to/your/nfo.txt
|
prismedia --file="yourvideo.mp4" --nfo /path/to/your/nfo.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
Use --help to get all available options:
|
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
|
## Enhanced use of NFO
|
||||||
Since Prismedia v0.9.0, the NFO system has been improved to allow hierarchical loading.
|
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`
|
`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:
|
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/
|
$ tree Recipes/
|
||||||
Recipes/
|
Recipes/
|
||||||
├── cli_nfo.txt
|
├── cli_nfo.txt
|
||||||
|
@ -228,7 +159,7 @@ Recipes/
|
||||||
```
|
```
|
||||||
|
|
||||||
By using
|
By using
|
||||||
```
|
```sh
|
||||||
prismedia --file=/path/to/Recipes/yourvideo1.mp4 --nfo=/path/to/Recipes/cli_nfo.txt --cca
|
prismedia --file=/path/to/Recipes/yourvideo1.mp4 --nfo=/path/to/Recipes/cli_nfo.txt --cca
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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
|
- If you use peertube before 1.0.0-beta4, use the version inside tag 1.0.0-beta3
|
||||||
|
|
||||||
## Inspirations
|
## 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
|
## 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
|
from future import standard_library
|
||||||
standard_library.install_aliases()
|
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 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 os import listdir
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger('Prismedia')
|
||||||
|
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
|
||||||
def genconfig():
|
def genconfig():
|
||||||
|
@ -8,7 +12,12 @@ def genconfig():
|
||||||
files = [f for f in listdir(path) if isfile(join(path, f))]
|
files = [f for f in listdir(path) if isfile(join(path, f))]
|
||||||
|
|
||||||
for f in files:
|
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__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -14,7 +14,8 @@ from tzlocal import get_localzone
|
||||||
from configparser import RawConfigParser
|
from configparser import RawConfigParser
|
||||||
from requests_oauthlib import OAuth2Session
|
from requests_oauthlib import OAuth2Session
|
||||||
from oauthlib.oauth2 import LegacyApplicationClient
|
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
|
from . import utils
|
||||||
logger = logging.getLogger('Prismedia')
|
logger = logging.getLogger('Prismedia')
|
||||||
|
@ -63,6 +64,13 @@ def get_channel_by_name(user_info, options):
|
||||||
return channel['id']
|
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):
|
def create_channel(oauth, url, options):
|
||||||
template = ('Peertube: Channel %s does not exist, creating it.')
|
template = ('Peertube: Channel %s does not exist, creating it.')
|
||||||
logger.info(template % (str(options.get('--channel'))))
|
logger.info(template % (str(options.get('--channel'))))
|
||||||
|
@ -255,16 +263,18 @@ def upload_video(oauth, secret, options):
|
||||||
publishAt = options.get('--publishAt')
|
publishAt = options.get('--publishAt')
|
||||||
|
|
||||||
if 'publishAt' in locals():
|
if 'publishAt' in locals():
|
||||||
publishAt = datetime.datetime.strptime(publishAt, '%Y-%m-%dT%H:%M:%S')
|
publishAt = convert_peertube_date(publishAt)
|
||||||
tz = get_localzone()
|
|
||||||
tz = pytz.timezone(str(tz))
|
|
||||||
publishAt = tz.localize(publishAt).isoformat()
|
|
||||||
fields.append(("scheduleUpdate[updateAt]", publishAt))
|
fields.append(("scheduleUpdate[updateAt]", publishAt))
|
||||||
fields.append(("scheduleUpdate[privacy]", str(PEERTUBE_PRIVACY["public"])))
|
fields.append(("scheduleUpdate[privacy]", str(PEERTUBE_PRIVACY["public"])))
|
||||||
fields.append(("privacy", str(PEERTUBE_PRIVACY["private"])))
|
fields.append(("privacy", str(PEERTUBE_PRIVACY["private"])))
|
||||||
else:
|
else:
|
||||||
fields.append(("privacy", str(PEERTUBE_PRIVACY[privacy or "private"])))
|
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'):
|
if options.get('--thumbnail'):
|
||||||
fields.append(("thumbnailfile", get_file(options.get('--thumbnail'))))
|
fields.append(("thumbnailfile", get_file(options.get('--thumbnail'))))
|
||||||
fields.append(("previewfile", 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'):
|
if options.get('--url-only') or options.get('--batch'):
|
||||||
logger_stdout = logging.getLogger('stdoutlogs')
|
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 = {
|
headers = {
|
||||||
'Content-Type': multipart_data.content_type
|
'Content-Type': multipart_data.content_type
|
||||||
|
@ -302,12 +317,14 @@ def upload_video(oauth, secret, options):
|
||||||
response = oauth.post(url + "/api/v1/videos/upload",
|
response = oauth.post(url + "/api/v1/videos/upload",
|
||||||
data=multipart_data,
|
data=multipart_data,
|
||||||
headers=headers)
|
headers=headers)
|
||||||
|
|
||||||
if response is not None:
|
if response is not None:
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
jresponse = response.json()
|
jresponse = response.json()
|
||||||
jresponse = jresponse['video']
|
jresponse = jresponse['video']
|
||||||
uuid = jresponse['uuid']
|
uuid = jresponse['uuid']
|
||||||
video_id = str(jresponse['id'])
|
video_id = str(jresponse['id'])
|
||||||
|
|
||||||
logger.info('Peertube: Video was successfully uploaded.')
|
logger.info('Peertube: Video was successfully uploaded.')
|
||||||
template = 'Peertube: Watch it at %s/videos/watch/%s.'
|
template = 'Peertube: Watch it at %s/videos/watch/%s.'
|
||||||
logger.info(template % (url, uuid))
|
logger.info(template % (url, uuid))
|
||||||
|
@ -325,9 +342,48 @@ def upload_video(oauth, secret, options):
|
||||||
exit(1)
|
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):
|
def run(options):
|
||||||
secret = RawConfigParser()
|
secret = RawConfigParser()
|
||||||
try:
|
try:
|
||||||
|
if options.get('--credentialsdir') :
|
||||||
|
secret.read(os.path.join(options.get('--credentialsdir'), PEERTUBE_SECRETS_FILE))
|
||||||
|
else :
|
||||||
secret.read(PEERTUBE_SECRETS_FILE)
|
secret.read(PEERTUBE_SECRETS_FILE)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical("Peertube: Error loading " + str(PEERTUBE_SECRETS_FILE) + ": " + str(e))
|
logger.critical("Peertube: Error loading " + str(PEERTUBE_SECRETS_FILE) + ": " + str(e))
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
# Some generic options for your videos
|
# Some generic options for your videos
|
||||||
cca = True
|
cca = True
|
||||||
privacy = private
|
privacy = private
|
||||||
disable-comments = True
|
disable-comments = False
|
||||||
channel = DefaultChannel
|
channel = DefaultChannel
|
||||||
channelCreate = True
|
channelCreate = True
|
||||||
|
auto-originalDate = True
|
|
@ -7,11 +7,12 @@ prismedia - tool to upload videos to Peertube and Youtube
|
||||||
Usage:
|
Usage:
|
||||||
prismedia --file=<FILE> [options]
|
prismedia --file=<FILE> [options]
|
||||||
prismedia -f <FILE> --tags=STRING [options]
|
prismedia -f <FILE> --tags=STRING [options]
|
||||||
|
prismedia --hearthbeat
|
||||||
prismedia -h | --help
|
prismedia -h | --help
|
||||||
prismedia --version
|
prismedia --version
|
||||||
|
|
||||||
Options:
|
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)
|
--name=NAME Name of the video to upload. (default to video filename)
|
||||||
-d, --description=STRING Description of the video. (default: default description)
|
-d, --description=STRING Description of the video. (default: default description)
|
||||||
-t, --tags=STRING Tags for the video. comma separated.
|
-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
|
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)
|
decode the file as UTF-8 (so make sure your nfo file is UTF-8 encoded)
|
||||||
See nfo_example.txt for more details
|
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.
|
--platform=STRING List of platform(s) to upload to, comma separated.
|
||||||
Supported platforms are youtube and peertube (default is both)
|
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)
|
--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
|
DATE should be in the future
|
||||||
--peertubeAt=DATE
|
--peertubeAt=DATE
|
||||||
--youtubeAt=DATE Override publishAt for the corresponding platform. Allow to create preview on specific platform
|
--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.
|
--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, .jpeg or .png
|
||||||
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)
|
--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.
|
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)
|
--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.
|
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)
|
--playlistCreate Create the playlist if not exists. (default do not create)
|
||||||
Only relevant if --playlist is set.
|
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.
|
-h --help Show this help.
|
||||||
--version Show version.
|
--version Show version.
|
||||||
|
|
||||||
|
@ -54,7 +63,6 @@ Logging options
|
||||||
-u --url-only Display generated URL after upload directly on stdout, implies --quiet
|
-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
|
--batch Display generated URL after upload with platform information for easier parsing. Implies --quiet
|
||||||
Be careful --batch and --url-only are mutually exclusives.
|
Be careful --batch and --url-only are mutually exclusives.
|
||||||
--debug (Deprecated) Alias for --log=debug. Ignored if --log is set
|
|
||||||
|
|
||||||
Strict options:
|
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
|
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
|
--withTags Prevent the upload without tags
|
||||||
--withPlaylist Prevent the upload if no playlist
|
--withPlaylist Prevent the upload if no playlist
|
||||||
--withPublishAt Prevent the upload if no schedule
|
--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
|
--withPlatform Prevent the upload if at least one platform is not specified
|
||||||
--withCategory Prevent the upload if no category
|
--withCategory Prevent the upload if no category
|
||||||
--withLanguage Prevent upload if no language
|
--withLanguage Prevent upload if no language
|
||||||
|
@ -100,12 +109,6 @@ import os
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger('Prismedia')
|
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
|
from docopt import docopt
|
||||||
|
|
||||||
|
@ -121,16 +124,8 @@ except ImportError:
|
||||||
' is installed: \n'
|
' is installed: \n'
|
||||||
'see https://github.com/halst/schema\n')
|
'see https://github.com/halst/schema\n')
|
||||||
exit(1)
|
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.3"
|
VERSION = "prismedia v0.12.2"
|
||||||
|
|
||||||
VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted')
|
VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted')
|
||||||
VALID_CATEGORIES = (
|
VALID_CATEGORIES = (
|
||||||
|
@ -145,20 +140,7 @@ VALID_LANGUAGES = ('arabic', 'english', 'french',
|
||||||
'german', 'hindi', 'italian',
|
'german', 'hindi', 'italian',
|
||||||
'japanese', 'korean', 'mandarin',
|
'japanese', 'korean', 'mandarin',
|
||||||
'portuguese', 'punjabi', 'russian', 'spanish')
|
'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):
|
def validateCategory(category):
|
||||||
|
@ -190,11 +172,11 @@ def validateLanguage(language):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def validatePublish(publish):
|
def validatePublishDate(publishDate):
|
||||||
# Check date format and if date is future
|
# Check date format and if date is future
|
||||||
try:
|
try:
|
||||||
now = datetime.datetime.now()
|
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:
|
if now >= publishAt:
|
||||||
return False
|
return False
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -202,13 +184,16 @@ def validatePublish(publish):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def validateThumbnail(thumbnail):
|
def validateOriginalDate(originalDate):
|
||||||
supported_types = ['image/jpg', 'image/jpeg']
|
# Check date format and if date is past
|
||||||
if os.path.exists(thumbnail) and \
|
try:
|
||||||
magic.from_file(thumbnail, mime=True) in supported_types:
|
now = datetime.datetime.now()
|
||||||
return thumbnail
|
originalDate = datetime.datetime.strptime(originalDate, '%Y-%m-%dT%H:%M:%S')
|
||||||
else:
|
if now <= originalDate:
|
||||||
return False
|
return False
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def validateLogLevel(loglevel):
|
def validateLogLevel(loglevel):
|
||||||
|
@ -217,6 +202,15 @@ def validateLogLevel(loglevel):
|
||||||
return False
|
return False
|
||||||
return True
|
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):
|
def _optionnalOrStrict(key, scope, error):
|
||||||
option = key.replace('-', '')
|
option = key.replace('-', '')
|
||||||
option = option[0].upper() + option[1:]
|
option = option[0].upper() + option[1:]
|
||||||
|
@ -234,19 +228,20 @@ def configureLogs(options):
|
||||||
if options.get('--batch') or options.get('--url-only'):
|
if options.get('--batch') or options.get('--url-only'):
|
||||||
options['--quiet'] = True
|
options['--quiet'] = True
|
||||||
|
|
||||||
|
for handler in logger.handlers or logger.parent.handlers:
|
||||||
if options.get('--quiet'):
|
if options.get('--quiet'):
|
||||||
# We need to set both log level in the same time
|
# We need to set both log level in the same time
|
||||||
logger.setLevel(50)
|
logger.setLevel(50)
|
||||||
ch.setLevel(50)
|
handler.setLevel(50)
|
||||||
elif options.get('--log'):
|
elif options.get('--log'):
|
||||||
numeric_level = getattr(logging, options["--log"], None)
|
numeric_level = getattr(logging, options["--log"], None)
|
||||||
# We need to set both log level in the same time
|
# We need to set both log level in the same time
|
||||||
logger.setLevel(numeric_level)
|
logger.setLevel(numeric_level)
|
||||||
ch.setLevel(numeric_level)
|
handler.setLevel(numeric_level)
|
||||||
elif options.get('--debug'):
|
elif options.get('--debug'):
|
||||||
logger.warning("DEPRECATION: --debug is deprecated, please use --log=debug instead")
|
# Deprecated,
|
||||||
logger.setLevel(10)
|
logger.setLevel(10)
|
||||||
ch.setLevel(10)
|
handler.setLevel(10)
|
||||||
|
|
||||||
|
|
||||||
def configureStdoutLogs():
|
def configureStdoutLogs():
|
||||||
|
@ -280,16 +275,18 @@ def main():
|
||||||
Optional('--withTags', default=False): bool,
|
Optional('--withTags', default=False): bool,
|
||||||
Optional('--withPlaylist', default=False): bool,
|
Optional('--withPlaylist', default=False): bool,
|
||||||
Optional('--withPublishAt', default=False): bool,
|
Optional('--withPublishAt', default=False): bool,
|
||||||
|
Optional('--withOriginalDate', default=False): bool,
|
||||||
Optional('--withPlatform', default=False): bool,
|
Optional('--withPlatform', default=False): bool,
|
||||||
Optional('--withCategory', default=False): bool,
|
Optional('--withCategory', default=False): bool,
|
||||||
Optional('--withLanguage', default=False): bool,
|
Optional('--withLanguage', default=False): bool,
|
||||||
Optional('--withChannel', 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
|
# This allow to return all other options for further use: https://github.com/keleshev/schema#extra-keys
|
||||||
object: object
|
object: object
|
||||||
})
|
})
|
||||||
|
|
||||||
schema = Schema({
|
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 #
|
# Strict option checks - at the moment Schema needs to check Hook and Optional separately #
|
||||||
Hook('--name', handler=_optionnalOrStrict): object,
|
Hook('--name', handler=_optionnalOrStrict): object,
|
||||||
Hook('--description', handler=_optionnalOrStrict): object,
|
Hook('--description', handler=_optionnalOrStrict): object,
|
||||||
|
@ -298,6 +295,7 @@ def main():
|
||||||
Hook('--language', handler=_optionnalOrStrict): object,
|
Hook('--language', handler=_optionnalOrStrict): object,
|
||||||
Hook('--platform', handler=_optionnalOrStrict): object,
|
Hook('--platform', handler=_optionnalOrStrict): object,
|
||||||
Hook('--publishAt', handler=_optionnalOrStrict): object,
|
Hook('--publishAt', handler=_optionnalOrStrict): object,
|
||||||
|
Hook('--originalDate', handler=_optionnalOrStrict): object,
|
||||||
Hook('--thumbnail', handler=_optionnalOrStrict): object,
|
Hook('--thumbnail', handler=_optionnalOrStrict): object,
|
||||||
Hook('--channel', handler=_optionnalOrStrict): object,
|
Hook('--channel', handler=_optionnalOrStrict): object,
|
||||||
Hook('--playlist', 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('--platform'): Or(None, And(str, validatePlatform, error="Sorry, upload platform not supported")),
|
||||||
Optional('--publishAt'): Or(None, And(
|
Optional('--publishAt'): Or(None, And(
|
||||||
str,
|
str,
|
||||||
validatePublish,
|
validatePublishDate,
|
||||||
error="DATE should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
|
error="Publish Date should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
|
||||||
),
|
),
|
||||||
Optional('--peertubeAt'): Or(None, And(
|
Optional('--peertubeAt'): Or(None, And(
|
||||||
str,
|
str,
|
||||||
validatePublish,
|
validatePublishDate,
|
||||||
error="DATE should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
|
error="Publish Date should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
|
||||||
),
|
),
|
||||||
Optional('--youtubeAt'): Or(None, And(
|
Optional('--youtubeAt'): Or(None, And(
|
||||||
str,
|
str,
|
||||||
validatePublish,
|
validatePublishDate,
|
||||||
error="DATE should be the form YYYY-MM-DDThh:mm:ss and has to be in the future")
|
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('--cca'): bool,
|
||||||
Optional('--disable-comments'): bool,
|
Optional('--disable-comments'): bool,
|
||||||
Optional('--nsfw'): bool,
|
Optional('--nsfw'): bool,
|
||||||
Optional('--thumbnail'): Or(None, And(
|
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('--channel'): Or(None, str),
|
||||||
Optional('--channelCreate'): bool,
|
Optional('--channelCreate'): bool,
|
||||||
Optional('--playlist'): Or(None, str),
|
Optional('--playlist'): Or(None, str),
|
||||||
Optional('--playlistCreate'): bool,
|
Optional('--playlistCreate'): bool,
|
||||||
|
Optional('--progress'): Or(None, And(str, validateProgress, error="Sorry, progress visualisation not supported")),
|
||||||
|
'--hearthbeat': bool,
|
||||||
'--help': bool,
|
'--help': bool,
|
||||||
'--version': bool,
|
'--version': bool,
|
||||||
# This allow to return all other options for further use: https://github.com/keleshev/schema#extra-keys
|
# This allow to return all other options for further use: https://github.com/keleshev/schema#extra-keys
|
||||||
object: object
|
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
|
# We need to validate early options first as withNFO and logs options should be prioritized
|
||||||
try:
|
try:
|
||||||
options = earlyoptionSchema.validate(options)
|
options = earlyoptionSchema.validate(options)
|
||||||
|
@ -377,6 +388,12 @@ def main():
|
||||||
|
|
||||||
options = utils.parseNFO(options)
|
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
|
# Once NFO are loaded, we need to revalidate strict options in case some were in NFO
|
||||||
try:
|
try:
|
||||||
options = earlyoptionSchema.validate(options)
|
options = earlyoptionSchema.validate(options)
|
||||||
|
|
|
@ -2,12 +2,11 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
from configparser import RawConfigParser, NoOptionError, NoSectionError
|
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
|
import re
|
||||||
from os import devnull
|
|
||||||
from subprocess import check_call, CalledProcessError, STDOUT
|
|
||||||
import unidecode
|
import unidecode
|
||||||
import logging
|
import logging
|
||||||
|
import datetime
|
||||||
|
|
||||||
logger = logging.getLogger('Prismedia')
|
logger = logging.getLogger('Prismedia')
|
||||||
|
|
||||||
|
@ -101,6 +100,15 @@ def getLanguage(language, platform):
|
||||||
return PEERTUBE_LANGUAGE[language.lower()]
|
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):
|
def remove_empty_kwargs(**kwargs):
|
||||||
good_kwargs = {}
|
good_kwargs = {}
|
||||||
if kwargs is not None:
|
if kwargs is not None:
|
||||||
|
@ -118,6 +126,8 @@ def searchThumbnail(options):
|
||||||
options['--thumbnail'] = video_directory + options.get('--name') + ".jpg"
|
options['--thumbnail'] = video_directory + options.get('--name') + ".jpg"
|
||||||
elif isfile(video_directory + options.get('--name') + ".jpeg"):
|
elif isfile(video_directory + options.get('--name') + ".jpeg"):
|
||||||
options['--thumbnail'] = 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
|
# Then, if we still not have thumbnail, check for thumbnail based on videofile name
|
||||||
if not options.get('--thumbnail'):
|
if not options.get('--thumbnail'):
|
||||||
video_file = splitext(basename(options.get('--file')))[0]
|
video_file = splitext(basename(options.get('--file')))[0]
|
||||||
|
@ -125,6 +135,8 @@ def searchThumbnail(options):
|
||||||
options['--thumbnail'] = video_directory + video_file + ".jpg"
|
options['--thumbnail'] = video_directory + video_file + ".jpg"
|
||||||
elif isfile(video_directory + video_file + ".jpeg"):
|
elif isfile(video_directory + video_file + ".jpeg"):
|
||||||
options['--thumbnail'] = 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
|
# Display some info after research
|
||||||
if not options.get('--thumbnail'):
|
if not options.get('--thumbnail'):
|
||||||
|
@ -135,6 +147,11 @@ def searchThumbnail(options):
|
||||||
return 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
|
# return the nfo as a RawConfigParser object
|
||||||
def loadNFO(filename):
|
def loadNFO(filename):
|
||||||
try:
|
try:
|
||||||
|
@ -196,7 +213,7 @@ def parseNFO(options):
|
||||||
if nfo:
|
if nfo:
|
||||||
# We need to check all options and replace it with the nfo value if not defined (None or False)
|
# 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():
|
for key, value in options.items():
|
||||||
key = key.replace("-", "")
|
key = key.replace("--", "")
|
||||||
try:
|
try:
|
||||||
# get string options
|
# get string options
|
||||||
if value is None and nfo.get('video', key):
|
if value is None and nfo.get('video', key):
|
||||||
|
|
|
@ -48,8 +48,8 @@ RETRIABLE_EXCEPTIONS = (
|
||||||
RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
|
RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
|
||||||
|
|
||||||
|
|
||||||
CLIENT_SECRETS_FILE = 'youtube_secret.json'
|
CLIENT_SECRETS_FILE_BASE = 'youtube_secret.json'
|
||||||
CREDENTIALS_PATH = ".youtube_credentials.json"
|
CREDENTIALS_PATH_BASE = ".youtube_credentials.json"
|
||||||
SCOPES = ['https://www.googleapis.com/auth/youtube.upload', 'https://www.googleapis.com/auth/youtube.force-ssl']
|
SCOPES = ['https://www.googleapis.com/auth/youtube.upload', 'https://www.googleapis.com/auth/youtube.force-ssl']
|
||||||
API_SERVICE_NAME = 'youtube'
|
API_SERVICE_NAME = 'youtube'
|
||||||
API_VERSION = 'v3'
|
API_VERSION = 'v3'
|
||||||
|
@ -60,6 +60,7 @@ def get_authenticated_service():
|
||||||
check_authenticated_scopes()
|
check_authenticated_scopes()
|
||||||
flow = InstalledAppFlow.from_client_secrets_file(
|
flow = InstalledAppFlow.from_client_secrets_file(
|
||||||
CLIENT_SECRETS_FILE, SCOPES)
|
CLIENT_SECRETS_FILE, SCOPES)
|
||||||
|
|
||||||
if exists(CREDENTIALS_PATH):
|
if exists(CREDENTIALS_PATH):
|
||||||
with open(CREDENTIALS_PATH, 'r') as f:
|
with open(CREDENTIALS_PATH, 'r') as f:
|
||||||
credential_params = json.load(f)
|
credential_params = json.load(f)
|
||||||
|
@ -89,6 +90,15 @@ def check_authenticated_scopes():
|
||||||
os.remove(CREDENTIALS_PATH)
|
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):
|
def initialize_upload(youtube, options):
|
||||||
path = options.get('--file')
|
path = options.get('--file')
|
||||||
tags = None
|
tags = None
|
||||||
|
@ -107,6 +117,8 @@ def initialize_upload(youtube, options):
|
||||||
if options.get('--cca'):
|
if options.get('--cca'):
|
||||||
license = "creativeCommon"
|
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 = {
|
body = {
|
||||||
"snippet": {
|
"snippet": {
|
||||||
"title": options.get('--name') or splitext(basename(path))[0],
|
"title": options.get('--name') or splitext(basename(path))[0],
|
||||||
|
@ -119,6 +131,9 @@ def initialize_upload(youtube, options):
|
||||||
"status": {
|
"status": {
|
||||||
"privacyStatus": str(options.get('--privacy') or "private"),
|
"privacyStatus": str(options.get('--privacy') or "private"),
|
||||||
"license": str(license or "youtube"),
|
"license": str(license or "youtube"),
|
||||||
|
},
|
||||||
|
"recordingDetails": {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,15 +143,16 @@ def initialize_upload(youtube, options):
|
||||||
elif options.get('--publishAt'):
|
elif options.get('--publishAt'):
|
||||||
publishAt = options.get('--publishAt')
|
publishAt = options.get('--publishAt')
|
||||||
|
|
||||||
|
# Check if publishAt variable exists in local variables
|
||||||
if 'publishAt' in locals():
|
if 'publishAt' in locals():
|
||||||
# Youtube needs microsecond and the local timezone from ISO 8601
|
publishAt = convert_youtube_date(publishAt)
|
||||||
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()
|
|
||||||
body['status']['publishAt'] = str(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'):
|
if options.get('--playlist'):
|
||||||
playlist_id = get_playlist_by_name(youtube, options.get('--playlist'))
|
playlist_id = get_playlist_by_name(youtube, options.get('--playlist'))
|
||||||
if not playlist_id and options.get('--playlistCreate'):
|
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):
|
def get_playlist_by_name(youtube, playlist_name):
|
||||||
|
pageToken = ""
|
||||||
|
while pageToken != None:
|
||||||
response = youtube.playlists().list(
|
response = youtube.playlists().list(
|
||||||
part='snippet,id',
|
part='snippet,id',
|
||||||
mine=True,
|
mine=True,
|
||||||
maxResults=50
|
maxResults=50,
|
||||||
|
pageToken=pageToken
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
for playlist in response["items"]:
|
for playlist in response["items"]:
|
||||||
if playlist["snippet"]['title'] == playlist_name:
|
if playlist["snippet"]["title"] == playlist_name:
|
||||||
return playlist['id']
|
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):
|
def create_playlist(youtube, playlist_name):
|
||||||
|
@ -316,7 +342,29 @@ def resumable_upload(request, resource, method, options):
|
||||||
time.sleep(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):
|
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()
|
youtube = get_authenticated_service()
|
||||||
try:
|
try:
|
||||||
initialize_upload(youtube, options)
|
initialize_upload(youtube, options)
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "prismedia"
|
name = "prismedia"
|
||||||
version = "0.10.3"
|
version = "0.13.0"
|
||||||
description = "scripting your way to upload videos on peertube and youtube"
|
description = "scripting your way to upload videos on peertube and youtube"
|
||||||
authors = [
|
authors = [
|
||||||
"LecygneNoir <git@lecygnenoir.info>",
|
"LecygneNoir <git@lecygnenoir.info>",
|
||||||
"Rigel Kent <sendmemail@rigelk.eu>",
|
"Rigel Kent <sendmemail@rigelk.eu>",
|
||||||
"Zykino"
|
"Zykino",
|
||||||
|
"YSalmon"
|
||||||
]
|
]
|
||||||
|
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
@ -17,33 +18,31 @@ homepage = "https://git.lecygnenoir.info/LecygneNoir/prismedia"
|
||||||
keywords = ['peertube', 'youtube', 'prismedia']
|
keywords = ['peertube', 'youtube', 'prismedia']
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.5"
|
python = ">=3.9"
|
||||||
configparser = "^3.7.1"
|
clint = ">=0.5.1"
|
||||||
docopt = "^0.6.2"
|
configparser = ">=3.7.1"
|
||||||
future = "^0.17.1"
|
docopt = ">=0.6.2"
|
||||||
|
future = ">=0.17.1"
|
||||||
google-api-python-client = ">=1.7.6"
|
google-api-python-client = ">=1.7.6"
|
||||||
google-auth = ">=1.6.1"
|
google-auth = ">=1.6.1"
|
||||||
google-auth-httplib2 = ">=0.0.3"
|
google-auth-httplib2 = ">=0.0.3"
|
||||||
google-auth-oauthlib = ">=0.2.0"
|
google-auth-oauthlib = ">=0.2.0"
|
||||||
httplib2 = "^0.12.1"
|
httplib2 = ">=0.12.1"
|
||||||
oauthlib = "^2.1.0"
|
oauthlib = "=2.1.0"
|
||||||
python-magic = "^0.4.15"
|
requests = ">=2.18.4"
|
||||||
python-magic-bin = { version = "^0.4.14", markers = "platform_system == 'Windows'" }
|
requests-oauthlib = "=1.1.0"
|
||||||
requests = "^2.18.4"
|
requests-toolbelt = ">=0.9.1"
|
||||||
requests-oauthlib = "^0.8.0"
|
pytz = "=2022.1"
|
||||||
requests-toolbelt = "^0.9.1"
|
|
||||||
schema = ">=0.7.1"
|
schema = ">=0.7.1"
|
||||||
tzlocal = "^1.5.1"
|
tzlocal = ">=1.5.1"
|
||||||
Unidecode = "^1.0.23"
|
Unidecode = ">=1.0.23"
|
||||||
uritemplate = "^3.0.0"
|
uritemplate = ">=3.0.0"
|
||||||
urllib3 = "^1.22"
|
urllib3 = ">=1.22"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
prismedia = 'prismedia.upload:main'
|
prismedia = 'prismedia.upload:main'
|
||||||
|
prismedia-init = 'prismedia.genconfig:genconfig'
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry>=0.12"]
|
requires = ["poetry>=0.12"]
|
||||||
build-backend = "poetry.masonry.api"
|
build-backend = "poetry.masonry.api"
|
||||||
|
|
||||||
|
|
322
requirements.txt
322
requirements.txt
|
@ -1,129 +1,195 @@
|
||||||
cachetools==3.1.1 \
|
args==0.1.0 ; python_version >= "3.9" \
|
||||||
--hash=sha256:428266a1c0d36dc5aca63a2d7c5942e88c2c898d72139fca0e97fdd2380517ae \
|
--hash=sha256:a785b8d837625e9b61c39108532d95b85274acd679693b71ebb5156848fcf814
|
||||||
--hash=sha256:8ea2d3ce97850f31e4a08b0e2b5e6c34997d7216a9d2c98e0f3978630d4da69a
|
cachetools==5.5.2 ; python_version >= "3.9" \
|
||||||
certifi==2020.4.5.1 \
|
--hash=sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4 \
|
||||||
--hash=sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304 \
|
--hash=sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a
|
||||||
--hash=sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519
|
certifi==2025.6.15 ; python_version >= "3.9" \
|
||||||
chardet==3.0.4 \
|
--hash=sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057 \
|
||||||
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 \
|
--hash=sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b
|
||||||
--hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae
|
charset-normalizer==3.4.2 ; python_version >= "3.9" \
|
||||||
configparser==3.8.1 \
|
--hash=sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4 \
|
||||||
--hash=sha256:45d1272aad6cfd7a8a06cf5c73f2ceb6a190f6acc1fa707e7f82a4c053b28b18 \
|
--hash=sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45 \
|
||||||
--hash=sha256:bc37850f0cc42a1725a796ef7d92690651bf1af37d744cc63161dac62cabee17
|
--hash=sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7 \
|
||||||
docopt==0.6.2 \
|
--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
|
--hash=sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491
|
||||||
future==0.17.1 \
|
future==1.0.0 ; python_version >= "3.9" \
|
||||||
--hash=sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8
|
--hash=sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216 \
|
||||||
google-api-core==1.16.0 \
|
--hash=sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05
|
||||||
--hash=sha256:92e962a087f1c4b8d1c5c88ade1c1dfd550047dcffb320c57ef6a534a20403e2 \
|
google-api-core==2.25.1 ; python_version >= "3.9" \
|
||||||
--hash=sha256:859f7392676761f2b160c6ee030c3422135ada4458f0948c5690a6a7c8d86294
|
--hash=sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7 \
|
||||||
google-api-python-client==1.8.0 \
|
--hash=sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8
|
||||||
--hash=sha256:0f5b42a14e2d2f7dee40f2e4514531dbe95ebde9c2173b1c4040a65c427e7900 \
|
google-api-python-client==2.174.0 ; python_version >= "3.9" \
|
||||||
--hash=sha256:5032ad1af5046889649b3848f2e871889fbb6ae440198a549fe1699581300386
|
--hash=sha256:9eb7616a820b38a9c12c5486f9b9055385c7feb18b20cbafc5c5a688b14f3515 \
|
||||||
google-auth==1.13.1 \
|
--hash=sha256:f695205ceec97bfaa1590a14282559c4109326c473b07352233a3584cdbf4b89
|
||||||
--hash=sha256:a5ee4c40fef77ea756cf2f1c0adcf475ecb53af6700cf9c133354cdc9b267148 \
|
google-auth-httplib2==0.2.0 ; python_version >= "3.9" \
|
||||||
--hash=sha256:cab6c707e6ee20e567e348168a5c69dc6480384f777a9e5159f4299ad177dcc0
|
--hash=sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05 \
|
||||||
google-auth-httplib2==0.0.3 \
|
--hash=sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d
|
||||||
--hash=sha256:098fade613c25b4527b2c08fa42d11f3c2037dda8995d86de0745228e965d445 \
|
google-auth-oauthlib==1.2.2 ; python_version >= "3.9" \
|
||||||
--hash=sha256:f1c437842155680cf9918df9bc51c1182fda41feef88c34004bd1978c8157e08
|
--hash=sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684 \
|
||||||
google-auth-oauthlib==0.2.0 \
|
--hash=sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2
|
||||||
--hash=sha256:226d1d0960f86ba5d9efd426a70b291eaba96f47d071657e0254ea969025728a \
|
google-auth==2.40.3 ; python_version >= "3.9" \
|
||||||
--hash=sha256:81ba22acada4d13b1d83f9371ab19fd61f1250a542d21cf49e4dcf0637a7344a
|
--hash=sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca \
|
||||||
googleapis-common-protos==1.51.0 \
|
--hash=sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77
|
||||||
--hash=sha256:013c91704279119150e44ef770086fdbba158c1f978a6402167d47d5409e226e
|
googleapis-common-protos==1.70.0 ; python_version >= "3.9" \
|
||||||
httplib2==0.12.3 \
|
--hash=sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257 \
|
||||||
--hash=sha256:23914b5487dfe8ef09db6656d6d63afb0cf3054ad9ebc50868ddc8e166b5f8e8 \
|
--hash=sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8
|
||||||
--hash=sha256:a18121c7c72a56689efbf1aef990139ad940fee1e64c6f2458831736cd593600
|
httplib2==0.22.0 ; python_version >= "3.9" \
|
||||||
idna==2.9 \
|
--hash=sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc \
|
||||||
--hash=sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa \
|
--hash=sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81
|
||||||
--hash=sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb
|
idna==3.10 ; python_version >= "3.9" \
|
||||||
oauthlib==2.1.0 \
|
--hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
|
||||||
--hash=sha256:d883b36b21a6ad813953803edfa563b1b579d79ca758fe950d1bc9e8b326025b \
|
--hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
|
||||||
--hash=sha256:ac35665a61c1685c56336bda97d5eefa246f1202618a1d6f34fccb1bdd404162
|
oauthlib==2.1.0 ; python_version >= "3.9" \
|
||||||
protobuf==3.11.3 \
|
--hash=sha256:ac35665a61c1685c56336bda97d5eefa246f1202618a1d6f34fccb1bdd404162 \
|
||||||
--hash=sha256:ef2c2e56aaf9ee914d3dccc3408d42661aaf7d9bb78eaa8f17b2e6282f214481 \
|
--hash=sha256:d883b36b21a6ad813953803edfa563b1b579d79ca758fe950d1bc9e8b326025b
|
||||||
--hash=sha256:dd9aa4401c36785ea1b6fff0552c674bdd1b641319cb07ed1fe2392388e9b0d7 \
|
proto-plus==1.26.1 ; python_version >= "3.9" \
|
||||||
--hash=sha256:310a7aca6e7f257510d0c750364774034272538d51796ca31d42c3925d12a52a \
|
--hash=sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66 \
|
||||||
--hash=sha256:e512b7f3a4dd780f59f1bf22c302740e27b10b5c97e858a6061772668cd6f961 \
|
--hash=sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012
|
||||||
--hash=sha256:fdfb6ad138dbbf92b5dbea3576d7c8ba7463173f7d2cb0ca1bd336ec88ddbd80 \
|
protobuf==6.31.1 ; python_version >= "3.9" \
|
||||||
--hash=sha256:e2f8a75261c26b2f5f3442b0525d50fd79a71aeca04b5ec270fc123536188306 \
|
--hash=sha256:0414e3aa5a5f3ff423828e1e6a6e907d6c65c1d5b7e6e975793d5590bdeecc16 \
|
||||||
--hash=sha256:c40973a0aee65422d8cb4e7d7cbded95dfeee0199caab54d5ab25b63bce8135a \
|
--hash=sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447 \
|
||||||
--hash=sha256:adf0e4d57b33881d0c63bb11e7f9038f98ee0c3e334c221f0858f826e8fb0151 \
|
--hash=sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6 \
|
||||||
--hash=sha256:0bae429443cc4748be2aadfdaf9633297cfaeb24a9a02d0ab15849175ce90fab \
|
--hash=sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402 \
|
||||||
--hash=sha256:e11df1ac6905e81b815ab6fd518e79be0a58b5dc427a2cf7208980f30694b956 \
|
--hash=sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e \
|
||||||
--hash=sha256:7774bbbaac81d3ba86de646c39f154afc8156717972bf0450c9dbfa1dc8dbea2 \
|
--hash=sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9 \
|
||||||
--hash=sha256:8eb9c93798b904f141d9de36a0ba9f9b73cc382869e67c9e642c0aba53b0fc07 \
|
--hash=sha256:8764cf4587791e7564051b35524b72844f845ad0bb011704c3736cce762d8fe9 \
|
||||||
--hash=sha256:fac513a9dc2a74b99abd2e17109b53945e364649ca03d9f7a0b96aa8d1807d0a \
|
--hash=sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39 \
|
||||||
--hash=sha256:82d7ac987715d8d1eb4068bf997f3053468e0ce0287e2729c30601feb6602fee \
|
--hash=sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a
|
||||||
--hash=sha256:73152776dc75f335c476d11d52ec6f0f6925774802cd48d6189f4d5d7fe753f4 \
|
pyasn1-modules==0.4.2 ; python_version >= "3.9" \
|
||||||
--hash=sha256:52e586072612c1eec18e1174f8e3bb19d08f075fc2e3f91d3b16c919078469d0 \
|
--hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \
|
||||||
--hash=sha256:2affcaba328c4662f3bc3c0e9576ea107906b2c2b6422344cdad961734ff6b93 \
|
--hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6
|
||||||
--hash=sha256:24e3b6ad259544d717902777b33966a1a069208c885576254c112663e6a5bb0f \
|
pyasn1==0.6.1 ; python_version >= "3.9" \
|
||||||
--hash=sha256:c77c974d1dadf246d789f6dad1c24426137c9091e930dbf50e0a29c1fcf00b1f
|
--hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \
|
||||||
pyasn1==0.4.8 \
|
--hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034
|
||||||
--hash=sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3 \
|
pyparsing==3.2.3 ; python_version >= "3.9" \
|
||||||
--hash=sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf \
|
--hash=sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf \
|
||||||
--hash=sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00 \
|
--hash=sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be
|
||||||
--hash=sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8 \
|
pytz==2022.1 ; python_version >= "3.9" \
|
||||||
--hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \
|
--hash=sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7 \
|
||||||
--hash=sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86 \
|
--hash=sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c
|
||||||
--hash=sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7 \
|
requests-oauthlib==1.1.0 ; python_version >= "3.9" \
|
||||||
--hash=sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576 \
|
--hash=sha256:be76f2bb72ca5525998e81d47913e09b1ca8b7957ae89b46f787a79e68ad5e61 \
|
||||||
--hash=sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12 \
|
--hash=sha256:eabd8eb700ebed81ba080c6ead96d39d6bdc39996094bd23000204f6965786b0
|
||||||
--hash=sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2 \
|
requests-toolbelt==1.0.0 ; python_version >= "3.9" \
|
||||||
--hash=sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359 \
|
--hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \
|
||||||
--hash=sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776 \
|
--hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06
|
||||||
--hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba
|
requests==2.32.4 ; python_version >= "3.9" \
|
||||||
pyasn1-modules==0.2.8 \
|
--hash=sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c \
|
||||||
--hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \
|
--hash=sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422
|
||||||
--hash=sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199 \
|
rsa==4.2 ; python_version >= "3.13" \
|
||||||
--hash=sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405 \
|
--hash=sha256:aaefa4b84752e3e99bd8333a2e1e3e7a7da64614042bd66f775573424370108a
|
||||||
--hash=sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb \
|
rsa==4.9.1 ; python_version >= "3.9" and python_version < "3.13" \
|
||||||
--hash=sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8 \
|
--hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \
|
||||||
--hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74 \
|
--hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75
|
||||||
--hash=sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d \
|
schema==0.7.7 ; python_version >= "3.9" \
|
||||||
--hash=sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45 \
|
--hash=sha256:5d976a5b50f36e74e2157b47097b60002bd4d42e65425fcc9c9befadb4255dde \
|
||||||
--hash=sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4 \
|
--hash=sha256:7da553abd2958a19dc2547c388cde53398b39196175a9be59ea1caf5ab0a1807
|
||||||
--hash=sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811 \
|
tzdata==2025.2 ; python_version >= "3.9" and platform_system == "Windows" \
|
||||||
--hash=sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed \
|
--hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \
|
||||||
--hash=sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0 \
|
--hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9
|
||||||
--hash=sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd
|
tzlocal==5.3.1 ; python_version >= "3.9" \
|
||||||
python-magic==0.4.15 \
|
--hash=sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd \
|
||||||
--hash=sha256:f3765c0f582d2dfc72c15f3b5a82aecfae9498bd29ca840d72f37d7bd38bfcd5 \
|
--hash=sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d
|
||||||
--hash=sha256:f2674dcfad52ae6c49d4803fa027809540b130db1dec928cfbb9240316831375
|
unidecode==1.4.0 ; python_version >= "3.9" \
|
||||||
python-magic-bin==0.4.14; platform_system == "Windows" \
|
--hash=sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021 \
|
||||||
--hash=sha256:7b1743b3dbf16601d6eedf4e7c2c9a637901b0faaf24ad4df4d4527e7d8f66a4 \
|
--hash=sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23
|
||||||
--hash=sha256:34a788c03adde7608028203e2dbb208f1f62225ad91518787ae26d603ae68892 \
|
uritemplate==4.2.0 ; python_version >= "3.9" \
|
||||||
--hash=sha256:90be6206ad31071a36065a2fc169c5afb5e0355cbe6030e87641c6c62edc2b69
|
--hash=sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e \
|
||||||
pytz==2019.3 \
|
--hash=sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686
|
||||||
--hash=sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d \
|
urllib3==2.5.0 ; python_version >= "3.9" \
|
||||||
--hash=sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be
|
--hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \
|
||||||
requests==2.23.0 \
|
--hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc
|
||||||
--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
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue